1414from astrbot import logger
1515from astrbot .api .provider import Provider
1616from astrbot .core .agent .message import ContentPart , ImageURLPart , TextPart
17+ from astrbot .core .exceptions import EmptyModelOutputError
1718from astrbot .core .message .message_event_result import MessageChain
1819from astrbot .core .provider .entities import LLMResponse , TokenUsage
1920from astrbot .core .provider .func_tool_manager import ToolSet
@@ -444,6 +445,23 @@ def _extract_usage(
444445 output = usage_metadata .candidates_token_count or 0 ,
445446 )
446447
448+ @staticmethod
449+ def _ensure_usable_response (
450+ llm_response : LLMResponse ,
451+ * ,
452+ response_id : str | None = None ,
453+ finish_reason : str | None = None ,
454+ ) -> None :
455+ has_text_output = bool ((llm_response .completion_text or "" ).strip ())
456+ has_reasoning_output = bool (llm_response .reasoning_content .strip ())
457+ has_tool_output = bool (llm_response .tools_call_args )
458+ if has_text_output or has_reasoning_output or has_tool_output :
459+ return
460+ raise EmptyModelOutputError (
461+ "Gemini completion has no usable output. "
462+ f"response_id={ response_id } , finish_reason={ finish_reason } "
463+ )
464+
447465 def _process_content_parts (
448466 self ,
449467 candidate : types .Candidate ,
@@ -452,7 +470,10 @@ def _process_content_parts(
452470 """处理内容部分并构建消息链"""
453471 if not candidate .content :
454472 logger .warning (f"收到的 candidate.content 为空: { candidate } " )
455- raise Exception ("API 返回的 candidate.content 为空。" )
473+ raise EmptyModelOutputError (
474+ "Gemini candidate content is empty. "
475+ f"finish_reason={ candidate .finish_reason } "
476+ )
456477
457478 finish_reason = candidate .finish_reason
458479 result_parts : list [types .Part ] | None = candidate .content .parts
@@ -474,7 +495,10 @@ def _process_content_parts(
474495
475496 if not result_parts :
476497 logger .warning (f"收到的 candidate.content.parts 为空: { candidate } " )
477- raise Exception ("API 返回的 candidate.content.parts 为空。" )
498+ raise EmptyModelOutputError (
499+ "Gemini candidate content parts are empty. "
500+ f"finish_reason={ candidate .finish_reason } "
501+ )
478502
479503 # 提取 reasoning content
480504 reasoning = self ._extract_reasoning_content (candidate )
@@ -525,7 +549,14 @@ def _process_content_parts(
525549 if ts := part .thought_signature :
526550 # only keep the last thinking signature
527551 llm_response .reasoning_signature = base64 .b64encode (ts ).decode ("utf-8" )
528- return MessageChain (chain = chain )
552+ chain_result = MessageChain (chain = chain )
553+ llm_response .result_chain = chain_result
554+ self ._ensure_usable_response (
555+ llm_response ,
556+ response_id = None ,
557+ finish_reason = str (finish_reason ) if finish_reason is not None else None ,
558+ )
559+ return chain_result
529560
530561 async def _query (self , payloads : dict , tools : ToolSet | None ) -> LLMResponse :
531562 """非流式请求 Gemini API"""
@@ -727,9 +758,12 @@ async def _query_stream(
727758 final_response .result_chain = MessageChain (
728759 chain = [Comp .Plain (accumulated_text )],
729760 )
730- elif not final_response .result_chain :
731- # If no text was accumulated and no final response was set, provide empty space
732- final_response .result_chain = MessageChain (chain = [Comp .Plain (" " )])
761+
762+ self ._ensure_usable_response (
763+ final_response ,
764+ response_id = getattr (final_response , "id" , None ),
765+ finish_reason = None ,
766+ )
733767
734768 yield final_response
735769
0 commit comments