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
@@ -0,0 +1,595 @@
1
+ """
2
+ HyperClaw Model Router
3
+ Intelligent routing to optimize cost, speed, and quality.
4
+ Routes simple tasks to ChatJimmy, complex tasks to Claude.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ from hyperclaw.api_utils import extract_text
10
+ import re
11
+ import time
12
+ from dataclasses import dataclass, field
13
+ from datetime import datetime, timedelta
14
+ from enum import Enum
15
+ from typing import Any, Optional
16
+
17
+ import httpx
18
+
19
+ from hyperclaw import cost_guard
20
+ from hyperclaw.cost_guard import CostCapReached
21
+
22
+ logger = logging.getLogger("hyperclaw.model_router")
23
+
24
+
25
+ class ModelTier(Enum):
26
+ """Model tiers by capability and cost."""
27
+ FAST = "fast" # ChatJimmy, Haiku - simple tasks, low cost
28
+ STANDARD = "standard" # Sonnet - balanced
29
+ PREMIUM = "premium" # Opus - complex reasoning
30
+
31
+
32
+ @dataclass
33
+ class ModelConfig:
34
+ """Configuration for a model."""
35
+ id: str
36
+ name: str
37
+ tier: ModelTier
38
+ provider: str # anthropic, chatjimmy, openai
39
+ cost_per_1k_input: float
40
+ cost_per_1k_output: float
41
+ max_tokens: int
42
+ latency_ms: int # typical latency
43
+ capabilities: list[str] = field(default_factory=list)
44
+ base_url: Optional[str] = None
45
+ api_key_env: str = ""
46
+
47
+
48
+ # Model ids come from models.yaml (providers.anthropic.models + the
49
+ # chatjimmy provider); the static strings below are only last-resort
50
+ # fallbacks when the registry is unavailable.
51
+ def _yaml_anthropic_models() -> dict:
52
+ try:
53
+ from hyperclaw.providers import registry
54
+ prov = registry().providers.get("anthropic")
55
+ return dict(prov.models) if prov else {}
56
+ except Exception:
57
+ return {}
58
+
59
+
60
+ def _yaml_chatjimmy() -> tuple:
61
+ """(model_id, base_url) for chatjimmy from the registry, or ("", "")."""
62
+ try:
63
+ from hyperclaw.providers import registry
64
+ prov = registry().providers.get("chatjimmy")
65
+ if prov:
66
+ return prov.model_for(), prov.base_url
67
+ except Exception:
68
+ pass
69
+ return "", ""
70
+
71
+
72
+ _ANTH = _yaml_anthropic_models()
73
+ _CJ_MODEL, _CJ_URL = _yaml_chatjimmy()
74
+
75
+ # Model registry
76
+ MODELS = {
77
+ # ChatJimmy - Fast & cheap for simple tasks
78
+ "chatjimmy": ModelConfig(
79
+ id=_CJ_MODEL or "chatjimmy",
80
+ name="ChatJimmy (Llama 3.1 8B)",
81
+ tier=ModelTier.FAST,
82
+ provider="chatjimmy",
83
+ cost_per_1k_input=0.00001,
84
+ cost_per_1k_output=0.00001,
85
+ max_tokens=2048,
86
+ latency_ms=50,
87
+ capabilities=["chat", "simple_qa", "classification", "extraction"],
88
+ base_url=_CJ_URL or None,
89
+ api_key_env="CHATJIMMY_API_KEY",
90
+ ),
91
+
92
+ # Claude Haiku - Fast Claude for moderate tasks
93
+ "claude-haiku": ModelConfig(
94
+ id=_ANTH.get("fast", "claude-haiku-4-5"),
95
+ name="Claude Haiku 4.5",
96
+ tier=ModelTier.FAST,
97
+ provider="anthropic",
98
+ cost_per_1k_input=0.001,
99
+ cost_per_1k_output=0.005,
100
+ max_tokens=4096,
101
+ latency_ms=500,
102
+ capabilities=["chat", "analysis", "coding", "writing"],
103
+ api_key_env="ANTHROPIC_API_KEY",
104
+ ),
105
+
106
+ # Claude Sonnet - Balanced for most tasks
107
+ "claude-sonnet": ModelConfig(
108
+ id=_ANTH.get("standard", "claude-sonnet-5"),
109
+ name="Claude Sonnet 5",
110
+ tier=ModelTier.STANDARD,
111
+ provider="anthropic",
112
+ cost_per_1k_input=0.002,
113
+ cost_per_1k_output=0.010,
114
+ max_tokens=8192,
115
+ latency_ms=1500,
116
+ capabilities=["chat", "analysis", "coding", "writing", "reasoning", "planning"],
117
+ api_key_env="ANTHROPIC_API_KEY",
118
+ ),
119
+
120
+ # Claude Opus - Premium for complex tasks
121
+ "claude-opus": ModelConfig(
122
+ id=_ANTH.get("premium", "claude-opus-5"),
123
+ name="Claude Opus 5",
124
+ tier=ModelTier.PREMIUM,
125
+ provider="anthropic",
126
+ cost_per_1k_input=0.005,
127
+ cost_per_1k_output=0.025,
128
+ max_tokens=8192,
129
+ latency_ms=3000,
130
+ capabilities=["chat", "analysis", "coding", "writing", "reasoning", "planning", "complex_reasoning", "research"],
131
+ api_key_env="ANTHROPIC_API_KEY",
132
+ ),
133
+ "claude-fable": ModelConfig(
134
+ id=_ANTH.get("flagship", "claude-fable-5"),
135
+ name="Claude Fable 5",
136
+ tier=ModelTier.PREMIUM,
137
+ provider="anthropic",
138
+ cost_per_1k_input=0.010,
139
+ cost_per_1k_output=0.050,
140
+ max_tokens=8192,
141
+ latency_ms=3000,
142
+ capabilities=["chat", "analysis", "coding", "writing", "reasoning", "planning", "complex_reasoning", "research"],
143
+ api_key_env="ANTHROPIC_API_KEY",
144
+ ),
145
+
146
+ # OpenRouter - Meta router for any model
147
+ "openrouter": ModelConfig(
148
+ id="openrouter/auto",
149
+ name="OpenRouter Auto",
150
+ tier=ModelTier.STANDARD,
151
+ provider="openrouter",
152
+ cost_per_1k_input=0.002,
153
+ cost_per_1k_output=0.006,
154
+ max_tokens=4096,
155
+ latency_ms=1500,
156
+ capabilities=["chat", "analysis", "coding", "writing"],
157
+ base_url="https://openrouter.ai/api/v1",
158
+ api_key_env="OPENROUTER_API_KEY",
159
+ ),
160
+ }
161
+
162
+ # Task complexity patterns
163
+ SIMPLE_TASK_PATTERNS = [
164
+ r"^(hi|hello|hey|thanks|thank you|ok|okay|yes|no|sure)[\s\.\!\?]*$",
165
+ r"^what('s| is) (the )?(time|date|day)",
166
+ r"^(list|show|get) (my )?(tasks|emails|events|calendar)",
167
+ r"^(send|forward) (a )?quick (message|note|reply)",
168
+ r"^(check|look up|find) (the )?(weather|status|price)",
169
+ r"^remind me",
170
+ r"^set (a )?(timer|alarm|reminder)",
171
+ r"^(open|launch|start) ",
172
+ r"^(yes|no|confirm|cancel|stop|done|finished)",
173
+ ]
174
+
175
+ COMPLEX_TASK_PATTERNS = [
176
+ r"(analyze|analysis|evaluate|assess|review)",
177
+ r"(strategy|strategic|plan|planning)",
178
+ r"(research|investigate|deep dive)",
179
+ r"(write|draft|compose) (a )?(report|proposal|document|article)",
180
+ r"(code|implement|build|develop|architect)",
181
+ r"(compare|contrast|difference|pros and cons)",
182
+ r"(explain|describe|elaborate) (in detail|thoroughly)",
183
+ r"(summarize|synthesize) (multiple|several|all)",
184
+ r"(debug|troubleshoot|diagnose)",
185
+ r"(optimize|improve|refactor)",
186
+ ]
187
+
188
+
189
+ @dataclass
190
+ class UsageStats:
191
+ """Track usage and costs."""
192
+ total_requests: int = 0
193
+ total_input_tokens: int = 0
194
+ total_output_tokens: int = 0
195
+ total_cost: float = 0.0
196
+ requests_by_model: dict = field(default_factory=dict)
197
+ cost_by_model: dict = field(default_factory=dict)
198
+ last_reset: datetime = field(default_factory=datetime.now)
199
+
200
+
201
+ class ModelRouter:
202
+ """
203
+ Intelligent model router that selects the best model based on:
204
+ - Task complexity
205
+ - Cost constraints
206
+ - Required capabilities
207
+ - Current load/latency
208
+ """
209
+
210
+ def __init__(self):
211
+ self.models = MODELS.copy()
212
+ self.stats = UsageStats()
213
+ self._clients: dict[str, Any] = {}
214
+ self._prefer_cheap = os.environ.get("PREFER_CHEAP_MODELS", "true").lower() == "true"
215
+
216
+ # The daily cap lives in the cost guard (hyperclaw/cost_guard.py); this
217
+ # attribute is kept for older callers. Setting it is an explicit cap
218
+ # change and is recorded in the activity log like `hyperclaw cost raise`.
219
+ @property
220
+ def _daily_budget(self) -> float:
221
+ return cost_guard.daily_cap()
222
+
223
+ @_daily_budget.setter
224
+ def _daily_budget(self, value: float) -> None:
225
+ cost_guard.raise_cap(cost_guard.DAILY, value, by="ModelRouter daily budget setting")
226
+
227
+ def _get_client(self, model_config: ModelConfig):
228
+ """Get or create API client for a model."""
229
+ provider = model_config.provider
230
+
231
+ if provider not in self._clients:
232
+ if provider == "anthropic":
233
+ import anthropic
234
+ api_key = os.environ.get(model_config.api_key_env, "")
235
+ self._clients[provider] = cost_guard.guard_client(
236
+ anthropic.Anthropic(api_key=api_key), "anthropic")
237
+
238
+ elif provider == "chatjimmy":
239
+ # ChatJimmy uses OpenAI-compatible API
240
+ api_key = os.environ.get(model_config.api_key_env, "")
241
+ self._clients[provider] = {
242
+ "base_url": model_config.base_url,
243
+ "api_key": api_key,
244
+ }
245
+
246
+ elif provider == "openrouter":
247
+ # OpenRouter uses OpenAI-compatible API
248
+ api_key = os.environ.get(model_config.api_key_env, "")
249
+ self._clients[provider] = {
250
+ "base_url": model_config.base_url,
251
+ "api_key": api_key,
252
+ }
253
+
254
+ return self._clients.get(provider)
255
+
256
+ def classify_complexity(self, message: str, context: dict = None) -> ModelTier:
257
+ """Classify task complexity to determine model tier."""
258
+ message_lower = message.lower().strip()
259
+
260
+ # Check for simple patterns
261
+ for pattern in SIMPLE_TASK_PATTERNS:
262
+ if re.search(pattern, message_lower, re.IGNORECASE):
263
+ return ModelTier.FAST
264
+
265
+ # Check for complex patterns
266
+ for pattern in COMPLEX_TASK_PATTERNS:
267
+ if re.search(pattern, message_lower, re.IGNORECASE):
268
+ return ModelTier.PREMIUM if len(message) > 500 else ModelTier.STANDARD
269
+
270
+ # Check message length as proxy for complexity
271
+ if len(message) < 50:
272
+ return ModelTier.FAST
273
+ elif len(message) < 200:
274
+ return ModelTier.STANDARD
275
+ else:
276
+ return ModelTier.STANDARD # Default to standard, not premium
277
+
278
+ def select_model(
279
+ self,
280
+ message: str,
281
+ required_capabilities: list[str] = None,
282
+ preferred_tier: ModelTier = None,
283
+ max_cost: float = None,
284
+ context: dict = None
285
+ ) -> ModelConfig:
286
+ """
287
+ Select the best model for a task.
288
+
289
+ Args:
290
+ message: The user message/task
291
+ required_capabilities: Capabilities the model must have
292
+ preferred_tier: Override automatic tier selection
293
+ max_cost: Maximum cost per request (approximate)
294
+ context: Additional context for routing
295
+
296
+ Returns:
297
+ Selected ModelConfig
298
+ """
299
+ # Determine tier
300
+ if preferred_tier:
301
+ tier = preferred_tier
302
+ else:
303
+ tier = self.classify_complexity(message, context)
304
+
305
+ # Budget: no downgrade here. Over a cap the cost guard refuses the
306
+ # call itself (CostCapReached) — a cheaper tier never keeps spending.
307
+
308
+ # Filter models by tier and capabilities
309
+ candidates = []
310
+ for model_id, config in self.models.items():
311
+ # Check tier
312
+ if self._prefer_cheap:
313
+ # Allow same tier or cheaper
314
+ if config.tier.value > tier.value:
315
+ continue
316
+ else:
317
+ # Exact tier match
318
+ if config.tier != tier:
319
+ continue
320
+
321
+ # Check capabilities
322
+ if required_capabilities:
323
+ if not all(cap in config.capabilities for cap in required_capabilities):
324
+ continue
325
+
326
+ # Check API key available
327
+ if config.api_key_env and not os.environ.get(config.api_key_env):
328
+ continue
329
+
330
+ candidates.append(config)
331
+
332
+ if not candidates:
333
+ # Fallback to any available model
334
+ for model_id, config in self.models.items():
335
+ if config.api_key_env and os.environ.get(config.api_key_env):
336
+ candidates.append(config)
337
+ break
338
+
339
+ if not candidates:
340
+ raise RuntimeError("No models available - check API keys")
341
+
342
+ # Sort by cost (cheapest first if prefer_cheap)
343
+ if self._prefer_cheap:
344
+ candidates.sort(key=lambda m: m.cost_per_1k_input + m.cost_per_1k_output)
345
+ else:
346
+ # Sort by capability (most capable first)
347
+ tier_order = {ModelTier.PREMIUM: 0, ModelTier.STANDARD: 1, ModelTier.FAST: 2}
348
+ candidates.sort(key=lambda m: tier_order.get(m.tier, 2))
349
+
350
+ selected = candidates[0]
351
+ logger.debug(f"Selected model: {selected.name} (tier={tier.value}, cost=${selected.cost_per_1k_input}/1k)")
352
+
353
+ return selected
354
+
355
+ async def call(
356
+ self,
357
+ message: str,
358
+ system: str = "",
359
+ history: list[dict] = None,
360
+ model_override: str = None,
361
+ **kwargs
362
+ ) -> tuple[str, dict]:
363
+ """
364
+ Call the appropriate model.
365
+
366
+ Returns:
367
+ Tuple of (response_text, metadata)
368
+ """
369
+ # Select model
370
+ if model_override and model_override in self.models:
371
+ model = self.models[model_override]
372
+ else:
373
+ model = self.select_model(message, context=kwargs.get("context"))
374
+
375
+ start_time = time.time()
376
+
377
+ try:
378
+ if model.provider == "anthropic":
379
+ response, metadata = await self._call_anthropic(model, message, system, history)
380
+ elif model.provider == "chatjimmy":
381
+ response, metadata = await self._call_chatjimmy(model, message, system, history)
382
+ elif model.provider == "openrouter":
383
+ response, metadata = await self._call_openrouter(model, message, system, history)
384
+ else:
385
+ raise ValueError(f"Unknown provider: {model.provider}")
386
+
387
+ # Track usage
388
+ latency_ms = int((time.time() - start_time) * 1000)
389
+ self._track_usage(model, metadata.get("input_tokens", 0), metadata.get("output_tokens", 0))
390
+
391
+ metadata["model"] = model.id
392
+ metadata["model_name"] = model.name
393
+ metadata["tier"] = model.tier.value
394
+ metadata["latency_ms"] = latency_ms
395
+
396
+ return response, metadata
397
+
398
+ except CostCapReached:
399
+ raise # at the cost cap: stop, never a cheaper model
400
+ except Exception as e:
401
+ logger.error(f"Model call failed ({model.name}): {e}")
402
+
403
+ # Try fallback to cheaper model (provider errors only, never the cost cap)
404
+ if model.tier != ModelTier.FAST:
405
+ logger.info("Attempting fallback to fast tier")
406
+ fallback = self.select_model(message, preferred_tier=ModelTier.FAST)
407
+ if fallback.id != model.id:
408
+ return await self.call(message, system, history, model_override=fallback.id, **kwargs)
409
+
410
+ raise
411
+
412
+ async def _call_anthropic(
413
+ self,
414
+ model: ModelConfig,
415
+ message: str,
416
+ system: str,
417
+ history: list[dict]
418
+ ) -> tuple[str, dict]:
419
+ """Call Anthropic API."""
420
+ import anthropic
421
+ import asyncio
422
+
423
+ client = self._get_client(model)
424
+
425
+ messages = []
426
+ if history:
427
+ messages.extend(history[-20:]) # Last 20 messages
428
+ messages.append({"role": "user", "content": message})
429
+
430
+ response = await asyncio.to_thread(
431
+ client.messages.create,
432
+ model=model.id,
433
+ max_tokens=_max_out(model),
434
+ system=system if system else None,
435
+ messages=messages,
436
+ )
437
+
438
+ return extract_text(response), {
439
+ "input_tokens": response.usage.input_tokens,
440
+ "output_tokens": response.usage.output_tokens,
441
+ }
442
+
443
+ async def _call_chatjimmy(
444
+ self,
445
+ model: ModelConfig,
446
+ message: str,
447
+ system: str,
448
+ history: list[dict]
449
+ ) -> tuple[str, dict]:
450
+ """Call ChatJimmy (OpenAI-compatible) API."""
451
+ config = self._get_client(model)
452
+
453
+ messages = []
454
+ if system:
455
+ messages.append({"role": "system", "content": system})
456
+ if history:
457
+ messages.extend(history[-10:]) # Smaller context for fast model
458
+ messages.append({"role": "user", "content": message})
459
+
460
+ with cost_guard.metered(model.provider, model.id, {"messages": messages},
461
+ _max_out(model)) as meter:
462
+ async with httpx.AsyncClient() as client:
463
+ response = await client.post(
464
+ f"{config['base_url']}/chat/completions",
465
+ headers={
466
+ "Authorization": f"Bearer {config['api_key']}",
467
+ "Content-Type": "application/json",
468
+ },
469
+ json={
470
+ "model": "llama-3.1-8b-instant",
471
+ "messages": messages,
472
+ "max_tokens": _max_out(model),
473
+ "temperature": 0.7,
474
+ },
475
+ timeout=30.0,
476
+ )
477
+ response.raise_for_status()
478
+ data = response.json()
479
+ meter.usage_from(data)
480
+
481
+ choice = data["choices"][0]
482
+ usage = data.get("usage", {})
483
+
484
+ return choice["message"]["content"], {
485
+ "input_tokens": usage.get("prompt_tokens", 0),
486
+ "output_tokens": usage.get("completion_tokens", 0),
487
+ }
488
+
489
+ async def _call_openrouter(
490
+ self,
491
+ model: ModelConfig,
492
+ message: str,
493
+ system: str,
494
+ history: list[dict]
495
+ ) -> tuple[str, dict]:
496
+ """Call OpenRouter API."""
497
+ config = self._get_client(model)
498
+
499
+ messages = []
500
+ if system:
501
+ messages.append({"role": "system", "content": system})
502
+ if history:
503
+ messages.extend(history[-15:])
504
+ messages.append({"role": "user", "content": message})
505
+
506
+ with cost_guard.metered(model.provider, model.id, {"messages": messages},
507
+ _max_out(model)) as meter:
508
+ async with httpx.AsyncClient() as client:
509
+ response = await client.post(
510
+ f"{config['base_url']}/chat/completions",
511
+ headers={
512
+ "Authorization": f"Bearer {config['api_key']}",
513
+ "Content-Type": "application/json",
514
+ "HTTP-Referer": "https://hyperclaw.ai",
515
+ "X-Title": "HyperClaw",
516
+ },
517
+ json={
518
+ "model": model.id,
519
+ "messages": messages,
520
+ "max_tokens": _max_out(model),
521
+ "temperature": 0.7,
522
+ },
523
+ timeout=60.0,
524
+ )
525
+ response.raise_for_status()
526
+ data = response.json()
527
+ meter.usage_from(data)
528
+
529
+ choice = data["choices"][0]
530
+ usage = data.get("usage", {})
531
+
532
+ return choice["message"]["content"], {
533
+ "input_tokens": usage.get("prompt_tokens", 0),
534
+ "output_tokens": usage.get("completion_tokens", 0),
535
+ }
536
+
537
+ def _track_usage(self, model: ModelConfig, input_tokens: int, output_tokens: int):
538
+ """Track usage statistics."""
539
+ cost = (
540
+ (input_tokens / 1000) * model.cost_per_1k_input +
541
+ (output_tokens / 1000) * model.cost_per_1k_output
542
+ )
543
+
544
+ self.stats.total_requests += 1
545
+ self.stats.total_input_tokens += input_tokens
546
+ self.stats.total_output_tokens += output_tokens
547
+ self.stats.total_cost += cost
548
+
549
+ if model.id not in self.stats.requests_by_model:
550
+ self.stats.requests_by_model[model.id] = 0
551
+ self.stats.cost_by_model[model.id] = 0.0
552
+
553
+ self.stats.requests_by_model[model.id] += 1
554
+ self.stats.cost_by_model[model.id] += cost
555
+
556
+ logger.debug(f"Usage: {model.name} - {input_tokens}+{output_tokens} tokens, ${cost:.4f}")
557
+
558
+ def get_stats(self) -> dict:
559
+ """Get usage statistics."""
560
+ return {
561
+ "total_requests": self.stats.total_requests,
562
+ "total_tokens": self.stats.total_input_tokens + self.stats.total_output_tokens,
563
+ "total_cost_usd": round(self.stats.total_cost, 4),
564
+ "daily_budget_usd": self._daily_budget,
565
+ "spent_today_usd": round(cost_guard.get_store().day_total(), 4),
566
+ "budget_remaining_usd": round(self._daily_budget - cost_guard.get_store().day_total(), 4),
567
+ "requests_by_model": self.stats.requests_by_model,
568
+ "cost_by_model": {k: round(v, 4) for k, v in self.stats.cost_by_model.items()},
569
+ "last_reset": self.stats.last_reset.isoformat(),
570
+ }
571
+
572
+ def reset_daily_stats(self):
573
+ """Reset daily statistics (call at midnight)."""
574
+ logger.info(f"Resetting daily stats. Previous: ${self.stats.total_cost:.2f}")
575
+ self.stats = UsageStats()
576
+
577
+
578
+ # Singleton
579
+ _router: Optional[ModelRouter] = None
580
+
581
+
582
+ def _max_out(model: "ModelConfig") -> int:
583
+ """The explicit max output for a router call: the model's own max_tokens,
584
+ capped by the cost guard (path "router") so no reservation exceeds the
585
+ per-task cap."""
586
+ return cost_guard.max_output("router", model.provider, model.id,
587
+ requested=model.max_tokens)
588
+
589
+
590
+ def get_model_router() -> ModelRouter:
591
+ """Get or create model router singleton."""
592
+ global _router
593
+ if _router is None:
594
+ _router = ModelRouter()
595
+ return _router
@@ -0,0 +1,72 @@
1
+ """
2
+ Model selector — tiered Claude routing.
3
+
4
+ Picks the right Claude model per request so the assistant switches across the whole ladder instead of
5
+ running one model for everything:
6
+
7
+ Routes by prompt complexity across THREE tiers (no Haiku, per policy):
8
+ SONNET (claude-sonnet-5) -> simple/everyday requests, quick questions, acks
9
+ OPUS (claude-opus-5) -> complex: analysis, planning, writing, research, judgement
10
+ FABLE (claude-fable-5, ACTIVE) -> hardest: code, architecture, quant, deep multi-step work; also the default backend model
11
+
12
+ Model IDs are env-overridable so the exact Fable id can be dropped in without code changes:
13
+ FABLE_MODEL the new model's API id (defaults to claude-fable-5)
14
+ HYPERCLAW_OPUS_MODEL / HYPERCLAW_SONNET_MODEL / HYPERCLAW_HAIKU_MODEL
15
+ Disable routing entirely with HYPERCLAW_MODEL_ROUTING=off (everything uses the passed default).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import re
22
+
23
+ DEFAULT_MODEL = "claude-fable-5"
24
+
25
+
26
+ def tiers() -> dict:
27
+ return {
28
+ # FABLE: claude-fable-5 (activated 2026-08-20). FABLE_MODEL env still overrides.
29
+ "fable": (os.environ.get("FABLE_MODEL", "").strip() or DEFAULT_MODEL),
30
+ "opus": os.environ.get("HYPERCLAW_OPUS_MODEL", "claude-opus-5"),
31
+ "sonnet": os.environ.get("HYPERCLAW_SONNET_MODEL", "claude-sonnet-5"),
32
+ "haiku": os.environ.get("HYPERCLAW_HAIKU_MODEL", "claude-haiku-4-5"),
33
+ }
34
+
35
+
36
+ # Hardest tier (FABLE): deep technical / engineering / quantitative / heavy multi-step work.
37
+ _FABLE_RE = re.compile(
38
+ r"\b(code|coding|debug|refactor|architect|architecture|algorithm|implement|build (a|an|the|me|out)|"
39
+ r"design (a|an|the|system|schema)|migrat|optimi[sz]|prove|valuation|forecast|financial model|"
40
+ r"legal|contract|root cause|deep dive|end.to.end|whole (system|codebase)|"
41
+ r"think (hard|deeply|carefully|step)|step.?by.?step|trading strateg|portfolio)\b",
42
+ re.IGNORECASE,
43
+ )
44
+
45
+ # Complex tier (OPUS): analysis, planning, writing, research, judgement.
46
+ _OPUS_RE = re.compile(
47
+ r"\b(analy[sz]e|analysis|plan|planning|research|draft|write (a|an|the|me)|essay|memo|report|"
48
+ r"compar|evaluat|assess|recommend|strateg|decision|review|investor|proposal|negotiat|"
49
+ r"summari[sz]e|explain (why|how)|reason|brief|pitch|roadmap|tradeoff|pros and cons)\b",
50
+ re.IGNORECASE,
51
+ )
52
+
53
+
54
+ def classify(message: str) -> str:
55
+ """Return a tier by prompt complexity: 'sonnet' (simple) | 'opus' (complex) | 'fable' (hardest).
56
+
57
+ Haiku tier is available via env override; default routing uses Sonnet/Opus/Fable."""
58
+ msg = (message or "").strip()
59
+ words = len(msg.split())
60
+ if _FABLE_RE.search(msg) or words >= 120:
61
+ return "fable"
62
+ if _OPUS_RE.search(msg) or words >= 30:
63
+ return "opus"
64
+ return "sonnet"
65
+
66
+
67
+ def pick_model(message: str, default: str = None) -> str:
68
+ """Pick a concrete model id for this request. Falls back to `default` when routing is off."""
69
+ default = default or DEFAULT_MODEL
70
+ if os.environ.get("HYPERCLAW_MODEL_ROUTING", "on").lower() in ("off", "0", "false", "no"):
71
+ return default
72
+ return tiers()[classify(message)]