Skip to content

fix: wrap plain-text tool results as JSON for Gemini compatibility#7216

Open
he-yufeng wants to merge 1 commit intoAstrBotDevs:masterfrom
he-yufeng:fix/gemini-tool-struct
Open

fix: wrap plain-text tool results as JSON for Gemini compatibility#7216
he-yufeng wants to merge 1 commit intoAstrBotDevs:masterfrom
he-yufeng:fix/gemini-tool-struct

Conversation

@he-yufeng
Copy link
Copy Markdown
Contributor

@he-yufeng he-yufeng commented Mar 31, 2026

Summary

Gemini API 的 function_response 要求 google.protobuf.Struct(即 JSON 对象),但当工具(如内置知识库)返回纯文本结果时,框架直接把字符串传给 API,导致 400 Invalid argument。

_finally_convert_payload 中检测 Gemini 模型的 tool 消息,如果 content 不是合法 JSON,自动包装成 {"result": content} 后再发送。已有的 JSON 格式内容不受影响。

Changes

  • openai_source.py: _finally_convert_payload() 增加 Gemini tool content JSON 包装逻辑

Test

  1. 配置 Gemini 模型(如 gemini-3.1-pro-preview)通过 OpenAI 兼容接口
  2. 启用内置知识库或任何返回纯文本的工具
  3. 发送触发工具调用的消息
  4. 验证不再出现 400 Invalid argument 错误

Fixes #7134

Summary by Sourcery

Bug Fixes:

  • Wrap plain-text tool message content as a JSON object for Gemini models to satisfy function_response requirements and avoid 400 Invalid argument errors.

Gemini API requires function_response to be a google.protobuf.Struct
(JSON object). When tool results are plain text strings, the API
returns 400 Invalid argument. Detect non-JSON tool content for Gemini
models and wrap it in {"result": content} before sending.

Fixes AstrBotDevs#7134
@auto-assign auto-assign bot requested review from Fridemn and Soulter March 31, 2026 03:23
@dosubot dosubot bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Mar 31, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The Gemini function_response path currently only wraps non-JSON strings, but valid JSON that is not an object (e.g. a number, string, or array) will pass through unchanged and still violate the Struct requirement; consider checking that json.loads(content) specifically yields a dict and wrapping any other JSON types as {"result": content}.
  • Model detection uses a substring check ("gemini" in model), which may accidentally trigger for third-party models with gemini in their name; if feasible, tightening this to known Gemini model prefixes or an explicit provider flag would make the behavior more predictable.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The Gemini `function_response` path currently only wraps non-JSON strings, but valid JSON that is not an object (e.g. a number, string, or array) will pass through unchanged and still violate the `Struct` requirement; consider checking that `json.loads(content)` specifically yields a `dict` and wrapping any other JSON types as `{"result": content}`.
- Model detection uses a substring check (`"gemini" in model`), which may accidentally trigger for third-party models with `gemini` in their name; if feasible, tightening this to known Gemini model prefixes or an explicit provider flag would make the behavior more predictable.

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.

@dosubot dosubot bot added the area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. label Mar 31, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

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 updates the _finally_convert_payload method in openai_source.py to ensure that tool responses for Gemini models are correctly formatted as JSON objects, preventing 400 errors caused by plain text responses. The review feedback suggests refining the validation logic to ensure the content is specifically a JSON dictionary, as json.loads might successfully parse non-object values (like numbers or strings) that would still be rejected by the Gemini API.

Comment on lines +866 to +871
try:
json.loads(content)
except (json.JSONDecodeError, ValueError):
message["content"] = json.dumps(
{"result": content}, ensure_ascii=False
)
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

Gemini 的 function_response 明确要求是一个 JSON 对象(对应 google.protobuf.Struct)。虽然 json.loads() 可以验证字符串是否为合法的 JSON,但它不能保证解析后是一个对象。例如,如果工具返回的是纯数字 123 或 JSON 字符串 "success"json.loads() 会成功解析,但如果代理层没有自动包装,可能仍然会导致 400 错误。建议检查解析后的结果是否为字典类型。

Suggested change
try:
json.loads(content)
except (json.JSONDecodeError, ValueError):
message["content"] = json.dumps(
{"result": content}, ensure_ascii=False
)
try:
if not isinstance(json.loads(content), dict):
raise ValueError()
except (json.JSONDecodeError, ValueError):
message["content"] = json.dumps(
{"result": content}, ensure_ascii=False
)

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.

[Bug] Gemini 模型调用工具时抛出 400 错误 (Protobuf Struct 格式不兼容)

1 participant