AstrBot 3.5.6__py3-none-any.whl → 4.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. astrbot/api/__init__.py +16 -4
  2. astrbot/api/all.py +2 -1
  3. astrbot/api/event/__init__.py +5 -6
  4. astrbot/api/event/filter/__init__.py +37 -34
  5. astrbot/api/platform/__init__.py +7 -8
  6. astrbot/api/provider/__init__.py +8 -7
  7. astrbot/api/star/__init__.py +3 -4
  8. astrbot/api/util/__init__.py +2 -2
  9. astrbot/cli/__init__.py +1 -0
  10. astrbot/cli/__main__.py +18 -197
  11. astrbot/cli/commands/__init__.py +6 -0
  12. astrbot/cli/commands/cmd_conf.py +209 -0
  13. astrbot/cli/commands/cmd_init.py +56 -0
  14. astrbot/cli/commands/cmd_plug.py +245 -0
  15. astrbot/cli/commands/cmd_run.py +62 -0
  16. astrbot/cli/utils/__init__.py +18 -0
  17. astrbot/cli/utils/basic.py +76 -0
  18. astrbot/cli/utils/plugin.py +246 -0
  19. astrbot/cli/utils/version_comparator.py +90 -0
  20. astrbot/core/__init__.py +17 -19
  21. astrbot/core/agent/agent.py +14 -0
  22. astrbot/core/agent/handoff.py +38 -0
  23. astrbot/core/agent/hooks.py +30 -0
  24. astrbot/core/agent/mcp_client.py +385 -0
  25. astrbot/core/agent/message.py +175 -0
  26. astrbot/core/agent/response.py +14 -0
  27. astrbot/core/agent/run_context.py +22 -0
  28. astrbot/core/agent/runners/__init__.py +3 -0
  29. astrbot/core/agent/runners/base.py +65 -0
  30. astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
  31. astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
  32. astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
  33. astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
  34. astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
  35. astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
  36. astrbot/core/agent/tool.py +285 -0
  37. astrbot/core/agent/tool_executor.py +17 -0
  38. astrbot/core/astr_agent_context.py +19 -0
  39. astrbot/core/astr_agent_hooks.py +36 -0
  40. astrbot/core/astr_agent_run_util.py +80 -0
  41. astrbot/core/astr_agent_tool_exec.py +246 -0
  42. astrbot/core/astrbot_config_mgr.py +275 -0
  43. astrbot/core/config/__init__.py +2 -2
  44. astrbot/core/config/astrbot_config.py +60 -20
  45. astrbot/core/config/default.py +1972 -453
  46. astrbot/core/config/i18n_utils.py +110 -0
  47. astrbot/core/conversation_mgr.py +285 -75
  48. astrbot/core/core_lifecycle.py +167 -62
  49. astrbot/core/db/__init__.py +305 -102
  50. astrbot/core/db/migration/helper.py +69 -0
  51. astrbot/core/db/migration/migra_3_to_4.py +357 -0
  52. astrbot/core/db/migration/migra_45_to_46.py +44 -0
  53. astrbot/core/db/migration/migra_webchat_session.py +131 -0
  54. astrbot/core/db/migration/shared_preferences_v3.py +48 -0
  55. astrbot/core/db/migration/sqlite_v3.py +497 -0
  56. astrbot/core/db/po.py +259 -55
  57. astrbot/core/db/sqlite.py +773 -528
  58. astrbot/core/db/vec_db/base.py +73 -0
  59. astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
  60. astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
  61. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
  62. astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
  63. astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
  64. astrbot/core/event_bus.py +26 -22
  65. astrbot/core/exceptions.py +9 -0
  66. astrbot/core/file_token_service.py +98 -0
  67. astrbot/core/initial_loader.py +19 -10
  68. astrbot/core/knowledge_base/chunking/__init__.py +9 -0
  69. astrbot/core/knowledge_base/chunking/base.py +25 -0
  70. astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
  71. astrbot/core/knowledge_base/chunking/recursive.py +161 -0
  72. astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
  73. astrbot/core/knowledge_base/kb_helper.py +642 -0
  74. astrbot/core/knowledge_base/kb_mgr.py +330 -0
  75. astrbot/core/knowledge_base/models.py +120 -0
  76. astrbot/core/knowledge_base/parsers/__init__.py +13 -0
  77. astrbot/core/knowledge_base/parsers/base.py +51 -0
  78. astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
  79. astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
  80. astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
  81. astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
  82. astrbot/core/knowledge_base/parsers/util.py +13 -0
  83. astrbot/core/knowledge_base/prompts.py +65 -0
  84. astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
  85. astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
  86. astrbot/core/knowledge_base/retrieval/manager.py +276 -0
  87. astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
  88. astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
  89. astrbot/core/log.py +21 -15
  90. astrbot/core/message/components.py +413 -287
  91. astrbot/core/message/message_event_result.py +35 -24
  92. astrbot/core/persona_mgr.py +192 -0
  93. astrbot/core/pipeline/__init__.py +14 -14
  94. astrbot/core/pipeline/content_safety_check/stage.py +13 -9
  95. astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
  96. astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +13 -14
  97. astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
  98. astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
  99. astrbot/core/pipeline/context.py +7 -1
  100. astrbot/core/pipeline/context_utils.py +107 -0
  101. astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
  102. astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
  103. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
  104. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
  105. astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
  106. astrbot/core/pipeline/process_stage/stage.py +21 -15
  107. astrbot/core/pipeline/process_stage/utils.py +125 -0
  108. astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
  109. astrbot/core/pipeline/respond/stage.py +142 -101
  110. astrbot/core/pipeline/result_decorate/stage.py +124 -57
  111. astrbot/core/pipeline/scheduler.py +21 -16
  112. astrbot/core/pipeline/session_status_check/stage.py +37 -0
  113. astrbot/core/pipeline/stage.py +11 -76
  114. astrbot/core/pipeline/waking_check/stage.py +69 -33
  115. astrbot/core/pipeline/whitelist_check/stage.py +10 -7
  116. astrbot/core/platform/__init__.py +6 -6
  117. astrbot/core/platform/astr_message_event.py +107 -129
  118. astrbot/core/platform/astrbot_message.py +32 -12
  119. astrbot/core/platform/manager.py +62 -18
  120. astrbot/core/platform/message_session.py +30 -0
  121. astrbot/core/platform/platform.py +16 -24
  122. astrbot/core/platform/platform_metadata.py +9 -4
  123. astrbot/core/platform/register.py +12 -7
  124. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
  125. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
  126. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
  127. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
  128. astrbot/core/platform/sources/discord/client.py +129 -0
  129. astrbot/core/platform/sources/discord/components.py +139 -0
  130. astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
  131. astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
  132. astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
  133. astrbot/core/platform/sources/lark/lark_event.py +39 -13
  134. astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
  135. astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
  136. astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
  137. astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
  138. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
  139. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
  140. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
  141. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
  142. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +14 -8
  143. astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
  144. astrbot/core/platform/sources/satori/satori_event.py +432 -0
  145. astrbot/core/platform/sources/slack/client.py +164 -0
  146. astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
  147. astrbot/core/platform/sources/slack/slack_event.py +253 -0
  148. astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
  149. astrbot/core/platform/sources/telegram/tg_event.py +136 -36
  150. astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
  151. astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
  152. astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
  153. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
  154. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
  155. astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
  156. astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
  157. astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
  158. astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
  159. astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
  160. astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
  161. astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
  162. astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
  163. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
  164. astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
  165. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
  166. astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
  167. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
  168. astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
  169. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
  170. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
  171. astrbot/core/platform_message_history_mgr.py +49 -0
  172. astrbot/core/provider/__init__.py +2 -3
  173. astrbot/core/provider/entites.py +8 -8
  174. astrbot/core/provider/entities.py +154 -98
  175. astrbot/core/provider/func_tool_manager.py +446 -458
  176. astrbot/core/provider/manager.py +345 -207
  177. astrbot/core/provider/provider.py +188 -73
  178. astrbot/core/provider/register.py +9 -7
  179. astrbot/core/provider/sources/anthropic_source.py +295 -115
  180. astrbot/core/provider/sources/azure_tts_source.py +224 -0
  181. astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
  182. astrbot/core/provider/sources/dashscope_tts.py +138 -14
  183. astrbot/core/provider/sources/edge_tts_source.py +24 -19
  184. astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
  185. astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
  186. astrbot/core/provider/sources/gemini_source.py +310 -132
  187. astrbot/core/provider/sources/gemini_tts_source.py +81 -0
  188. astrbot/core/provider/sources/groq_source.py +15 -0
  189. astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
  190. astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
  191. astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
  192. astrbot/core/provider/sources/openai_embedding_source.py +40 -0
  193. astrbot/core/provider/sources/openai_source.py +241 -145
  194. astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
  195. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
  196. astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
  197. astrbot/core/provider/sources/volcengine_tts.py +115 -0
  198. astrbot/core/provider/sources/whisper_api_source.py +18 -13
  199. astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
  200. astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
  201. astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
  202. astrbot/core/provider/sources/zhipu_source.py +6 -73
  203. astrbot/core/star/__init__.py +43 -11
  204. astrbot/core/star/config.py +17 -18
  205. astrbot/core/star/context.py +362 -138
  206. astrbot/core/star/filter/__init__.py +4 -3
  207. astrbot/core/star/filter/command.py +111 -35
  208. astrbot/core/star/filter/command_group.py +46 -34
  209. astrbot/core/star/filter/custom_filter.py +6 -5
  210. astrbot/core/star/filter/event_message_type.py +4 -2
  211. astrbot/core/star/filter/permission.py +4 -2
  212. astrbot/core/star/filter/platform_adapter_type.py +45 -12
  213. astrbot/core/star/filter/regex.py +4 -2
  214. astrbot/core/star/register/__init__.py +19 -15
  215. astrbot/core/star/register/star.py +41 -13
  216. astrbot/core/star/register/star_handler.py +236 -86
  217. astrbot/core/star/session_llm_manager.py +280 -0
  218. astrbot/core/star/session_plugin_manager.py +170 -0
  219. astrbot/core/star/star.py +36 -43
  220. astrbot/core/star/star_handler.py +47 -85
  221. astrbot/core/star/star_manager.py +442 -260
  222. astrbot/core/star/star_tools.py +167 -45
  223. astrbot/core/star/updator.py +17 -20
  224. astrbot/core/umop_config_router.py +106 -0
  225. astrbot/core/updator.py +38 -13
  226. astrbot/core/utils/astrbot_path.py +39 -0
  227. astrbot/core/utils/command_parser.py +1 -1
  228. astrbot/core/utils/io.py +119 -60
  229. astrbot/core/utils/log_pipe.py +1 -1
  230. astrbot/core/utils/metrics.py +11 -10
  231. astrbot/core/utils/migra_helper.py +73 -0
  232. astrbot/core/utils/path_util.py +63 -62
  233. astrbot/core/utils/pip_installer.py +37 -15
  234. astrbot/core/utils/session_lock.py +29 -0
  235. astrbot/core/utils/session_waiter.py +19 -20
  236. astrbot/core/utils/shared_preferences.py +174 -34
  237. astrbot/core/utils/t2i/__init__.py +4 -1
  238. astrbot/core/utils/t2i/local_strategy.py +386 -238
  239. astrbot/core/utils/t2i/network_strategy.py +109 -49
  240. astrbot/core/utils/t2i/renderer.py +29 -14
  241. astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
  242. astrbot/core/utils/t2i/template_manager.py +111 -0
  243. astrbot/core/utils/tencent_record_helper.py +115 -1
  244. astrbot/core/utils/version_comparator.py +10 -13
  245. astrbot/core/zip_updator.py +112 -65
  246. astrbot/dashboard/routes/__init__.py +20 -13
  247. astrbot/dashboard/routes/auth.py +20 -9
  248. astrbot/dashboard/routes/chat.py +297 -141
  249. astrbot/dashboard/routes/config.py +652 -55
  250. astrbot/dashboard/routes/conversation.py +107 -37
  251. astrbot/dashboard/routes/file.py +26 -0
  252. astrbot/dashboard/routes/knowledge_base.py +1244 -0
  253. astrbot/dashboard/routes/log.py +27 -2
  254. astrbot/dashboard/routes/persona.py +202 -0
  255. astrbot/dashboard/routes/plugin.py +197 -139
  256. astrbot/dashboard/routes/route.py +27 -7
  257. astrbot/dashboard/routes/session_management.py +354 -0
  258. astrbot/dashboard/routes/stat.py +85 -18
  259. astrbot/dashboard/routes/static_file.py +5 -2
  260. astrbot/dashboard/routes/t2i.py +233 -0
  261. astrbot/dashboard/routes/tools.py +184 -120
  262. astrbot/dashboard/routes/update.py +59 -36
  263. astrbot/dashboard/server.py +96 -36
  264. astrbot/dashboard/utils.py +165 -0
  265. astrbot-4.7.0.dist-info/METADATA +294 -0
  266. astrbot-4.7.0.dist-info/RECORD +274 -0
  267. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
  268. astrbot/core/db/plugin/sqlite_impl.py +0 -112
  269. astrbot/core/db/sqlite_init.sql +0 -50
  270. astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
  271. astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
  272. astrbot/core/platform/sources/gewechat/client.py +0 -806
  273. astrbot/core/platform/sources/gewechat/downloader.py +0 -55
  274. astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
  275. astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
  276. astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
  277. astrbot/core/provider/sources/dashscope_source.py +0 -203
  278. astrbot/core/provider/sources/dify_source.py +0 -281
  279. astrbot/core/provider/sources/llmtuner_source.py +0 -132
  280. astrbot/core/rag/embedding/openai_source.py +0 -20
  281. astrbot/core/rag/knowledge_db_mgr.py +0 -94
  282. astrbot/core/rag/store/__init__.py +0 -9
  283. astrbot/core/rag/store/chroma_db.py +0 -42
  284. astrbot/core/utils/dify_api_client.py +0 -152
  285. astrbot-3.5.6.dist-info/METADATA +0 -249
  286. astrbot-3.5.6.dist-info/RECORD +0 -158
  287. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
  288. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,7 +3,6 @@ import base64
3
3
  import json
4
4
  import logging
5
5
  import random
6
- from typing import Dict, List, Optional
7
6
  from collections.abc import AsyncGenerator
8
7
 
9
8
  from google import genai
@@ -12,11 +11,10 @@ from google.genai.errors import APIError
12
11
 
13
12
  import astrbot.core.message.components as Comp
14
13
  from astrbot import logger
15
- from astrbot.api.provider import Personality, Provider
16
- from astrbot.core.db import BaseDatabase
14
+ from astrbot.api.provider import Provider
17
15
  from astrbot.core.message.message_event_result import MessageChain
18
16
  from astrbot.core.provider.entities import LLMResponse
19
- from astrbot.core.provider.func_tool_manager import FuncCall
17
+ from astrbot.core.provider.func_tool_manager import ToolSet
20
18
  from astrbot.core.utils.io import download_image_by_url
21
19
 
22
20
  from ..register import register_provider_adapter
@@ -33,7 +31,8 @@ logging.getLogger("google_genai.types").addFilter(SuppressNonTextPartsWarning())
33
31
 
34
32
 
35
33
  @register_provider_adapter(
36
- "googlegenai_chat_completion", "Google Gemini Chat Completion 提供商适配器"
34
+ "googlegenai_chat_completion",
35
+ "Google Gemini Chat Completion 提供商适配器",
37
36
  )
38
37
  class ProviderGoogleGenAI(Provider):
39
38
  CATEGORY_MAPPING = {
@@ -52,24 +51,18 @@ class ProviderGoogleGenAI(Provider):
52
51
 
53
52
  def __init__(
54
53
  self,
55
- provider_config: dict,
56
- provider_settings: dict,
57
- db_helper: BaseDatabase,
58
- persistant_history=True,
59
- default_persona: Personality = None,
54
+ provider_config,
55
+ provider_settings,
60
56
  ) -> None:
61
57
  super().__init__(
62
58
  provider_config,
63
59
  provider_settings,
64
- persistant_history,
65
- db_helper,
66
- default_persona,
67
60
  )
68
- self.api_keys: List = provider_config.get("key", [])
69
- self.chosen_api_key: str = self.api_keys[0] if len(self.api_keys) > 0 else None
61
+ self.api_keys: list = super().get_keys()
62
+ self.chosen_api_key: str = self.api_keys[0] if len(self.api_keys) > 0 else ""
70
63
  self.timeout: int = int(provider_config.get("timeout", 180))
71
64
 
72
- self.api_base: Optional[str] = provider_config.get("api_base", None)
65
+ self.api_base: str | None = provider_config.get("api_base", None)
73
66
  if self.api_base and self.api_base.endswith("/"):
74
67
  self.api_base = self.api_base[:-1]
75
68
 
@@ -92,41 +85,43 @@ class ProviderGoogleGenAI(Provider):
92
85
  user_safety_config = self.provider_config.get("gm_safety_settings", {})
93
86
  self.safety_settings = [
94
87
  types.SafetySetting(
95
- category=harm_category, threshold=self.THRESHOLD_MAPPING[threshold_str]
88
+ category=harm_category,
89
+ threshold=self.THRESHOLD_MAPPING[threshold_str],
96
90
  )
97
91
  for config_key, harm_category in self.CATEGORY_MAPPING.items()
98
92
  if (threshold_str := user_safety_config.get(config_key))
99
93
  and threshold_str in self.THRESHOLD_MAPPING
100
94
  ]
101
95
 
102
- async def _handle_api_error(self, e: APIError, keys: List[str]) -> bool:
96
+ async def _handle_api_error(self, e: APIError, keys: list[str]) -> bool:
103
97
  """处理API错误,返回是否需要重试"""
98
+ if e.message is None:
99
+ e.message = ""
100
+
104
101
  if e.code == 429 or "API key not valid" in e.message:
105
102
  keys.remove(self.chosen_api_key)
106
103
  if len(keys) > 0:
107
104
  self.set_key(random.choice(keys))
108
105
  logger.info(
109
- f"检测到 Key 异常({e.message}),正在尝试更换 API Key 重试... 当前 Key: {self.chosen_api_key[:12]}..."
106
+ f"检测到 Key 异常({e.message}),正在尝试更换 API Key 重试... 当前 Key: {self.chosen_api_key[:12]}...",
110
107
  )
111
108
  await asyncio.sleep(1)
112
109
  return True
113
- else:
114
- logger.error(
115
- f"检测到 Key 异常({e.message}),且已没有可用的 Key。 当前 Key: {self.chosen_api_key[:12]}..."
116
- )
117
- raise Exception("达到了 Gemini 速率限制, 请稍后再试...")
118
- else:
119
110
  logger.error(
120
- f"发生了错误(gemini_source)。Provider 配置如下: {self.provider_config}"
111
+ f"检测到 Key 异常({e.message}),且已没有可用的 Key当前 Key: {self.chosen_api_key[:12]}...",
121
112
  )
122
- raise e
113
+ raise Exception("达到了 Gemini 速率限制, 请稍后再试...")
114
+ logger.error(
115
+ f"发生了错误(gemini_source)。Provider 配置如下: {self.provider_config}",
116
+ )
117
+ raise e
123
118
 
124
119
  async def _prepare_query_config(
125
120
  self,
126
121
  payloads: dict,
127
- tools: Optional[FuncCall] = None,
128
- system_instruction: Optional[str] = None,
129
- modalities: Optional[List[str]] = None,
122
+ tools: ToolSet | None = None,
123
+ system_instruction: str | None = None,
124
+ modalities: list[str] | None = None,
130
125
  temperature: float = 0.7,
131
126
  ) -> types.GenerateContentConfig:
132
127
  """准备查询配置"""
@@ -141,24 +136,66 @@ class ProviderGoogleGenAI(Provider):
141
136
  logger.warning("流式输出不支持图片模态,已自动降级为文本模态")
142
137
  modalities = ["Text"]
143
138
 
144
- tool_list = None
139
+ tool_list = []
140
+ model_name = self.get_model()
145
141
  native_coderunner = self.provider_config.get("gm_native_coderunner", False)
146
142
  native_search = self.provider_config.get("gm_native_search", False)
143
+ url_context = self.provider_config.get("gm_url_context", False)
144
+
145
+ if "gemini-2.5" in model_name:
146
+ if native_coderunner:
147
+ tool_list.append(types.Tool(code_execution=types.ToolCodeExecution()))
148
+ if native_search:
149
+ logger.warning("代码执行工具与搜索工具互斥,已忽略搜索工具")
150
+ if url_context:
151
+ logger.warning(
152
+ "代码执行工具与URL上下文工具互斥,已忽略URL上下文工具",
153
+ )
154
+ else:
155
+ if native_search:
156
+ tool_list.append(types.Tool(google_search=types.GoogleSearch()))
147
157
 
148
- if native_coderunner:
149
- tool_list = [types.Tool(code_execution=types.ToolCodeExecution())]
150
- if native_search:
151
- logger.warning("已启用代码执行工具,搜索工具将被忽略")
152
- if tools:
153
- logger.warning("已启用代码执行工具,函数工具将被忽略")
154
- elif native_search:
155
- tool_list = [types.Tool(google_search=types.GoogleSearch())]
156
- if tools:
157
- logger.warning("已启用搜索工具,函数工具将被忽略")
158
+ if url_context:
159
+ if hasattr(types, "UrlContext"):
160
+ tool_list.append(types.Tool(url_context=types.UrlContext()))
161
+ else:
162
+ logger.warning(
163
+ "当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包",
164
+ )
165
+
166
+ elif "gemini-2.0-lite" in model_name:
167
+ if native_coderunner or native_search or url_context:
168
+ logger.warning(
169
+ "gemini-2.0-lite 不支持代码执行、搜索工具和URL上下文,将忽略这些设置",
170
+ )
171
+ tool_list = None
172
+
173
+ else:
174
+ if native_coderunner:
175
+ tool_list.append(types.Tool(code_execution=types.ToolCodeExecution()))
176
+ if native_search:
177
+ logger.warning("代码执行工具与搜索工具互斥,已忽略搜索工具")
178
+ elif native_search:
179
+ tool_list.append(types.Tool(google_search=types.GoogleSearch()))
180
+
181
+ if url_context and not native_coderunner:
182
+ if hasattr(types, "UrlContext"):
183
+ tool_list.append(types.Tool(url_context=types.UrlContext()))
184
+ else:
185
+ logger.warning(
186
+ "当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包",
187
+ )
188
+
189
+ if not tool_list:
190
+ tool_list = None
191
+
192
+ if tools and tool_list:
193
+ logger.warning("已启用原生工具,函数工具将被忽略")
158
194
  elif tools and (func_desc := tools.get_func_desc_google_genai_style()):
159
195
  tool_list = [
160
- types.Tool(function_declarations=func_desc["function_declarations"])
196
+ types.Tool(function_declarations=func_desc["function_declarations"]),
161
197
  ]
198
+
162
199
  return types.GenerateContentConfig(
163
200
  system_instruction=system_instruction,
164
201
  temperature=temperature,
@@ -178,24 +215,28 @@ class ProviderGoogleGenAI(Provider):
178
215
  response_modalities=modalities,
179
216
  tools=tool_list,
180
217
  safety_settings=self.safety_settings if self.safety_settings else None,
181
- thinking_config=types.ThinkingConfig(
182
- thinking_budget=min(
183
- int(
184
- self.provider_config.get("gm_thinking_config", {}).get(
185
- "budget", 0
186
- )
218
+ thinking_config=(
219
+ types.ThinkingConfig(
220
+ thinking_budget=min(
221
+ int(
222
+ self.provider_config.get("gm_thinking_config", {}).get(
223
+ "budget",
224
+ 0,
225
+ ),
226
+ ),
227
+ 24576,
187
228
  ),
188
- 24576,
189
- ),
190
- )
191
- if "gemini-2.5-flash" in self.get_model()
192
- else None,
229
+ )
230
+ if "gemini-2.5-flash" in self.get_model()
231
+ and hasattr(types.ThinkingConfig, "thinking_budget")
232
+ else None
233
+ ),
193
234
  automatic_function_calling=types.AutomaticFunctionCallingConfig(
194
- disable=True
235
+ disable=True,
195
236
  ),
196
237
  )
197
238
 
198
- def _prepare_conversation(self, payloads: Dict) -> List[types.Content]:
239
+ def _prepare_conversation(self, payloads: dict) -> list[types.Content]:
199
240
  """准备 Gemini SDK 的 Content 列表"""
200
241
 
201
242
  def create_text_part(text: str) -> types.Part:
@@ -220,12 +261,12 @@ class ProviderGoogleGenAI(Provider):
220
261
  else:
221
262
  contents.append(content_cls(parts=part))
222
263
 
223
- gemini_contents: List[types.Content] = []
264
+ gemini_contents: list[types.Content] = []
224
265
  native_tool_enabled = any(
225
266
  [
226
267
  self.provider_config.get("gm_native_coderunner", False),
227
268
  self.provider_config.get("gm_native_search", False),
228
- ]
269
+ ],
229
270
  )
230
271
  for message in payloads["messages"]:
231
272
  role, content = message["role"], message.get("content")
@@ -233,9 +274,11 @@ class ProviderGoogleGenAI(Provider):
233
274
  if role == "user":
234
275
  if isinstance(content, list):
235
276
  parts = [
236
- types.Part.from_text(text=item["text"] or " ")
237
- if item["type"] == "text"
238
- else process_image_url(item["image_url"])
277
+ (
278
+ types.Part.from_text(text=item["text"] or " ")
279
+ if item["type"] == "text"
280
+ else process_image_url(item["image_url"])
281
+ )
239
282
  for item in content
240
283
  ]
241
284
  else:
@@ -247,19 +290,30 @@ class ProviderGoogleGenAI(Provider):
247
290
  parts = [types.Part.from_text(text=content)]
248
291
  append_or_extend(gemini_contents, parts, types.ModelContent)
249
292
  elif not native_tool_enabled and "tool_calls" in message:
250
- parts = [
251
- types.Part.from_function_call(
293
+ parts = []
294
+ for tool in message["tool_calls"]:
295
+ part = types.Part.from_function_call(
252
296
  name=tool["function"]["name"],
253
297
  args=json.loads(tool["function"]["arguments"]),
254
298
  )
255
- for tool in message["tool_calls"]
256
- ]
299
+ # we should set thought_signature back to part if exists
300
+ # for more info about thought_signature, see:
301
+ # https://ai.google.dev/gemini-api/docs/thought-signatures
302
+ if "extra_content" in tool and tool["extra_content"]:
303
+ ts_bs64 = (
304
+ tool["extra_content"]
305
+ .get("google", {})
306
+ .get("thought_signature")
307
+ )
308
+ if ts_bs64:
309
+ part.thought_signature = base64.b64decode(ts_bs64)
310
+ parts.append(part)
257
311
  append_or_extend(gemini_contents, parts, types.ModelContent)
258
312
  else:
259
313
  logger.warning("assistant 角色的消息内容为空,已添加空格占位")
260
314
  if native_tool_enabled and "tool_calls" in message:
261
315
  logger.warning(
262
- "检测到启用Gemini原生工具,且上下文中存在函数调用,建议使用 /reset 重置上下文"
316
+ "检测到启用Gemini原生工具,且上下文中存在函数调用,建议使用 /reset 重置上下文",
263
317
  )
264
318
  parts = [types.Part.from_text(text=" ")]
265
319
  append_or_extend(gemini_contents, parts, types.ModelContent)
@@ -272,7 +326,7 @@ class ProviderGoogleGenAI(Provider):
272
326
  "name": message["tool_call_id"],
273
327
  "content": message["content"],
274
328
  },
275
- )
329
+ ),
276
330
  ]
277
331
  append_or_extend(gemini_contents, parts, types.UserContent)
278
332
 
@@ -281,58 +335,94 @@ class ProviderGoogleGenAI(Provider):
281
335
 
282
336
  return gemini_contents
283
337
 
284
- @staticmethod
338
+ def _extract_reasoning_content(self, candidate: types.Candidate) -> str:
339
+ """Extract reasoning content from candidate parts"""
340
+ if not candidate.content or not candidate.content.parts:
341
+ return ""
342
+
343
+ thought_buf: list[str] = [
344
+ (p.text or "") for p in candidate.content.parts if p.thought
345
+ ]
346
+ return "".join(thought_buf).strip()
347
+
285
348
  def _process_content_parts(
286
- result: types.GenerateContentResponse, llm_response: LLMResponse
349
+ self,
350
+ candidate: types.Candidate,
351
+ llm_response: LLMResponse,
287
352
  ) -> MessageChain:
288
353
  """处理内容部分并构建消息链"""
289
- finish_reason = result.candidates[0].finish_reason
290
- result_parts: Optional[types.Part] = result.candidates[0].content.parts
354
+ if not candidate.content:
355
+ logger.warning(f"收到的 candidate.content 为空: {candidate}")
356
+ raise Exception("API 返回的 candidate.content 为空。")
357
+
358
+ finish_reason = candidate.finish_reason
359
+ result_parts: list[types.Part] | None = candidate.content.parts
291
360
 
292
361
  if finish_reason == types.FinishReason.SAFETY:
293
- raise Exception("模型生成内容未通过用户定义的内容安全检查")
362
+ raise Exception("模型生成内容未通过 Gemini 平台的安全检查")
294
363
 
295
364
  if finish_reason in {
296
365
  types.FinishReason.PROHIBITED_CONTENT,
297
366
  types.FinishReason.SPII,
298
367
  types.FinishReason.BLOCKLIST,
299
368
  }:
300
- raise Exception("模型生成内容违反Gemini平台政策")
369
+ raise Exception("模型生成内容违反 Gemini 平台政策")
301
370
 
302
371
  # 防止旧版本SDK不存在IMAGE_SAFETY
303
372
  if hasattr(types.FinishReason, "IMAGE_SAFETY"):
304
373
  if finish_reason == types.FinishReason.IMAGE_SAFETY:
305
- raise Exception("模型生成内容违反Gemini平台政策")
374
+ raise Exception("模型生成内容违反 Gemini 平台政策")
306
375
 
307
376
  if not result_parts:
308
- logger.debug(result.candidates)
309
- raise Exception("API 返回的内容为空。")
377
+ logger.warning(f"收到的 candidate.content.parts 为空: {candidate}")
378
+ raise Exception("API 返回的 candidate.content.parts 为空。")
379
+
380
+ # 提取 reasoning content
381
+ reasoning = self._extract_reasoning_content(candidate)
382
+ if reasoning:
383
+ llm_response.reasoning_content = reasoning
310
384
 
311
385
  chain = []
312
386
  part: types.Part
313
387
 
314
388
  # 暂时这样Fallback
315
389
  if all(
316
- part.inline_data and part.inline_data.mime_type.startswith("image/")
390
+ part.inline_data
391
+ and part.inline_data.mime_type
392
+ and part.inline_data.mime_type.startswith("image/")
317
393
  for part in result_parts
318
394
  ):
319
395
  chain.append(Comp.Plain("这是图片"))
320
396
  for part in result_parts:
321
397
  if part.text:
322
398
  chain.append(Comp.Plain(part.text))
323
- elif part.function_call:
399
+ elif (
400
+ part.function_call
401
+ and part.function_call.name is not None
402
+ and part.function_call.args is not None
403
+ ):
324
404
  llm_response.role = "tool"
325
405
  llm_response.tools_call_name.append(part.function_call.name)
326
406
  llm_response.tools_call_args.append(part.function_call.args)
327
- # gemini 返回的 function_call.id 可能为 None
328
- llm_response.tools_call_ids.append(
329
- part.function_call.id or part.function_call.name
330
- )
331
- elif part.inline_data and part.inline_data.mime_type.startswith("image/"):
407
+ # function_call.id might be None, use name as fallback
408
+ tool_call_id = part.function_call.id or part.function_call.name
409
+ llm_response.tools_call_ids.append(tool_call_id)
410
+ # extra_content
411
+ if part.thought_signature:
412
+ ts_bs64 = base64.b64encode(part.thought_signature).decode("utf-8")
413
+ llm_response.tools_call_extra_content[tool_call_id] = {
414
+ "google": {"thought_signature": ts_bs64}
415
+ }
416
+ elif (
417
+ part.inline_data
418
+ and part.inline_data.mime_type
419
+ and part.inline_data.mime_type.startswith("image/")
420
+ and part.inline_data.data
421
+ ):
332
422
  chain.append(Comp.Image.fromBytes(part.inline_data.data))
333
423
  return MessageChain(chain=chain)
334
424
 
335
- async def _query(self, payloads: dict, tools: FuncCall) -> LLMResponse:
425
+ async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
336
426
  """非流式请求 Gemini API"""
337
427
  system_instruction = next(
338
428
  (msg["content"] for msg in payloads["messages"] if msg["role"] == "system"),
@@ -346,33 +436,44 @@ class ProviderGoogleGenAI(Provider):
346
436
  conversation = self._prepare_conversation(payloads)
347
437
  temperature = payloads.get("temperature", 0.7)
348
438
 
349
- result: Optional[types.GenerateContentResponse] = None
439
+ result: types.GenerateContentResponse | None = None
350
440
  while True:
351
441
  try:
352
442
  config = await self._prepare_query_config(
353
- payloads, tools, system_instruction, modalities, temperature
443
+ payloads,
444
+ tools,
445
+ system_instruction,
446
+ modalities,
447
+ temperature,
354
448
  )
355
449
  result = await self.client.models.generate_content(
356
450
  model=self.get_model(),
357
451
  contents=conversation,
358
452
  config=config,
359
453
  )
454
+ logger.debug(f"genai result: {result}")
455
+
456
+ if not result.candidates:
457
+ logger.error(f"请求失败, 返回的 candidates 为空: {result}")
458
+ raise Exception("请求失败, 返回的 candidates 为空。")
360
459
 
361
460
  if result.candidates[0].finish_reason == types.FinishReason.RECITATION:
362
461
  if temperature > 2:
363
462
  raise Exception("温度参数已超过最大值2,仍然发生recitation")
364
463
  temperature += 0.2
365
464
  logger.warning(
366
- f"发生了recitation,正在提高温度至{temperature:.1f}重试..."
465
+ f"发生了recitation,正在提高温度至{temperature:.1f}重试...",
367
466
  )
368
467
  continue
369
468
 
370
469
  break
371
470
 
372
471
  except APIError as e:
472
+ if e.message is None:
473
+ e.message = ""
373
474
  if "Developer instruction is not enabled" in e.message:
374
475
  logger.warning(
375
- f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)"
476
+ f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)",
376
477
  )
377
478
  system_instruction = None
378
479
  elif "Function calling is not enabled" in e.message:
@@ -385,7 +486,7 @@ class ProviderGoogleGenAI(Provider):
385
486
  or "only supports text output" in e.message
386
487
  ):
387
488
  logger.warning(
388
- f"{self.get_model()} 不支持多模态输出,降级为文本模态"
489
+ f"{self.get_model()} 不支持多模态输出,降级为文本模态",
389
490
  )
390
491
  modalities = ["Text"]
391
492
  else:
@@ -393,11 +494,17 @@ class ProviderGoogleGenAI(Provider):
393
494
  continue
394
495
 
395
496
  llm_response = LLMResponse("assistant")
396
- llm_response.result_chain = self._process_content_parts(result, llm_response)
497
+ llm_response.raw_completion = result
498
+ llm_response.result_chain = self._process_content_parts(
499
+ result.candidates[0],
500
+ llm_response,
501
+ )
397
502
  return llm_response
398
503
 
399
504
  async def _query_stream(
400
- self, payloads: dict, tools: FuncCall
505
+ self,
506
+ payloads: dict,
507
+ tools: ToolSet | None,
401
508
  ) -> AsyncGenerator[LLMResponse, None]:
402
509
  """流式请求 Gemini API"""
403
510
  system_instruction = next(
@@ -411,7 +518,9 @@ class ProviderGoogleGenAI(Provider):
411
518
  while True:
412
519
  try:
413
520
  config = await self._prepare_query_config(
414
- payloads, tools, system_instruction
521
+ payloads,
522
+ tools,
523
+ system_instruction,
415
524
  )
416
525
  result = await self.client.models.generate_content_stream(
417
526
  model=self.get_model(),
@@ -420,9 +529,11 @@ class ProviderGoogleGenAI(Provider):
420
529
  )
421
530
  break
422
531
  except APIError as e:
532
+ if e.message is None:
533
+ e.message = ""
423
534
  if "Developer instruction is not enabled" in e.message:
424
535
  logger.warning(
425
- f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)"
536
+ f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)",
426
537
  )
427
538
  system_instruction = None
428
539
  elif "Function calling is not enabled" in e.message:
@@ -432,47 +543,98 @@ class ProviderGoogleGenAI(Provider):
432
543
  raise
433
544
  continue
434
545
 
546
+ # Accumulate the complete response text for the final response
547
+ accumulated_text = ""
548
+ accumulated_reasoning = ""
549
+ final_response = None
550
+
435
551
  async for chunk in result:
436
552
  llm_response = LLMResponse("assistant", is_chunk=True)
437
553
 
554
+ if not chunk.candidates:
555
+ logger.warning(f"收到的 chunk 中 candidates 为空: {chunk}")
556
+ continue
557
+ if not chunk.candidates[0].content:
558
+ logger.warning(f"收到的 chunk 中 content 为空: {chunk}")
559
+ continue
560
+
438
561
  if chunk.candidates[0].content.parts and any(
439
562
  part.function_call for part in chunk.candidates[0].content.parts
440
563
  ):
441
564
  llm_response = LLMResponse("assistant", is_chunk=False)
565
+ llm_response.raw_completion = chunk
442
566
  llm_response.result_chain = self._process_content_parts(
443
- chunk, llm_response
567
+ chunk.candidates[0],
568
+ llm_response,
444
569
  )
445
570
  yield llm_response
446
- break
571
+ return
447
572
 
573
+ _f = False
574
+
575
+ # 提取 reasoning content
576
+ reasoning = self._extract_reasoning_content(chunk.candidates[0])
577
+ if reasoning:
578
+ _f = True
579
+ accumulated_reasoning += reasoning
580
+ llm_response.reasoning_content = reasoning
448
581
  if chunk.text:
582
+ _f = True
583
+ accumulated_text += chunk.text
449
584
  llm_response.result_chain = MessageChain(chain=[Comp.Plain(chunk.text)])
585
+ if _f:
450
586
  yield llm_response
451
587
 
452
588
  if chunk.candidates[0].finish_reason:
453
- llm_response = LLMResponse("assistant", is_chunk=False)
454
- if not chunk.candidates[0].content.parts:
455
- llm_response.result_chain = MessageChain(chain=[Comp.Plain(" ")])
456
- else:
457
- llm_response.result_chain = self._process_content_parts(
458
- chunk, llm_response
589
+ # Process the final chunk for potential tool calls or other content
590
+ if chunk.candidates[0].content.parts:
591
+ final_response = LLMResponse("assistant", is_chunk=False)
592
+ final_response.raw_completion = chunk
593
+ final_response.result_chain = self._process_content_parts(
594
+ chunk.candidates[0],
595
+ final_response,
459
596
  )
460
- yield llm_response
461
597
  break
462
598
 
599
+ # Yield final complete response with accumulated text
600
+ if not final_response:
601
+ final_response = LLMResponse("assistant", is_chunk=False)
602
+
603
+ # Set the complete accumulated reasoning in the final response
604
+ if accumulated_reasoning:
605
+ final_response.reasoning_content = accumulated_reasoning
606
+
607
+ # Set the complete accumulated text in the final response
608
+ if accumulated_text:
609
+ final_response.result_chain = MessageChain(
610
+ chain=[Comp.Plain(accumulated_text)],
611
+ )
612
+ elif not final_response.result_chain:
613
+ # If no text was accumulated and no final response was set, provide empty space
614
+ final_response.result_chain = MessageChain(chain=[Comp.Plain(" ")])
615
+
616
+ yield final_response
617
+
463
618
  async def text_chat(
464
619
  self,
465
- prompt: str,
466
- session_id: str = None,
467
- image_urls: List[str] = None,
468
- func_tool: FuncCall = None,
469
- contexts=[],
620
+ prompt=None,
621
+ session_id=None,
622
+ image_urls=None,
623
+ func_tool=None,
624
+ contexts=None,
470
625
  system_prompt=None,
471
626
  tool_calls_result=None,
627
+ model=None,
472
628
  **kwargs,
473
629
  ) -> LLMResponse:
474
- new_record = await self.assemble_context(prompt, image_urls)
475
- context_query = [*contexts, new_record]
630
+ if contexts is None:
631
+ contexts = []
632
+ new_record = None
633
+ if prompt is not None:
634
+ new_record = await self.assemble_context(prompt, image_urls)
635
+ context_query = self._ensure_message_to_dicts(contexts)
636
+ if new_record:
637
+ context_query.append(new_record)
476
638
  if system_prompt:
477
639
  context_query.insert(0, {"role": "system", "content": system_prompt})
478
640
 
@@ -482,10 +644,14 @@ class ProviderGoogleGenAI(Provider):
482
644
 
483
645
  # tool calls result
484
646
  if tool_calls_result:
485
- context_query.extend(tool_calls_result.to_openai_messages())
647
+ if not isinstance(tool_calls_result, list):
648
+ context_query.extend(tool_calls_result.to_openai_messages())
649
+ else:
650
+ for tcr in tool_calls_result:
651
+ context_query.extend(tcr.to_openai_messages())
486
652
 
487
653
  model_config = self.provider_config.get("model_config", {})
488
- model_config["model"] = self.get_model()
654
+ model_config["model"] = model or self.get_model()
489
655
 
490
656
  payloads = {"messages": context_query, **model_config}
491
657
 
@@ -500,19 +666,28 @@ class ProviderGoogleGenAI(Provider):
500
666
  continue
501
667
  break
502
668
 
669
+ raise Exception("请求失败。")
670
+
503
671
  async def text_chat_stream(
504
672
  self,
505
- prompt: str,
506
- session_id: str = None,
507
- image_urls: List[str] = [],
508
- func_tool: FuncCall = None,
509
- contexts=[],
673
+ prompt=None,
674
+ session_id=None,
675
+ image_urls=None,
676
+ func_tool=None,
677
+ contexts=None,
510
678
  system_prompt=None,
511
679
  tool_calls_result=None,
680
+ model=None,
512
681
  **kwargs,
513
682
  ) -> AsyncGenerator[LLMResponse, None]:
514
- new_record = await self.assemble_context(prompt, image_urls)
515
- context_query = [*contexts, new_record]
683
+ if contexts is None:
684
+ contexts = []
685
+ new_record = None
686
+ if prompt is not None:
687
+ new_record = await self.assemble_context(prompt, image_urls)
688
+ context_query = self._ensure_message_to_dicts(contexts)
689
+ if new_record:
690
+ context_query.append(new_record)
516
691
  if system_prompt:
517
692
  context_query.insert(0, {"role": "system", "content": system_prompt})
518
693
 
@@ -522,10 +697,14 @@ class ProviderGoogleGenAI(Provider):
522
697
 
523
698
  # tool calls result
524
699
  if tool_calls_result:
525
- context_query.extend(tool_calls_result.to_openai_messages())
700
+ if not isinstance(tool_calls_result, list):
701
+ context_query.extend(tool_calls_result.to_openai_messages())
702
+ else:
703
+ for tcr in tool_calls_result:
704
+ context_query.extend(tcr.to_openai_messages())
526
705
 
527
706
  model_config = self.provider_config.get("model_config", {})
528
- model_config["model"] = self.get_model()
707
+ model_config["model"] = model or self.get_model()
529
708
 
530
709
  payloads = {"messages": context_query, **model_config}
531
710
 
@@ -548,7 +727,9 @@ class ProviderGoogleGenAI(Provider):
548
727
  return [
549
728
  m.name.replace("models/", "")
550
729
  for m in models
551
- if "generateContent" in m.supported_actions
730
+ if m.supported_actions
731
+ and "generateContent" in m.supported_actions
732
+ and m.name
552
733
  ]
553
734
  except APIError as e:
554
735
  raise Exception(f"获取模型列表失败: {e.message}")
@@ -556,17 +737,15 @@ class ProviderGoogleGenAI(Provider):
556
737
  def get_current_key(self) -> str:
557
738
  return self.chosen_api_key
558
739
 
559
- def get_keys(self) -> List[str]:
740
+ def get_keys(self) -> list[str]:
560
741
  return self.api_keys
561
742
 
562
743
  def set_key(self, key):
563
744
  self.chosen_api_key = key
564
745
  self._init_client()
565
746
 
566
- async def assemble_context(self, text: str, image_urls: List[str] = None):
567
- """
568
- 组装上下文。
569
- """
747
+ async def assemble_context(self, text: str, image_urls: list[str] | None = None):
748
+ """组装上下文。"""
570
749
  if image_urls:
571
750
  user_content = {
572
751
  "role": "user",
@@ -585,22 +764,21 @@ class ProviderGoogleGenAI(Provider):
585
764
  logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
586
765
  continue
587
766
  user_content["content"].append(
588
- {"type": "image_url", "image_url": {"url": image_data}}
767
+ {
768
+ "type": "image_url",
769
+ "image_url": {"url": image_data},
770
+ },
589
771
  )
590
772
  return user_content
591
- else:
592
- return {"role": "user", "content": text}
773
+ return {"role": "user", "content": text}
593
774
 
594
775
  async def encode_image_bs64(self, image_url: str) -> str:
595
- """
596
- 将图片转换为 base64
597
- """
776
+ """将图片转换为 base64"""
598
777
  if image_url.startswith("base64://"):
599
778
  return image_url.replace("base64://", "data:image/jpeg;base64,")
600
779
  with open(image_url, "rb") as f:
601
780
  image_bs64 = base64.b64encode(f.read()).decode("utf-8")
602
781
  return "data:image/jpeg;base64," + image_bs64
603
- return ""
604
782
 
605
783
  async def terminate(self):
606
784
  logger.info("Google GenAI 适配器已终止。")