aria-code 4.1.3__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 (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. workspace/verify.py +113 -0
@@ -0,0 +1,282 @@
1
+ """
2
+ providers/llm/openai_compat.py — OpenAI 兼容协议通用 Provider
3
+ ==============================================================
4
+ DeepSeek / OpenAI / Groq / Together / LM Studio / vLLM / llama.cpp
5
+ 全部走同一套 /v1/chat/completions SSE 协议。
6
+
7
+ 各 provider 只需继承并声明 DEFAULT_BASE_URL 和 DEFAULT_MODEL。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ import os
15
+ from typing import Any, AsyncIterator, Dict, List, Optional
16
+
17
+ from .base import BaseLLMProvider, Message, ProviderConfig
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class OpenAICompatProvider(BaseLLMProvider):
23
+ """
24
+ 通用 OpenAI 兼容 Provider。
25
+ 子类覆盖 DEFAULT_BASE_URL / DEFAULT_MODEL / provider_name。
26
+ """
27
+
28
+ provider_name = "openai_compat"
29
+ supports_tools = True
30
+ supports_thinking = False
31
+ local = False
32
+
33
+ DEFAULT_BASE_URL = "https://api.openai.com"
34
+ DEFAULT_MODEL = "gpt-4o-mini"
35
+
36
+ def __init__(self, config: ProviderConfig):
37
+ super().__init__(config)
38
+ self.base_url = (config.base_url or self.DEFAULT_BASE_URL).rstrip("/")
39
+ self.model = config.model or self.DEFAULT_MODEL
40
+ self.api_key = config.api_key or ""
41
+
42
+ async def is_available(self) -> bool:
43
+ return bool(self.api_key)
44
+
45
+ async def stream(
46
+ self,
47
+ messages: List[Message],
48
+ tools: Optional[List[Dict]] = None,
49
+ temperature: Optional[float] = None,
50
+ max_tokens: Optional[int] = None,
51
+ cancel_event=None,
52
+ ) -> AsyncIterator[Dict[str, Any]]:
53
+ import aiohttp
54
+
55
+ temp = temperature if temperature is not None else self.config.temperature
56
+ n_tokens = max_tokens if max_tokens is not None else self.config.max_tokens
57
+
58
+ headers = {
59
+ "Authorization": f"Bearer {self.api_key}",
60
+ "Content-Type": "application/json",
61
+ }
62
+ payload: Dict[str, Any] = {
63
+ "model": self.model,
64
+ "messages": [{"role": m.role, "content": m.content} for m in messages],
65
+ "temperature": temp,
66
+ "max_tokens": n_tokens,
67
+ "stream": True,
68
+ }
69
+ if tools:
70
+ payload["tools"] = [
71
+ {"type": "function", "function": t} for t in tools
72
+ ]
73
+ payload["tool_choice"] = "auto"
74
+
75
+ url = f"{self.base_url}/v1/chat/completions"
76
+ # aiohttp 不自动读系统代理,需显式传入
77
+ proxy = (os.getenv("HTTPS_PROXY") or os.getenv("https_proxy")
78
+ or os.getenv("HTTP_PROXY") or os.getenv("http_proxy"))
79
+
80
+ try:
81
+ async with aiohttp.ClientSession() as sess:
82
+ async with sess.post(
83
+ url, json=payload, headers=headers,
84
+ proxy=proxy,
85
+ timeout=aiohttp.ClientTimeout(total=self.config.timeout)
86
+ ) as resp:
87
+ if resp.status != 200:
88
+ body = await resp.text()
89
+ yield {"type": "error",
90
+ "message": f"HTTP {resp.status}: {body[:300]}"}
91
+ return
92
+
93
+ pending_tool: Dict = {}
94
+ async for raw in resp.content:
95
+ if cancel_event and cancel_event.is_set():
96
+ return
97
+
98
+ line = raw.decode("utf-8", errors="ignore").strip()
99
+ if not line or line == "data: [DONE]":
100
+ continue
101
+ if not line.startswith("data: "):
102
+ continue
103
+
104
+ try:
105
+ data = json.loads(line[6:])
106
+ except json.JSONDecodeError:
107
+ continue
108
+
109
+ # 处理 thinking tokens(DeepSeek-R1 等)
110
+ reasoning = (
111
+ ((data.get("choices") or [{}])[0]
112
+ .get("delta") or {})
113
+ .get("reasoning_content")
114
+ )
115
+ if reasoning:
116
+ yield {"type": "thinking", "text": reasoning}
117
+
118
+ # 普通 token
119
+ delta = ((data.get("choices") or [{}])[0].get("delta") or {})
120
+ token = delta.get("content") or ""
121
+ if token:
122
+ yield {"type": "token", "text": token}
123
+
124
+ # 工具调用(SSE 增量拼接)
125
+ for tc in delta.get("tool_calls") or []:
126
+ fn = tc.get("function") or {}
127
+ idx = tc.get("index", 0)
128
+ name = fn.get("name", "")
129
+ args_chunk = fn.get("arguments", "")
130
+
131
+ if name:
132
+ if pending_tool:
133
+ # emit previous tool
134
+ try:
135
+ args = json.loads(pending_tool["args"])
136
+ except Exception:
137
+ args = {"_raw": pending_tool["args"]}
138
+ yield {"type": "tool_call",
139
+ "name": pending_tool["name"],
140
+ "arguments": args}
141
+ pending_tool = {"name": name, "args": "", "idx": idx}
142
+ if args_chunk:
143
+ pending_tool["args"] = pending_tool.get("args", "") + args_chunk
144
+
145
+ # finish reason
146
+ finish = ((data.get("choices") or [{}])[0].get("finish_reason"))
147
+ if finish in ("stop", "tool_calls", "length"):
148
+ if pending_tool:
149
+ try:
150
+ args = json.loads(pending_tool["args"])
151
+ except Exception:
152
+ args = {"_raw": pending_tool["args"]}
153
+ yield {"type": "tool_call",
154
+ "name": pending_tool["name"],
155
+ "arguments": args}
156
+ pending_tool = {}
157
+ yield {"type": "done"}
158
+ return
159
+
160
+ except aiohttp.ClientConnectorError as e:
161
+ yield {"type": "error", "message": f"连接 {self.base_url} 失败: {e}"}
162
+ except Exception as e:
163
+ yield {"type": "error", "message": f"Provider 错误: {e}"}
164
+
165
+
166
+ # ── 具体 Provider 子类(只需声明几个属性)────────────────────────────────────
167
+
168
+ class DeepSeekProvider(OpenAICompatProvider):
169
+ provider_name = "deepseek"
170
+ DEFAULT_BASE_URL = "https://api.deepseek.com"
171
+ DEFAULT_MODEL = "deepseek-chat"
172
+ supports_thinking = True # deepseek-reasoner 支持
173
+
174
+ def __init__(self, config: ProviderConfig):
175
+ if not config.api_key:
176
+ config.api_key = os.getenv("DEEPSEEK_API_KEY", "")
177
+ super().__init__(config)
178
+ # 思考模型别名
179
+ if self.model in ("deepseek-reasoner", "deepseek-r1"):
180
+ self.supports_thinking = True
181
+
182
+
183
+ class OpenAIProvider(OpenAICompatProvider):
184
+ provider_name = "openai"
185
+ DEFAULT_BASE_URL = "https://api.openai.com"
186
+ DEFAULT_MODEL = "gpt-4o-mini"
187
+
188
+ def __init__(self, config: ProviderConfig):
189
+ if not config.api_key:
190
+ config.api_key = os.getenv("OPENAI_API_KEY", "")
191
+ super().__init__(config)
192
+
193
+
194
+ class GroqProvider(OpenAICompatProvider):
195
+ provider_name = "groq"
196
+ DEFAULT_BASE_URL = "https://api.groq.com/openai"
197
+ DEFAULT_MODEL = "llama-3.3-70b-versatile"
198
+
199
+ def __init__(self, config: ProviderConfig):
200
+ if not config.api_key:
201
+ config.api_key = os.getenv("GROQ_API_KEY", "")
202
+ super().__init__(config)
203
+
204
+
205
+ class TogetherProvider(OpenAICompatProvider):
206
+ provider_name = "together"
207
+ DEFAULT_BASE_URL = "https://api.together.xyz"
208
+ DEFAULT_MODEL = "meta-llama/Llama-3.3-70B-Instruct-Turbo"
209
+
210
+ def __init__(self, config: ProviderConfig):
211
+ if not config.api_key:
212
+ config.api_key = os.getenv("TOGETHER_API_KEY", "")
213
+ super().__init__(config)
214
+
215
+
216
+ class DashScopeProvider(OpenAICompatProvider):
217
+ """阿里云通义(OpenAI 兼容端点)"""
218
+ provider_name = "dashscope"
219
+ DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode"
220
+ DEFAULT_MODEL = "qwen-plus"
221
+
222
+ def __init__(self, config: ProviderConfig):
223
+ if not config.api_key:
224
+ config.api_key = os.getenv("DASHSCOPE_API_KEY", "")
225
+ super().__init__(config)
226
+
227
+
228
+ class LMStudioProvider(OpenAICompatProvider):
229
+ """LM Studio 本地服务"""
230
+ provider_name = "lmstudio"
231
+ DEFAULT_BASE_URL = "http://localhost:1234"
232
+ DEFAULT_MODEL = "loaded-model"
233
+ local = True
234
+
235
+ async def is_available(self) -> bool:
236
+ import urllib.request
237
+ try:
238
+ urllib.request.urlopen(
239
+ f"{self.base_url}/v1/models", timeout=2
240
+ ).close()
241
+ return True
242
+ except Exception:
243
+ return False
244
+
245
+
246
+ # ── 国内可访问 Provider(OpenAI 兼容协议)────────────────────────────────────
247
+
248
+ class SiliconFlowProvider(OpenAICompatProvider):
249
+ """硅基流动 — 中国大陆可直连,支持 DeepSeek-V3/R1、Qwen 等主流模型"""
250
+ provider_name = "siliconflow"
251
+ DEFAULT_BASE_URL = "https://api.siliconflow.cn"
252
+ DEFAULT_MODEL = "deepseek-ai/DeepSeek-V3"
253
+ supports_thinking = True # DeepSeek-R1 在此运行
254
+
255
+ def __init__(self, config: ProviderConfig):
256
+ if not config.api_key:
257
+ config.api_key = os.getenv("SILICONFLOW_API_KEY", "")
258
+ super().__init__(config)
259
+
260
+
261
+ class MoonshotProvider(OpenAICompatProvider):
262
+ """Moonshot / Kimi — 中国大陆可直连"""
263
+ provider_name = "moonshot"
264
+ DEFAULT_BASE_URL = "https://api.moonshot.cn/v1"
265
+ DEFAULT_MODEL = "moonshot-v1-8k"
266
+
267
+ def __init__(self, config: ProviderConfig):
268
+ if not config.api_key:
269
+ config.api_key = os.getenv("MOONSHOT_API_KEY", "")
270
+ super().__init__(config)
271
+
272
+
273
+ class ZhiPuProvider(OpenAICompatProvider):
274
+ """智谱 GLM — 中国大陆可直连"""
275
+ provider_name = "zhipu"
276
+ DEFAULT_BASE_URL = "https://open.bigmodel.cn/api/paas/v4"
277
+ DEFAULT_MODEL = "glm-4-flash"
278
+
279
+ def __init__(self, config: ProviderConfig):
280
+ if not config.api_key:
281
+ config.api_key = os.getenv("ZHIPUAI_API_KEY", "")
282
+ super().__init__(config)
@@ -0,0 +1,358 @@
1
+ """
2
+ providers/llm/registry.py — LLM Provider 注册中心
3
+ ==================================================
4
+ • 从 ~/.aria/providers.yaml 或 .aria.json 加载用户配置
5
+ • 按优先级自动路由:本地 Ollama → DeepSeek → OpenAI → Anthropic → Groq
6
+ • 提供 stream_cloud_fallback() 供 aria_cli.py 调用
7
+
8
+ 用户配置示例 (~/.aria/providers.yaml):
9
+ llm:
10
+ default: ollama/qwen2.5:7b
11
+ fallback:
12
+ - deepseek/deepseek-chat
13
+ - openai/gpt-4o-mini
14
+ - anthropic/claude-3-5-haiku-latest
15
+ code_tasks: ollama/qwen2.5-coder:7b
16
+ heavy_analysis: anthropic/claude-3-5-sonnet-20241022
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import logging
23
+ import os
24
+ from pathlib import Path
25
+ from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Tuple, Type
26
+
27
+ import yaml
28
+
29
+ from .base import BaseLLMProvider, Message, ProviderConfig
30
+ from .ollama import OllamaProvider
31
+ from .openai_compat import (
32
+ DeepSeekProvider, OpenAIProvider, GroqProvider,
33
+ TogetherProvider, DashScopeProvider, LMStudioProvider,
34
+ SiliconFlowProvider, MoonshotProvider, ZhiPuProvider,
35
+ )
36
+ from .anthropic import AnthropicProvider
37
+ from packages.aria_services.provider_health import (
38
+ GLOBAL_PROVIDER_HEALTH,
39
+ ProviderHealthRegistry,
40
+ classify_provider_error,
41
+ )
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ # ── Provider 目录:name → class ──────────────────────────────────────────────
46
+ _PROVIDER_CLASSES: Dict[str, Type[BaseLLMProvider]] = {
47
+ "ollama": OllamaProvider,
48
+ "deepseek": DeepSeekProvider,
49
+ "openai": OpenAIProvider,
50
+ "anthropic": AnthropicProvider,
51
+ "groq": GroqProvider,
52
+ "together": TogetherProvider,
53
+ "dashscope": DashScopeProvider,
54
+ "lmstudio": LMStudioProvider,
55
+ # 国内可访问
56
+ "siliconflow": SiliconFlowProvider,
57
+ "moonshot": MoonshotProvider,
58
+ "zhipu": ZhiPuProvider,
59
+ }
60
+
61
+ # ── 默认 fallback 优先级(无用户配置时)────────────────────────────────────
62
+ # 国内环境优先走 DeepSeek / SiliconFlow / DashScope,再尝试 OpenAI / Groq
63
+ _DEFAULT_FALLBACK_CHAIN = [
64
+ ("ollama", None, None),
65
+ ("deepseek", "DEEPSEEK_API_KEY", "deepseek-chat"),
66
+ ("siliconflow", "SILICONFLOW_API_KEY", "deepseek-ai/DeepSeek-V3"),
67
+ ("dashscope", "DASHSCOPE_API_KEY", "qwen-plus"),
68
+ ("moonshot", "MOONSHOT_API_KEY", "moonshot-v1-8k"),
69
+ ("zhipu", "ZHIPUAI_API_KEY", "glm-4-flash"),
70
+ ("openai", "OPENAI_API_KEY", "gpt-4o-mini"),
71
+ ("anthropic", "ANTHROPIC_API_KEY", "claude-3-5-haiku-latest"),
72
+ ("groq", "GROQ_API_KEY", "llama-3.3-70b-versatile"),
73
+ ]
74
+
75
+ # ── 用户可注册自定义 provider ─────────────────────────────────────────────────
76
+ def register_provider(name: str, cls: Type[BaseLLMProvider]) -> None:
77
+ """注册自定义 provider 类(供插件/用户扩展使用)"""
78
+ _PROVIDER_CLASSES[name.lower()] = cls
79
+ logger.info(f"✓ 注册自定义 provider: {name}")
80
+
81
+
82
+ # ── 配置加载 ──────────────────────────────────────────────────────────────────
83
+ _CONFIG_PATHS = [
84
+ # ~/.arthera/providers.json is the primary path used by the aria-code CLI (/apikey command)
85
+ Path.home() / ".arthera" / "providers.json",
86
+ # Legacy / alternative paths
87
+ Path.home() / ".aria" / "providers.yaml",
88
+ Path.home() / ".aria" / "providers.json",
89
+ Path(".aria.json"),
90
+ Path(".aria.yaml"),
91
+ ]
92
+
93
+ def _load_user_config() -> Dict:
94
+ for p in _CONFIG_PATHS:
95
+ if p.exists():
96
+ try:
97
+ with open(p, encoding="utf-8") as f:
98
+ data = yaml.safe_load(f) if p.suffix in (".yaml",".yml") \
99
+ else __import__("json").load(f)
100
+ return data.get("llm", data) if isinstance(data, dict) else {}
101
+ except Exception as e:
102
+ logger.debug(f"加载配置 {p} 失败: {e}")
103
+ return {}
104
+
105
+
106
+ def _load_provider_cfg_from_file(name: str) -> Dict[str, str]:
107
+ """
108
+ 从 ~/.arthera/providers.json 的 llm 节读取指定 provider 的 api_key / base_url。
109
+ 这是 /apikey set 命令写入的位置;ProviderConfig.from_env() 只读环境变量,
110
+ 此函数补足文件侧的配置,让两者合并后才能正确工作。
111
+ """
112
+ import json as _json
113
+ primary = Path.home() / ".arthera" / "providers.json"
114
+ for p in [primary] + _CONFIG_PATHS:
115
+ if not p.exists():
116
+ continue
117
+ try:
118
+ raw = _json.loads(p.read_text(encoding="utf-8")) if p.suffix == ".json" \
119
+ else yaml.safe_load(p.read_text(encoding="utf-8"))
120
+ llm = raw.get("llm", raw) if isinstance(raw, dict) else {}
121
+ entry = llm.get(name.lower(), {})
122
+ if entry:
123
+ return {k: v for k, v in entry.items() if v}
124
+ except Exception:
125
+ pass
126
+ return {}
127
+
128
+
129
+ def _parse_provider_spec(spec: str) -> Tuple[str, Optional[str]]:
130
+ """
131
+ 解析 'deepseek/deepseek-chat' → ('deepseek', 'deepseek-chat')
132
+ 解析 'ollama' → ('ollama', None)
133
+ """
134
+ if "/" in spec:
135
+ name, model = spec.split("/", 1)
136
+ return name.strip().lower(), model.strip()
137
+ return spec.strip().lower(), None
138
+
139
+
140
+ def get_provider(
141
+ spec: str,
142
+ api_key: Optional[str] = None,
143
+ base_url: Optional[str] = None,
144
+ ) -> BaseLLMProvider:
145
+ """
146
+ 按 spec 字符串实例化 provider。
147
+
148
+ Examples:
149
+ get_provider("ollama/qwen2.5:7b")
150
+ get_provider("deepseek/deepseek-chat")
151
+ get_provider("anthropic/claude-3-5-haiku-latest")
152
+ """
153
+ name, model = _parse_provider_spec(spec)
154
+ cls = _PROVIDER_CLASSES.get(name)
155
+ if not cls:
156
+ raise ValueError(
157
+ f"未知 provider: '{name}'。"
158
+ f"可用: {', '.join(_PROVIDER_CLASSES)}"
159
+ )
160
+ cfg = _build_cfg(name, model)
161
+ # 调用方显式传入的参数优先级最高
162
+ if api_key:
163
+ cfg.api_key = api_key
164
+ if base_url:
165
+ cfg.base_url = base_url
166
+ return cls(cfg)
167
+
168
+
169
+ def list_available_providers() -> List[Dict[str, Any]]:
170
+ """返回所有 provider 及其可用状态(同步,用于 /config 命令显示)"""
171
+ result = []
172
+ for name, cls in _PROVIDER_CLASSES.items():
173
+ cfg = _build_cfg(name) # 合并环境变量 + providers.json
174
+ available = cfg.is_configured()
175
+ result.append({
176
+ "name": name,
177
+ "available": available,
178
+ "local": cls.local,
179
+ "tools": cls.supports_tools,
180
+ "thinking": cls.supports_thinking,
181
+ })
182
+ return result
183
+
184
+
185
+ def _build_cfg(name: str, model: Optional[str] = None) -> ProviderConfig:
186
+ """
187
+ 构建 ProviderConfig:环境变量优先,再回落到 providers.json,
188
+ 确保 /apikey set 保存的 key 能被实际使用。
189
+ """
190
+ cfg = ProviderConfig.from_env(name)
191
+ file_cfg = _load_provider_cfg_from_file(name)
192
+
193
+ # 补充 api_key(文件里的)— 环境变量已在 from_env() 中优先读取;
194
+ # 文件是后备:提示用户改用环境变量以避免明文存储 key。
195
+ if not cfg.api_key and file_cfg.get("api_key"):
196
+ cfg.api_key = file_cfg["api_key"]
197
+ _env_names = {
198
+ "deepseek": "DEEPSEEK_API_KEY", "openai": "OPENAI_API_KEY",
199
+ "anthropic": "ANTHROPIC_API_KEY", "groq": "GROQ_API_KEY",
200
+ "siliconflow": "SILICONFLOW_API_KEY", "moonshot": "MOONSHOT_API_KEY",
201
+ "zhipu": "ZHIPUAI_API_KEY", "dashscope": "DASHSCOPE_API_KEY",
202
+ }
203
+ if name.lower() in _env_names:
204
+ logger.warning(
205
+ "⚠ API key for '%s' loaded from ~/.arthera/providers.json (plaintext). "
206
+ "Migrate to env var: export %s=<key> then remove api_key from providers.json.",
207
+ name, _env_names[name.lower()],
208
+ )
209
+ # 补充 base_url(支持用户自定义端点 / 代理)
210
+ if not cfg.base_url and file_cfg.get("base_url"):
211
+ cfg.base_url = file_cfg["base_url"]
212
+
213
+ if model:
214
+ cfg.model = model
215
+ return cfg
216
+
217
+
218
+ async def _try_provider(
219
+ spec: str,
220
+ messages: List[Message],
221
+ on_token: Optional[Callable] = None,
222
+ cancel_event=None,
223
+ *,
224
+ health: ProviderHealthRegistry | None = None,
225
+ ) -> Optional[Dict[str, Any]]:
226
+ """尝试用指定 provider 完成对话,失败返回 None。"""
227
+ health = health or GLOBAL_PROVIDER_HEALTH
228
+ try:
229
+ name, model = _parse_provider_spec(spec)
230
+ cls = _PROVIDER_CLASSES.get(name)
231
+ if not cls:
232
+ return None
233
+
234
+ cfg = _build_cfg(name, model)
235
+ provider = cls(cfg)
236
+
237
+ if health.provider_in_cooldown(name):
238
+ logger.debug(f"[{name}] cooling down, skipped")
239
+ return None
240
+
241
+ if not await provider.is_available():
242
+ logger.debug(f"[{name}] 不可用,跳过")
243
+ health.mark_issue(classify_provider_error(name, "provider unavailable"))
244
+ return None
245
+
246
+ logger.info(f"[{name}] 尝试生成响应 (model={cfg.model})")
247
+ full_text = ""
248
+ async for event in provider.stream(
249
+ messages, cancel_event=cancel_event
250
+ ):
251
+ t = event.get("type")
252
+ if t == "token":
253
+ tok = event.get("text", "")
254
+ full_text += tok
255
+ if on_token:
256
+ on_token(tok)
257
+ elif t == "error":
258
+ err = event.get("message")
259
+ logger.warning(f"[{name}] 流式错误: {err}")
260
+ health.mark_issue(classify_provider_error(name, err))
261
+ return None
262
+ elif t == "done":
263
+ break
264
+
265
+ if not full_text.strip():
266
+ health.mark_issue(classify_provider_error(name, "provider returned no usable data"))
267
+ return None
268
+
269
+ health.mark_success(name)
270
+ return {
271
+ "success": True,
272
+ "response": full_text,
273
+ "provider": name,
274
+ "model": cfg.model or "unknown",
275
+ }
276
+ except Exception as e:
277
+ logger.debug(f"[{spec}] 异常: {e}")
278
+ health.mark_issue(classify_provider_error(_parse_provider_spec(spec)[0], e))
279
+ return None
280
+
281
+
282
+ async def stream_cloud_fallback(
283
+ message: str,
284
+ history: List[Dict],
285
+ on_token: Optional[Callable] = None,
286
+ cancel_event=None,
287
+ *,
288
+ health: ProviderHealthRegistry | None = None,
289
+ ) -> Dict[str, Any]:
290
+ """
291
+ CLI fallback 入口:当 Ollama 不可用时调用。
292
+ 按优先级依次尝试云端 provider,首个成功的直接返回。
293
+
294
+ 优先级:
295
+ 1. 用户 ~/.aria/providers.yaml 里的 fallback 列表
296
+ 2. 内置默认链: DeepSeek → OpenAI → Anthropic → Groq → DashScope
297
+ """
298
+ # 构建消息列表
299
+ msgs: List[Message] = [
300
+ Message(role="system", content=(
301
+ "You are Aria, an AI-native quantitative investment assistant. "
302
+ "Answer concisely and accurately. If asked about real-time data "
303
+ "you cannot access, say so clearly."
304
+ ))
305
+ ]
306
+ for h in (history or [])[-12:]:
307
+ role = h.get("role", "user")
308
+ if role in ("user", "assistant"):
309
+ msgs.append(Message(role=role, content=h.get("content", "")))
310
+ msgs.append(Message(role="user", content=message))
311
+
312
+ # 加载用户配置的 fallback 链
313
+ user_cfg = _load_user_config()
314
+ user_fallback: List[str] = user_cfg.get("fallback", [])
315
+
316
+ health = health or GLOBAL_PROVIDER_HEALTH
317
+
318
+ # 云端 provider 列表(跳过本地)
319
+ cloud_specs: List[str] = []
320
+ for spec in user_fallback:
321
+ name, _ = _parse_provider_spec(spec)
322
+ cls = _PROVIDER_CLASSES.get(name)
323
+ if cls and not cls.local and not health.provider_in_cooldown(name):
324
+ cloud_specs.append(spec)
325
+
326
+ # 补充内置默认链中未出现的
327
+ for name, env_var, model in _DEFAULT_FALLBACK_CHAIN:
328
+ cls = _PROVIDER_CLASSES.get(name)
329
+ if not cls or cls.local:
330
+ continue
331
+ spec = f"{name}/{model}" if model else name
332
+ if not any(s.startswith(name) for s in cloud_specs):
333
+ # 环境变量 OR providers.json 任一有 key 即可
334
+ has_key = (env_var and os.getenv(env_var)) or \
335
+ bool(_load_provider_cfg_from_file(name).get("api_key"))
336
+ if has_key and not health.provider_in_cooldown(name):
337
+ cloud_specs.append(spec)
338
+
339
+ if not cloud_specs:
340
+ return {
341
+ "success": False,
342
+ "error": "no_cloud_provider",
343
+ "response": "",
344
+ "provider": "none",
345
+ }
346
+
347
+ for spec in cloud_specs:
348
+ result = await _try_provider(spec, msgs, on_token=on_token,
349
+ cancel_event=cancel_event, health=health)
350
+ if result:
351
+ return result
352
+
353
+ return {
354
+ "success": False,
355
+ "error": "all_providers_failed",
356
+ "response": "",
357
+ "provider": "none",
358
+ }