Compare commits

...

3 Commits

3 changed files with 88 additions and 101 deletions

View File

@@ -186,7 +186,6 @@ class ResultDecorateStage(Stage):
# 流式输出不执行下面的逻辑
if is_stream:
logger.info("流式输出已启用,跳过结果装饰阶段")
return
# 需要再获取一次。插件可能直接对 chain 进行了替换。

View File

@@ -90,7 +90,7 @@ class PipelineScheduler:
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
await event.send(None)
logger.debug("pipeline 执行完毕。")
logger.debug("pipeline execution completed.")
finally:
event.cleanup_temporary_local_files()
active_event_registry.unregister(event)

View File

@@ -97,7 +97,6 @@ class ProviderGoogleGenAI(Provider):
if proxy:
async_client_kwargs["proxy"] = proxy
async_client_kwargs["trust_env"] = False
logger.info("[Gemini] 使用代理")
else:
async_client_kwargs["trust_env"] = True
@@ -137,15 +136,18 @@ class ProviderGoogleGenAI(Provider):
keys.remove(self.chosen_api_key)
if len(keys) > 0:
self.set_key(random.choice(keys))
logger.info(
f"检测到 Key 异常({e.message}),正在尝试更换 API Key 重试... 当前 Key: {self.chosen_api_key[:12]}...",
logger.warning(
"Retrying with a different API key due to detected key issue: %s. Current key: %s...",
e.message,
self.chosen_api_key[:12],
)
await asyncio.sleep(1)
return True
logger.error(
f"检测到 Key 异常({e.message}),且已没有可用的 Key。 当前 Key: {self.chosen_api_key[:12]}...",
"No valid API keys remaining. Current key: %s...",
self.chosen_api_key[:12],
)
raise Exception("达到了 Gemini 速率限制, 请稍后再试...")
raise Exception("Gemini API rate limit reached or API key issue detected.")
# 连接错误处理
if is_connection_error(e):
@@ -172,7 +174,9 @@ class ProviderGoogleGenAI(Provider):
self.provider_settings.get("streaming_response", False)
and "IMAGE" in modalities
):
logger.warning("流式输出不支持图片模态,已自动降级为文本模态")
logger.warning(
"Streaming responses do not support IMAGE modality, falling back to TEXT modality."
)
modalities = ["TEXT"]
tool_list: list[types.Tool] | None = []
@@ -181,60 +185,28 @@ class ProviderGoogleGenAI(Provider):
native_search = self.provider_config.get("gm_native_search", False)
url_context = self.provider_config.get("gm_url_context", False)
if "gemini-2.5" in model_name:
if native_coderunner:
tool_list.append(types.Tool(code_execution=types.ToolCodeExecution()))
if native_search:
logger.warning("代码执行工具与搜索工具互斥,已忽略搜索工具")
if url_context:
logger.warning(
"代码执行工具与URL上下文工具互斥已忽略URL上下文工具",
)
else:
if native_search:
tool_list.append(types.Tool(google_search=types.GoogleSearch()))
if url_context:
if hasattr(types, "UrlContext"):
tool_list.append(types.Tool(url_context=types.UrlContext()))
else:
logger.warning(
"当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包",
)
elif "gemini-2.0-lite" in model_name:
if "gemini-2.0-lite" in model_name:
if native_coderunner or native_search or url_context:
logger.warning(
"gemini-2.0-lite 不支持代码执行、搜索工具和URL上下文将忽略这些设置",
"gemini-2.0-lite does not support native code execution, search, or URL context tools. These settings will be ignored.",
)
tool_list = None
else:
if native_coderunner:
tool_list.append(types.Tool(code_execution=types.ToolCodeExecution()))
if native_search:
logger.warning("代码执行工具与搜索工具互斥,已忽略搜索工具")
elif native_search:
if native_search:
tool_list.append(types.Tool(google_search=types.GoogleSearch()))
if url_context:
tool_list.append(types.Tool(url_context=types.UrlContext()))
if url_context and not native_coderunner:
if hasattr(types, "UrlContext"):
tool_list.append(types.Tool(url_context=types.UrlContext()))
else:
logger.warning(
"当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包",
)
if tools:
func_desc = tools.get_func_desc_google_genai_style()
tool_list.append(
types.Tool(function_declarations=func_desc["function_declarations"]),
)
if not tool_list:
tool_list = None
if tools and tool_list:
logger.warning("已启用原生工具,函数工具将被忽略")
elif tools and (func_desc := tools.get_func_desc_google_genai_style()):
tool_list = [
types.Tool(function_declarations=func_desc["function_declarations"]),
]
tool_config = None
has_func_decl = tool_list and any(t.function_declarations for t in tool_list)
if has_func_decl:
@@ -322,7 +294,7 @@ class ProviderGoogleGenAI(Provider):
def create_text_part(text: str) -> types.Part:
content_a = text if text else " "
if not text:
logger.warning("文本内容为空,已添加空格占位")
logger.warning("Text content is empty, added a space as placeholder.")
return types.Part.from_text(text=content_a)
def process_image_url(image_url_dict: dict) -> types.Part:
@@ -349,12 +321,6 @@ class ProviderGoogleGenAI(Provider):
contents.append(content_cls(parts=part))
gemini_contents: list[types.Content] = []
native_tool_enabled = any(
[
self.provider_config.get("gm_native_coderunner", False),
self.provider_config.get("gm_native_search", False),
],
)
for message in payloads["messages"]:
role, content = message["role"], message.get("content")
@@ -377,11 +343,10 @@ class ProviderGoogleGenAI(Provider):
append_or_extend(gemini_contents, parts, types.UserContent)
elif role == "assistant":
parts = []
if isinstance(content, str):
parts = [types.Part.from_text(text=content)]
append_or_extend(gemini_contents, parts, types.ModelContent)
parts.append(types.Part.from_text(text=content))
elif isinstance(content, list):
parts = []
thinking_signature = None
text = ""
for part in content:
@@ -400,16 +365,31 @@ class ProviderGoogleGenAI(Provider):
exc_info=True,
)
thinking_signature = None
parts.append(
types.Part(
text=text,
thought_signature=thinking_signature,
)
)
append_or_extend(gemini_contents, parts, types.ModelContent)
elif not native_tool_enabled and "tool_calls" in message:
parts = []
if (
not text
and thinking_signature
and "tool_calls" in message
and any(
isinstance(tool, dict)
and isinstance(tool.get("extra_content"), dict)
and isinstance(tool["extra_content"].get("google"), dict)
and tool["extra_content"]["google"].get("thought_signature")
for tool in message["tool_calls"]
)
):
# If the main content is empty but tool calls have thought signatures,
# skip adding an empty text part to deduplicate the thinking signature in the main content and tool calls.
pass
else:
parts.append(
types.Part(
text=text,
thought_signature=thinking_signature,
)
)
if "tool_calls" in message:
for tool in message["tool_calls"]:
part = types.Part.from_function_call(
name=tool["function"]["name"],
@@ -427,17 +407,13 @@ class ProviderGoogleGenAI(Provider):
if ts_bs64:
part.thought_signature = base64.b64decode(ts_bs64)
parts.append(part)
append_or_extend(gemini_contents, parts, types.ModelContent)
else:
logger.warning("assistant 角色的消息内容为空,已添加空格占位")
if native_tool_enabled and "tool_calls" in message:
logger.warning(
"检测到启用Gemini原生工具且上下文中存在函数调用建议使用 /reset 重置上下文",
)
parts = [types.Part.from_text(text=" ")]
append_or_extend(gemini_contents, parts, types.ModelContent)
elif role == "tool" and not native_tool_enabled:
if not parts:
parts = [types.Part.from_text(text=" ")]
append_or_extend(gemini_contents, parts, types.ModelContent)
elif role == "tool":
func_name = message.get("name", message["tool_call_id"])
part = types.Part.from_function_response(
name=func_name,
@@ -501,7 +477,7 @@ class ProviderGoogleGenAI(Provider):
) -> MessageChain:
"""处理内容部分并构建消息链"""
if not candidate.content:
logger.warning(f"收到的 candidate.content 为空: {candidate}")
logger.warning(f"Gemini candidate.content is empty: {candidate}")
if validate_output:
raise EmptyModelOutputError(
"Gemini candidate content is empty. "
@@ -514,22 +490,22 @@ class ProviderGoogleGenAI(Provider):
result_parts: list[types.Part] | None = candidate.content.parts
if finish_reason == types.FinishReason.SAFETY:
raise Exception("模型生成内容未通过 Gemini 平台的安全检查")
raise Exception("The model output failed Gemini platform safety checks.")
if finish_reason in {
types.FinishReason.PROHIBITED_CONTENT,
types.FinishReason.SPII,
types.FinishReason.BLOCKLIST,
}:
raise Exception("模型生成内容违反 Gemini 平台政策")
raise Exception("The model output violates Gemini platform policy.")
# 防止旧版本SDK不存在IMAGE_SAFETY
if hasattr(types.FinishReason, "IMAGE_SAFETY"):
if finish_reason == types.FinishReason.IMAGE_SAFETY:
raise Exception("模型生成内容违反 Gemini 平台政策")
raise Exception("The model output violates Gemini platform policy.")
if not result_parts:
logger.warning(f"收到的 candidate.content.parts 为空: {candidate}")
logger.warning(f"Gemini candidate.content.parts is empty: {candidate}")
if validate_output:
raise EmptyModelOutputError(
"Gemini candidate content parts are empty. "
@@ -636,15 +612,19 @@ class ProviderGoogleGenAI(Provider):
logger.debug(f"genai result: {result}")
if not result.candidates:
logger.error(f"请求失败, 返回的 candidates 为空: {result}")
raise Exception("请求失败, 返回的 candidates 为空。")
logger.error(
f"Gemini request failed: candidates is empty: {result}"
)
raise Exception("Gemini request failed: candidates is empty.")
if result.candidates[0].finish_reason == types.FinishReason.RECITATION:
if temperature > 2:
raise Exception("温度参数已超过最大值2仍然发生recitation")
raise Exception(
"Temperature exceeded the maximum value of 2, but Gemini recitation still occurred."
)
temperature += 0.2
logger.warning(
f"发生了recitation,正在提高温度至{temperature:.1f}重试...",
f"Gemini recitation detected; increasing temperature to {temperature:.1f} and retrying...",
)
continue
@@ -655,11 +635,13 @@ class ProviderGoogleGenAI(Provider):
e.message = ""
if "Developer instruction is not enabled" in e.message:
logger.warning(
f"{model} 不支持 system prompt已自动去除(影响人格设置)",
f"{model} does not support system prompts; removing it automatically. This may affect persona settings.",
)
system_instruction = None
elif "Function calling is not enabled" in e.message:
logger.warning(f"{model} 不支持函数调用,已自动去除")
logger.warning(
f"{model} does not support function calling; removing tools automatically."
)
tools = None
elif (
"Multi-modal output is not supported" in e.message
@@ -668,7 +650,7 @@ class ProviderGoogleGenAI(Provider):
or "only supports text output" in e.message
):
logger.warning(
f"{model} 不支持多模态输出,降级为文本模态",
f"{model} does not support multimodal output; falling back to TEXT modality.",
)
modalities = ["TEXT"]
else:
@@ -719,11 +701,13 @@ class ProviderGoogleGenAI(Provider):
e.message = ""
if "Developer instruction is not enabled" in e.message:
logger.warning(
f"{model} 不支持 system prompt已自动去除(影响人格设置)",
f"{model} does not support system prompts; removing it automatically. This may affect persona settings.",
)
system_instruction = None
elif "Function calling is not enabled" in e.message:
logger.warning(f"{model} 不支持函数调用,已自动去除")
logger.warning(
f"{model} does not support function calling; removing tools automatically."
)
tools = None
else:
raise
@@ -738,10 +722,10 @@ class ProviderGoogleGenAI(Provider):
llm_response = LLMResponse("assistant", is_chunk=True)
if not chunk.candidates:
logger.warning(f"收到的 chunk 中 candidates 为空: {chunk}")
logger.warning(f"Gemini stream chunk has empty candidates: {chunk}")
continue
if not chunk.candidates[0].content:
logger.warning(f"收到的 chunk 中 content 为空: {chunk}")
logger.warning(f"Gemini stream chunk has empty content: {chunk}")
continue
if chunk.candidates[0].content.parts and any(
@@ -872,7 +856,7 @@ class ProviderGoogleGenAI(Provider):
continue
break
raise Exception("请求失败。")
raise Exception("Gemini request failed.")
async def text_chat_stream(
self,
@@ -947,7 +931,7 @@ class ProviderGoogleGenAI(Provider):
and m.name
]
except APIError as e:
raise Exception(f"获取模型列表失败: {e.message}")
raise Exception(f"Failed to fetch Gemini model list: {e.message}")
def get_current_key(self) -> str:
return self.chosen_api_key
@@ -974,7 +958,7 @@ class ProviderGoogleGenAI(Provider):
media_type="image",
)
if not image_data:
logger.warning("图片预处理结果为空,将忽略。")
logger.warning("Image preprocessing returned no data; ignoring it.")
return None
return {
"type": "image_url",
@@ -989,11 +973,13 @@ class ProviderGoogleGenAI(Provider):
strict=True,
)
except Exception as exc:
logger.warning("音频预处理失败,将忽略。错误: %s", exc)
logger.warning(
"Audio preprocessing failed; ignoring it. Error: %s", exc
)
return None
if not audio_data:
logger.warning("音频预处理结果为空,将忽略。")
logger.warning("Audio preprocessing returned no data; ignoring it.")
return None
return {
"type": "audio_url",
@@ -1029,7 +1015,9 @@ class ProviderGoogleGenAI(Provider):
if audio_part:
content_blocks.append(audio_part)
else:
raise ValueError(f"不支持的额外内容块类型: {type(part)}")
raise ValueError(
f"Unsupported extra content part type: {type(part)}"
)
# 3. 图片内容
if image_urls: