-
-
Notifications
You must be signed in to change notification settings - Fork 2k
fix: empty model output error may misfire when use gemini #7377
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -461,14 +461,19 @@ def _process_content_parts( | |
| self, | ||
| candidate: types.Candidate, | ||
| llm_response: LLMResponse, | ||
| *, | ||
| validate_output: bool = True, | ||
| ) -> MessageChain: | ||
| """处理内容部分并构建消息链""" | ||
| if not candidate.content: | ||
| logger.warning(f"收到的 candidate.content 为空: {candidate}") | ||
| raise EmptyModelOutputError( | ||
| "Gemini candidate content is empty. " | ||
| f"finish_reason={candidate.finish_reason}" | ||
| ) | ||
| if validate_output: | ||
| raise EmptyModelOutputError( | ||
| "Gemini candidate content is empty. " | ||
| f"finish_reason={candidate.finish_reason}" | ||
| ) | ||
| llm_response.result_chain = MessageChain(chain=[]) | ||
| return llm_response.result_chain | ||
|
Comment on lines
+470
to
+476
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The early return (line 476) and the potential exception (line 471) occur before the safety and policy checks (lines 481-494). When Gemini blocks a response due to safety filters, it often returns a candidate with no content. In such cases, this function will either raise a generic Consider moving the |
||
|
|
||
| finish_reason = candidate.finish_reason | ||
| result_parts: list[types.Part] | None = candidate.content.parts | ||
|
|
@@ -490,10 +495,13 @@ def _process_content_parts( | |
|
|
||
| if not result_parts: | ||
| logger.warning(f"收到的 candidate.content.parts 为空: {candidate}") | ||
| raise EmptyModelOutputError( | ||
| "Gemini candidate content parts are empty. " | ||
| f"finish_reason={candidate.finish_reason}" | ||
| ) | ||
| if validate_output: | ||
| raise EmptyModelOutputError( | ||
| "Gemini candidate content parts are empty. " | ||
| f"finish_reason={candidate.finish_reason}" | ||
| ) | ||
| llm_response.result_chain = MessageChain(chain=[]) | ||
| return llm_response.result_chain | ||
|
|
||
| # 提取 reasoning content | ||
| reasoning = self._extract_reasoning_content(candidate) | ||
|
|
@@ -550,11 +558,12 @@ def _process_content_parts( | |
| llm_response.reasoning_signature = base64.b64encode(ts).decode("utf-8") | ||
| chain_result = MessageChain(chain=chain) | ||
| llm_response.result_chain = chain_result | ||
| self._ensure_usable_response( | ||
| llm_response, | ||
| response_id=None, | ||
| finish_reason=str(finish_reason) if finish_reason is not None else None, | ||
| ) | ||
| if validate_output: | ||
| self._ensure_usable_response( | ||
| llm_response, | ||
| response_id=None, | ||
| finish_reason=str(finish_reason) if finish_reason is not None else None, | ||
| ) | ||
| return chain_result | ||
|
|
||
| async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse: | ||
|
|
@@ -708,6 +717,7 @@ async def _query_stream( | |
| llm_response.result_chain = self._process_content_parts( | ||
| chunk.candidates[0], | ||
| llm_response, | ||
| validate_output=False, | ||
|
Comment on lines
717
to
+720
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (bug_risk): Streaming path now treats all chunks as non-validating, including the final one Because both the streaming branch and the Suggested implementation: llm_response.result_chain = self._process_content_parts(
chunk.candidates[0],
llm_response,
validate_output=False,
)
llm_response.id = chunk.response_id
if chunk.usage_metadata:
# For the aggregated final_response, enable validation so that
# the fully-assembled output is checked strictly.
final_response.result_chain = self._process_content_parts(
chunk.candidates[0],
final_response,
validate_output=True,
)
final_response.id = chunk.response_id
if chunk.usage_metadata:To fully meet your comment (validating only the final aggregated response, not every chunk):
|
||
| ) | ||
| llm_response.id = chunk.response_id | ||
| if chunk.usage_metadata: | ||
|
|
@@ -738,6 +748,7 @@ async def _query_stream( | |
| final_response.result_chain = self._process_content_parts( | ||
| chunk.candidates[0], | ||
| final_response, | ||
| validate_output=False, | ||
| ) | ||
|
Comment on lines
748
to
752
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Consider extracting the metadata ( |
||
| final_response.id = chunk.response_id | ||
| if chunk.usage_metadata: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): Returning an empty
MessageChainwhencandidate.contentis empty may mask streaming heartbeats/keep-alive chunksIn the streaming path,
_process_content_parts(..., validate_output=False)will hit this branch for chunks wherecandidate.contentis empty (e.g., heartbeats or tool-only updates). That now resetsllm_response.result_chainto an emptyMessageChain, discarding previously accumulated tokens. Before, this would raise and stop processing, making the issue visible. For streaming, it would be safer to treat emptycandidate.contentas a no-op (return the existingresult_chainunchanged) to avoid losing prior content.