hyperagents 1.2.3rc5.post3__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 (361) hide show
  1. agents/__init__.py +31 -0
  2. agents/background_monitor.py +453 -0
  3. agents/learning.py +460 -0
  4. agents/planner.py +421 -0
  5. agents/transcriber.py +341 -0
  6. civilization/__init__.py +51 -0
  7. civilization/api/__init__.py +7 -0
  8. civilization/api/civilization_api.py +361 -0
  9. civilization/graph/__init__.py +19 -0
  10. civilization/graph/knowledge_linker.py +348 -0
  11. civilization/graph/org_graph.py +348 -0
  12. civilization/graph/process_graph.py +386 -0
  13. civilization/ingestion/__init__.py +20 -0
  14. civilization/ingestion/chunker.py +273 -0
  15. civilization/ingestion/document_ingestor.py +358 -0
  16. civilization/ingestion/embedder.py +262 -0
  17. civilization/ingestion/extractor.py +224 -0
  18. civilization/interview/__init__.py +19 -0
  19. civilization/interview/gap_detector.py +380 -0
  20. civilization/interview/interviewer.py +323 -0
  21. civilization/interview/questioner.py +363 -0
  22. civilization/retrieval/__init__.py +17 -0
  23. civilization/retrieval/civilization_rag.py +316 -0
  24. civilization/retrieval/context_injector.py +342 -0
  25. civilization/retrieval/relevance_ranker.py +287 -0
  26. civilization/schema.py +299 -0
  27. civilization/store.py +177 -0
  28. civilization/sync/__init__.py +16 -0
  29. civilization/sync/confluence_sync.py +317 -0
  30. civilization/sync/gdrive_sync.py +262 -0
  31. civilization/sync/notion_sync.py +237 -0
  32. civilization/versioning/__init__.py +19 -0
  33. civilization/versioning/diff_engine.py +351 -0
  34. civilization/versioning/staleness_detector.py +303 -0
  35. civilization/versioning/version_manager.py +293 -0
  36. cli/__init__.py +1 -0
  37. cli/doctor.py +334 -0
  38. cli/hyperclaw.py +1357 -0
  39. cli/onboarding.py +469 -0
  40. core/__init__.py +1 -0
  41. core/connectors/__init__.py +119 -0
  42. core/connectors/infrastructure.py +772 -0
  43. core/connectors/messaging.py +586 -0
  44. core/connectors/productivity.py +764 -0
  45. core/connectors/smarthome.py +528 -0
  46. core/connectors/social.py +730 -0
  47. core/hyperrouter/__init__.py +23 -0
  48. core/hyperrouter/bandit.py +218 -0
  49. core/hyperrouter/fast_loop.py +68 -0
  50. core/hyperrouter/slow_loop.py +143 -0
  51. core/hyperstate/__init__.py +24 -0
  52. core/hyperstate/certifier.py +154 -0
  53. core/hyperstate/schema.py +160 -0
  54. core/hyperstate/state_manager.py +103 -0
  55. core/hyperstate/store.py +163 -0
  56. core/providers/__init__.py +121 -0
  57. core/providers/embeddings.py +389 -0
  58. core/providers/image.py +534 -0
  59. core/providers/llm.py +1935 -0
  60. core/providers/search.py +608 -0
  61. core/providers/voice.py +474 -0
  62. hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
  63. hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
  64. hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
  65. hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
  66. hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
  67. hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
  68. hyperclaw/__init__.py +67 -0
  69. hyperclaw/__main__.py +220 -0
  70. hyperclaw/agent.py +775 -0
  71. hyperclaw/agent_coordinator.py +577 -0
  72. hyperclaw/anthropic_params.py +225 -0
  73. hyperclaw/api_utils.py +85 -0
  74. hyperclaw/approval_telegram.py +209 -0
  75. hyperclaw/approval_terminal.py +80 -0
  76. hyperclaw/attachments.py +177 -0
  77. hyperclaw/branding.py +55 -0
  78. hyperclaw/brief/__init__.py +11 -0
  79. hyperclaw/brief/cli.py +204 -0
  80. hyperclaw/brief/collect.py +146 -0
  81. hyperclaw/brief/compose.py +333 -0
  82. hyperclaw/brief/diff.py +110 -0
  83. hyperclaw/brief/errors.py +99 -0
  84. hyperclaw/brief/google.py +269 -0
  85. hyperclaw/brief/google_auth.py +294 -0
  86. hyperclaw/brief/links.py +124 -0
  87. hyperclaw/brief/service.py +424 -0
  88. hyperclaw/brief/store.py +205 -0
  89. hyperclaw/brief/tasks.py +200 -0
  90. hyperclaw/chat_scope.py +115 -0
  91. hyperclaw/cli_tui.py +497 -0
  92. hyperclaw/client_guard.py +717 -0
  93. hyperclaw/connection_check.py +113 -0
  94. hyperclaw/cost_guard.py +1728 -0
  95. hyperclaw/default_config/agents.yaml +534 -0
  96. hyperclaw/default_config/hyperclaw.yaml +27 -0
  97. hyperclaw/default_config/models.yaml +161 -0
  98. hyperclaw/deployment.py +39 -0
  99. hyperclaw/digest/__init__.py +9 -0
  100. hyperclaw/digest/cli.py +117 -0
  101. hyperclaw/digest/errors.py +100 -0
  102. hyperclaw/digest/gitlab.py +187 -0
  103. hyperclaw/digest/http.py +113 -0
  104. hyperclaw/digest/redact.py +32 -0
  105. hyperclaw/digest/service.py +271 -0
  106. hyperclaw/digest/slack.py +223 -0
  107. hyperclaw/digest/state.py +124 -0
  108. hyperclaw/digest/summarize.py +198 -0
  109. hyperclaw/digest/tokens.py +143 -0
  110. hyperclaw/distribution.py +46 -0
  111. hyperclaw/documents.py +351 -0
  112. hyperclaw/feeds.py +339 -0
  113. hyperclaw/google_workspace/__init__.py +5 -0
  114. hyperclaw/google_workspace/api.py +151 -0
  115. hyperclaw/google_workspace/auth.py +397 -0
  116. hyperclaw/google_workspace/calendar.py +157 -0
  117. hyperclaw/google_workspace/cli.py +72 -0
  118. hyperclaw/google_workspace/drive.py +236 -0
  119. hyperclaw/google_workspace/errors.py +58 -0
  120. hyperclaw/google_workspace/gmail.py +295 -0
  121. hyperclaw/google_workspace/meet.py +122 -0
  122. hyperclaw/google_workspace/tools.py +319 -0
  123. hyperclaw/hyperspeed.py +57 -0
  124. hyperclaw/integrations_layer.py +1427 -0
  125. hyperclaw/live_state.py +188 -0
  126. hyperclaw/managed_config.py +245 -0
  127. hyperclaw/media_hub.py +179 -0
  128. hyperclaw/memory_manager.py +584 -0
  129. hyperclaw/model_catalog.py +551 -0
  130. hyperclaw/model_policy.py +211 -0
  131. hyperclaw/model_router.py +595 -0
  132. hyperclaw/model_selector.py +72 -0
  133. hyperclaw/naming.py +284 -0
  134. hyperclaw/notices.py +65 -0
  135. hyperclaw/oai_stream.py +106 -0
  136. hyperclaw/orchestrator.py +591 -0
  137. hyperclaw/outbox.py +82 -0
  138. hyperclaw/persona.py +367 -0
  139. hyperclaw/prometheus.py +219 -0
  140. hyperclaw/prompt_assembly.py +157 -0
  141. hyperclaw/provider_setup.py +146 -0
  142. hyperclaw/providers.py +340 -0
  143. hyperclaw/qualification.py +341 -0
  144. hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
  145. hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
  146. hyperclaw/reasoning_display.py +35 -0
  147. hyperclaw/scheduler.py +454 -0
  148. hyperclaw/server.py +847 -0
  149. hyperclaw/setup.py +627 -0
  150. hyperclaw/skills.py +717 -0
  151. hyperclaw/solomon.py +442 -0
  152. hyperclaw/swarm.py +533 -0
  153. hyperclaw/taint.py +832 -0
  154. hyperclaw/team/__init__.py +28 -0
  155. hyperclaw/team/dispatcher.py +431 -0
  156. hyperclaw/team/genesis.py +106 -0
  157. hyperclaw/team/lead.py +110 -0
  158. hyperclaw/team/model.py +135 -0
  159. hyperclaw/team/morning.py +238 -0
  160. hyperclaw/team/repos.py +66 -0
  161. hyperclaw/team/roster.py +264 -0
  162. hyperclaw/team/specialist.py +235 -0
  163. hyperclaw/team/status.py +163 -0
  164. hyperclaw/team/watchers.py +112 -0
  165. hyperclaw/teamsetup/__init__.py +17 -0
  166. hyperclaw/teamsetup/checks.py +208 -0
  167. hyperclaw/teamsetup/item.py +391 -0
  168. hyperclaw/teamsetup/oauth_loopback.py +84 -0
  169. hyperclaw/teamsetup/onboard.py +550 -0
  170. hyperclaw/teamsetup/profile.py +82 -0
  171. hyperclaw/teamsetup/secrets.py +313 -0
  172. hyperclaw/teamsetup/selfcheck.py +295 -0
  173. hyperclaw/teamsetup/service.py +495 -0
  174. hyperclaw/teamsetup/slack_oauth.py +442 -0
  175. hyperclaw/teamsetup/tty.py +62 -0
  176. hyperclaw/telegram_bot.py +1106 -0
  177. hyperclaw/telegram_intake.py +126 -0
  178. hyperclaw/telegram_pairing.py +360 -0
  179. hyperclaw/terminal_provider.py +323 -0
  180. hyperclaw/tools/__init__.py +1 -0
  181. hyperclaw/tools/approval.py +949 -0
  182. hyperclaw/tools/args.py +212 -0
  183. hyperclaw/tools/catalog.py +244 -0
  184. hyperclaw/tools/classify.py +266 -0
  185. hyperclaw/tools/effects.py +724 -0
  186. hyperclaw/tools/egress.py +210 -0
  187. hyperclaw/tools/envelope.py +188 -0
  188. hyperclaw/tools/guard.py +367 -0
  189. hyperclaw/tools/ledger.py +339 -0
  190. hyperclaw/tools/receipts.py +70 -0
  191. hyperclaw/tools/registry.py +318 -0
  192. hyperclaw/tools/service_guard.py +151 -0
  193. hyperclaw/tools/surfaces.py +54 -0
  194. hyperclaw/tools/workspace.py +87 -0
  195. hyperclaw/trading/__init__.py +77 -0
  196. hyperclaw/truth_guard.py +791 -0
  197. hyperclaw/tts.py +124 -0
  198. hyperclaw/tui.py +4255 -0
  199. hyperclaw/tui_bridge.py +938 -0
  200. hyperclaw/unattended.py +60 -0
  201. hyperclaw/updates/__init__.py +54 -0
  202. hyperclaw/updates/check.py +131 -0
  203. hyperclaw/updates/index.py +178 -0
  204. hyperclaw/updates/install.py +110 -0
  205. hyperclaw/updates/managed.py +161 -0
  206. hyperclaw/updates/manifest.py +138 -0
  207. hyperclaw/updates/notices.py +86 -0
  208. hyperclaw/updates/paths.py +53 -0
  209. hyperclaw/updates/release.py +146 -0
  210. hyperclaw/updates/upgrade.py +253 -0
  211. hyperclaw/updates/versioning.py +104 -0
  212. hyperclaw/userconfig.py +119 -0
  213. hyperclaw/xai.py +83 -0
  214. integrations/__init__.py +31 -0
  215. integrations/automation/__init__.py +1 -0
  216. integrations/automation/make.py +87 -0
  217. integrations/automation/zapier.py +87 -0
  218. integrations/base.py +476 -0
  219. integrations/communication/__init__.py +1 -0
  220. integrations/communication/twilio.py +279 -0
  221. integrations/communication/zoom.py +290 -0
  222. integrations/data/__init__.py +1 -0
  223. integrations/data/google_analytics.py +135 -0
  224. integrations/data/postgres.py +182 -0
  225. integrations/data/supabase_connector.py +202 -0
  226. integrations/data/weather_feeds.py +450 -0
  227. integrations/developer/__init__.py +9 -0
  228. integrations/developer/github.py +406 -0
  229. integrations/developer/gitlab.py +298 -0
  230. integrations/enterprise/__init__.py +13 -0
  231. integrations/enterprise/confluence.py +266 -0
  232. integrations/enterprise/hubspot.py +293 -0
  233. integrations/enterprise/jira.py +314 -0
  234. integrations/enterprise/salesforce.py +286 -0
  235. integrations/finance/__init__.py +1 -0
  236. integrations/finance/polymarket_client.py +247 -0
  237. integrations/finance/quickbooks.py +281 -0
  238. integrations/finance/stripe.py +261 -0
  239. integrations/gateway.py +238 -0
  240. integrations/messaging/__init__.py +32 -0
  241. integrations/messaging/discord.py +159 -0
  242. integrations/messaging/email.py +401 -0
  243. integrations/messaging/imessage.py +443 -0
  244. integrations/messaging/signal.py +175 -0
  245. integrations/messaging/slack.py +217 -0
  246. integrations/messaging/sms.py +175 -0
  247. integrations/messaging/teams.py +208 -0
  248. integrations/messaging/telegram.py +260 -0
  249. integrations/messaging/whatsapp.py +203 -0
  250. integrations/productivity/__init__.py +17 -0
  251. integrations/productivity/airtable.py +217 -0
  252. integrations/productivity/asana.py +248 -0
  253. integrations/productivity/google/__init__.py +19 -0
  254. integrations/productivity/google/base.py +89 -0
  255. integrations/productivity/google/calendar.py +272 -0
  256. integrations/productivity/google/docs.py +202 -0
  257. integrations/productivity/google/drive.py +268 -0
  258. integrations/productivity/google/gmail.py +279 -0
  259. integrations/productivity/google/meet.py +218 -0
  260. integrations/productivity/google/sheets.py +222 -0
  261. integrations/productivity/google/tasks.py +199 -0
  262. integrations/productivity/linear.py +279 -0
  263. integrations/productivity/microsoft/__init__.py +13 -0
  264. integrations/productivity/microsoft/base.py +93 -0
  265. integrations/productivity/microsoft/calendar.py +207 -0
  266. integrations/productivity/microsoft/onedrive.py +222 -0
  267. integrations/productivity/microsoft/outlook.py +241 -0
  268. integrations/productivity/microsoft/sharepoint.py +211 -0
  269. integrations/productivity/notion.py +243 -0
  270. integrations/productivity/todoist.py +250 -0
  271. integrations/productivity/trello.py +244 -0
  272. integrations/storage/__init__.py +1 -0
  273. integrations/storage/box.py +279 -0
  274. integrations/storage/dropbox.py +215 -0
  275. integrations/telegram_inbound.py +179 -0
  276. integrations/telegram_poller.py +151 -0
  277. memory/__init__.py +1 -0
  278. memory/causal_graph.py +125 -0
  279. memory/impact_tracker.py +88 -0
  280. memory/vector_memory.py +436 -0
  281. models/__init__.py +12 -0
  282. models/chatjimmy_client.py +210 -0
  283. models/claude_client.py +235 -0
  284. models/claude_code_subagent.py +192 -0
  285. models/openai_client.py +120 -0
  286. models/router.py +162 -0
  287. recursive/discovery_loop.py +118 -0
  288. recursive/optimization_loop.py +135 -0
  289. recursive/validation_loop.py +38 -0
  290. security/__init__.py +1 -0
  291. security/audit_logger.py +106 -0
  292. security/filesystem_guard.py +100 -0
  293. security/hypershield.py +99 -0
  294. security/network_guard.py +97 -0
  295. security/policy_engine.py +198 -0
  296. swarm/__init__.py +1 -0
  297. swarm/agents/__init__.py +0 -0
  298. swarm/agents/base.py +230 -0
  299. swarm/agents/business/__init__.py +0 -0
  300. swarm/agents/business/counsel.py +31 -0
  301. swarm/agents/business/herald.py +32 -0
  302. swarm/agents/business/ledger.py +26 -0
  303. swarm/agents/business/nexus.py +30 -0
  304. swarm/agents/business/ops.py +30 -0
  305. swarm/agents/business/pipeline.py +28 -0
  306. swarm/agents/business/revenue.py +30 -0
  307. swarm/agents/business/sovereign.py +31 -0
  308. swarm/agents/business/strategos.py +27 -0
  309. swarm/agents/business/talent.py +27 -0
  310. swarm/agents/business/venture.py +32 -0
  311. swarm/agents/comms/__init__.py +1 -0
  312. swarm/agents/comms/cipher.py +32 -0
  313. swarm/agents/comms/echo_comms.py +31 -0
  314. swarm/agents/comms/envoy.py +31 -0
  315. swarm/agents/comms/herald_scheduler.py +33 -0
  316. swarm/agents/comms/pulse.py +29 -0
  317. swarm/agents/creative/__init__.py +0 -0
  318. swarm/agents/creative/author.py +27 -0
  319. swarm/agents/creative/lens.py +25 -0
  320. swarm/agents/executive/__init__.py +1 -0
  321. swarm/agents/intelligence/__init__.py +1 -0
  322. swarm/agents/intelligence/arbiter.py +30 -0
  323. swarm/agents/intelligence/sentinel.py +30 -0
  324. swarm/agents/media/__init__.py +0 -0
  325. swarm/agents/personal/__init__.py +0 -0
  326. swarm/agents/personal/atlas.py +27 -0
  327. swarm/agents/personal/hearth.py +26 -0
  328. swarm/agents/personal/midas.py +27 -0
  329. swarm/agents/personal/navigator.py +27 -0
  330. swarm/agents/personal/nourish.py +31 -0
  331. swarm/agents/personal/vitals.py +33 -0
  332. swarm/agents/recursive/__init__.py +0 -0
  333. swarm/agents/recursive/alchemist.py +73 -0
  334. swarm/agents/recursive/calibrator.py +45 -0
  335. swarm/agents/recursive/scout.py +97 -0
  336. swarm/agents/scientific/__init__.py +0 -0
  337. swarm/agents/scientific/cosmos.py +36 -0
  338. swarm/agents/scientific/gaia.py +26 -0
  339. swarm/agents/scientific/medicus.py +33 -0
  340. swarm/agents/scientific/oracle_agent.py +39 -0
  341. swarm/agents/scientific/scribe.py +39 -0
  342. swarm/agents/talent/__init__.py +0 -0
  343. swarm/agents/talent/deal.py +28 -0
  344. swarm/agents/talent/roster.py +29 -0
  345. swarm/agents/talent/scout.py +29 -0
  346. swarm/agents/talent/stage.py +27 -0
  347. swarm/agents/tech/__init__.py +0 -0
  348. swarm/agents/tech/aegis.py +28 -0
  349. swarm/agents/tech/bridge.py +28 -0
  350. swarm/agents/tech/forge.py +28 -0
  351. swarm/agents/trading/__init__.py +1 -0
  352. swarm/agents/trading/global_prediction_engine.py +611 -0
  353. swarm/agents/trading/polymarket_trader.py +165 -0
  354. swarm/agents/trading/prediction_strategist.py +218 -0
  355. swarm/autogen_bridge.py +68 -0
  356. swarm/bid_protocol.py +196 -0
  357. swarm/dispatcher.py +527 -0
  358. swarm/nexus.py +181 -0
  359. swarm/registry.py +112 -0
  360. telegram_listener.py +403 -0
  361. ui/tui/app.py +214 -0
core/providers/llm.py ADDED
@@ -0,0 +1,1935 @@
1
+ """
2
+ LLM Provider Abstraction Layer.
3
+ Unified interface for 20+ AI/LLM providers.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import logging
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass, field
11
+ from typing import AsyncIterator, Any
12
+
13
+ import httpx
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class Message:
20
+ """Chat message."""
21
+ role: str # "user", "assistant", "system"
22
+ content: str
23
+
24
+
25
+ @dataclass
26
+ class CompletionResponse:
27
+ """Unified completion response."""
28
+ content: str
29
+ model: str
30
+ input_tokens: int = 0
31
+ output_tokens: int = 0
32
+ finish_reason: str = "stop"
33
+ raw: dict = field(default_factory=dict)
34
+
35
+
36
+ class LLMProvider(ABC):
37
+ """Abstract base for LLM providers."""
38
+
39
+ name: str = "base"
40
+ supports_streaming: bool = True
41
+ supports_vision: bool = False
42
+ supports_tools: bool = True
43
+
44
+ def __init_subclass__(cls, **kwargs):
45
+ # Every provider's complete/stream goes through the cost guard (spec §9).
46
+ super().__init_subclass__(**kwargs)
47
+ from hyperclaw.cost_guard import guard_provider_class
48
+ guard_provider_class(cls)
49
+
50
+ @abstractmethod
51
+ async def complete(
52
+ self,
53
+ messages: list[Message],
54
+ model: str | None = None,
55
+ max_tokens: int = 4096,
56
+ temperature: float = 0.7,
57
+ system: str | None = None,
58
+ **kwargs,
59
+ ) -> CompletionResponse:
60
+ """Generate a completion."""
61
+ pass
62
+
63
+ @abstractmethod
64
+ async def stream(
65
+ self,
66
+ messages: list[Message],
67
+ model: str | None = None,
68
+ max_tokens: int = 4096,
69
+ temperature: float = 0.7,
70
+ system: str | None = None,
71
+ **kwargs,
72
+ ) -> AsyncIterator[str]:
73
+ """Stream a completion."""
74
+ pass
75
+
76
+ def available_models(self) -> list[str]:
77
+ """Return list of available models."""
78
+ return []
79
+
80
+
81
+ # ═══════════════════════════════════════════════════════════════════════════════
82
+ # ANTHROPIC
83
+ # ═══════════════════════════════════════════════════════════════════════════════
84
+
85
+ class AnthropicProvider(LLMProvider):
86
+ """Anthropic Claude provider."""
87
+
88
+ name = "anthropic"
89
+ supports_vision = True
90
+ default_model = "claude-opus-5"
91
+
92
+ MODELS = [
93
+ "claude-opus-5",
94
+ "claude-sonnet-5",
95
+ "claude-haiku-4-5",
96
+ "claude-fable-5",
97
+ ]
98
+
99
+ def __init__(self, api_key: str | None = None):
100
+ self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
101
+ self.base_url = "https://api.anthropic.com/v1"
102
+
103
+ async def complete(
104
+ self,
105
+ messages: list[Message],
106
+ model: str | None = None,
107
+ max_tokens: int = 4096,
108
+ temperature: float = 0.7,
109
+ system: str | None = None,
110
+ **kwargs,
111
+ ) -> CompletionResponse:
112
+ model = model or self.default_model
113
+
114
+ async with httpx.AsyncClient() as client:
115
+ # NOTE: temperature is intentionally omitted — sampling params
116
+ # were removed on the Claude 5 family and return HTTP 400.
117
+ payload = {
118
+ "model": model,
119
+ "max_tokens": max_tokens,
120
+ "messages": [{"role": m.role, "content": m.content} for m in messages],
121
+ }
122
+ if system:
123
+ payload["system"] = system
124
+
125
+ response = await client.post(
126
+ f"{self.base_url}/messages",
127
+ headers={
128
+ "x-api-key": self.api_key,
129
+ "anthropic-version": "2023-06-01",
130
+ "content-type": "application/json",
131
+ },
132
+ json=payload,
133
+ timeout=120.0,
134
+ )
135
+ response.raise_for_status()
136
+ data = response.json()
137
+
138
+ return CompletionResponse(
139
+ content=data["content"][0]["text"],
140
+ model=data["model"],
141
+ input_tokens=data["usage"]["input_tokens"],
142
+ output_tokens=data["usage"]["output_tokens"],
143
+ finish_reason=data["stop_reason"],
144
+ raw=data,
145
+ )
146
+
147
+ async def stream(
148
+ self,
149
+ messages: list[Message],
150
+ model: str | None = None,
151
+ max_tokens: int = 4096,
152
+ temperature: float = 0.7,
153
+ system: str | None = None,
154
+ **kwargs,
155
+ ) -> AsyncIterator[str]:
156
+ model = model or self.default_model
157
+
158
+ async with httpx.AsyncClient() as client:
159
+ # temperature omitted — rejected (400) on the Claude 5 family
160
+ payload = {
161
+ "model": model,
162
+ "max_tokens": max_tokens,
163
+ "messages": [{"role": m.role, "content": m.content} for m in messages],
164
+ "stream": True,
165
+ }
166
+ if system:
167
+ payload["system"] = system
168
+
169
+ async with client.stream(
170
+ "POST",
171
+ f"{self.base_url}/messages",
172
+ headers={
173
+ "x-api-key": self.api_key,
174
+ "anthropic-version": "2023-06-01",
175
+ "content-type": "application/json",
176
+ },
177
+ json=payload,
178
+ timeout=120.0,
179
+ ) as response:
180
+ async for line in response.aiter_lines():
181
+ if line.startswith("data: "):
182
+ import json
183
+ try:
184
+ data = json.loads(line[6:])
185
+ if data.get("type") == "content_block_delta":
186
+ yield data["delta"].get("text", "")
187
+ except json.JSONDecodeError:
188
+ pass
189
+
190
+ def available_models(self) -> list[str]:
191
+ return self.MODELS
192
+
193
+
194
+ # ═══════════════════════════════════════════════════════════════════════════════
195
+ # OPENAI
196
+ # ═══════════════════════════════════════════════════════════════════════════════
197
+
198
+ class OpenAIProvider(LLMProvider):
199
+ """OpenAI GPT provider."""
200
+
201
+ name = "openai"
202
+ supports_vision = True
203
+ default_model = "gpt-4o"
204
+
205
+ MODELS = [
206
+ "gpt-4o",
207
+ "gpt-4o-mini",
208
+ "gpt-4-turbo",
209
+ "gpt-4",
210
+ "gpt-3.5-turbo",
211
+ "o1-preview",
212
+ "o1-mini",
213
+ ]
214
+
215
+ def __init__(self, api_key: str | None = None, base_url: str | None = None):
216
+ self.api_key = api_key or os.environ.get("OPENAI_API_KEY")
217
+ self.base_url = base_url or "https://api.openai.com/v1"
218
+
219
+ async def complete(
220
+ self,
221
+ messages: list[Message],
222
+ model: str | None = None,
223
+ max_tokens: int = 4096,
224
+ temperature: float = 0.7,
225
+ system: str | None = None,
226
+ **kwargs,
227
+ ) -> CompletionResponse:
228
+ model = model or self.default_model
229
+
230
+ msgs = []
231
+ if system:
232
+ msgs.append({"role": "system", "content": system})
233
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
234
+
235
+ async with httpx.AsyncClient() as client:
236
+ response = await client.post(
237
+ f"{self.base_url}/chat/completions",
238
+ headers={"Authorization": f"Bearer {self.api_key}"},
239
+ json={
240
+ "model": model,
241
+ "messages": msgs,
242
+ "max_tokens": max_tokens,
243
+ "temperature": temperature,
244
+ },
245
+ timeout=120.0,
246
+ )
247
+ response.raise_for_status()
248
+ data = response.json()
249
+
250
+ return CompletionResponse(
251
+ content=data["choices"][0]["message"]["content"],
252
+ model=data["model"],
253
+ input_tokens=data["usage"]["prompt_tokens"],
254
+ output_tokens=data["usage"]["completion_tokens"],
255
+ finish_reason=data["choices"][0]["finish_reason"],
256
+ raw=data,
257
+ )
258
+
259
+ async def stream(
260
+ self,
261
+ messages: list[Message],
262
+ model: str | None = None,
263
+ max_tokens: int = 4096,
264
+ temperature: float = 0.7,
265
+ system: str | None = None,
266
+ **kwargs,
267
+ ) -> AsyncIterator[str]:
268
+ model = model or self.default_model
269
+
270
+ msgs = []
271
+ if system:
272
+ msgs.append({"role": "system", "content": system})
273
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
274
+
275
+ async with httpx.AsyncClient() as client:
276
+ async with client.stream(
277
+ "POST",
278
+ f"{self.base_url}/chat/completions",
279
+ headers={"Authorization": f"Bearer {self.api_key}"},
280
+ json={
281
+ "model": model,
282
+ "messages": msgs,
283
+ "max_tokens": max_tokens,
284
+ "temperature": temperature,
285
+ "stream": True,
286
+ },
287
+ timeout=120.0,
288
+ ) as response:
289
+ async for line in response.aiter_lines():
290
+ if line.startswith("data: ") and line != "data: [DONE]":
291
+ import json
292
+ try:
293
+ data = json.loads(line[6:])
294
+ delta = data["choices"][0].get("delta", {})
295
+ if "content" in delta:
296
+ yield delta["content"]
297
+ except json.JSONDecodeError:
298
+ pass
299
+
300
+ def available_models(self) -> list[str]:
301
+ return self.MODELS
302
+
303
+
304
+ # ═══════════════════════════════════════════════════════════════════════════════
305
+ # GOOGLE GEMINI
306
+ # ═══════════════════════════════════════════════════════════════════════════════
307
+
308
+ class GeminiProvider(LLMProvider):
309
+ """Google Gemini provider."""
310
+
311
+ name = "gemini"
312
+ supports_vision = True
313
+ default_model = "gemini-1.5-pro"
314
+
315
+ MODELS = [
316
+ "gemini-2.0-flash",
317
+ "gemini-1.5-pro",
318
+ "gemini-1.5-flash",
319
+ "gemini-1.0-pro",
320
+ ]
321
+
322
+ def __init__(self, api_key: str | None = None):
323
+ self.api_key = api_key or os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
324
+ self.base_url = "https://generativelanguage.googleapis.com/v1beta"
325
+
326
+ async def complete(
327
+ self,
328
+ messages: list[Message],
329
+ model: str | None = None,
330
+ max_tokens: int = 4096,
331
+ temperature: float = 0.7,
332
+ system: str | None = None,
333
+ **kwargs,
334
+ ) -> CompletionResponse:
335
+ model = model or self.default_model
336
+
337
+ contents = []
338
+ for m in messages:
339
+ role = "user" if m.role == "user" else "model"
340
+ contents.append({"role": role, "parts": [{"text": m.content}]})
341
+
342
+ payload: dict[str, Any] = {
343
+ "contents": contents,
344
+ "generationConfig": {
345
+ "maxOutputTokens": max_tokens,
346
+ "temperature": temperature,
347
+ },
348
+ }
349
+ if system:
350
+ payload["systemInstruction"] = {"parts": [{"text": system}]}
351
+
352
+ async with httpx.AsyncClient() as client:
353
+ response = await client.post(
354
+ f"{self.base_url}/models/{model}:generateContent",
355
+ params={"key": self.api_key},
356
+ json=payload,
357
+ timeout=120.0,
358
+ )
359
+ response.raise_for_status()
360
+ data = response.json()
361
+
362
+ content = data["candidates"][0]["content"]["parts"][0]["text"]
363
+ usage = data.get("usageMetadata", {})
364
+
365
+ return CompletionResponse(
366
+ content=content,
367
+ model=model,
368
+ input_tokens=usage.get("promptTokenCount", 0),
369
+ output_tokens=usage.get("candidatesTokenCount", 0),
370
+ finish_reason=data["candidates"][0].get("finishReason", "STOP"),
371
+ raw=data,
372
+ )
373
+
374
+ async def stream(
375
+ self,
376
+ messages: list[Message],
377
+ model: str | None = None,
378
+ max_tokens: int = 4096,
379
+ temperature: float = 0.7,
380
+ system: str | None = None,
381
+ **kwargs,
382
+ ) -> AsyncIterator[str]:
383
+ model = model or self.default_model
384
+
385
+ contents = []
386
+ for m in messages:
387
+ role = "user" if m.role == "user" else "model"
388
+ contents.append({"role": role, "parts": [{"text": m.content}]})
389
+
390
+ payload: dict[str, Any] = {
391
+ "contents": contents,
392
+ "generationConfig": {
393
+ "maxOutputTokens": max_tokens,
394
+ "temperature": temperature,
395
+ },
396
+ }
397
+ if system:
398
+ payload["systemInstruction"] = {"parts": [{"text": system}]}
399
+
400
+ async with httpx.AsyncClient() as client:
401
+ async with client.stream(
402
+ "POST",
403
+ f"{self.base_url}/models/{model}:streamGenerateContent",
404
+ params={"key": self.api_key, "alt": "sse"},
405
+ json=payload,
406
+ timeout=120.0,
407
+ ) as response:
408
+ async for line in response.aiter_lines():
409
+ if line.startswith("data: "):
410
+ import json
411
+ try:
412
+ data = json.loads(line[6:])
413
+ if "candidates" in data:
414
+ parts = data["candidates"][0].get("content", {}).get("parts", [])
415
+ for part in parts:
416
+ if "text" in part:
417
+ yield part["text"]
418
+ except json.JSONDecodeError:
419
+ pass
420
+
421
+ def available_models(self) -> list[str]:
422
+ return self.MODELS
423
+
424
+
425
+ # ═══════════════════════════════════════════════════════════════════════════════
426
+ # MISTRAL
427
+ # ═══════════════════════════════════════════════════════════════════════════════
428
+
429
+ class MistralProvider(LLMProvider):
430
+ """Mistral AI provider."""
431
+
432
+ name = "mistral"
433
+ supports_vision = False
434
+ default_model = "mistral-large-latest"
435
+
436
+ MODELS = [
437
+ "mistral-large-latest",
438
+ "mistral-medium-latest",
439
+ "mistral-small-latest",
440
+ "open-mixtral-8x22b",
441
+ "open-mixtral-8x7b",
442
+ "codestral-latest",
443
+ ]
444
+
445
+ def __init__(self, api_key: str | None = None):
446
+ self.api_key = api_key or os.environ.get("MISTRAL_API_KEY")
447
+ self.base_url = "https://api.mistral.ai/v1"
448
+
449
+ async def complete(
450
+ self,
451
+ messages: list[Message],
452
+ model: str | None = None,
453
+ max_tokens: int = 4096,
454
+ temperature: float = 0.7,
455
+ system: str | None = None,
456
+ **kwargs,
457
+ ) -> CompletionResponse:
458
+ model = model or self.default_model
459
+
460
+ msgs = []
461
+ if system:
462
+ msgs.append({"role": "system", "content": system})
463
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
464
+
465
+ async with httpx.AsyncClient() as client:
466
+ response = await client.post(
467
+ f"{self.base_url}/chat/completions",
468
+ headers={"Authorization": f"Bearer {self.api_key}"},
469
+ json={
470
+ "model": model,
471
+ "messages": msgs,
472
+ "max_tokens": max_tokens,
473
+ "temperature": temperature,
474
+ },
475
+ timeout=120.0,
476
+ )
477
+ response.raise_for_status()
478
+ data = response.json()
479
+
480
+ return CompletionResponse(
481
+ content=data["choices"][0]["message"]["content"],
482
+ model=data["model"],
483
+ input_tokens=data["usage"]["prompt_tokens"],
484
+ output_tokens=data["usage"]["completion_tokens"],
485
+ finish_reason=data["choices"][0]["finish_reason"],
486
+ raw=data,
487
+ )
488
+
489
+ async def stream(
490
+ self,
491
+ messages: list[Message],
492
+ model: str | None = None,
493
+ max_tokens: int = 4096,
494
+ temperature: float = 0.7,
495
+ system: str | None = None,
496
+ **kwargs,
497
+ ) -> AsyncIterator[str]:
498
+ model = model or self.default_model
499
+
500
+ msgs = []
501
+ if system:
502
+ msgs.append({"role": "system", "content": system})
503
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
504
+
505
+ async with httpx.AsyncClient() as client:
506
+ async with client.stream(
507
+ "POST",
508
+ f"{self.base_url}/chat/completions",
509
+ headers={"Authorization": f"Bearer {self.api_key}"},
510
+ json={
511
+ "model": model,
512
+ "messages": msgs,
513
+ "max_tokens": max_tokens,
514
+ "temperature": temperature,
515
+ "stream": True,
516
+ },
517
+ timeout=120.0,
518
+ ) as response:
519
+ async for line in response.aiter_lines():
520
+ if line.startswith("data: ") and line != "data: [DONE]":
521
+ import json
522
+ try:
523
+ data = json.loads(line[6:])
524
+ delta = data["choices"][0].get("delta", {})
525
+ if "content" in delta:
526
+ yield delta["content"]
527
+ except json.JSONDecodeError:
528
+ pass
529
+
530
+ def available_models(self) -> list[str]:
531
+ return self.MODELS
532
+
533
+
534
+ # ═══════════════════════════════════════════════════════════════════════════════
535
+ # GROQ
536
+ # ═══════════════════════════════════════════════════════════════════════════════
537
+
538
+ class GroqProvider(LLMProvider):
539
+ """Groq ultra-fast inference provider."""
540
+
541
+ name = "groq"
542
+ supports_vision = False
543
+ default_model = "llama-3.3-70b-versatile"
544
+
545
+ MODELS = [
546
+ "llama-3.3-70b-versatile",
547
+ "llama-3.1-70b-versatile",
548
+ "llama-3.1-8b-instant",
549
+ "mixtral-8x7b-32768",
550
+ "gemma2-9b-it",
551
+ ]
552
+
553
+ def __init__(self, api_key: str | None = None):
554
+ self.api_key = api_key or os.environ.get("GROQ_API_KEY")
555
+ self.base_url = "https://api.groq.com/openai/v1"
556
+
557
+ async def complete(
558
+ self,
559
+ messages: list[Message],
560
+ model: str | None = None,
561
+ max_tokens: int = 4096,
562
+ temperature: float = 0.7,
563
+ system: str | None = None,
564
+ **kwargs,
565
+ ) -> CompletionResponse:
566
+ model = model or self.default_model
567
+
568
+ msgs = []
569
+ if system:
570
+ msgs.append({"role": "system", "content": system})
571
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
572
+
573
+ async with httpx.AsyncClient() as client:
574
+ response = await client.post(
575
+ f"{self.base_url}/chat/completions",
576
+ headers={"Authorization": f"Bearer {self.api_key}"},
577
+ json={
578
+ "model": model,
579
+ "messages": msgs,
580
+ "max_tokens": max_tokens,
581
+ "temperature": temperature,
582
+ },
583
+ timeout=60.0,
584
+ )
585
+ response.raise_for_status()
586
+ data = response.json()
587
+
588
+ return CompletionResponse(
589
+ content=data["choices"][0]["message"]["content"],
590
+ model=data["model"],
591
+ input_tokens=data["usage"]["prompt_tokens"],
592
+ output_tokens=data["usage"]["completion_tokens"],
593
+ finish_reason=data["choices"][0]["finish_reason"],
594
+ raw=data,
595
+ )
596
+
597
+ async def stream(
598
+ self,
599
+ messages: list[Message],
600
+ model: str | None = None,
601
+ max_tokens: int = 4096,
602
+ temperature: float = 0.7,
603
+ system: str | None = None,
604
+ **kwargs,
605
+ ) -> AsyncIterator[str]:
606
+ model = model or self.default_model
607
+
608
+ msgs = []
609
+ if system:
610
+ msgs.append({"role": "system", "content": system})
611
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
612
+
613
+ async with httpx.AsyncClient() as client:
614
+ async with client.stream(
615
+ "POST",
616
+ f"{self.base_url}/chat/completions",
617
+ headers={"Authorization": f"Bearer {self.api_key}"},
618
+ json={
619
+ "model": model,
620
+ "messages": msgs,
621
+ "max_tokens": max_tokens,
622
+ "temperature": temperature,
623
+ "stream": True,
624
+ },
625
+ timeout=60.0,
626
+ ) as response:
627
+ async for line in response.aiter_lines():
628
+ if line.startswith("data: ") and line != "data: [DONE]":
629
+ import json
630
+ try:
631
+ data = json.loads(line[6:])
632
+ delta = data["choices"][0].get("delta", {})
633
+ if "content" in delta:
634
+ yield delta["content"]
635
+ except json.JSONDecodeError:
636
+ pass
637
+
638
+ def available_models(self) -> list[str]:
639
+ return self.MODELS
640
+
641
+
642
+ # ═══════════════════════════════════════════════════════════════════════════════
643
+ # XAI (GROK)
644
+ # ═══════════════════════════════════════════════════════════════════════════════
645
+
646
+ class XAIProvider(LLMProvider):
647
+ """xAI Grok provider."""
648
+
649
+ name = "xai"
650
+ supports_vision = True
651
+ default_model = "grok-2"
652
+
653
+ MODELS = ["grok-2", "grok-2-vision", "grok-beta"]
654
+
655
+ def __init__(self, api_key: str | None = None):
656
+ self.api_key = api_key or os.environ.get("XAI_API_KEY")
657
+ self.base_url = "https://api.x.ai/v1"
658
+
659
+ async def complete(
660
+ self,
661
+ messages: list[Message],
662
+ model: str | None = None,
663
+ max_tokens: int = 4096,
664
+ temperature: float = 0.7,
665
+ system: str | None = None,
666
+ **kwargs,
667
+ ) -> CompletionResponse:
668
+ model = model or self.default_model
669
+
670
+ msgs = []
671
+ if system:
672
+ msgs.append({"role": "system", "content": system})
673
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
674
+
675
+ async with httpx.AsyncClient() as client:
676
+ response = await client.post(
677
+ f"{self.base_url}/chat/completions",
678
+ headers={"Authorization": f"Bearer {self.api_key}"},
679
+ json={
680
+ "model": model,
681
+ "messages": msgs,
682
+ "max_tokens": max_tokens,
683
+ "temperature": temperature,
684
+ },
685
+ timeout=120.0,
686
+ )
687
+ response.raise_for_status()
688
+ data = response.json()
689
+
690
+ return CompletionResponse(
691
+ content=data["choices"][0]["message"]["content"],
692
+ model=data["model"],
693
+ input_tokens=data["usage"]["prompt_tokens"],
694
+ output_tokens=data["usage"]["completion_tokens"],
695
+ finish_reason=data["choices"][0]["finish_reason"],
696
+ raw=data,
697
+ )
698
+
699
+ async def stream(
700
+ self,
701
+ messages: list[Message],
702
+ model: str | None = None,
703
+ max_tokens: int = 4096,
704
+ temperature: float = 0.7,
705
+ system: str | None = None,
706
+ **kwargs,
707
+ ) -> AsyncIterator[str]:
708
+ # xAI uses OpenAI-compatible streaming
709
+ model = model or self.default_model
710
+
711
+ msgs = []
712
+ if system:
713
+ msgs.append({"role": "system", "content": system})
714
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
715
+
716
+ async with httpx.AsyncClient() as client:
717
+ async with client.stream(
718
+ "POST",
719
+ f"{self.base_url}/chat/completions",
720
+ headers={"Authorization": f"Bearer {self.api_key}"},
721
+ json={
722
+ "model": model,
723
+ "messages": msgs,
724
+ "max_tokens": max_tokens,
725
+ "temperature": temperature,
726
+ "stream": True,
727
+ },
728
+ timeout=120.0,
729
+ ) as response:
730
+ async for line in response.aiter_lines():
731
+ if line.startswith("data: ") and line != "data: [DONE]":
732
+ import json
733
+ try:
734
+ data = json.loads(line[6:])
735
+ delta = data["choices"][0].get("delta", {})
736
+ if "content" in delta:
737
+ yield delta["content"]
738
+ except json.JSONDecodeError:
739
+ pass
740
+
741
+ def available_models(self) -> list[str]:
742
+ return self.MODELS
743
+
744
+
745
+ # ═══════════════════════════════════════════════════════════════════════════════
746
+ # AMAZON BEDROCK
747
+ # ═══════════════════════════════════════════════════════════════════════════════
748
+
749
+ class BedrockProvider(LLMProvider):
750
+ """AWS Bedrock provider."""
751
+
752
+ name = "bedrock"
753
+ supports_vision = True
754
+ default_model = "anthropic.claude-sonnet-5"
755
+
756
+ MODELS = [
757
+ "anthropic.claude-sonnet-5",
758
+ "anthropic.claude-opus-5",
759
+ "anthropic.claude-haiku-4-5",
760
+ "amazon.titan-text-premier-v1:0",
761
+ "meta.llama3-70b-instruct-v1:0",
762
+ "cohere.command-r-plus-v1:0",
763
+ ]
764
+
765
+ def __init__(
766
+ self,
767
+ access_key: str | None = None,
768
+ secret_key: str | None = None,
769
+ region: str = "us-east-1",
770
+ ):
771
+ self.access_key = access_key or os.environ.get("AWS_ACCESS_KEY_ID")
772
+ self.secret_key = secret_key or os.environ.get("AWS_SECRET_ACCESS_KEY")
773
+ self.region = region or os.environ.get("AWS_REGION", "us-east-1")
774
+
775
+ async def complete(
776
+ self,
777
+ messages: list[Message],
778
+ model: str | None = None,
779
+ max_tokens: int = 4096,
780
+ temperature: float = 0.7,
781
+ system: str | None = None,
782
+ **kwargs,
783
+ ) -> CompletionResponse:
784
+ # Bedrock requires boto3 for proper AWS signing
785
+ try:
786
+ import boto3
787
+ except ImportError:
788
+ raise ImportError("boto3 required for Bedrock: pip install boto3")
789
+
790
+ model = model or self.default_model
791
+ client = boto3.client(
792
+ "bedrock-runtime",
793
+ region_name=self.region,
794
+ aws_access_key_id=self.access_key,
795
+ aws_secret_access_key=self.secret_key,
796
+ )
797
+
798
+ # Format depends on model family
799
+ if "anthropic" in model:
800
+ # temperature omitted — rejected (400) on the Claude 5 family
801
+ body = {
802
+ "anthropic_version": "bedrock-2023-05-31",
803
+ "max_tokens": max_tokens,
804
+ "messages": [{"role": m.role, "content": m.content} for m in messages],
805
+ }
806
+ if system:
807
+ body["system"] = system
808
+ else:
809
+ # Generic format for other models
810
+ body = {
811
+ "inputText": "\n".join([f"{m.role}: {m.content}" for m in messages]),
812
+ "textGenerationConfig": {
813
+ "maxTokenCount": max_tokens,
814
+ "temperature": temperature,
815
+ },
816
+ }
817
+
818
+ import json
819
+ response = client.invoke_model(
820
+ modelId=model,
821
+ body=json.dumps(body),
822
+ contentType="application/json",
823
+ )
824
+
825
+ result = json.loads(response["body"].read())
826
+
827
+ if "anthropic" in model:
828
+ return CompletionResponse(
829
+ content=result["content"][0]["text"],
830
+ model=model,
831
+ input_tokens=result["usage"]["input_tokens"],
832
+ output_tokens=result["usage"]["output_tokens"],
833
+ raw=result,
834
+ )
835
+ else:
836
+ return CompletionResponse(
837
+ content=result.get("results", [{}])[0].get("outputText", ""),
838
+ model=model,
839
+ raw=result,
840
+ )
841
+
842
+ async def stream(
843
+ self,
844
+ messages: list[Message],
845
+ model: str | None = None,
846
+ max_tokens: int = 4096,
847
+ temperature: float = 0.7,
848
+ system: str | None = None,
849
+ **kwargs,
850
+ ) -> AsyncIterator[str]:
851
+ # Bedrock streaming requires invoke_model_with_response_stream
852
+ # For simplicity, we'll do a non-streaming call and yield the result
853
+ response = await self.complete(messages, model, max_tokens, temperature, system, **kwargs)
854
+ yield response.content
855
+
856
+ def available_models(self) -> list[str]:
857
+ return self.MODELS
858
+
859
+
860
+ # ═══════════════════════════════════════════════════════════════════════════════
861
+ # OPENROUTER
862
+ # ═══════════════════════════════════════════════════════════════════════════════
863
+
864
+ class OpenRouterProvider(LLMProvider):
865
+ """OpenRouter multi-model gateway."""
866
+
867
+ name = "openrouter"
868
+ supports_vision = True
869
+ default_model = "anthropic/claude-sonnet-5"
870
+
871
+ MODELS = [
872
+ "anthropic/claude-sonnet-5",
873
+ "anthropic/claude-opus-5",
874
+ "openai/gpt-4o",
875
+ "openai/gpt-4-turbo",
876
+ "google/gemini-pro-1.5",
877
+ "meta-llama/llama-3.1-405b-instruct",
878
+ "mistralai/mistral-large",
879
+ ]
880
+
881
+ def __init__(self, api_key: str | None = None):
882
+ self.api_key = api_key or os.environ.get("OPENROUTER_API_KEY")
883
+ self.base_url = "https://openrouter.ai/api/v1"
884
+
885
+ async def complete(
886
+ self,
887
+ messages: list[Message],
888
+ model: str | None = None,
889
+ max_tokens: int = 4096,
890
+ temperature: float = 0.7,
891
+ system: str | None = None,
892
+ **kwargs,
893
+ ) -> CompletionResponse:
894
+ model = model or self.default_model
895
+
896
+ msgs = []
897
+ if system:
898
+ msgs.append({"role": "system", "content": system})
899
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
900
+
901
+ async with httpx.AsyncClient() as client:
902
+ response = await client.post(
903
+ f"{self.base_url}/chat/completions",
904
+ headers={
905
+ "Authorization": f"Bearer {self.api_key}",
906
+ "HTTP-Referer": "https://hyperclaw.ai",
907
+ },
908
+ json={
909
+ "model": model,
910
+ "messages": msgs,
911
+ "max_tokens": max_tokens,
912
+ "temperature": temperature,
913
+ },
914
+ timeout=120.0,
915
+ )
916
+ response.raise_for_status()
917
+ data = response.json()
918
+
919
+ usage = data.get("usage", {})
920
+ return CompletionResponse(
921
+ content=data["choices"][0]["message"]["content"],
922
+ model=data.get("model", model),
923
+ input_tokens=usage.get("prompt_tokens", 0),
924
+ output_tokens=usage.get("completion_tokens", 0),
925
+ finish_reason=data["choices"][0].get("finish_reason", "stop"),
926
+ raw=data,
927
+ )
928
+
929
+ async def stream(
930
+ self,
931
+ messages: list[Message],
932
+ model: str | None = None,
933
+ max_tokens: int = 4096,
934
+ temperature: float = 0.7,
935
+ system: str | None = None,
936
+ **kwargs,
937
+ ) -> AsyncIterator[str]:
938
+ model = model or self.default_model
939
+
940
+ msgs = []
941
+ if system:
942
+ msgs.append({"role": "system", "content": system})
943
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
944
+
945
+ async with httpx.AsyncClient() as client:
946
+ async with client.stream(
947
+ "POST",
948
+ f"{self.base_url}/chat/completions",
949
+ headers={
950
+ "Authorization": f"Bearer {self.api_key}",
951
+ "HTTP-Referer": "https://hyperclaw.ai",
952
+ },
953
+ json={
954
+ "model": model,
955
+ "messages": msgs,
956
+ "max_tokens": max_tokens,
957
+ "temperature": temperature,
958
+ "stream": True,
959
+ },
960
+ timeout=120.0,
961
+ ) as response:
962
+ async for line in response.aiter_lines():
963
+ if line.startswith("data: ") and line != "data: [DONE]":
964
+ import json
965
+ try:
966
+ data = json.loads(line[6:])
967
+ delta = data["choices"][0].get("delta", {})
968
+ if "content" in delta:
969
+ yield delta["content"]
970
+ except json.JSONDecodeError:
971
+ pass
972
+
973
+ def available_models(self) -> list[str]:
974
+ return self.MODELS
975
+
976
+
977
+ # ═══════════════════════════════════════════════════════════════════════════════
978
+ # TOGETHER AI
979
+ # ═══════════════════════════════════════════════════════════════════════════════
980
+
981
+ class TogetherProvider(LLMProvider):
982
+ """Together AI provider."""
983
+
984
+ name = "together"
985
+ supports_vision = False
986
+ default_model = "meta-llama/Llama-3.3-70B-Instruct-Turbo"
987
+
988
+ MODELS = [
989
+ "meta-llama/Llama-3.3-70B-Instruct-Turbo",
990
+ "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo",
991
+ "mistralai/Mixtral-8x22B-Instruct-v0.1",
992
+ "Qwen/Qwen2.5-72B-Instruct-Turbo",
993
+ "deepseek-ai/DeepSeek-V3",
994
+ ]
995
+
996
+ def __init__(self, api_key: str | None = None):
997
+ self.api_key = api_key or os.environ.get("TOGETHER_API_KEY")
998
+ self.base_url = "https://api.together.xyz/v1"
999
+
1000
+ async def complete(
1001
+ self,
1002
+ messages: list[Message],
1003
+ model: str | None = None,
1004
+ max_tokens: int = 4096,
1005
+ temperature: float = 0.7,
1006
+ system: str | None = None,
1007
+ **kwargs,
1008
+ ) -> CompletionResponse:
1009
+ model = model or self.default_model
1010
+
1011
+ msgs = []
1012
+ if system:
1013
+ msgs.append({"role": "system", "content": system})
1014
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1015
+
1016
+ async with httpx.AsyncClient() as client:
1017
+ response = await client.post(
1018
+ f"{self.base_url}/chat/completions",
1019
+ headers={"Authorization": f"Bearer {self.api_key}"},
1020
+ json={
1021
+ "model": model,
1022
+ "messages": msgs,
1023
+ "max_tokens": max_tokens,
1024
+ "temperature": temperature,
1025
+ },
1026
+ timeout=120.0,
1027
+ )
1028
+ response.raise_for_status()
1029
+ data = response.json()
1030
+
1031
+ return CompletionResponse(
1032
+ content=data["choices"][0]["message"]["content"],
1033
+ model=data["model"],
1034
+ input_tokens=data["usage"]["prompt_tokens"],
1035
+ output_tokens=data["usage"]["completion_tokens"],
1036
+ finish_reason=data["choices"][0]["finish_reason"],
1037
+ raw=data,
1038
+ )
1039
+
1040
+ async def stream(
1041
+ self,
1042
+ messages: list[Message],
1043
+ model: str | None = None,
1044
+ max_tokens: int = 4096,
1045
+ temperature: float = 0.7,
1046
+ system: str | None = None,
1047
+ **kwargs,
1048
+ ) -> AsyncIterator[str]:
1049
+ model = model or self.default_model
1050
+
1051
+ msgs = []
1052
+ if system:
1053
+ msgs.append({"role": "system", "content": system})
1054
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1055
+
1056
+ async with httpx.AsyncClient() as client:
1057
+ async with client.stream(
1058
+ "POST",
1059
+ f"{self.base_url}/chat/completions",
1060
+ headers={"Authorization": f"Bearer {self.api_key}"},
1061
+ json={
1062
+ "model": model,
1063
+ "messages": msgs,
1064
+ "max_tokens": max_tokens,
1065
+ "temperature": temperature,
1066
+ "stream": True,
1067
+ },
1068
+ timeout=120.0,
1069
+ ) as response:
1070
+ async for line in response.aiter_lines():
1071
+ if line.startswith("data: ") and line != "data: [DONE]":
1072
+ import json
1073
+ try:
1074
+ data = json.loads(line[6:])
1075
+ delta = data["choices"][0].get("delta", {})
1076
+ if "content" in delta:
1077
+ yield delta["content"]
1078
+ except json.JSONDecodeError:
1079
+ pass
1080
+
1081
+ def available_models(self) -> list[str]:
1082
+ return self.MODELS
1083
+
1084
+
1085
+ # ═══════════════════════════════════════════════════════════════════════════════
1086
+ # PERPLEXITY
1087
+ # ═══════════════════════════════════════════════════════════════════════════════
1088
+
1089
+ class PerplexityProvider(LLMProvider):
1090
+ """Perplexity AI provider (search-augmented)."""
1091
+
1092
+ name = "perplexity"
1093
+ supports_vision = False
1094
+ default_model = "llama-3.1-sonar-large-128k-online"
1095
+
1096
+ MODELS = [
1097
+ "llama-3.1-sonar-large-128k-online",
1098
+ "llama-3.1-sonar-small-128k-online",
1099
+ "llama-3.1-sonar-huge-128k-online",
1100
+ ]
1101
+
1102
+ def __init__(self, api_key: str | None = None):
1103
+ self.api_key = api_key or os.environ.get("PERPLEXITY_API_KEY")
1104
+ self.base_url = "https://api.perplexity.ai"
1105
+
1106
+ async def complete(
1107
+ self,
1108
+ messages: list[Message],
1109
+ model: str | None = None,
1110
+ max_tokens: int = 4096,
1111
+ temperature: float = 0.7,
1112
+ system: str | None = None,
1113
+ **kwargs,
1114
+ ) -> CompletionResponse:
1115
+ model = model or self.default_model
1116
+
1117
+ msgs = []
1118
+ if system:
1119
+ msgs.append({"role": "system", "content": system})
1120
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1121
+
1122
+ async with httpx.AsyncClient() as client:
1123
+ response = await client.post(
1124
+ f"{self.base_url}/chat/completions",
1125
+ headers={"Authorization": f"Bearer {self.api_key}"},
1126
+ json={
1127
+ "model": model,
1128
+ "messages": msgs,
1129
+ "max_tokens": max_tokens,
1130
+ "temperature": temperature,
1131
+ },
1132
+ timeout=120.0,
1133
+ )
1134
+ response.raise_for_status()
1135
+ data = response.json()
1136
+
1137
+ return CompletionResponse(
1138
+ content=data["choices"][0]["message"]["content"],
1139
+ model=data["model"],
1140
+ input_tokens=data["usage"]["prompt_tokens"],
1141
+ output_tokens=data["usage"]["completion_tokens"],
1142
+ finish_reason=data["choices"][0]["finish_reason"],
1143
+ raw=data,
1144
+ )
1145
+
1146
+ async def stream(
1147
+ self,
1148
+ messages: list[Message],
1149
+ model: str | None = None,
1150
+ max_tokens: int = 4096,
1151
+ temperature: float = 0.7,
1152
+ system: str | None = None,
1153
+ **kwargs,
1154
+ ) -> AsyncIterator[str]:
1155
+ model = model or self.default_model
1156
+
1157
+ msgs = []
1158
+ if system:
1159
+ msgs.append({"role": "system", "content": system})
1160
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1161
+
1162
+ async with httpx.AsyncClient() as client:
1163
+ async with client.stream(
1164
+ "POST",
1165
+ f"{self.base_url}/chat/completions",
1166
+ headers={"Authorization": f"Bearer {self.api_key}"},
1167
+ json={
1168
+ "model": model,
1169
+ "messages": msgs,
1170
+ "max_tokens": max_tokens,
1171
+ "temperature": temperature,
1172
+ "stream": True,
1173
+ },
1174
+ timeout=120.0,
1175
+ ) as response:
1176
+ async for line in response.aiter_lines():
1177
+ if line.startswith("data: ") and line != "data: [DONE]":
1178
+ import json
1179
+ try:
1180
+ data = json.loads(line[6:])
1181
+ delta = data["choices"][0].get("delta", {})
1182
+ if "content" in delta:
1183
+ yield delta["content"]
1184
+ except json.JSONDecodeError:
1185
+ pass
1186
+
1187
+ def available_models(self) -> list[str]:
1188
+ return self.MODELS
1189
+
1190
+
1191
+ # ═══════════════════════════════════════════════════════════════════════════════
1192
+ # DEEPSEEK
1193
+ # ═══════════════════════════════════════════════════════════════════════════════
1194
+
1195
+ class DeepSeekProvider(LLMProvider):
1196
+ """DeepSeek provider."""
1197
+
1198
+ name = "deepseek"
1199
+ supports_vision = False
1200
+ default_model = "deepseek-chat"
1201
+
1202
+ MODELS = ["deepseek-chat", "deepseek-coder", "deepseek-reasoner"]
1203
+
1204
+ def __init__(self, api_key: str | None = None):
1205
+ self.api_key = api_key or os.environ.get("DEEPSEEK_API_KEY")
1206
+ self.base_url = "https://api.deepseek.com/v1"
1207
+
1208
+ async def complete(
1209
+ self,
1210
+ messages: list[Message],
1211
+ model: str | None = None,
1212
+ max_tokens: int = 4096,
1213
+ temperature: float = 0.7,
1214
+ system: str | None = None,
1215
+ **kwargs,
1216
+ ) -> CompletionResponse:
1217
+ model = model or self.default_model
1218
+
1219
+ msgs = []
1220
+ if system:
1221
+ msgs.append({"role": "system", "content": system})
1222
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1223
+
1224
+ async with httpx.AsyncClient() as client:
1225
+ response = await client.post(
1226
+ f"{self.base_url}/chat/completions",
1227
+ headers={"Authorization": f"Bearer {self.api_key}"},
1228
+ json={
1229
+ "model": model,
1230
+ "messages": msgs,
1231
+ "max_tokens": max_tokens,
1232
+ "temperature": temperature,
1233
+ },
1234
+ timeout=120.0,
1235
+ )
1236
+ response.raise_for_status()
1237
+ data = response.json()
1238
+
1239
+ return CompletionResponse(
1240
+ content=data["choices"][0]["message"]["content"],
1241
+ model=data["model"],
1242
+ input_tokens=data["usage"]["prompt_tokens"],
1243
+ output_tokens=data["usage"]["completion_tokens"],
1244
+ finish_reason=data["choices"][0]["finish_reason"],
1245
+ raw=data,
1246
+ )
1247
+
1248
+ async def stream(
1249
+ self,
1250
+ messages: list[Message],
1251
+ model: str | None = None,
1252
+ max_tokens: int = 4096,
1253
+ temperature: float = 0.7,
1254
+ system: str | None = None,
1255
+ **kwargs,
1256
+ ) -> AsyncIterator[str]:
1257
+ model = model or self.default_model
1258
+
1259
+ msgs = []
1260
+ if system:
1261
+ msgs.append({"role": "system", "content": system})
1262
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1263
+
1264
+ async with httpx.AsyncClient() as client:
1265
+ async with client.stream(
1266
+ "POST",
1267
+ f"{self.base_url}/chat/completions",
1268
+ headers={"Authorization": f"Bearer {self.api_key}"},
1269
+ json={
1270
+ "model": model,
1271
+ "messages": msgs,
1272
+ "max_tokens": max_tokens,
1273
+ "temperature": temperature,
1274
+ "stream": True,
1275
+ },
1276
+ timeout=120.0,
1277
+ ) as response:
1278
+ async for line in response.aiter_lines():
1279
+ if line.startswith("data: ") and line != "data: [DONE]":
1280
+ import json
1281
+ try:
1282
+ data = json.loads(line[6:])
1283
+ delta = data["choices"][0].get("delta", {})
1284
+ if "content" in delta:
1285
+ yield delta["content"]
1286
+ except json.JSONDecodeError:
1287
+ pass
1288
+
1289
+ def available_models(self) -> list[str]:
1290
+ return self.MODELS
1291
+
1292
+
1293
+ # ═══════════════════════════════════════════════════════════════════════════════
1294
+ # QWEN (ALIBABA)
1295
+ # ═══════════════════════════════════════════════════════════════════════════════
1296
+
1297
+ class QwenProvider(LLMProvider):
1298
+ """Alibaba Qwen provider."""
1299
+
1300
+ name = "qwen"
1301
+ supports_vision = True
1302
+ default_model = "qwen-max"
1303
+
1304
+ MODELS = ["qwen-max", "qwen-plus", "qwen-turbo", "qwen-vl-max"]
1305
+
1306
+ def __init__(self, api_key: str | None = None):
1307
+ self.api_key = api_key or os.environ.get("QWEN_API_KEY") or os.environ.get("DASHSCOPE_API_KEY")
1308
+ self.base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
1309
+
1310
+ async def complete(
1311
+ self,
1312
+ messages: list[Message],
1313
+ model: str | None = None,
1314
+ max_tokens: int = 4096,
1315
+ temperature: float = 0.7,
1316
+ system: str | None = None,
1317
+ **kwargs,
1318
+ ) -> CompletionResponse:
1319
+ model = model or self.default_model
1320
+
1321
+ msgs = []
1322
+ if system:
1323
+ msgs.append({"role": "system", "content": system})
1324
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1325
+
1326
+ async with httpx.AsyncClient() as client:
1327
+ response = await client.post(
1328
+ f"{self.base_url}/chat/completions",
1329
+ headers={"Authorization": f"Bearer {self.api_key}"},
1330
+ json={
1331
+ "model": model,
1332
+ "messages": msgs,
1333
+ "max_tokens": max_tokens,
1334
+ "temperature": temperature,
1335
+ },
1336
+ timeout=120.0,
1337
+ )
1338
+ response.raise_for_status()
1339
+ data = response.json()
1340
+
1341
+ return CompletionResponse(
1342
+ content=data["choices"][0]["message"]["content"],
1343
+ model=data["model"],
1344
+ input_tokens=data["usage"]["prompt_tokens"],
1345
+ output_tokens=data["usage"]["completion_tokens"],
1346
+ finish_reason=data["choices"][0]["finish_reason"],
1347
+ raw=data,
1348
+ )
1349
+
1350
+ async def stream(
1351
+ self,
1352
+ messages: list[Message],
1353
+ model: str | None = None,
1354
+ max_tokens: int = 4096,
1355
+ temperature: float = 0.7,
1356
+ system: str | None = None,
1357
+ **kwargs,
1358
+ ) -> AsyncIterator[str]:
1359
+ model = model or self.default_model
1360
+
1361
+ msgs = []
1362
+ if system:
1363
+ msgs.append({"role": "system", "content": system})
1364
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1365
+
1366
+ async with httpx.AsyncClient() as client:
1367
+ async with client.stream(
1368
+ "POST",
1369
+ f"{self.base_url}/chat/completions",
1370
+ headers={"Authorization": f"Bearer {self.api_key}"},
1371
+ json={
1372
+ "model": model,
1373
+ "messages": msgs,
1374
+ "max_tokens": max_tokens,
1375
+ "temperature": temperature,
1376
+ "stream": True,
1377
+ },
1378
+ timeout=120.0,
1379
+ ) as response:
1380
+ async for line in response.aiter_lines():
1381
+ if line.startswith("data: ") and line != "data: [DONE]":
1382
+ import json
1383
+ try:
1384
+ data = json.loads(line[6:])
1385
+ delta = data["choices"][0].get("delta", {})
1386
+ if "content" in delta:
1387
+ yield delta["content"]
1388
+ except json.JSONDecodeError:
1389
+ pass
1390
+
1391
+ def available_models(self) -> list[str]:
1392
+ return self.MODELS
1393
+
1394
+
1395
+ # ═══════════════════════════════════════════════════════════════════════════════
1396
+ # CEREBRAS
1397
+ # ═══════════════════════════════════════════════════════════════════════════════
1398
+
1399
+ class CerebrasProvider(LLMProvider):
1400
+ """Cerebras ultra-fast inference provider."""
1401
+
1402
+ name = "cerebras"
1403
+ supports_vision = False
1404
+ default_model = "llama3.1-70b"
1405
+
1406
+ MODELS = ["llama3.1-70b", "llama3.1-8b"]
1407
+
1408
+ def __init__(self, api_key: str | None = None):
1409
+ self.api_key = api_key or os.environ.get("CEREBRAS_API_KEY")
1410
+ self.base_url = "https://api.cerebras.ai/v1"
1411
+
1412
+ async def complete(
1413
+ self,
1414
+ messages: list[Message],
1415
+ model: str | None = None,
1416
+ max_tokens: int = 4096,
1417
+ temperature: float = 0.7,
1418
+ system: str | None = None,
1419
+ **kwargs,
1420
+ ) -> CompletionResponse:
1421
+ model = model or self.default_model
1422
+
1423
+ msgs = []
1424
+ if system:
1425
+ msgs.append({"role": "system", "content": system})
1426
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1427
+
1428
+ async with httpx.AsyncClient() as client:
1429
+ response = await client.post(
1430
+ f"{self.base_url}/chat/completions",
1431
+ headers={"Authorization": f"Bearer {self.api_key}"},
1432
+ json={
1433
+ "model": model,
1434
+ "messages": msgs,
1435
+ "max_tokens": max_tokens,
1436
+ "temperature": temperature,
1437
+ },
1438
+ timeout=60.0,
1439
+ )
1440
+ response.raise_for_status()
1441
+ data = response.json()
1442
+
1443
+ return CompletionResponse(
1444
+ content=data["choices"][0]["message"]["content"],
1445
+ model=data["model"],
1446
+ input_tokens=data["usage"]["prompt_tokens"],
1447
+ output_tokens=data["usage"]["completion_tokens"],
1448
+ finish_reason=data["choices"][0]["finish_reason"],
1449
+ raw=data,
1450
+ )
1451
+
1452
+ async def stream(
1453
+ self,
1454
+ messages: list[Message],
1455
+ model: str | None = None,
1456
+ max_tokens: int = 4096,
1457
+ temperature: float = 0.7,
1458
+ system: str | None = None,
1459
+ **kwargs,
1460
+ ) -> AsyncIterator[str]:
1461
+ model = model or self.default_model
1462
+
1463
+ msgs = []
1464
+ if system:
1465
+ msgs.append({"role": "system", "content": system})
1466
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1467
+
1468
+ async with httpx.AsyncClient() as client:
1469
+ async with client.stream(
1470
+ "POST",
1471
+ f"{self.base_url}/chat/completions",
1472
+ headers={"Authorization": f"Bearer {self.api_key}"},
1473
+ json={
1474
+ "model": model,
1475
+ "messages": msgs,
1476
+ "max_tokens": max_tokens,
1477
+ "temperature": temperature,
1478
+ "stream": True,
1479
+ },
1480
+ timeout=60.0,
1481
+ ) as response:
1482
+ async for line in response.aiter_lines():
1483
+ if line.startswith("data: ") and line != "data: [DONE]":
1484
+ import json
1485
+ try:
1486
+ data = json.loads(line[6:])
1487
+ delta = data["choices"][0].get("delta", {})
1488
+ if "content" in delta:
1489
+ yield delta["content"]
1490
+ except json.JSONDecodeError:
1491
+ pass
1492
+
1493
+ def available_models(self) -> list[str]:
1494
+ return self.MODELS
1495
+
1496
+
1497
+ # ═══════════════════════════════════════════════════════════════════════════════
1498
+ # HUGGING FACE
1499
+ # ═══════════════════════════════════════════════════════════════════════════════
1500
+
1501
+ class HuggingFaceProvider(LLMProvider):
1502
+ """Hugging Face Inference API provider."""
1503
+
1504
+ name = "huggingface"
1505
+ supports_vision = False
1506
+ default_model = "meta-llama/Llama-3.1-70B-Instruct"
1507
+
1508
+ MODELS = [
1509
+ "meta-llama/Llama-3.1-70B-Instruct",
1510
+ "mistralai/Mixtral-8x7B-Instruct-v0.1",
1511
+ "HuggingFaceH4/zephyr-7b-beta",
1512
+ ]
1513
+
1514
+ def __init__(self, api_key: str | None = None):
1515
+ self.api_key = api_key or os.environ.get("HUGGINGFACE_API_KEY") or os.environ.get("HF_TOKEN")
1516
+ self.base_url = "https://api-inference.huggingface.co/models"
1517
+
1518
+ async def complete(
1519
+ self,
1520
+ messages: list[Message],
1521
+ model: str | None = None,
1522
+ max_tokens: int = 4096,
1523
+ temperature: float = 0.7,
1524
+ system: str | None = None,
1525
+ **kwargs,
1526
+ ) -> CompletionResponse:
1527
+ model = model or self.default_model
1528
+
1529
+ # Format as conversation
1530
+ prompt = ""
1531
+ if system:
1532
+ prompt += f"<|system|>\n{system}\n"
1533
+ for m in messages:
1534
+ prompt += f"<|{m.role}|>\n{m.content}\n"
1535
+ prompt += "<|assistant|>\n"
1536
+
1537
+ async with httpx.AsyncClient() as client:
1538
+ response = await client.post(
1539
+ f"{self.base_url}/{model}",
1540
+ headers={"Authorization": f"Bearer {self.api_key}"},
1541
+ json={
1542
+ "inputs": prompt,
1543
+ "parameters": {
1544
+ "max_new_tokens": max_tokens,
1545
+ "temperature": temperature,
1546
+ "return_full_text": False,
1547
+ },
1548
+ },
1549
+ timeout=120.0,
1550
+ )
1551
+ response.raise_for_status()
1552
+ data = response.json()
1553
+
1554
+ content = data[0]["generated_text"] if isinstance(data, list) else data.get("generated_text", "")
1555
+
1556
+ return CompletionResponse(
1557
+ content=content,
1558
+ model=model,
1559
+ raw=data,
1560
+ )
1561
+
1562
+ async def stream(
1563
+ self,
1564
+ messages: list[Message],
1565
+ model: str | None = None,
1566
+ max_tokens: int = 4096,
1567
+ temperature: float = 0.7,
1568
+ system: str | None = None,
1569
+ **kwargs,
1570
+ ) -> AsyncIterator[str]:
1571
+ # HF Inference API has limited streaming support
1572
+ response = await self.complete(messages, model, max_tokens, temperature, system, **kwargs)
1573
+ yield response.content
1574
+
1575
+ def available_models(self) -> list[str]:
1576
+ return self.MODELS
1577
+
1578
+
1579
+ # ═══════════════════════════════════════════════════════════════════════════════
1580
+ # OLLAMA (LOCAL)
1581
+ # ═══════════════════════════════════════════════════════════════════════════════
1582
+
1583
+ class OllamaProvider(LLMProvider):
1584
+ """Ollama local inference provider."""
1585
+
1586
+ name = "ollama"
1587
+ supports_vision = True
1588
+ default_model = "llama3.2"
1589
+
1590
+ MODELS = ["llama3.2", "llama3.1", "mistral", "mixtral", "codellama", "llava"]
1591
+
1592
+ def __init__(self, base_url: str | None = None):
1593
+ self.base_url = base_url or os.environ.get("OLLAMA_HOST", "http://localhost:11434")
1594
+
1595
+ async def complete(
1596
+ self,
1597
+ messages: list[Message],
1598
+ model: str | None = None,
1599
+ max_tokens: int = 4096,
1600
+ temperature: float = 0.7,
1601
+ system: str | None = None,
1602
+ **kwargs,
1603
+ ) -> CompletionResponse:
1604
+ model = model or self.default_model
1605
+
1606
+ msgs = []
1607
+ if system:
1608
+ msgs.append({"role": "system", "content": system})
1609
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1610
+
1611
+ async with httpx.AsyncClient() as client:
1612
+ response = await client.post(
1613
+ f"{self.base_url}/api/chat",
1614
+ json={
1615
+ "model": model,
1616
+ "messages": msgs,
1617
+ "options": {
1618
+ "num_predict": max_tokens,
1619
+ "temperature": temperature,
1620
+ },
1621
+ "stream": False,
1622
+ },
1623
+ timeout=300.0,
1624
+ )
1625
+ response.raise_for_status()
1626
+ data = response.json()
1627
+
1628
+ return CompletionResponse(
1629
+ content=data["message"]["content"],
1630
+ model=model,
1631
+ input_tokens=data.get("prompt_eval_count", 0),
1632
+ output_tokens=data.get("eval_count", 0),
1633
+ raw=data,
1634
+ )
1635
+
1636
+ async def stream(
1637
+ self,
1638
+ messages: list[Message],
1639
+ model: str | None = None,
1640
+ max_tokens: int = 4096,
1641
+ temperature: float = 0.7,
1642
+ system: str | None = None,
1643
+ **kwargs,
1644
+ ) -> AsyncIterator[str]:
1645
+ model = model or self.default_model
1646
+
1647
+ msgs = []
1648
+ if system:
1649
+ msgs.append({"role": "system", "content": system})
1650
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1651
+
1652
+ async with httpx.AsyncClient() as client:
1653
+ async with client.stream(
1654
+ "POST",
1655
+ f"{self.base_url}/api/chat",
1656
+ json={
1657
+ "model": model,
1658
+ "messages": msgs,
1659
+ "options": {
1660
+ "num_predict": max_tokens,
1661
+ "temperature": temperature,
1662
+ },
1663
+ "stream": True,
1664
+ },
1665
+ timeout=300.0,
1666
+ ) as response:
1667
+ async for line in response.aiter_lines():
1668
+ if line:
1669
+ import json
1670
+ try:
1671
+ data = json.loads(line)
1672
+ if "message" in data:
1673
+ yield data["message"].get("content", "")
1674
+ except json.JSONDecodeError:
1675
+ pass
1676
+
1677
+ def available_models(self) -> list[str]:
1678
+ return self.MODELS
1679
+
1680
+
1681
+ # ═══════════════════════════════════════════════════════════════════════════════
1682
+ # VLLM (LOCAL)
1683
+ # ═══════════════════════════════════════════════════════════════════════════════
1684
+
1685
+ class VLLMProvider(LLMProvider):
1686
+ """vLLM local inference provider (OpenAI-compatible)."""
1687
+
1688
+ name = "vllm"
1689
+ supports_vision = False
1690
+ default_model = "default"
1691
+
1692
+ def __init__(self, base_url: str | None = None):
1693
+ self.base_url = base_url or os.environ.get("VLLM_HOST", "http://localhost:8000/v1")
1694
+
1695
+ async def complete(
1696
+ self,
1697
+ messages: list[Message],
1698
+ model: str | None = None,
1699
+ max_tokens: int = 4096,
1700
+ temperature: float = 0.7,
1701
+ system: str | None = None,
1702
+ **kwargs,
1703
+ ) -> CompletionResponse:
1704
+ model = model or self.default_model
1705
+
1706
+ msgs = []
1707
+ if system:
1708
+ msgs.append({"role": "system", "content": system})
1709
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1710
+
1711
+ async with httpx.AsyncClient() as client:
1712
+ response = await client.post(
1713
+ f"{self.base_url}/chat/completions",
1714
+ json={
1715
+ "model": model,
1716
+ "messages": msgs,
1717
+ "max_tokens": max_tokens,
1718
+ "temperature": temperature,
1719
+ },
1720
+ timeout=300.0,
1721
+ )
1722
+ response.raise_for_status()
1723
+ data = response.json()
1724
+
1725
+ return CompletionResponse(
1726
+ content=data["choices"][0]["message"]["content"],
1727
+ model=data["model"],
1728
+ input_tokens=data["usage"]["prompt_tokens"],
1729
+ output_tokens=data["usage"]["completion_tokens"],
1730
+ finish_reason=data["choices"][0]["finish_reason"],
1731
+ raw=data,
1732
+ )
1733
+
1734
+ async def stream(
1735
+ self,
1736
+ messages: list[Message],
1737
+ model: str | None = None,
1738
+ max_tokens: int = 4096,
1739
+ temperature: float = 0.7,
1740
+ system: str | None = None,
1741
+ **kwargs,
1742
+ ) -> AsyncIterator[str]:
1743
+ model = model or self.default_model
1744
+
1745
+ msgs = []
1746
+ if system:
1747
+ msgs.append({"role": "system", "content": system})
1748
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1749
+
1750
+ async with httpx.AsyncClient() as client:
1751
+ async with client.stream(
1752
+ "POST",
1753
+ f"{self.base_url}/chat/completions",
1754
+ json={
1755
+ "model": model,
1756
+ "messages": msgs,
1757
+ "max_tokens": max_tokens,
1758
+ "temperature": temperature,
1759
+ "stream": True,
1760
+ },
1761
+ timeout=300.0,
1762
+ ) as response:
1763
+ async for line in response.aiter_lines():
1764
+ if line.startswith("data: ") and line != "data: [DONE]":
1765
+ import json
1766
+ try:
1767
+ data = json.loads(line[6:])
1768
+ delta = data["choices"][0].get("delta", {})
1769
+ if "content" in delta:
1770
+ yield delta["content"]
1771
+ except json.JSONDecodeError:
1772
+ pass
1773
+
1774
+ def available_models(self) -> list[str]:
1775
+ return [self.default_model]
1776
+
1777
+
1778
+ # ═══════════════════════════════════════════════════════════════════════════════
1779
+ # LITELLM (UNIVERSAL PROXY)
1780
+ # ═══════════════════════════════════════════════════════════════════════════════
1781
+
1782
+ class LiteLLMProvider(LLMProvider):
1783
+ """LiteLLM universal proxy — routes to any provider."""
1784
+
1785
+ name = "litellm"
1786
+ supports_vision = True
1787
+ default_model = "gpt-4o"
1788
+
1789
+ def __init__(self, base_url: str | None = None, api_key: str | None = None):
1790
+ self.base_url = base_url or os.environ.get("LITELLM_HOST", "http://localhost:4000/v1")
1791
+ self.api_key = api_key or os.environ.get("LITELLM_API_KEY", "")
1792
+
1793
+ async def complete(
1794
+ self,
1795
+ messages: list[Message],
1796
+ model: str | None = None,
1797
+ max_tokens: int = 4096,
1798
+ temperature: float = 0.7,
1799
+ system: str | None = None,
1800
+ **kwargs,
1801
+ ) -> CompletionResponse:
1802
+ model = model or self.default_model
1803
+
1804
+ msgs = []
1805
+ if system:
1806
+ msgs.append({"role": "system", "content": system})
1807
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1808
+
1809
+ headers = {}
1810
+ if self.api_key:
1811
+ headers["Authorization"] = f"Bearer {self.api_key}"
1812
+
1813
+ async with httpx.AsyncClient() as client:
1814
+ response = await client.post(
1815
+ f"{self.base_url}/chat/completions",
1816
+ headers=headers,
1817
+ json={
1818
+ "model": model,
1819
+ "messages": msgs,
1820
+ "max_tokens": max_tokens,
1821
+ "temperature": temperature,
1822
+ },
1823
+ timeout=120.0,
1824
+ )
1825
+ response.raise_for_status()
1826
+ data = response.json()
1827
+
1828
+ return CompletionResponse(
1829
+ content=data["choices"][0]["message"]["content"],
1830
+ model=data["model"],
1831
+ input_tokens=data["usage"]["prompt_tokens"],
1832
+ output_tokens=data["usage"]["completion_tokens"],
1833
+ finish_reason=data["choices"][0]["finish_reason"],
1834
+ raw=data,
1835
+ )
1836
+
1837
+ async def stream(
1838
+ self,
1839
+ messages: list[Message],
1840
+ model: str | None = None,
1841
+ max_tokens: int = 4096,
1842
+ temperature: float = 0.7,
1843
+ system: str | None = None,
1844
+ **kwargs,
1845
+ ) -> AsyncIterator[str]:
1846
+ model = model or self.default_model
1847
+
1848
+ msgs = []
1849
+ if system:
1850
+ msgs.append({"role": "system", "content": system})
1851
+ msgs.extend([{"role": m.role, "content": m.content} for m in messages])
1852
+
1853
+ headers = {}
1854
+ if self.api_key:
1855
+ headers["Authorization"] = f"Bearer {self.api_key}"
1856
+
1857
+ async with httpx.AsyncClient() as client:
1858
+ async with client.stream(
1859
+ "POST",
1860
+ f"{self.base_url}/chat/completions",
1861
+ headers=headers,
1862
+ json={
1863
+ "model": model,
1864
+ "messages": msgs,
1865
+ "max_tokens": max_tokens,
1866
+ "temperature": temperature,
1867
+ "stream": True,
1868
+ },
1869
+ timeout=120.0,
1870
+ ) as response:
1871
+ async for line in response.aiter_lines():
1872
+ if line.startswith("data: ") and line != "data: [DONE]":
1873
+ import json
1874
+ try:
1875
+ data = json.loads(line[6:])
1876
+ delta = data["choices"][0].get("delta", {})
1877
+ if "content" in delta:
1878
+ yield delta["content"]
1879
+ except json.JSONDecodeError:
1880
+ pass
1881
+
1882
+ def available_models(self) -> list[str]:
1883
+ return [] # LiteLLM can route to any model
1884
+
1885
+
1886
+ # ═══════════════════════════════════════════════════════════════════════════════
1887
+ # PROVIDER FACTORY
1888
+ # ═══════════════════════════════════════════════════════════════════════════════
1889
+
1890
+ PROVIDERS: dict[str, type[LLMProvider]] = {
1891
+ "anthropic": AnthropicProvider,
1892
+ "openai": OpenAIProvider,
1893
+ "gemini": GeminiProvider,
1894
+ "google": GeminiProvider,
1895
+ "mistral": MistralProvider,
1896
+ "groq": GroqProvider,
1897
+ "xai": XAIProvider,
1898
+ "grok": XAIProvider,
1899
+ "bedrock": BedrockProvider,
1900
+ "aws": BedrockProvider,
1901
+ "openrouter": OpenRouterProvider,
1902
+ "together": TogetherProvider,
1903
+ "perplexity": PerplexityProvider,
1904
+ "deepseek": DeepSeekProvider,
1905
+ "qwen": QwenProvider,
1906
+ "alibaba": QwenProvider,
1907
+ "cerebras": CerebrasProvider,
1908
+ "huggingface": HuggingFaceProvider,
1909
+ "hf": HuggingFaceProvider,
1910
+ "ollama": OllamaProvider,
1911
+ "vllm": VLLMProvider,
1912
+ "litellm": LiteLLMProvider,
1913
+ }
1914
+
1915
+
1916
+ def get_llm_provider(name: str | None = None, **kwargs) -> LLMProvider:
1917
+ """
1918
+ Get an LLM provider by name.
1919
+
1920
+ Args:
1921
+ name: Provider name (anthropic, openai, gemini, etc.)
1922
+ Defaults to HYPERCLAW_LLM_PROVIDER env var or "anthropic"
1923
+ **kwargs: Provider-specific configuration
1924
+
1925
+ Returns:
1926
+ Configured LLM provider instance
1927
+ """
1928
+ name = name or os.environ.get("HYPERCLAW_LLM_PROVIDER", "anthropic")
1929
+ name = name.lower()
1930
+
1931
+ if name not in PROVIDERS:
1932
+ available = ", ".join(sorted(PROVIDERS.keys()))
1933
+ raise ValueError(f"Unknown provider: {name}. Available: {available}")
1934
+
1935
+ return PROVIDERS[name](**kwargs)