Skip to content

[Bug]: Poolside's Laguna models' reasoning parser is broken: the </think> block leaks into the model's answer #17146

Description

@1MrazorT1

System Info

  • CPU architecture: x86_64
  • Host memory size: 1.5TB
  • GPU properties:
    • GPU name: H100
    • GPU memory size: 4x80GB
  • Libraries
    • TensorRT-LLM: 1.3.0rc22
    • Container used: nvcr.io/nvidia/tensorrt-llm/release:1.3.0rc22
  • NVIDIA driver version: NVIDIA-SMI 595.45.04 Driver Version: 595.45.04 CUDA Version: 13.2
  • OS: Ubuntu 24.04

Who can help?

No response

Information

  • The official example scripts
  • My own modified scripts

Tasks

  • An officially supported task in the examples folder (such as GLUE/SQuAD, ...)
  • My own task or dataset (give details below)

Reproduction

Bring up the server:

trtllm-serve poolside/Laguna-S-2.1 --tool_parser poolside_v1 --host 0.0.0.0 --port 8000 --trust_remote_code --reasoning_parser laguna --tensor_parallel_size 4 --moe_expert_parallel_size 4 --max_seq_len 262144

After the application startup, run a simple chat/completions request:

curl -sX 'POST' 'http://127.0.0.1:8000/v1/chat/completions' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"model": "Laguna", "messages": [{"role": "user", "content": "hello"}]}' | jq ".choices[0].message.content"

Expected behavior

"Hi there! How can I assist you today?"

actual behavior

"</think>Hi there! How can I assist you today?"

additional notes

In this section, I will show other behaviors that will help me later explain how I fixed this issue. This will be accomplished with the help of the enable_thinking parameter.

Let's first set enable_thinking = true:

Test command:

curl -sX 'POST' 'http://127.0.0.1:8000/v1/chat/completions' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"model": "Laguna", "messages": [{"role": "user", "content": "hello"}], "chat_template_kwargs": {"enable_thinking": true}}' | jq ".choices[0].message"

Output:

{
  "role": "assistant",
  "content": "</think>Hello! How can I assist you today?",
  "reasoning_content": "",
  "reasoning": null,
  "tool_calls": []
}

We notice here that the </think> token leaked into the message's content while reasoning is empty.

Please note here that, reasoning content being empty, is not faulty behavior, even though we explicitly told the model to think. This is actually normal behavior because further tests can show that Laguna S2.1 model chooses not to think on "easy" or "seen before" user messages.

For reference, here's a "harder" query:

curl -sX 'POST' 'http://127.0.0.1:8000/v1/chat/completions' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"model": "Laguna", "messages": [{"role": "user", "content": "est ce que les jumeaux sont à risque de hypothyroidie ?"}], "chat_template_kwargs": {"enable_thinking": true}}' | jq ".choices[0].message"

Output:

{
  "role": "assistant",
  "content": "Okay, the user is asking if (etc) and the importance of monitoring if there's a family history.</think>Oui, les jumeaux, en particulier (etc)",
  "reasoning_content": "",
  "reasoning": null,
  "tool_calls": []
}

The output is most important here, because it shows a bigger problem: both, actual reasoning, and the </think> are present in the message delivered to the user, the parser is broken.

Now let's set enable_thinking = false:

Test command:

curl -sX 'POST' 'http://127.0.0.1:8000/v1/chat/completions' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"model": "Laguna", "messages": [{"role": "user", "content": "hello"}], "chat_template_kwargs": {"enable_thinking": false}}' | jq ".choices[0].message"

Output:

{
  "role": "assistant",
  "content": "Hello! How can I assist you today?",
  "reasoning_content": "",
  "reasoning": null,
  "tool_calls": []
}

We notice here that the output is correctly parsed.
This means that the issue lies when the model is in reasoning mode.

Why does this issue happen ?

After looking at the model's config files (the entire model family uses the same template mechanism), more precisely, the jinja template file, I have found the following:

{%- if add_generation_prompt -%}
  {{- "<assistant>" -}}
  {#- ───── Include reasoning mode directive ───── -#}
  {%- if enable_thinking -%}
    {{- '<think>' -}}
  {%- else -%}
    {{- '</think>' -}}
  {%- endif -%}
{%- endif -%}

This means that, when reasoning, the template literally puts the <think> block alongside the actual input that is processed by the model, and since decoding only happens to the newly generated tokens, the model itself never emits <think> in the output, which means that the parser never actually sees the <think> block.

Now, let's take a look at how is the Laguna parser defined:

From tensorrt_llm/llmapi/reasoning_parser.py:

@register_reasoning_parser("laguna")
@register_reasoning_parser("qwen3")
@register_reasoning_parser("qwen3_5", reasoning_at_start=True)
@register_reasoning_parser("minimax_m2", reasoning_at_start=True)
@register_reasoning_parser("minimax_m2_append_think", reasoning_at_start=True)
class DeepSeekR1Parser(BaseReasoningParser):

We can see here that laguna's parser is registered as a DeepSeekR1 parser which is the following:

def parse(self, text: str) -> ReasoningParserResult:
  if not self.reasoning_at_start:
      splits = text.partition(self.reasoning_start)
      if splits[1] == "":
          # no reasoning start tag found
          return ReasoningParserResult(content=text)
      # reasoning start tag found
      # text before reasoning start tag is dropped
      text = splits[2]
  splits = text.partition(self.reasoning_end)
  reasoning_content, content = splits[0], splits[2]
  return ReasoningParserResult(content=content,
                                reasoning_content=reasoning_content)

Because the laguna's parser does not set reasoning at start to true, it tries to split the output using the <think> block, however, we have proved earlier that the model never emits that block in its output. This is why the parser never finds that block so it returns the entire reasoning + </think> + actual content in content.

So let's try to flip the flag to reasoning_at_start=True

After bringing up the server again with that modification, let's enforce thinking again and analyze the output:
Run again:

curl -sX 'POST' 'http://127.0.0.1:8000/v1/chat/completions' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"model": "Laguna", "messages": [{"role": "user", "content": "est ce que les jumeaux sont à risque de hypothyroidie ?"}], "chat_template_kwargs": {"enable_thinking": true}}' | jq ".choices[0].message"

Output:

{
  "role": "assistant",
  "content": "Les jumeaux, en particulier les jumeaux mono-identiques (etc)",
  "reasoning_content": "Okay, the user is asking if twins are at risk for hypothyroidism. Let me start by (etc)",
  "reasoning": null,
  "tool_calls": []
}

We can see here that the parser worked correctly, content fields are correctly separated.

Let's test with non thinking mode to ensure that the parser works as intended in both cases:

curl -sX 'POST' 'http://127.0.0.1:8000/v1/chat/completions' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"model": "Laguna", "messages": [{"role": "user", "content": "What is the weather today ?"}], "chat_template_kwargs": {"enable_thinking": false}}' | jq ".choices[0].message"

Output:

{
  "role": "assistant",
  "content": "",
  "reasoning_content": "I don't have access to real-time data, including current weather information. To check today's weather, I recommend using a weather website or app like Weather.com, AccuWeather, or your device's built-in weather app. If you share your location (city or region), I can help guide you further!",
  "reasoning": null,
  "tool_calls": []
}

This is where the bigger problem lies: it's true that no reasoning content exists, and it's true that both <think> and </think> blocks are not present, but, as shown in the output, the actual content, the message that should be printed to the user, is all rendered in the reasoning_content block.

This is again another issue introduced by the incompatibility of DeepSeekR1 parser and Laguna's chat template: when reasoning is disabled, the template prefills input with </think> which is then never emitted in the output because it is not a new token generated by the model. And while reasoning at start is true, the if block is ignored, and then the parser looks for </think>, does not find it, and therefore, emits everything as reasoning_content.

This means that setting reasoning_at_start=true does not solve the issue entirely, and therefore, Laguna cannot be registered as a DeepSeekR1 parser.

The actual solution

I will link the PR that actually solves this issue. The rest of my analysis and implementation/testing steps will be described in it.

Thank you.

Before submitting a new issue...

  • Make sure you already searched for relevant issues, and checked the documentation and examples for answers to frequently asked questions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions