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.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- 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
|
+
}
|