#13240·dynamo

[BUG]: SGLang reasoning-enabled required tool calls intermittently fail through Dynamo Rust frontend

Author: indrajit96Created Aug 14, 2026Updated Sep 19, 2026
Labelsbuglanguage::rustdynamo-llmbackend::sglangfrontendtool-calling

Describe the bug

When a reasoning model is served with SGLang through Dynamo's Rust frontend, a request with tool_choice="required" can return normal assistant text instead of the required tool call.

The failed response consumes the full max_tokens value and ends with finish_reason="length". No tool_calls are returned.

This was reproduced with Kimi K2.6 when all of the following are true:

  • The request uses tool_choice="required".
  • Thinking/reasoning is enabled.
  • SGLang is started with an engine-side reasoning parser.
  • The request goes through Dynamo's Rust frontend.

The failure is intermittent. The exact request below passed 16 out of 20 runs and failed 4 out of 20 runs.

This was tested after applying the separate Kimi native structural-tag fix tracked by ai-dynamo/dynamo#13231. It therefore appears that another reasoning-related issue remains after the generic-JSON versus native-Kimi-format mismatch is fixed.

Startup commands

Start the Dynamo Rust frontend:

bash
python3 -m dynamo.frontend \
  --http-port 8000 \
  --router-mode round-robin \
  --discovery-backend file \
  --event-plane zmq

Start the SGLang worker:

bash
python3 -m dynamo.sglang \
  --model-path nvidia/Kimi-K2.6-NVFP4 \
  --served-model-name moonshotai/Kimi-K2.6 \
  --discovery-backend file \
  --trust-remote-code \
  --host 0.0.0.0 \
  --tp 4 \
  --context-length 8192 \
  --max-running-requests 16 \
  --chunked-prefill-size 8192 \
  --mem-fraction-static 0.9 \
  --reasoning-parser kimi_k2 \
  --dyn-tool-call-parser kimi_k2 \
  --dyn-reasoning-parser kimi_k25 \
  --language-only \
  --model-loader-extra-config '{"enable_multithread_load":false}' \
  --disable-prefill-cuda-graph \
  --disable-flashinfer-autotune \
  --constrained-json-disable-any-whitespace \
  --event-plane zmq

The important arguments are:

  • --reasoning-parser kimi_k2: SGLang's engine-side reasoning parser.
  • --dyn-reasoning-parser kimi_k25: Dynamo's response reasoning parser.
  • --dyn-tool-call-parser kimi_k2: Dynamo's Kimi tool-call parser.

The failing deployment did not use --dyn-enable-structural-tag. After ai-dynamo/dynamo#13231, Kimi required and named tool choices use the native Kimi structural tag even when the global structural-tag option is off.

Request

Send this request through the Dynamo frontend:

bash
curl -sS http://127.0.0.1:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d @request.json

request.json:

json
{
  "model": "moonshotai/Kimi-K2.6",
  "messages": [
    {
      "role": "user",
      "content": "Hi, how are you?"
    }
  ],
  "max_tokens": 512,
  "tool_choice": "required",
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Get the current weather in a given location",
        "parameters": {
          "type": "object",
          "properties": {
            "location": {
              "type": "string",
              "description": "City and state, e.g., San Francisco, CA"
            },
            "unit": {
              "type": "string",
              "enum": [
                "celsius",
                "fahrenheit"
              ]
            }
          },
          "required": [
            "location",
            "unit"
          ]
        }
      }
    }
  ]
}

Because the problem is intermittent, send the same request 20 times.

Actual failing response

This is one of the recorded failures. Only the long, repetitive assistant content has been shortened below. The response fields and initial generated text are from the real response.

json
{
  "choices": [
    {
      "finish_reason": "length",
      "index": 0,
      "message": {
        "content": "Hi there! I'm doing well, thanks for asking. How can I help you today? Is there anything specific you'd like to talk about or need assistance with? I'm happy to help with questions, information, or even check something like the weather if you'd like! ... [ordinary assistant text continued until max_tokens]",
        "reasoning_content": "The user is asking \"Hi, how are you?\" This is a simple greeting. I should respond politely and helpfully, asking how I can assist them today. I don't need any tools for this - it's just a conversational opening.",
        "role": "assistant"
      }
    }
  ],
  "id": "chatcmpl-59d57b5a-0bbb-49e9-a5d7-4da651324ffd",
  "model": "moonshotai/Kimi-K2.6",
  "object": "chat.completion",
  "usage": {
    "completion_tokens": 512,
    "prompt_tokens": 119,
    "total_tokens": 631
  }
}

Important parts of the failure:

  • finish_reason is length instead of tool_calls.
  • There is no message.tool_calls field.
  • The model returns ordinary assistant content even though tool_choice="required".
  • All 512 completion tokens are consumed.

Expected behavior

Dynamo should return a structured get_weather tool call and finish with:

json
{
  "finish_reason": "tool_calls",
  "message": {
    "tool_calls": [
      {
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{...}"
        }
      }
    ]
  }
}

Raw Kimi reasoning or tool-call markers should not leak into assistant content.

Reproduction results

Using the exact request above:

  • Thinking enabled: 16/20 passed and 4/20 failed.
  • Thinking disabled: 20/20 passed.

A broader 38-request tool-calling suite passed 34/38. All four failed executions were reasoning-enabled tool_choice="required" requests:

  • required_forces_weather, streaming and nonstreaming.
  • required_echo_context_probe, streaming and nonstreaming.

The weather case returned normal assistant text until the length limit. The echo case returned structured tool calls but also leaked a raw <|tool_call_end|> marker into assistant content. The marker leak may be related, but it should be treated as a separate parser-boundary problem if it remains after the main failure is fixed.

Working hypothesis

This looks like a double-reasoning constraint between Dynamo and SGLang.

For this request, Dynamo internally sends require_reasoning=true to SGLang. This is an internal per-request value, not a user-facing SGLang command-line option.

SGLang's reasoning-aware grammar backend then behaves like this:

  1. Let the model reason without applying the tool grammar.
  2. Detect the model's reasoning-end token.
  3. Apply the tool-call grammar after reasoning has ended.

However, the structural tag generated by Dynamo's Rust frontend still contains its own reasoning section. When SGLang activates that grammar after the real reasoning phase, the grammar appears to expect another reasoning section before the tool call.

In simple terms, both SGLang and Dynamo think they are responsible for the reasoning part of the constrained output. The model is effectively asked to finish reasoning twice before it can call the tool.

This explains why:

  • Thinking-disabled requests pass consistently.
  • Reasoning-enabled requests can run until max_tokens without producing the required call.
  • Some runs still pass because model generation can sometimes satisfy or escape the accidental second boundary.

SGLang's native OpenAI frontend already accounts for this ownership: when its engine-side reasoning parser owns the reasoning transition, it builds the tool constraint without another reasoning prefix. Dynamo's Rust frontend likely needs equivalent runtime information.

Possible fix direction

Dynamo's SGLang worker could publish a runtime capability indicating that SGLang can delay the structural grammar until reasoning ends.

The Rust frontend should remove the reasoning section from the structural tag only when both conditions are true:

  1. The SGLang worker advertises that capability.
  2. Dynamo is sending require_reasoning=true for the current request.

The existing behavior should remain unchanged when the capability is missing, the SGLang version does not support require_reasoning, or no engine-side reasoning parser is configured.

This should be a backend-capability decision, not a Kimi-specific condition.

Scope

This issue is for the Dynamo Rust frontend and SGLang integration. It is not currently being reported as an upstream SGLang bug because the failure has not been reproduced through SGLang's native OpenAI frontend.