Skip to content

fix(provider): recognize thinking tags in compatible completions - #10116

Open
konodiodaaaaa1 wants to merge 1 commit into
AstrBotDevs:masterfrom
konodiodaaaaa1:fix/openai-thinking-tag-compat
Open

konodiodaaaaa1 wants to merge 1 commit into
AstrBotDevs:masterfrom
konodiodaaaaa1:fix/openai-thinking-tag-compat

Conversation

@konodiodaaaaa1

@konodiodaaaaa1 konodiodaaaaa1 commented Sep 17, 2026

Copy link
Copy Markdown

Problem and fix

An OpenAI-compatible Gemini gateway returned <thinking>...</thinking> inside message.content. AstrBot's non-streaming parser only recognizes <think>...</think>, so the reasoning remains in the visible answer even with display_reasoning_text=false.

This adds the thinking spelling to the existing extraction and trailing-close-tag cleanup. Opening and closing tags must match. Extracted text goes into LLMResponse.reasoning_content, preserving the existing precedence of the structured reasoning field (including an explicitly empty string).

Example: <thinking>Reasoning</thinking>Answer now yields answer Answer and reasoning Reasoning.

The observed <thinking> wrapper is gateway behavior; it is not presented here as Google's native Gemini wire protocol. This is a small compatibility fix, with no new dependency or configuration option.

Upstream check

Checked on 2026-09-17 against official master at e0aa8d386121ead06825fb6d1e423a41a3d14a83; latest published release at the time was v4.28.1.

Those related changes address different extraction paths. I did not find an existing change covering this exact tag spelling in the reviewed master and PR diffs.

Validation

  • Regression before the change: 7 failing cases reproduced the missing <thinking> support.
  • python -m pytest tests/test_openai_thinking_tags.py tests/test_openai_source.py -q: 76 passed after the change.
  • 13 regression cases cover both spellings, multiline/multiple blocks, empty and reasoning-only blocks, trailing closing tags, ordinary text, unrelated tags, structured-field precedence, unfinished blocks, and mismatched tag names.
  • ruff check ., ruff format --check . using the repository-pinned Ruff 0.15.22, and git diff --check: passed.
  • Tests import this clean official-source worktree using an existing Python 3.10 dependency environment. No live provider requests were made for this PR.

Follow-up considerations for maintainers

This patch deliberately leaves broader policy and protocol decisions to separate work:

  1. Streaming: partial opening/closing tags can span deltas. Final-response regex cleanup alone cannot retract text already emitted. Please consider the thinking spelling and split-tag tests alongside fix: 移除流式响应中MiniMax思考内容的输出 #7314.
  2. Truncated or literal tags: an unfinished block keeps the existing behavior. Literal <think>/<thinking> examples in prose or code also share the existing regex ambiguity. A future policy should decide whether filtering is provider-specific and how truncation is handled.
  3. Structured fields: reasoning_content, reasoning, content blocks, and reasoning_details need protocol-aware extraction. fix(provider): make OpenAI-compatible reasoning field name configurable per channel (#9783) #9829 covers configurable field names, while structured arrays need their own treatment.
  4. History and signatures: hiding reasoning in the user-facing answer should preserve any provider-required reasoning history, signatures, or encrypted state. Such data should not be stringified into the visible answer.
Official protocol references reviewed (not a claim that this PR implements every format)

The same model can have different native, hosted, and gateway formats. References below describe specific interfaces/model families, rather than asserting that every model from a vendor uses one universal delimiter.

Provider / interface Documented representation Official reference
OpenAI Responses type: reasoning output items, summary entries of type: summary_text, and opaque encrypted_content Reasoning guide
Google Gemini GenerateContent SDK Part.thought distinguishes reasoning text; thought_signature is opaque state SDK Part schema
Google Gemini Interactions Thought steps contain signature and optional summary; streaming uses thought_summary and thought_signature deltas Thinking guide
Anthropic Claude thinking blocks with a thinking field; thinking_delta / signature_delta events; redacted_thinking has opaque data Thinking, streaming, redacted block schema
DeepSeek reasoning_content beside content; reasoning-history requirements depend on the API and tool-use mode Thinking mode
Qwen Hosted API: reasoning_content; Qwen3 raw generation: <think>...</think> Hosted API, Qwen3 model card
Moonshot Kimi Kimi K2 Thinking API examples use reasoning_content; its tokenizer includes <think> and </think> Model card, tokenizer
Z.ai GLM reasoning_content; preserved-thinking mode requires unchanged reasoning history Thinking mode
Xiaomi MiMo reasoning_content alongside tool calls; the documented tool workflow preserves it in subsequent messages MiMo-V2-Flash model card
MiniMax OpenAI-compatible API Inline <think>; with reasoning_split, separate reasoning_content / reasoning_details OpenAI-compatible API
OpenRouter reasoning / reasoning_content alias and reasoning_details entries such as reasoning.text, reasoning.summary, reasoning.encrypted Reasoning tokens
Ollama native API message.thinking for chat, top-level thinking for generate; answer stays in message.content / response Thinking
xAI Reasoning-capable interfaces expose SDK reasoning_content, Responses reasoning delta events, and optional encrypted reasoning state; availability varies by model Reasoning
Tencent Hunyuan-A13B Raw inference example extracts <think>...</think> and <answer>...</answer> Model card
Cohere reasoning models Typed thinking content blocks with thinking text, separate from text blocks Reasoning
OpenAI gpt-oss / Harmony Message channels such as analysis and final; protocol parsing is channel-based Model card, Harmony

These references support keeping structured provider handling separate from a small allowlist of observed inline tags. This patch adds only the confirmed <thinking> spelling. It does not infer extra XML tags from generic words such as “analysis” or “reasoning”.

Summary by Sourcery

Support <thinking> reasoning tags in non-streaming OpenAI-compatible completions while preserving existing structured reasoning behavior.

Bug Fixes:

  • Recognize matching <thinking>...</thinking> blocks in OpenAI-compatible completion content and move their text into reasoning_content instead of the visible answer.
  • Clean up trailing <thinking> closing tags alongside existing <think> tags.

Enhancements:

  • Preserve structured reasoning fields, including explicitly empty values, over inline tag extraction.

Tests:

  • Add regression coverage for both reasoning tag spellings, multiple and multiline blocks, empty or reasoning-only content, trailing tags, mismatched tags, unfinished blocks, unrelated text, and structured-field precedence.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Approved.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

buyun14 pushed a commit to buyun14/AstrBot that referenced this pull request Sep 17, 2026
buyun14 pushed a commit to buyun14/AstrBot that referenced this pull request Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant