Skip to content

Fix GLM OpenAI-compatible content format handling#9293

Open
WINE92 wants to merge 1 commit into
AstrBotDevs:masterfrom
WINE92:fix/glm-content-format
Open

Fix GLM OpenAI-compatible content format handling#9293
WINE92 wants to merge 1 commit into
AstrBotDevs:masterfrom
WINE92:fix/glm-content-format

Conversation

@WINE92

@WINE92 WINE92 commented Jul 15, 2026

Copy link
Copy Markdown

Description

This PR fixes compatibility with Zhipu GLM OpenAI-Compatible APIs.

Currently AstrBot sends:

content: [
  {
    "type": "text",
    "text": "..."
  }
]

However GLM expects:

content: "..."

which causes the API to return:

messages.content.type 参数非法,取值范围 ['text']

Changes

  • Move GLM compatibility logic into _finally_convert_payload().
  • Apply conversion only when the model name contains glm.
  • Convert text-only content arrays into plain strings.
  • Restrict the string conversion in assemble_context() to GLM only.

Tested

Tested with:

  • GLM-4.7-Flash
  • OpenAI-Compatible API

The API now works correctly without affecting Gemini, DeepSeek, or other providers.

This patch was generated with assistance from ChatGPT Codex and verified through actual testing.

Summary by Sourcery

Improve compatibility with Zhipu GLM models in the OpenAI-compatible provider by normalizing chat message content formatting.

Enhancements:

  • Normalize GLM chat messages by converting text-only content arrays into plain string content in the final payload conversion.
  • Adjust user context assembly to emit plain string content specifically for GLM models when all parts are text-only.

@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. labels Jul 15, 2026

@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 found 1 issue, and left some high level feedback:

  • In _finally_convert_payload, the GLM conversion will overwrite message['content'] with joined text even when the original list contains non-text parts; consider only converting when all parts are type == 'text' to avoid silently dropping multimodal content.
  • The GLM-specific text concatenation behavior is inconsistent between assemble_context (no separator) and _finally_convert_payload (joined with "\n"); aligning these or centralizing the logic would make the behavior more predictable.
  • You now have GLM-specific text-only normalization logic both in assemble_context and _finally_convert_payload; consider factoring this into a shared helper to reduce duplication and keep future GLM rules in one place.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_finally_convert_payload`, the GLM conversion will overwrite `message['content']` with joined text even when the original list contains non-text parts; consider only converting when *all* parts are `type == 'text'` to avoid silently dropping multimodal content.
- The GLM-specific text concatenation behavior is inconsistent between `assemble_context` (no separator) and `_finally_convert_payload` (joined with `"\n"`); aligning these or centralizing the logic would make the behavior more predictable.
- You now have GLM-specific text-only normalization logic both in `assemble_context` and `_finally_convert_payload`; consider factoring this into a shared helper to reduce duplication and keep future GLM rules in one place.

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/sources/openai_source.py" line_range="1007-1011" />
<code_context>
+                        if isinstance(part, dict) and part.get("type") == "text":
+                            texts.append(part.get("text", ""))
+
+                    message["content"] = "\n".join(texts)
+
         is_gemini = "gemini" in model
</code_context>
<issue_to_address>
**suggestion:** Align GLM text concatenation behavior between payload preparation and context assembly.

Here you join text parts with newlines, whereas `assemble_context` concatenates them without a separator. That means the same multi-part content is formatted differently depending on the path taken. Please standardize the behavior (e.g., always use `"\n".join(...)` or a space) or make the distinction explicit so GLM sees consistent text across both stages.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +1007 to 1011
message["content"] = "\n".join(texts)

is_gemini = "gemini" in model
_deepseek_v4_markers = ("deepseek-v4-pro", "deepseek-v4-flash", "deepseek-v4")
is_deepseek_v4_reasoning = (

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.

suggestion: Align GLM text concatenation behavior between payload preparation and context assembly.

Here you join text parts with newlines, whereas assemble_context concatenates them without a separator. That means the same multi-part content is formatted differently depending on the path taken. Please standardize the behavior (e.g., always use "\n".join(...) or a space) or make the distinction explicit so GLM sees consistent text across both stages.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces compatibility handling for GLM models in the OpenAI source provider, ensuring that message payloads and contexts are formatted as plain strings rather than multimodal lists. The review feedback highlights two potential TypeError bugs where part.get("text", "") could return None if the "text" key is explicitly set to None, which would cause the string joining operations to fail. Code suggestions were provided to safely handle and coerce these values.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1003 to +1005
for part in content:
if isinstance(part, dict) and part.get("type") == "text":
texts.append(part.get("text", ""))

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.

high

If part.get("text") is None, part.get("text", "") will still return None because the key "text" exists but its value is None. This will cause texts.append(None) to be executed, and subsequently "\n".join(texts) will raise a TypeError: sequence item expected str instance, NoneType found. To prevent this, we should defensively coerce the value to a string or handle None values safely, similar to how it is done in _normalize_content.

Suggested change
for part in content:
if isinstance(part, dict) and part.get("type") == "text":
texts.append(part.get("text", ""))
for part in content:
if isinstance(part, dict) and part.get("type") == "text":
val = part.get("text")
texts.append(str(val) if val is not None else "")

if "glm" in self.get_model().lower() and all(
part.get("type") == "text" for part in content_blocks
):
text_content = "".join(part.get("text", "") for part in content_blocks)

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.

medium

Similar to the payload conversion, using part.get("text", "") can return None if the key "text" is present but its value is None. To ensure robustness and prevent potential TypeError during "".join(), we should safely filter out or coerce None values.

Suggested change
text_content = "".join(part.get("text", "") for part in content_blocks)
text_content = "".join(str(part.get("text")) for part in content_blocks if part.get("text") is not None)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant