calvyn-code 0.14.0

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 (1718) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/README.zh-CN.md +180 -0
  4. package/acp_adapter/__init__.py +1 -0
  5. package/acp_adapter/__main__.py +5 -0
  6. package/acp_adapter/auth.py +68 -0
  7. package/acp_adapter/bootstrap/__init__.py +0 -0
  8. package/acp_adapter/bootstrap/bootstrap_browser_tools.ps1 +288 -0
  9. package/acp_adapter/bootstrap/bootstrap_browser_tools.sh +399 -0
  10. package/acp_adapter/entry.py +292 -0
  11. package/acp_adapter/events.py +265 -0
  12. package/acp_adapter/permissions.py +148 -0
  13. package/acp_adapter/server.py +1713 -0
  14. package/acp_adapter/session.py +629 -0
  15. package/acp_adapter/tools.py +1180 -0
  16. package/agent/__init__.py +6 -0
  17. package/agent/__pycache__/__init__.cpython-312.pyc +0 -0
  18. package/agent/__pycache__/account_usage.cpython-312.pyc +0 -0
  19. package/agent/__pycache__/anthropic_adapter.cpython-312.pyc +0 -0
  20. package/agent/__pycache__/async_utils.cpython-312.pyc +0 -0
  21. package/agent/__pycache__/auxiliary_client.cpython-312.pyc +0 -0
  22. package/agent/__pycache__/codex_responses_adapter.cpython-312.pyc +0 -0
  23. package/agent/__pycache__/context_compressor.cpython-312.pyc +0 -0
  24. package/agent/__pycache__/context_engine.cpython-312.pyc +0 -0
  25. package/agent/__pycache__/context_references.cpython-312.pyc +0 -0
  26. package/agent/__pycache__/credential_pool.cpython-312.pyc +0 -0
  27. package/agent/__pycache__/curator.cpython-312.pyc +0 -0
  28. package/agent/__pycache__/display.cpython-312.pyc +0 -0
  29. package/agent/__pycache__/error_classifier.cpython-312.pyc +0 -0
  30. package/agent/__pycache__/file_safety.cpython-312.pyc +0 -0
  31. package/agent/__pycache__/google_code_assist.cpython-312.pyc +0 -0
  32. package/agent/__pycache__/google_oauth.cpython-312.pyc +0 -0
  33. package/agent/__pycache__/i18n.cpython-312.pyc +0 -0
  34. package/agent/__pycache__/image_gen_provider.cpython-312.pyc +0 -0
  35. package/agent/__pycache__/image_gen_registry.cpython-312.pyc +0 -0
  36. package/agent/__pycache__/insights.cpython-312.pyc +0 -0
  37. package/agent/__pycache__/lmstudio_reasoning.cpython-312.pyc +0 -0
  38. package/agent/__pycache__/manual_compression_feedback.cpython-312.pyc +0 -0
  39. package/agent/__pycache__/markdown_tables.cpython-312.pyc +0 -0
  40. package/agent/__pycache__/memory_manager.cpython-312.pyc +0 -0
  41. package/agent/__pycache__/memory_provider.cpython-312.pyc +0 -0
  42. package/agent/__pycache__/model_metadata.cpython-312.pyc +0 -0
  43. package/agent/__pycache__/models_dev.cpython-312.pyc +0 -0
  44. package/agent/__pycache__/moonshot_schema.cpython-312.pyc +0 -0
  45. package/agent/__pycache__/onboarding.cpython-312.pyc +0 -0
  46. package/agent/__pycache__/portal_tags.cpython-312.pyc +0 -0
  47. package/agent/__pycache__/prompt_builder.cpython-312.pyc +0 -0
  48. package/agent/__pycache__/prompt_caching.cpython-312.pyc +0 -0
  49. package/agent/__pycache__/redact.cpython-312.pyc +0 -0
  50. package/agent/__pycache__/retry_utils.cpython-312.pyc +0 -0
  51. package/agent/__pycache__/shell_hooks.cpython-312.pyc +0 -0
  52. package/agent/__pycache__/skill_commands.cpython-312.pyc +0 -0
  53. package/agent/__pycache__/skill_preprocessing.cpython-312.pyc +0 -0
  54. package/agent/__pycache__/skill_utils.cpython-312.pyc +0 -0
  55. package/agent/__pycache__/subdirectory_hints.cpython-312.pyc +0 -0
  56. package/agent/__pycache__/think_scrubber.cpython-312.pyc +0 -0
  57. package/agent/__pycache__/title_generator.cpython-312.pyc +0 -0
  58. package/agent/__pycache__/tool_guardrails.cpython-312.pyc +0 -0
  59. package/agent/__pycache__/tool_result_classification.cpython-312.pyc +0 -0
  60. package/agent/__pycache__/trajectory.cpython-312.pyc +0 -0
  61. package/agent/__pycache__/usage_pricing.cpython-312.pyc +0 -0
  62. package/agent/__pycache__/video_gen_provider.cpython-312.pyc +0 -0
  63. package/agent/__pycache__/video_gen_registry.cpython-312.pyc +0 -0
  64. package/agent/__pycache__/web_search_provider.cpython-312.pyc +0 -0
  65. package/agent/__pycache__/web_search_registry.cpython-312.pyc +0 -0
  66. package/agent/account_usage.py +326 -0
  67. package/agent/anthropic_adapter.py +2087 -0
  68. package/agent/async_utils.py +68 -0
  69. package/agent/auxiliary_client.py +4893 -0
  70. package/agent/bedrock_adapter.py +1276 -0
  71. package/agent/codex_responses_adapter.py +1084 -0
  72. package/agent/context_compressor.py +1583 -0
  73. package/agent/context_engine.py +211 -0
  74. package/agent/context_references.py +519 -0
  75. package/agent/copilot_acp_client.py +684 -0
  76. package/agent/credential_pool.py +1780 -0
  77. package/agent/credential_sources.py +449 -0
  78. package/agent/curator.py +1782 -0
  79. package/agent/curator_backup.py +694 -0
  80. package/agent/display.py +987 -0
  81. package/agent/error_classifier.py +1058 -0
  82. package/agent/file_safety.py +112 -0
  83. package/agent/gemini_cloudcode_adapter.py +909 -0
  84. package/agent/gemini_native_adapter.py +971 -0
  85. package/agent/gemini_schema.py +99 -0
  86. package/agent/google_code_assist.py +452 -0
  87. package/agent/google_oauth.py +1062 -0
  88. package/agent/i18n.py +258 -0
  89. package/agent/image_gen_provider.py +243 -0
  90. package/agent/image_gen_registry.py +145 -0
  91. package/agent/image_routing.py +301 -0
  92. package/agent/insights.py +931 -0
  93. package/agent/lmstudio_reasoning.py +48 -0
  94. package/agent/lsp/__init__.py +106 -0
  95. package/agent/lsp/__pycache__/__init__.cpython-312.pyc +0 -0
  96. package/agent/lsp/__pycache__/cli.cpython-312.pyc +0 -0
  97. package/agent/lsp/__pycache__/client.cpython-312.pyc +0 -0
  98. package/agent/lsp/__pycache__/eventlog.cpython-312.pyc +0 -0
  99. package/agent/lsp/__pycache__/manager.cpython-312.pyc +0 -0
  100. package/agent/lsp/__pycache__/protocol.cpython-312.pyc +0 -0
  101. package/agent/lsp/__pycache__/servers.cpython-312.pyc +0 -0
  102. package/agent/lsp/__pycache__/workspace.cpython-312.pyc +0 -0
  103. package/agent/lsp/cli.py +308 -0
  104. package/agent/lsp/client.py +930 -0
  105. package/agent/lsp/eventlog.py +213 -0
  106. package/agent/lsp/install.py +376 -0
  107. package/agent/lsp/manager.py +644 -0
  108. package/agent/lsp/protocol.py +196 -0
  109. package/agent/lsp/range_shift.py +149 -0
  110. package/agent/lsp/reporter.py +78 -0
  111. package/agent/lsp/servers.py +1040 -0
  112. package/agent/lsp/workspace.py +223 -0
  113. package/agent/manual_compression_feedback.py +49 -0
  114. package/agent/markdown_tables.py +309 -0
  115. package/agent/memory_manager.py +556 -0
  116. package/agent/memory_provider.py +279 -0
  117. package/agent/model_metadata.py +1827 -0
  118. package/agent/models_dev.py +724 -0
  119. package/agent/moonshot_schema.py +231 -0
  120. package/agent/nous_rate_guard.py +326 -0
  121. package/agent/onboarding.py +193 -0
  122. package/agent/plugin_llm.py +1046 -0
  123. package/agent/portal_tags.py +64 -0
  124. package/agent/prompt_builder.py +1457 -0
  125. package/agent/prompt_caching.py +79 -0
  126. package/agent/rate_limit_tracker.py +246 -0
  127. package/agent/redact.py +403 -0
  128. package/agent/retry_utils.py +57 -0
  129. package/agent/shell_hooks.py +837 -0
  130. package/agent/skill_commands.py +502 -0
  131. package/agent/skill_preprocessing.py +131 -0
  132. package/agent/skill_utils.py +512 -0
  133. package/agent/subdirectory_hints.py +224 -0
  134. package/agent/think_scrubber.py +386 -0
  135. package/agent/title_generator.py +171 -0
  136. package/agent/tool_guardrails.py +458 -0
  137. package/agent/tool_result_classification.py +26 -0
  138. package/agent/trajectory.py +56 -0
  139. package/agent/transports/__init__.py +68 -0
  140. package/agent/transports/__pycache__/__init__.cpython-312.pyc +0 -0
  141. package/agent/transports/__pycache__/anthropic.cpython-312.pyc +0 -0
  142. package/agent/transports/__pycache__/base.cpython-312.pyc +0 -0
  143. package/agent/transports/__pycache__/bedrock.cpython-312.pyc +0 -0
  144. package/agent/transports/__pycache__/chat_completions.cpython-312.pyc +0 -0
  145. package/agent/transports/__pycache__/codex.cpython-312.pyc +0 -0
  146. package/agent/transports/__pycache__/types.cpython-312.pyc +0 -0
  147. package/agent/transports/anthropic.py +179 -0
  148. package/agent/transports/base.py +89 -0
  149. package/agent/transports/bedrock.py +154 -0
  150. package/agent/transports/chat_completions.py +614 -0
  151. package/agent/transports/codex.py +283 -0
  152. package/agent/transports/codex_app_server.py +368 -0
  153. package/agent/transports/codex_app_server_session.py +810 -0
  154. package/agent/transports/codex_event_projector.py +312 -0
  155. package/agent/transports/hermes_tools_mcp_server.py +233 -0
  156. package/agent/transports/types.py +162 -0
  157. package/agent/usage_pricing.py +877 -0
  158. package/agent/video_gen_provider.py +300 -0
  159. package/agent/video_gen_registry.py +117 -0
  160. package/agent/web_search_provider.py +221 -0
  161. package/agent/web_search_registry.py +262 -0
  162. package/assets/banner.png +0 -0
  163. package/batch_runner.py +1303 -0
  164. package/bin/calvyn.js +67 -0
  165. package/calvyn_bootstrap.py +130 -0
  166. package/calvyn_constants.py +346 -0
  167. package/calvyn_logging.py +390 -0
  168. package/calvyn_state.py +2967 -0
  169. package/calvyn_time.py +105 -0
  170. package/cli.py +14160 -0
  171. package/cron/__init__.py +42 -0
  172. package/cron/__pycache__/__init__.cpython-312.pyc +0 -0
  173. package/cron/__pycache__/jobs.cpython-312.pyc +0 -0
  174. package/cron/__pycache__/scheduler.cpython-312.pyc +0 -0
  175. package/cron/jobs.py +1160 -0
  176. package/cron/scheduler.py +1832 -0
  177. package/gateway/__init__.py +35 -0
  178. package/gateway/__pycache__/__init__.cpython-312.pyc +0 -0
  179. package/gateway/__pycache__/channel_directory.cpython-312.pyc +0 -0
  180. package/gateway/__pycache__/config.cpython-312.pyc +0 -0
  181. package/gateway/__pycache__/delivery.cpython-312.pyc +0 -0
  182. package/gateway/__pycache__/display_config.cpython-312.pyc +0 -0
  183. package/gateway/__pycache__/hooks.cpython-312.pyc +0 -0
  184. package/gateway/__pycache__/pairing.cpython-312.pyc +0 -0
  185. package/gateway/__pycache__/platform_registry.cpython-312.pyc +0 -0
  186. package/gateway/__pycache__/restart.cpython-312.pyc +0 -0
  187. package/gateway/__pycache__/run.cpython-312.pyc +0 -0
  188. package/gateway/__pycache__/runtime_footer.cpython-312.pyc +0 -0
  189. package/gateway/__pycache__/session.cpython-312.pyc +0 -0
  190. package/gateway/__pycache__/session_context.cpython-312.pyc +0 -0
  191. package/gateway/__pycache__/shutdown_forensics.cpython-312.pyc +0 -0
  192. package/gateway/__pycache__/slash_access.cpython-312.pyc +0 -0
  193. package/gateway/__pycache__/status.cpython-312.pyc +0 -0
  194. package/gateway/__pycache__/stream_consumer.cpython-312.pyc +0 -0
  195. package/gateway/__pycache__/whatsapp_identity.cpython-312.pyc +0 -0
  196. package/gateway/assets/telegram-botfather-threads-settings.jpg +0 -0
  197. package/gateway/builtin_hooks/__init__.py +1 -0
  198. package/gateway/channel_directory.py +357 -0
  199. package/gateway/config.py +1873 -0
  200. package/gateway/delivery.py +258 -0
  201. package/gateway/display_config.py +206 -0
  202. package/gateway/hooks.py +210 -0
  203. package/gateway/mirror.py +179 -0
  204. package/gateway/pairing.py +322 -0
  205. package/gateway/platform_registry.py +260 -0
  206. package/gateway/platforms/ADDING_A_PLATFORM.md +374 -0
  207. package/gateway/platforms/__init__.py +45 -0
  208. package/gateway/platforms/__pycache__/__init__.cpython-312.pyc +0 -0
  209. package/gateway/platforms/__pycache__/base.cpython-312.pyc +0 -0
  210. package/gateway/platforms/__pycache__/helpers.cpython-312.pyc +0 -0
  211. package/gateway/platforms/__pycache__/telegram.cpython-312.pyc +0 -0
  212. package/gateway/platforms/__pycache__/telegram_network.cpython-312.pyc +0 -0
  213. package/gateway/platforms/__pycache__/yuanbao.cpython-312.pyc +0 -0
  214. package/gateway/platforms/__pycache__/yuanbao_media.cpython-312.pyc +0 -0
  215. package/gateway/platforms/__pycache__/yuanbao_proto.cpython-312.pyc +0 -0
  216. package/gateway/platforms/_http_client_limits.py +84 -0
  217. package/gateway/platforms/api_server.py +3488 -0
  218. package/gateway/platforms/base.py +3747 -0
  219. package/gateway/platforms/bluebubbles.py +937 -0
  220. package/gateway/platforms/dingtalk.py +1473 -0
  221. package/gateway/platforms/discord.py +5584 -0
  222. package/gateway/platforms/email.py +773 -0
  223. package/gateway/platforms/feishu.py +5059 -0
  224. package/gateway/platforms/feishu_comment.py +1382 -0
  225. package/gateway/platforms/feishu_comment_rules.py +430 -0
  226. package/gateway/platforms/helpers.py +279 -0
  227. package/gateway/platforms/homeassistant.py +449 -0
  228. package/gateway/platforms/matrix.py +2777 -0
  229. package/gateway/platforms/mattermost.py +852 -0
  230. package/gateway/platforms/msgraph_webhook.py +397 -0
  231. package/gateway/platforms/qqbot/__init__.py +91 -0
  232. package/gateway/platforms/qqbot/adapter.py +3072 -0
  233. package/gateway/platforms/qqbot/chunked_upload.py +602 -0
  234. package/gateway/platforms/qqbot/constants.py +74 -0
  235. package/gateway/platforms/qqbot/crypto.py +45 -0
  236. package/gateway/platforms/qqbot/keyboards.py +473 -0
  237. package/gateway/platforms/qqbot/onboard.py +220 -0
  238. package/gateway/platforms/qqbot/utils.py +71 -0
  239. package/gateway/platforms/signal.py +1518 -0
  240. package/gateway/platforms/signal_rate_limit.py +369 -0
  241. package/gateway/platforms/slack.py +3028 -0
  242. package/gateway/platforms/sms.py +377 -0
  243. package/gateway/platforms/telegram.py +4836 -0
  244. package/gateway/platforms/telegram_network.py +249 -0
  245. package/gateway/platforms/webhook.py +806 -0
  246. package/gateway/platforms/wecom.py +1610 -0
  247. package/gateway/platforms/wecom_callback.py +403 -0
  248. package/gateway/platforms/wecom_crypto.py +142 -0
  249. package/gateway/platforms/weixin.py +2170 -0
  250. package/gateway/platforms/whatsapp.py +1283 -0
  251. package/gateway/platforms/yuanbao.py +4873 -0
  252. package/gateway/platforms/yuanbao_media.py +645 -0
  253. package/gateway/platforms/yuanbao_proto.py +1209 -0
  254. package/gateway/platforms/yuanbao_sticker.py +558 -0
  255. package/gateway/restart.py +20 -0
  256. package/gateway/run.py +17074 -0
  257. package/gateway/runtime_footer.py +150 -0
  258. package/gateway/session.py +1399 -0
  259. package/gateway/session_context.py +156 -0
  260. package/gateway/shutdown_forensics.py +462 -0
  261. package/gateway/slash_access.py +229 -0
  262. package/gateway/status.py +972 -0
  263. package/gateway/sticker_cache.py +111 -0
  264. package/gateway/stream_consumer.py +1286 -0
  265. package/gateway/whatsapp_identity.py +156 -0
  266. package/hermes_cli/__init__.py +47 -0
  267. package/hermes_cli/__pycache__/__init__.cpython-312.pyc +0 -0
  268. package/hermes_cli/__pycache__/_parser.cpython-312.pyc +0 -0
  269. package/hermes_cli/__pycache__/auth.cpython-312.pyc +0 -0
  270. package/hermes_cli/__pycache__/banner.cpython-312.pyc +0 -0
  271. package/hermes_cli/__pycache__/browser_connect.cpython-312.pyc +0 -0
  272. package/hermes_cli/__pycache__/callbacks.cpython-312.pyc +0 -0
  273. package/hermes_cli/__pycache__/checkpoints.cpython-312.pyc +0 -0
  274. package/hermes_cli/__pycache__/cli_output.cpython-312.pyc +0 -0
  275. package/hermes_cli/__pycache__/codex_models.cpython-312.pyc +0 -0
  276. package/hermes_cli/__pycache__/codex_runtime_switch.cpython-312.pyc +0 -0
  277. package/hermes_cli/__pycache__/colors.cpython-312.pyc +0 -0
  278. package/hermes_cli/__pycache__/commands.cpython-312.pyc +0 -0
  279. package/hermes_cli/__pycache__/config.cpython-312.pyc +0 -0
  280. package/hermes_cli/__pycache__/copilot_auth.cpython-312.pyc +0 -0
  281. package/hermes_cli/__pycache__/curator.cpython-312.pyc +0 -0
  282. package/hermes_cli/__pycache__/curses_ui.cpython-312.pyc +0 -0
  283. package/hermes_cli/__pycache__/debug.cpython-312.pyc +0 -0
  284. package/hermes_cli/__pycache__/default_soul.cpython-312.pyc +0 -0
  285. package/hermes_cli/__pycache__/env_loader.cpython-312.pyc +0 -0
  286. package/hermes_cli/__pycache__/fallback_cmd.cpython-312.pyc +0 -0
  287. package/hermes_cli/__pycache__/gateway.cpython-312.pyc +0 -0
  288. package/hermes_cli/__pycache__/gateway_windows.cpython-312.pyc +0 -0
  289. package/hermes_cli/__pycache__/goals.cpython-312.pyc +0 -0
  290. package/hermes_cli/__pycache__/inventory.cpython-312.pyc +0 -0
  291. package/hermes_cli/__pycache__/kanban.cpython-312.pyc +0 -0
  292. package/hermes_cli/__pycache__/kanban_db.cpython-312.pyc +0 -0
  293. package/hermes_cli/__pycache__/main.cpython-312.pyc +0 -0
  294. package/hermes_cli/__pycache__/model_catalog.cpython-312.pyc +0 -0
  295. package/hermes_cli/__pycache__/model_normalize.cpython-312.pyc +0 -0
  296. package/hermes_cli/__pycache__/model_switch.cpython-312.pyc +0 -0
  297. package/hermes_cli/__pycache__/models.cpython-312.pyc +0 -0
  298. package/hermes_cli/__pycache__/nous_subscription.cpython-312.pyc +0 -0
  299. package/hermes_cli/__pycache__/pairing.cpython-312.pyc +0 -0
  300. package/hermes_cli/__pycache__/platforms.cpython-312.pyc +0 -0
  301. package/hermes_cli/__pycache__/plugins.cpython-312.pyc +0 -0
  302. package/hermes_cli/__pycache__/profiles.cpython-312.pyc +0 -0
  303. package/hermes_cli/__pycache__/providers.cpython-312.pyc +0 -0
  304. package/hermes_cli/__pycache__/pt_input_extras.cpython-312.pyc +0 -0
  305. package/hermes_cli/__pycache__/runtime_provider.cpython-312.pyc +0 -0
  306. package/hermes_cli/__pycache__/security_advisories.cpython-312.pyc +0 -0
  307. package/hermes_cli/__pycache__/setup.cpython-312.pyc +0 -0
  308. package/hermes_cli/__pycache__/skills_hub.cpython-312.pyc +0 -0
  309. package/hermes_cli/__pycache__/skin_engine.cpython-312.pyc +0 -0
  310. package/hermes_cli/__pycache__/stdio.cpython-312.pyc +0 -0
  311. package/hermes_cli/__pycache__/timeouts.cpython-312.pyc +0 -0
  312. package/hermes_cli/__pycache__/tips.cpython-312.pyc +0 -0
  313. package/hermes_cli/__pycache__/tools_config.cpython-312.pyc +0 -0
  314. package/hermes_cli/__pycache__/voice.cpython-312.pyc +0 -0
  315. package/hermes_cli/_parser.py +365 -0
  316. package/hermes_cli/_subprocess_compat.py +175 -0
  317. package/hermes_cli/auth.py +6299 -0
  318. package/hermes_cli/auth_commands.py +749 -0
  319. package/hermes_cli/azure_detect.py +300 -0
  320. package/hermes_cli/backup.py +938 -0
  321. package/hermes_cli/banner.py +703 -0
  322. package/hermes_cli/browser_connect.py +139 -0
  323. package/hermes_cli/callbacks.py +243 -0
  324. package/hermes_cli/checkpoints.py +244 -0
  325. package/hermes_cli/claw.py +810 -0
  326. package/hermes_cli/cli_output.py +78 -0
  327. package/hermes_cli/clipboard.py +495 -0
  328. package/hermes_cli/codex_models.py +198 -0
  329. package/hermes_cli/codex_runtime_plugin_migration.py +757 -0
  330. package/hermes_cli/codex_runtime_switch.py +266 -0
  331. package/hermes_cli/colors.py +38 -0
  332. package/hermes_cli/commands.py +1728 -0
  333. package/hermes_cli/completion.py +315 -0
  334. package/hermes_cli/config.py +5382 -0
  335. package/hermes_cli/copilot_auth.py +392 -0
  336. package/hermes_cli/cron.py +313 -0
  337. package/hermes_cli/curator.py +598 -0
  338. package/hermes_cli/curses_ui.py +472 -0
  339. package/hermes_cli/debug.py +747 -0
  340. package/hermes_cli/default_soul.py +11 -0
  341. package/hermes_cli/dep_ensure.py +107 -0
  342. package/hermes_cli/dingtalk_auth.py +293 -0
  343. package/hermes_cli/doctor.py +1863 -0
  344. package/hermes_cli/dump.py +326 -0
  345. package/hermes_cli/env_loader.py +175 -0
  346. package/hermes_cli/fallback_cmd.py +361 -0
  347. package/hermes_cli/gateway.py +5422 -0
  348. package/hermes_cli/gateway_windows.py +692 -0
  349. package/hermes_cli/goals.py +757 -0
  350. package/hermes_cli/hooks.py +385 -0
  351. package/hermes_cli/inventory.py +240 -0
  352. package/hermes_cli/kanban.py +2252 -0
  353. package/hermes_cli/kanban_db.py +4840 -0
  354. package/hermes_cli/kanban_diagnostics.py +776 -0
  355. package/hermes_cli/kanban_specify.py +266 -0
  356. package/hermes_cli/logs.py +391 -0
  357. package/hermes_cli/main.py +12396 -0
  358. package/hermes_cli/mcp_config.py +781 -0
  359. package/hermes_cli/memory_setup.py +465 -0
  360. package/hermes_cli/model_catalog.py +330 -0
  361. package/hermes_cli/model_normalize.py +473 -0
  362. package/hermes_cli/model_switch.py +1777 -0
  363. package/hermes_cli/models.py +3789 -0
  364. package/hermes_cli/nous_subscription.py +799 -0
  365. package/hermes_cli/oneshot.py +351 -0
  366. package/hermes_cli/pairing.py +115 -0
  367. package/hermes_cli/platforms.py +83 -0
  368. package/hermes_cli/plugins.py +1562 -0
  369. package/hermes_cli/plugins_cmd.py +1587 -0
  370. package/hermes_cli/profile_distribution.py +703 -0
  371. package/hermes_cli/profiles.py +1319 -0
  372. package/hermes_cli/providers.py +720 -0
  373. package/hermes_cli/proxy/__init__.py +20 -0
  374. package/hermes_cli/proxy/adapters/__init__.py +35 -0
  375. package/hermes_cli/proxy/adapters/base.py +94 -0
  376. package/hermes_cli/proxy/adapters/nous_portal.py +137 -0
  377. package/hermes_cli/proxy/cli.py +141 -0
  378. package/hermes_cli/proxy/server.py +265 -0
  379. package/hermes_cli/pt_input_extras.py +83 -0
  380. package/hermes_cli/pty_bridge.py +237 -0
  381. package/hermes_cli/relaunch.py +205 -0
  382. package/hermes_cli/runtime_provider.py +1428 -0
  383. package/hermes_cli/security_advisories.py +452 -0
  384. package/hermes_cli/setup.py +3559 -0
  385. package/hermes_cli/skills_config.py +177 -0
  386. package/hermes_cli/skills_hub.py +1595 -0
  387. package/hermes_cli/skin_engine.py +929 -0
  388. package/hermes_cli/slack_cli.py +160 -0
  389. package/hermes_cli/status.py +550 -0
  390. package/hermes_cli/stdio.py +252 -0
  391. package/hermes_cli/timeouts.py +82 -0
  392. package/hermes_cli/tips.py +487 -0
  393. package/hermes_cli/tools_config.py +3151 -0
  394. package/hermes_cli/uninstall.py +681 -0
  395. package/hermes_cli/vercel_auth.py +70 -0
  396. package/hermes_cli/voice.py +846 -0
  397. package/hermes_cli/web_server.py +4438 -0
  398. package/hermes_cli/webhook.py +275 -0
  399. package/locales/af.yaml +350 -0
  400. package/locales/de.yaml +350 -0
  401. package/locales/en.yaml +365 -0
  402. package/locales/es.yaml +350 -0
  403. package/locales/fr.yaml +350 -0
  404. package/locales/ga.yaml +354 -0
  405. package/locales/hu.yaml +350 -0
  406. package/locales/it.yaml +350 -0
  407. package/locales/ja.yaml +350 -0
  408. package/locales/ko.yaml +350 -0
  409. package/locales/pt.yaml +350 -0
  410. package/locales/ru.yaml +350 -0
  411. package/locales/tr.yaml +350 -0
  412. package/locales/uk.yaml +350 -0
  413. package/locales/zh-hant.yaml +350 -0
  414. package/locales/zh.yaml +350 -0
  415. package/mcp_serve.py +898 -0
  416. package/model_tools.py +899 -0
  417. package/optional-skills/DESCRIPTION.md +24 -0
  418. package/optional-skills/autonomous-ai-agents/DESCRIPTION.md +2 -0
  419. package/optional-skills/autonomous-ai-agents/blackbox/SKILL.md +144 -0
  420. package/optional-skills/autonomous-ai-agents/honcho/SKILL.md +431 -0
  421. package/optional-skills/blockchain/evm/SKILL.md +211 -0
  422. package/optional-skills/blockchain/evm/scripts/evm_client.py +1508 -0
  423. package/optional-skills/blockchain/hyperliquid/SKILL.md +211 -0
  424. package/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1660 -0
  425. package/optional-skills/blockchain/solana/SKILL.md +208 -0
  426. package/optional-skills/blockchain/solana/scripts/solana_client.py +698 -0
  427. package/optional-skills/communication/DESCRIPTION.md +1 -0
  428. package/optional-skills/communication/one-three-one-rule/SKILL.md +104 -0
  429. package/optional-skills/creative/blender-mcp/SKILL.md +117 -0
  430. package/optional-skills/creative/concept-diagrams/SKILL.md +362 -0
  431. package/optional-skills/creative/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
  432. package/optional-skills/creative/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
  433. package/optional-skills/creative/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
  434. package/optional-skills/creative/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
  435. package/optional-skills/creative/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
  436. package/optional-skills/creative/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
  437. package/optional-skills/creative/concept-diagrams/examples/electricity-grid-flow.md +182 -0
  438. package/optional-skills/creative/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
  439. package/optional-skills/creative/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
  440. package/optional-skills/creative/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
  441. package/optional-skills/creative/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
  442. package/optional-skills/creative/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
  443. package/optional-skills/creative/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
  444. package/optional-skills/creative/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
  445. package/optional-skills/creative/concept-diagrams/examples/wind-turbine-structure.md +338 -0
  446. package/optional-skills/creative/concept-diagrams/references/dashboard-patterns.md +43 -0
  447. package/optional-skills/creative/concept-diagrams/references/infrastructure-patterns.md +144 -0
  448. package/optional-skills/creative/concept-diagrams/references/physical-shape-cookbook.md +42 -0
  449. package/optional-skills/creative/concept-diagrams/templates/template.html +174 -0
  450. package/optional-skills/creative/hyperframes/SKILL.md +191 -0
  451. package/optional-skills/creative/hyperframes/references/cli.md +185 -0
  452. package/optional-skills/creative/hyperframes/references/composition.md +129 -0
  453. package/optional-skills/creative/hyperframes/references/features.md +289 -0
  454. package/optional-skills/creative/hyperframes/references/gsap.md +136 -0
  455. package/optional-skills/creative/hyperframes/references/troubleshooting.md +137 -0
  456. package/optional-skills/creative/hyperframes/references/website-to-video.md +145 -0
  457. package/optional-skills/creative/hyperframes/scripts/setup.sh +135 -0
  458. package/optional-skills/creative/kanban-video-orchestrator/SKILL.md +207 -0
  459. package/optional-skills/creative/kanban-video-orchestrator/assets/brief.md.tmpl +79 -0
  460. package/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +185 -0
  461. package/optional-skills/creative/kanban-video-orchestrator/assets/soul.md.tmpl +38 -0
  462. package/optional-skills/creative/kanban-video-orchestrator/references/examples.md +227 -0
  463. package/optional-skills/creative/kanban-video-orchestrator/references/intake.md +166 -0
  464. package/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +276 -0
  465. package/optional-skills/creative/kanban-video-orchestrator/references/monitoring.md +180 -0
  466. package/optional-skills/creative/kanban-video-orchestrator/references/role-archetypes.md +298 -0
  467. package/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +317 -0
  468. package/optional-skills/creative/kanban-video-orchestrator/scripts/bootstrap_pipeline.py +501 -0
  469. package/optional-skills/creative/kanban-video-orchestrator/scripts/monitor.py +195 -0
  470. package/optional-skills/creative/meme-generation/EXAMPLES.md +46 -0
  471. package/optional-skills/creative/meme-generation/SKILL.md +130 -0
  472. package/optional-skills/creative/meme-generation/scripts/generate_meme.py +471 -0
  473. package/optional-skills/creative/meme-generation/scripts/templates.json +97 -0
  474. package/optional-skills/devops/cli/SKILL.md +156 -0
  475. package/optional-skills/devops/cli/references/app-discovery.md +112 -0
  476. package/optional-skills/devops/cli/references/authentication.md +59 -0
  477. package/optional-skills/devops/cli/references/cli-reference.md +104 -0
  478. package/optional-skills/devops/cli/references/running-apps.md +171 -0
  479. package/optional-skills/devops/docker-management/SKILL.md +281 -0
  480. package/optional-skills/devops/pinggy-tunnel/SKILL.md +309 -0
  481. package/optional-skills/devops/watchers/SKILL.md +112 -0
  482. package/optional-skills/devops/watchers/scripts/_watermark.py +148 -0
  483. package/optional-skills/devops/watchers/scripts/watch_github.py +168 -0
  484. package/optional-skills/devops/watchers/scripts/watch_http_json.py +131 -0
  485. package/optional-skills/devops/watchers/scripts/watch_rss.py +121 -0
  486. package/optional-skills/dogfood/DESCRIPTION.md +3 -0
  487. package/optional-skills/dogfood/adversarial-ux-test/SKILL.md +191 -0
  488. package/optional-skills/email/agentmail/SKILL.md +126 -0
  489. package/optional-skills/finance/3-statement-model/SKILL.md +433 -0
  490. package/optional-skills/finance/3-statement-model/references/formatting.md +118 -0
  491. package/optional-skills/finance/3-statement-model/references/formulas.md +292 -0
  492. package/optional-skills/finance/3-statement-model/references/sec-filings.md +125 -0
  493. package/optional-skills/finance/comps-analysis/SKILL.md +662 -0
  494. package/optional-skills/finance/dcf-model/SKILL.md +1270 -0
  495. package/optional-skills/finance/dcf-model/TROUBLESHOOTING.md +40 -0
  496. package/optional-skills/finance/dcf-model/requirements.txt +7 -0
  497. package/optional-skills/finance/dcf-model/scripts/validate_dcf.py +292 -0
  498. package/optional-skills/finance/excel-author/SKILL.md +244 -0
  499. package/optional-skills/finance/excel-author/scripts/recalc.py +88 -0
  500. package/optional-skills/finance/lbo-model/SKILL.md +291 -0
  501. package/optional-skills/finance/merger-model/SKILL.md +144 -0
  502. package/optional-skills/finance/pptx-author/SKILL.md +173 -0
  503. package/optional-skills/finance/stocks/SKILL.md +95 -0
  504. package/optional-skills/finance/stocks/scripts/stocks_client.py +755 -0
  505. package/optional-skills/health/DESCRIPTION.md +1 -0
  506. package/optional-skills/health/fitness-nutrition/SKILL.md +256 -0
  507. package/optional-skills/health/fitness-nutrition/references/FORMULAS.md +100 -0
  508. package/optional-skills/health/fitness-nutrition/scripts/body_calc.py +210 -0
  509. package/optional-skills/health/fitness-nutrition/scripts/nutrition_search.py +86 -0
  510. package/optional-skills/health/neuroskill-bci/SKILL.md +459 -0
  511. package/optional-skills/health/neuroskill-bci/references/api.md +286 -0
  512. package/optional-skills/health/neuroskill-bci/references/metrics.md +220 -0
  513. package/optional-skills/health/neuroskill-bci/references/protocols.md +452 -0
  514. package/optional-skills/mcp/DESCRIPTION.md +3 -0
  515. package/optional-skills/mcp/fastmcp/SKILL.md +300 -0
  516. package/optional-skills/mcp/fastmcp/references/fastmcp-cli.md +110 -0
  517. package/optional-skills/mcp/fastmcp/scripts/scaffold_fastmcp.py +56 -0
  518. package/optional-skills/mcp/fastmcp/templates/api_wrapper.py +54 -0
  519. package/optional-skills/mcp/fastmcp/templates/database_server.py +77 -0
  520. package/optional-skills/mcp/fastmcp/templates/file_processor.py +55 -0
  521. package/optional-skills/mcp/mcporter/SKILL.md +123 -0
  522. package/optional-skills/migration/DESCRIPTION.md +2 -0
  523. package/optional-skills/migration/openclaw-migration/SKILL.md +298 -0
  524. package/optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py +3136 -0
  525. package/optional-skills/mlops/accelerate/SKILL.md +336 -0
  526. package/optional-skills/mlops/accelerate/references/custom-plugins.md +453 -0
  527. package/optional-skills/mlops/accelerate/references/megatron-integration.md +489 -0
  528. package/optional-skills/mlops/accelerate/references/performance.md +525 -0
  529. package/optional-skills/mlops/chroma/SKILL.md +410 -0
  530. package/optional-skills/mlops/chroma/references/integration.md +38 -0
  531. package/optional-skills/mlops/clip/SKILL.md +257 -0
  532. package/optional-skills/mlops/clip/references/applications.md +207 -0
  533. package/optional-skills/mlops/faiss/SKILL.md +225 -0
  534. package/optional-skills/mlops/faiss/references/index_types.md +280 -0
  535. package/optional-skills/mlops/flash-attention/SKILL.md +367 -0
  536. package/optional-skills/mlops/flash-attention/references/benchmarks.md +215 -0
  537. package/optional-skills/mlops/flash-attention/references/transformers-integration.md +293 -0
  538. package/optional-skills/mlops/guidance/SKILL.md +576 -0
  539. package/optional-skills/mlops/guidance/references/backends.md +554 -0
  540. package/optional-skills/mlops/guidance/references/constraints.md +674 -0
  541. package/optional-skills/mlops/guidance/references/examples.md +767 -0
  542. package/optional-skills/mlops/huggingface-tokenizers/SKILL.md +520 -0
  543. package/optional-skills/mlops/huggingface-tokenizers/references/algorithms.md +653 -0
  544. package/optional-skills/mlops/huggingface-tokenizers/references/integration.md +637 -0
  545. package/optional-skills/mlops/huggingface-tokenizers/references/pipeline.md +723 -0
  546. package/optional-skills/mlops/huggingface-tokenizers/references/training.md +565 -0
  547. package/optional-skills/mlops/inference/outlines/SKILL.md +656 -0
  548. package/optional-skills/mlops/inference/outlines/references/backends.md +615 -0
  549. package/optional-skills/mlops/inference/outlines/references/examples.md +773 -0
  550. package/optional-skills/mlops/inference/outlines/references/json_generation.md +652 -0
  551. package/optional-skills/mlops/instructor/SKILL.md +744 -0
  552. package/optional-skills/mlops/instructor/references/examples.md +107 -0
  553. package/optional-skills/mlops/instructor/references/providers.md +70 -0
  554. package/optional-skills/mlops/instructor/references/validation.md +606 -0
  555. package/optional-skills/mlops/lambda-labs/SKILL.md +549 -0
  556. package/optional-skills/mlops/lambda-labs/references/advanced-usage.md +611 -0
  557. package/optional-skills/mlops/lambda-labs/references/troubleshooting.md +530 -0
  558. package/optional-skills/mlops/llava/SKILL.md +308 -0
  559. package/optional-skills/mlops/llava/references/training.md +197 -0
  560. package/optional-skills/mlops/modal/SKILL.md +345 -0
  561. package/optional-skills/mlops/modal/references/advanced-usage.md +503 -0
  562. package/optional-skills/mlops/modal/references/troubleshooting.md +494 -0
  563. package/optional-skills/mlops/nemo-curator/SKILL.md +387 -0
  564. package/optional-skills/mlops/nemo-curator/references/deduplication.md +87 -0
  565. package/optional-skills/mlops/nemo-curator/references/filtering.md +102 -0
  566. package/optional-skills/mlops/peft/SKILL.md +435 -0
  567. package/optional-skills/mlops/peft/references/advanced-usage.md +514 -0
  568. package/optional-skills/mlops/peft/references/troubleshooting.md +480 -0
  569. package/optional-skills/mlops/pinecone/SKILL.md +362 -0
  570. package/optional-skills/mlops/pinecone/references/deployment.md +181 -0
  571. package/optional-skills/mlops/pytorch-fsdp/SKILL.md +130 -0
  572. package/optional-skills/mlops/pytorch-fsdp/references/index.md +7 -0
  573. package/optional-skills/mlops/pytorch-fsdp/references/other.md +4261 -0
  574. package/optional-skills/mlops/pytorch-lightning/SKILL.md +350 -0
  575. package/optional-skills/mlops/pytorch-lightning/references/callbacks.md +436 -0
  576. package/optional-skills/mlops/pytorch-lightning/references/distributed.md +490 -0
  577. package/optional-skills/mlops/pytorch-lightning/references/hyperparameter-tuning.md +556 -0
  578. package/optional-skills/mlops/qdrant/SKILL.md +497 -0
  579. package/optional-skills/mlops/qdrant/references/advanced-usage.md +648 -0
  580. package/optional-skills/mlops/qdrant/references/troubleshooting.md +631 -0
  581. package/optional-skills/mlops/saelens/SKILL.md +390 -0
  582. package/optional-skills/mlops/saelens/references/README.md +69 -0
  583. package/optional-skills/mlops/saelens/references/api.md +333 -0
  584. package/optional-skills/mlops/saelens/references/tutorials.md +318 -0
  585. package/optional-skills/mlops/simpo/SKILL.md +223 -0
  586. package/optional-skills/mlops/simpo/references/datasets.md +478 -0
  587. package/optional-skills/mlops/simpo/references/hyperparameters.md +452 -0
  588. package/optional-skills/mlops/simpo/references/loss-functions.md +350 -0
  589. package/optional-skills/mlops/slime/SKILL.md +468 -0
  590. package/optional-skills/mlops/slime/references/api-reference.md +392 -0
  591. package/optional-skills/mlops/slime/references/troubleshooting.md +386 -0
  592. package/optional-skills/mlops/stable-diffusion/SKILL.md +523 -0
  593. package/optional-skills/mlops/stable-diffusion/references/advanced-usage.md +716 -0
  594. package/optional-skills/mlops/stable-diffusion/references/troubleshooting.md +555 -0
  595. package/optional-skills/mlops/tensorrt-llm/SKILL.md +191 -0
  596. package/optional-skills/mlops/tensorrt-llm/references/multi-gpu.md +298 -0
  597. package/optional-skills/mlops/tensorrt-llm/references/optimization.md +242 -0
  598. package/optional-skills/mlops/tensorrt-llm/references/serving.md +470 -0
  599. package/optional-skills/mlops/torchtitan/SKILL.md +362 -0
  600. package/optional-skills/mlops/torchtitan/references/checkpoint.md +181 -0
  601. package/optional-skills/mlops/torchtitan/references/custom-models.md +258 -0
  602. package/optional-skills/mlops/torchtitan/references/float8.md +133 -0
  603. package/optional-skills/mlops/torchtitan/references/fsdp.md +126 -0
  604. package/optional-skills/mlops/training/axolotl/SKILL.md +166 -0
  605. package/optional-skills/mlops/training/axolotl/references/api.md +5548 -0
  606. package/optional-skills/mlops/training/axolotl/references/dataset-formats.md +1029 -0
  607. package/optional-skills/mlops/training/axolotl/references/index.md +15 -0
  608. package/optional-skills/mlops/training/axolotl/references/other.md +3563 -0
  609. package/optional-skills/mlops/training/trl-fine-tuning/SKILL.md +463 -0
  610. package/optional-skills/mlops/training/trl-fine-tuning/references/dpo-variants.md +227 -0
  611. package/optional-skills/mlops/training/trl-fine-tuning/references/grpo-training.md +504 -0
  612. package/optional-skills/mlops/training/trl-fine-tuning/references/online-rl.md +82 -0
  613. package/optional-skills/mlops/training/trl-fine-tuning/references/reward-modeling.md +122 -0
  614. package/optional-skills/mlops/training/trl-fine-tuning/references/sft-training.md +168 -0
  615. package/optional-skills/mlops/training/trl-fine-tuning/templates/basic_grpo_training.py +228 -0
  616. package/optional-skills/mlops/training/unsloth/SKILL.md +84 -0
  617. package/optional-skills/mlops/training/unsloth/references/index.md +7 -0
  618. package/optional-skills/mlops/training/unsloth/references/llms-full.md +16799 -0
  619. package/optional-skills/mlops/training/unsloth/references/llms-txt.md +12044 -0
  620. package/optional-skills/mlops/training/unsloth/references/llms.md +82 -0
  621. package/optional-skills/mlops/whisper/SKILL.md +321 -0
  622. package/optional-skills/mlops/whisper/references/languages.md +189 -0
  623. package/optional-skills/productivity/canvas/SKILL.md +98 -0
  624. package/optional-skills/productivity/canvas/scripts/canvas_api.py +157 -0
  625. package/optional-skills/productivity/here-now/SKILL.md +217 -0
  626. package/optional-skills/productivity/here-now/scripts/drive.sh +406 -0
  627. package/optional-skills/productivity/here-now/scripts/publish.sh +445 -0
  628. package/optional-skills/productivity/memento-flashcards/SKILL.md +324 -0
  629. package/optional-skills/productivity/memento-flashcards/scripts/memento_cards.py +353 -0
  630. package/optional-skills/productivity/memento-flashcards/scripts/youtube_quiz.py +88 -0
  631. package/optional-skills/productivity/shop-app/SKILL.md +340 -0
  632. package/optional-skills/productivity/shopify/SKILL.md +373 -0
  633. package/optional-skills/productivity/siyuan/SKILL.md +298 -0
  634. package/optional-skills/productivity/telephony/SKILL.md +418 -0
  635. package/optional-skills/productivity/telephony/scripts/telephony.py +1343 -0
  636. package/optional-skills/research/bioinformatics/SKILL.md +235 -0
  637. package/optional-skills/research/darwinian-evolver/SKILL.md +199 -0
  638. package/optional-skills/research/darwinian-evolver/scripts/parrot_openrouter.py +218 -0
  639. package/optional-skills/research/darwinian-evolver/scripts/show_snapshot.py +69 -0
  640. package/optional-skills/research/darwinian-evolver/templates/custom_problem_template.py +240 -0
  641. package/optional-skills/research/domain-intel/SKILL.md +97 -0
  642. package/optional-skills/research/domain-intel/scripts/domain_intel.py +397 -0
  643. package/optional-skills/research/drug-discovery/SKILL.md +227 -0
  644. package/optional-skills/research/drug-discovery/references/ADMET_REFERENCE.md +66 -0
  645. package/optional-skills/research/drug-discovery/scripts/chembl_target.py +53 -0
  646. package/optional-skills/research/drug-discovery/scripts/ro5_screen.py +44 -0
  647. package/optional-skills/research/duckduckgo-search/SKILL.md +238 -0
  648. package/optional-skills/research/duckduckgo-search/scripts/duckduckgo.sh +28 -0
  649. package/optional-skills/research/gitnexus-explorer/SKILL.md +214 -0
  650. package/optional-skills/research/gitnexus-explorer/scripts/proxy.mjs +92 -0
  651. package/optional-skills/research/osint-investigation/SKILL.md +277 -0
  652. package/optional-skills/research/osint-investigation/references/sources/courtlistener.md +98 -0
  653. package/optional-skills/research/osint-investigation/references/sources/gdelt.md +104 -0
  654. package/optional-skills/research/osint-investigation/references/sources/icij-offshore.md +104 -0
  655. package/optional-skills/research/osint-investigation/references/sources/nyc-acris.md +90 -0
  656. package/optional-skills/research/osint-investigation/references/sources/ofac-sdn.md +92 -0
  657. package/optional-skills/research/osint-investigation/references/sources/opencorporates.md +103 -0
  658. package/optional-skills/research/osint-investigation/references/sources/sec-edgar.md +83 -0
  659. package/optional-skills/research/osint-investigation/references/sources/senate-ld.md +89 -0
  660. package/optional-skills/research/osint-investigation/references/sources/usaspending.md +97 -0
  661. package/optional-skills/research/osint-investigation/references/sources/wayback.md +93 -0
  662. package/optional-skills/research/osint-investigation/references/sources/wikipedia.md +107 -0
  663. package/optional-skills/research/osint-investigation/scripts/_http.py +82 -0
  664. package/optional-skills/research/osint-investigation/scripts/_normalize.py +67 -0
  665. package/optional-skills/research/osint-investigation/scripts/build_findings.py +221 -0
  666. package/optional-skills/research/osint-investigation/scripts/entity_resolution.py +228 -0
  667. package/optional-skills/research/osint-investigation/scripts/fetch_courtlistener.py +149 -0
  668. package/optional-skills/research/osint-investigation/scripts/fetch_gdelt.py +162 -0
  669. package/optional-skills/research/osint-investigation/scripts/fetch_icij_offshore.py +234 -0
  670. package/optional-skills/research/osint-investigation/scripts/fetch_nyc_acris.py +203 -0
  671. package/optional-skills/research/osint-investigation/scripts/fetch_ofac_sdn.py +175 -0
  672. package/optional-skills/research/osint-investigation/scripts/fetch_opencorporates.py +192 -0
  673. package/optional-skills/research/osint-investigation/scripts/fetch_sec_edgar.py +184 -0
  674. package/optional-skills/research/osint-investigation/scripts/fetch_senate_ld.py +146 -0
  675. package/optional-skills/research/osint-investigation/scripts/fetch_usaspending.py +170 -0
  676. package/optional-skills/research/osint-investigation/scripts/fetch_wayback.py +142 -0
  677. package/optional-skills/research/osint-investigation/scripts/fetch_wikipedia.py +267 -0
  678. package/optional-skills/research/osint-investigation/scripts/timing_analysis.py +253 -0
  679. package/optional-skills/research/osint-investigation/templates/source-template.md +59 -0
  680. package/optional-skills/research/parallel-cli/SKILL.md +391 -0
  681. package/optional-skills/research/qmd/SKILL.md +441 -0
  682. package/optional-skills/research/scrapling/SKILL.md +336 -0
  683. package/optional-skills/research/searxng-search/SKILL.md +212 -0
  684. package/optional-skills/research/searxng-search/scripts/searxng.sh +22 -0
  685. package/optional-skills/security/1password/SKILL.md +163 -0
  686. package/optional-skills/security/1password/references/cli-examples.md +31 -0
  687. package/optional-skills/security/1password/references/get-started.md +21 -0
  688. package/optional-skills/security/DESCRIPTION.md +3 -0
  689. package/optional-skills/security/oss-forensics/SKILL.md +423 -0
  690. package/optional-skills/security/oss-forensics/references/evidence-types.md +89 -0
  691. package/optional-skills/security/oss-forensics/references/github-archive-guide.md +184 -0
  692. package/optional-skills/security/oss-forensics/references/investigation-templates.md +131 -0
  693. package/optional-skills/security/oss-forensics/references/recovery-techniques.md +164 -0
  694. package/optional-skills/security/oss-forensics/scripts/evidence-store.py +313 -0
  695. package/optional-skills/security/oss-forensics/templates/forensic-report.md +151 -0
  696. package/optional-skills/security/oss-forensics/templates/malicious-package-report.md +43 -0
  697. package/optional-skills/security/sherlock/SKILL.md +193 -0
  698. package/optional-skills/software-development/rest-graphql-debug/SKILL.md +514 -0
  699. package/optional-skills/web-development/DESCRIPTION.md +5 -0
  700. package/optional-skills/web-development/page-agent/SKILL.md +190 -0
  701. package/package.json +78 -0
  702. package/plugins/__init__.py +1 -0
  703. package/plugins/__pycache__/__init__.cpython-312.pyc +0 -0
  704. package/plugins/context_engine/__init__.py +219 -0
  705. package/plugins/disk-cleanup/README.md +51 -0
  706. package/plugins/disk-cleanup/__init__.py +316 -0
  707. package/plugins/disk-cleanup/disk_cleanup.py +497 -0
  708. package/plugins/disk-cleanup/plugin.yaml +7 -0
  709. package/plugins/example-dashboard/dashboard/manifest.json +14 -0
  710. package/plugins/example-dashboard/dashboard/plugin_api.py +17 -0
  711. package/plugins/google_meet/README.md +131 -0
  712. package/plugins/google_meet/SKILL.md +148 -0
  713. package/plugins/google_meet/__init__.py +103 -0
  714. package/plugins/google_meet/audio_bridge.py +244 -0
  715. package/plugins/google_meet/cli.py +479 -0
  716. package/plugins/google_meet/meet_bot.py +852 -0
  717. package/plugins/google_meet/node/__init__.py +54 -0
  718. package/plugins/google_meet/node/cli.py +125 -0
  719. package/plugins/google_meet/node/client.py +107 -0
  720. package/plugins/google_meet/node/protocol.py +124 -0
  721. package/plugins/google_meet/node/registry.py +113 -0
  722. package/plugins/google_meet/node/server.py +201 -0
  723. package/plugins/google_meet/plugin.yaml +16 -0
  724. package/plugins/google_meet/process_manager.py +324 -0
  725. package/plugins/google_meet/realtime/__init__.py +10 -0
  726. package/plugins/google_meet/realtime/openai_client.py +332 -0
  727. package/plugins/google_meet/tools.py +348 -0
  728. package/plugins/hermes-achievements/LICENSE +21 -0
  729. package/plugins/hermes-achievements/README.md +150 -0
  730. package/plugins/hermes-achievements/dashboard/dist/index.js +732 -0
  731. package/plugins/hermes-achievements/dashboard/dist/style.css +146 -0
  732. package/plugins/hermes-achievements/dashboard/manifest.json +11 -0
  733. package/plugins/hermes-achievements/dashboard/plugin_api.py +1062 -0
  734. package/plugins/hermes-achievements/docs/achievements-performance-implementation-plan.md +157 -0
  735. package/plugins/hermes-achievements/docs/achievements-performance-implementation-spec.md +219 -0
  736. package/plugins/hermes-achievements/docs/achievements-performance-spec.md +174 -0
  737. package/plugins/hermes-achievements/docs/assets/achievements-dashboard-hd.png +0 -0
  738. package/plugins/hermes-achievements/docs/assets/achievements-tier-showcase-hd.png +0 -0
  739. package/plugins/hermes-achievements/tests/test_achievement_engine.py +156 -0
  740. package/plugins/image_gen/openai/__init__.py +303 -0
  741. package/plugins/image_gen/openai/__pycache__/__init__.cpython-312.pyc +0 -0
  742. package/plugins/image_gen/openai/plugin.yaml +7 -0
  743. package/plugins/image_gen/openai-codex/__init__.py +378 -0
  744. package/plugins/image_gen/openai-codex/__pycache__/__init__.cpython-312.pyc +0 -0
  745. package/plugins/image_gen/openai-codex/plugin.yaml +5 -0
  746. package/plugins/image_gen/xai/__init__.py +316 -0
  747. package/plugins/image_gen/xai/__pycache__/__init__.cpython-312.pyc +0 -0
  748. package/plugins/image_gen/xai/plugin.yaml +7 -0
  749. package/plugins/kanban/dashboard/dist/index.js +3143 -0
  750. package/plugins/kanban/dashboard/dist/style.css +1500 -0
  751. package/plugins/kanban/dashboard/manifest.json +14 -0
  752. package/plugins/kanban/dashboard/plugin_api.py +1612 -0
  753. package/plugins/kanban/systemd/hermes-kanban-dispatcher.service +32 -0
  754. package/plugins/memory/__init__.py +408 -0
  755. package/plugins/memory/byterover/README.md +41 -0
  756. package/plugins/memory/byterover/__init__.py +384 -0
  757. package/plugins/memory/byterover/plugin.yaml +9 -0
  758. package/plugins/memory/hindsight/README.md +138 -0
  759. package/plugins/memory/hindsight/__init__.py +1758 -0
  760. package/plugins/memory/hindsight/plugin.yaml +8 -0
  761. package/plugins/memory/holographic/README.md +36 -0
  762. package/plugins/memory/holographic/__init__.py +409 -0
  763. package/plugins/memory/holographic/holographic.py +203 -0
  764. package/plugins/memory/holographic/plugin.yaml +5 -0
  765. package/plugins/memory/holographic/retrieval.py +593 -0
  766. package/plugins/memory/holographic/store.py +579 -0
  767. package/plugins/memory/honcho/README.md +328 -0
  768. package/plugins/memory/honcho/__init__.py +1329 -0
  769. package/plugins/memory/honcho/cli.py +1452 -0
  770. package/plugins/memory/honcho/client.py +784 -0
  771. package/plugins/memory/honcho/plugin.yaml +7 -0
  772. package/plugins/memory/honcho/session.py +1255 -0
  773. package/plugins/memory/mem0/README.md +38 -0
  774. package/plugins/memory/mem0/__init__.py +374 -0
  775. package/plugins/memory/mem0/plugin.yaml +5 -0
  776. package/plugins/memory/openviking/README.md +40 -0
  777. package/plugins/memory/openviking/__init__.py +945 -0
  778. package/plugins/memory/openviking/plugin.yaml +9 -0
  779. package/plugins/memory/retaindb/README.md +40 -0
  780. package/plugins/memory/retaindb/__init__.py +767 -0
  781. package/plugins/memory/retaindb/plugin.yaml +7 -0
  782. package/plugins/memory/supermemory/README.md +99 -0
  783. package/plugins/memory/supermemory/__init__.py +792 -0
  784. package/plugins/memory/supermemory/plugin.yaml +5 -0
  785. package/plugins/model-providers/README.md +70 -0
  786. package/plugins/model-providers/ai-gateway/__init__.py +43 -0
  787. package/plugins/model-providers/ai-gateway/__pycache__/__init__.cpython-312.pyc +0 -0
  788. package/plugins/model-providers/ai-gateway/plugin.yaml +5 -0
  789. package/plugins/model-providers/alibaba/__init__.py +13 -0
  790. package/plugins/model-providers/alibaba/__pycache__/__init__.cpython-312.pyc +0 -0
  791. package/plugins/model-providers/alibaba/plugin.yaml +5 -0
  792. package/plugins/model-providers/alibaba-coding-plan/__init__.py +21 -0
  793. package/plugins/model-providers/alibaba-coding-plan/__pycache__/__init__.cpython-312.pyc +0 -0
  794. package/plugins/model-providers/alibaba-coding-plan/plugin.yaml +5 -0
  795. package/plugins/model-providers/anthropic/__init__.py +52 -0
  796. package/plugins/model-providers/anthropic/__pycache__/__init__.cpython-312.pyc +0 -0
  797. package/plugins/model-providers/anthropic/plugin.yaml +5 -0
  798. package/plugins/model-providers/arcee/__init__.py +13 -0
  799. package/plugins/model-providers/arcee/__pycache__/__init__.cpython-312.pyc +0 -0
  800. package/plugins/model-providers/arcee/plugin.yaml +5 -0
  801. package/plugins/model-providers/azure-foundry/__init__.py +21 -0
  802. package/plugins/model-providers/azure-foundry/__pycache__/__init__.cpython-312.pyc +0 -0
  803. package/plugins/model-providers/azure-foundry/plugin.yaml +5 -0
  804. package/plugins/model-providers/bedrock/__init__.py +29 -0
  805. package/plugins/model-providers/bedrock/__pycache__/__init__.cpython-312.pyc +0 -0
  806. package/plugins/model-providers/bedrock/plugin.yaml +5 -0
  807. package/plugins/model-providers/copilot/__init__.py +58 -0
  808. package/plugins/model-providers/copilot/__pycache__/__init__.cpython-312.pyc +0 -0
  809. package/plugins/model-providers/copilot/plugin.yaml +5 -0
  810. package/plugins/model-providers/copilot-acp/__init__.py +34 -0
  811. package/plugins/model-providers/copilot-acp/__pycache__/__init__.cpython-312.pyc +0 -0
  812. package/plugins/model-providers/copilot-acp/plugin.yaml +5 -0
  813. package/plugins/model-providers/custom/__init__.py +68 -0
  814. package/plugins/model-providers/custom/__pycache__/__init__.cpython-312.pyc +0 -0
  815. package/plugins/model-providers/custom/plugin.yaml +5 -0
  816. package/plugins/model-providers/deepseek/__init__.py +99 -0
  817. package/plugins/model-providers/deepseek/__pycache__/__init__.cpython-312.pyc +0 -0
  818. package/plugins/model-providers/deepseek/plugin.yaml +5 -0
  819. package/plugins/model-providers/gemini/__init__.py +72 -0
  820. package/plugins/model-providers/gemini/__pycache__/__init__.cpython-312.pyc +0 -0
  821. package/plugins/model-providers/gemini/plugin.yaml +5 -0
  822. package/plugins/model-providers/gmi/__init__.py +31 -0
  823. package/plugins/model-providers/gmi/__pycache__/__init__.cpython-312.pyc +0 -0
  824. package/plugins/model-providers/gmi/plugin.yaml +5 -0
  825. package/plugins/model-providers/huggingface/__init__.py +20 -0
  826. package/plugins/model-providers/huggingface/__pycache__/__init__.cpython-312.pyc +0 -0
  827. package/plugins/model-providers/huggingface/plugin.yaml +5 -0
  828. package/plugins/model-providers/kilocode/__init__.py +14 -0
  829. package/plugins/model-providers/kilocode/__pycache__/__init__.cpython-312.pyc +0 -0
  830. package/plugins/model-providers/kilocode/plugin.yaml +5 -0
  831. package/plugins/model-providers/kimi-coding/__init__.py +71 -0
  832. package/plugins/model-providers/kimi-coding/__pycache__/__init__.cpython-312.pyc +0 -0
  833. package/plugins/model-providers/kimi-coding/plugin.yaml +5 -0
  834. package/plugins/model-providers/minimax/__init__.py +45 -0
  835. package/plugins/model-providers/minimax/__pycache__/__init__.cpython-312.pyc +0 -0
  836. package/plugins/model-providers/minimax/plugin.yaml +5 -0
  837. package/plugins/model-providers/nous/__init__.py +54 -0
  838. package/plugins/model-providers/nous/__pycache__/__init__.cpython-312.pyc +0 -0
  839. package/plugins/model-providers/nous/plugin.yaml +5 -0
  840. package/plugins/model-providers/novita/__init__.py +27 -0
  841. package/plugins/model-providers/novita/__pycache__/__init__.cpython-312.pyc +0 -0
  842. package/plugins/model-providers/novita/plugin.yaml +5 -0
  843. package/plugins/model-providers/nvidia/__init__.py +21 -0
  844. package/plugins/model-providers/nvidia/__pycache__/__init__.cpython-312.pyc +0 -0
  845. package/plugins/model-providers/nvidia/plugin.yaml +5 -0
  846. package/plugins/model-providers/ollama-cloud/__init__.py +14 -0
  847. package/plugins/model-providers/ollama-cloud/__pycache__/__init__.cpython-312.pyc +0 -0
  848. package/plugins/model-providers/ollama-cloud/plugin.yaml +5 -0
  849. package/plugins/model-providers/openai-codex/__init__.py +15 -0
  850. package/plugins/model-providers/openai-codex/__pycache__/__init__.cpython-312.pyc +0 -0
  851. package/plugins/model-providers/openai-codex/plugin.yaml +5 -0
  852. package/plugins/model-providers/opencode-zen/__init__.py +30 -0
  853. package/plugins/model-providers/opencode-zen/__pycache__/__init__.cpython-312.pyc +0 -0
  854. package/plugins/model-providers/opencode-zen/plugin.yaml +5 -0
  855. package/plugins/model-providers/openrouter/__init__.py +115 -0
  856. package/plugins/model-providers/openrouter/__pycache__/__init__.cpython-312.pyc +0 -0
  857. package/plugins/model-providers/openrouter/plugin.yaml +5 -0
  858. package/plugins/model-providers/qwen-oauth/__init__.py +82 -0
  859. package/plugins/model-providers/qwen-oauth/__pycache__/__init__.cpython-312.pyc +0 -0
  860. package/plugins/model-providers/qwen-oauth/plugin.yaml +5 -0
  861. package/plugins/model-providers/stepfun/__init__.py +14 -0
  862. package/plugins/model-providers/stepfun/__pycache__/__init__.cpython-312.pyc +0 -0
  863. package/plugins/model-providers/stepfun/plugin.yaml +5 -0
  864. package/plugins/model-providers/xai/__init__.py +15 -0
  865. package/plugins/model-providers/xai/__pycache__/__init__.cpython-312.pyc +0 -0
  866. package/plugins/model-providers/xai/plugin.yaml +5 -0
  867. package/plugins/model-providers/xiaomi/__init__.py +14 -0
  868. package/plugins/model-providers/xiaomi/__pycache__/__init__.cpython-312.pyc +0 -0
  869. package/plugins/model-providers/xiaomi/plugin.yaml +5 -0
  870. package/plugins/model-providers/zai/__init__.py +21 -0
  871. package/plugins/model-providers/zai/__pycache__/__init__.cpython-312.pyc +0 -0
  872. package/plugins/model-providers/zai/plugin.yaml +5 -0
  873. package/plugins/observability/langfuse/README.md +53 -0
  874. package/plugins/observability/langfuse/__init__.py +1004 -0
  875. package/plugins/observability/langfuse/plugin.yaml +14 -0
  876. package/plugins/platforms/google_chat/__init__.py +3 -0
  877. package/plugins/platforms/google_chat/__pycache__/__init__.cpython-312.pyc +0 -0
  878. package/plugins/platforms/google_chat/__pycache__/adapter.cpython-312.pyc +0 -0
  879. package/plugins/platforms/google_chat/adapter.py +3343 -0
  880. package/plugins/platforms/google_chat/oauth.py +639 -0
  881. package/plugins/platforms/google_chat/plugin.yaml +39 -0
  882. package/plugins/platforms/irc/__init__.py +3 -0
  883. package/plugins/platforms/irc/__pycache__/__init__.cpython-312.pyc +0 -0
  884. package/plugins/platforms/irc/__pycache__/adapter.cpython-312.pyc +0 -0
  885. package/plugins/platforms/irc/adapter.py +969 -0
  886. package/plugins/platforms/irc/plugin.yaml +54 -0
  887. package/plugins/platforms/line/__init__.py +3 -0
  888. package/plugins/platforms/line/__pycache__/__init__.cpython-312.pyc +0 -0
  889. package/plugins/platforms/line/__pycache__/adapter.cpython-312.pyc +0 -0
  890. package/plugins/platforms/line/adapter.py +1639 -0
  891. package/plugins/platforms/line/plugin.yaml +65 -0
  892. package/plugins/platforms/simplex/__init__.py +3 -0
  893. package/plugins/platforms/simplex/__pycache__/__init__.cpython-312.pyc +0 -0
  894. package/plugins/platforms/simplex/__pycache__/adapter.cpython-312.pyc +0 -0
  895. package/plugins/platforms/simplex/adapter.py +746 -0
  896. package/plugins/platforms/simplex/plugin.yaml +37 -0
  897. package/plugins/platforms/teams/__init__.py +3 -0
  898. package/plugins/platforms/teams/__pycache__/__init__.cpython-312.pyc +0 -0
  899. package/plugins/platforms/teams/__pycache__/adapter.cpython-312.pyc +0 -0
  900. package/plugins/platforms/teams/adapter.py +1188 -0
  901. package/plugins/platforms/teams/plugin.yaml +48 -0
  902. package/plugins/spotify/__init__.py +66 -0
  903. package/plugins/spotify/__pycache__/__init__.cpython-312.pyc +0 -0
  904. package/plugins/spotify/__pycache__/client.cpython-312.pyc +0 -0
  905. package/plugins/spotify/__pycache__/tools.cpython-312.pyc +0 -0
  906. package/plugins/spotify/client.py +435 -0
  907. package/plugins/spotify/plugin.yaml +13 -0
  908. package/plugins/spotify/tools.py +454 -0
  909. package/plugins/teams_pipeline/__init__.py +23 -0
  910. package/plugins/teams_pipeline/cli.py +463 -0
  911. package/plugins/teams_pipeline/meetings.py +333 -0
  912. package/plugins/teams_pipeline/models.py +350 -0
  913. package/plugins/teams_pipeline/pipeline.py +692 -0
  914. package/plugins/teams_pipeline/plugin.yaml +9 -0
  915. package/plugins/teams_pipeline/runtime.py +135 -0
  916. package/plugins/teams_pipeline/store.py +194 -0
  917. package/plugins/teams_pipeline/subscriptions.py +249 -0
  918. package/plugins/video_gen/fal/__init__.py +523 -0
  919. package/plugins/video_gen/fal/__pycache__/__init__.cpython-312.pyc +0 -0
  920. package/plugins/video_gen/fal/plugin.yaml +7 -0
  921. package/plugins/video_gen/xai/__init__.py +441 -0
  922. package/plugins/video_gen/xai/__pycache__/__init__.cpython-312.pyc +0 -0
  923. package/plugins/video_gen/xai/plugin.yaml +7 -0
  924. package/plugins/web/__init__.py +7 -0
  925. package/plugins/web/__pycache__/__init__.cpython-312.pyc +0 -0
  926. package/plugins/web/brave_free/__init__.py +14 -0
  927. package/plugins/web/brave_free/__pycache__/__init__.cpython-312.pyc +0 -0
  928. package/plugins/web/brave_free/__pycache__/provider.cpython-312.pyc +0 -0
  929. package/plugins/web/brave_free/plugin.yaml +7 -0
  930. package/plugins/web/brave_free/provider.py +137 -0
  931. package/plugins/web/ddgs/__init__.py +15 -0
  932. package/plugins/web/ddgs/__pycache__/__init__.cpython-312.pyc +0 -0
  933. package/plugins/web/ddgs/__pycache__/provider.cpython-312.pyc +0 -0
  934. package/plugins/web/ddgs/plugin.yaml +7 -0
  935. package/plugins/web/ddgs/provider.py +104 -0
  936. package/plugins/web/exa/__init__.py +15 -0
  937. package/plugins/web/exa/__pycache__/__init__.cpython-312.pyc +0 -0
  938. package/plugins/web/exa/__pycache__/provider.cpython-312.pyc +0 -0
  939. package/plugins/web/exa/plugin.yaml +7 -0
  940. package/plugins/web/exa/provider.py +212 -0
  941. package/plugins/web/firecrawl/__init__.py +28 -0
  942. package/plugins/web/firecrawl/__pycache__/__init__.cpython-312.pyc +0 -0
  943. package/plugins/web/firecrawl/__pycache__/provider.cpython-312.pyc +0 -0
  944. package/plugins/web/firecrawl/plugin.yaml +7 -0
  945. package/plugins/web/firecrawl/provider.py +773 -0
  946. package/plugins/web/parallel/__init__.py +16 -0
  947. package/plugins/web/parallel/__pycache__/__init__.cpython-312.pyc +0 -0
  948. package/plugins/web/parallel/__pycache__/provider.cpython-312.pyc +0 -0
  949. package/plugins/web/parallel/plugin.yaml +7 -0
  950. package/plugins/web/parallel/provider.py +291 -0
  951. package/plugins/web/searxng/__init__.py +15 -0
  952. package/plugins/web/searxng/__pycache__/__init__.cpython-312.pyc +0 -0
  953. package/plugins/web/searxng/__pycache__/provider.cpython-312.pyc +0 -0
  954. package/plugins/web/searxng/plugin.yaml +7 -0
  955. package/plugins/web/searxng/provider.py +140 -0
  956. package/plugins/web/tavily/__init__.py +15 -0
  957. package/plugins/web/tavily/__pycache__/__init__.cpython-312.pyc +0 -0
  958. package/plugins/web/tavily/__pycache__/provider.cpython-312.pyc +0 -0
  959. package/plugins/web/tavily/plugin.yaml +7 -0
  960. package/plugins/web/tavily/provider.py +285 -0
  961. package/providers/README.md +78 -0
  962. package/providers/__init__.py +192 -0
  963. package/providers/__pycache__/__init__.cpython-312.pyc +0 -0
  964. package/providers/__pycache__/base.cpython-312.pyc +0 -0
  965. package/providers/base.py +184 -0
  966. package/pyproject.toml +255 -0
  967. package/run_agent.py +16409 -0
  968. package/scripts/benchmark_browser_eval.py +138 -0
  969. package/scripts/build_model_catalog.py +95 -0
  970. package/scripts/build_skills_index.py +325 -0
  971. package/scripts/check-windows-footguns.py +624 -0
  972. package/scripts/contributor_audit.py +473 -0
  973. package/scripts/discord-voice-doctor.py +396 -0
  974. package/scripts/hermes-gateway +416 -0
  975. package/scripts/install.cmd +28 -0
  976. package/scripts/install.ps1 +1611 -0
  977. package/scripts/install.sh +2007 -0
  978. package/scripts/install_psutil_android.py +117 -0
  979. package/scripts/keystroke_diagnostic.py +81 -0
  980. package/scripts/kill_modal.sh +34 -0
  981. package/scripts/lib/node-bootstrap.sh +238 -0
  982. package/scripts/lint_diff.py +207 -0
  983. package/scripts/postinstall.js +150 -0
  984. package/scripts/profile-tui.py +626 -0
  985. package/scripts/release.py +1680 -0
  986. package/scripts/run_tests.sh +129 -0
  987. package/scripts/sample_and_compress.py +409 -0
  988. package/scripts/setup_open_webui.sh +349 -0
  989. package/scripts/whatsapp-bridge/allowlist.js +88 -0
  990. package/scripts/whatsapp-bridge/allowlist.test.mjs +80 -0
  991. package/scripts/whatsapp-bridge/bridge.js +729 -0
  992. package/scripts/whatsapp-bridge/package-lock.json +2141 -0
  993. package/scripts/whatsapp-bridge/package.json +19 -0
  994. package/skills/apple/DESCRIPTION.md +2 -0
  995. package/skills/apple/apple-notes/SKILL.md +90 -0
  996. package/skills/apple/apple-reminders/SKILL.md +98 -0
  997. package/skills/apple/findmy/SKILL.md +131 -0
  998. package/skills/apple/imessage/SKILL.md +102 -0
  999. package/skills/apple/macos-computer-use/SKILL.md +201 -0
  1000. package/skills/autonomous-ai-agents/DESCRIPTION.md +3 -0
  1001. package/skills/autonomous-ai-agents/claude-code/SKILL.md +745 -0
  1002. package/skills/autonomous-ai-agents/codex/SKILL.md +130 -0
  1003. package/skills/autonomous-ai-agents/hermes-agent/SKILL.md +1014 -0
  1004. package/skills/autonomous-ai-agents/opencode/SKILL.md +219 -0
  1005. package/skills/creative/DESCRIPTION.md +3 -0
  1006. package/skills/creative/architecture-diagram/SKILL.md +148 -0
  1007. package/skills/creative/architecture-diagram/templates/template.html +319 -0
  1008. package/skills/creative/ascii-art/SKILL.md +322 -0
  1009. package/skills/creative/ascii-video/README.md +290 -0
  1010. package/skills/creative/ascii-video/SKILL.md +241 -0
  1011. package/skills/creative/ascii-video/references/architecture.md +802 -0
  1012. package/skills/creative/ascii-video/references/composition.md +892 -0
  1013. package/skills/creative/ascii-video/references/effects.md +1865 -0
  1014. package/skills/creative/ascii-video/references/inputs.md +685 -0
  1015. package/skills/creative/ascii-video/references/optimization.md +688 -0
  1016. package/skills/creative/ascii-video/references/scenes.md +1011 -0
  1017. package/skills/creative/ascii-video/references/shaders.md +1385 -0
  1018. package/skills/creative/ascii-video/references/troubleshooting.md +367 -0
  1019. package/skills/creative/baoyu-comic/PORT_NOTES.md +77 -0
  1020. package/skills/creative/baoyu-comic/SKILL.md +247 -0
  1021. package/skills/creative/baoyu-comic/references/analysis-framework.md +176 -0
  1022. package/skills/creative/baoyu-comic/references/art-styles/chalk.md +101 -0
  1023. package/skills/creative/baoyu-comic/references/art-styles/ink-brush.md +97 -0
  1024. package/skills/creative/baoyu-comic/references/art-styles/ligne-claire.md +75 -0
  1025. package/skills/creative/baoyu-comic/references/art-styles/manga.md +93 -0
  1026. package/skills/creative/baoyu-comic/references/art-styles/minimalist.md +84 -0
  1027. package/skills/creative/baoyu-comic/references/art-styles/realistic.md +89 -0
  1028. package/skills/creative/baoyu-comic/references/auto-selection.md +71 -0
  1029. package/skills/creative/baoyu-comic/references/base-prompt.md +98 -0
  1030. package/skills/creative/baoyu-comic/references/character-template.md +180 -0
  1031. package/skills/creative/baoyu-comic/references/layouts/cinematic.md +23 -0
  1032. package/skills/creative/baoyu-comic/references/layouts/dense.md +23 -0
  1033. package/skills/creative/baoyu-comic/references/layouts/four-panel.md +40 -0
  1034. package/skills/creative/baoyu-comic/references/layouts/mixed.md +23 -0
  1035. package/skills/creative/baoyu-comic/references/layouts/splash.md +23 -0
  1036. package/skills/creative/baoyu-comic/references/layouts/standard.md +23 -0
  1037. package/skills/creative/baoyu-comic/references/layouts/webtoon.md +30 -0
  1038. package/skills/creative/baoyu-comic/references/ohmsha-guide.md +85 -0
  1039. package/skills/creative/baoyu-comic/references/partial-workflows.md +106 -0
  1040. package/skills/creative/baoyu-comic/references/presets/concept-story.md +121 -0
  1041. package/skills/creative/baoyu-comic/references/presets/four-panel.md +107 -0
  1042. package/skills/creative/baoyu-comic/references/presets/ohmsha.md +114 -0
  1043. package/skills/creative/baoyu-comic/references/presets/shoujo.md +116 -0
  1044. package/skills/creative/baoyu-comic/references/presets/wuxia.md +110 -0
  1045. package/skills/creative/baoyu-comic/references/storyboard-template.md +143 -0
  1046. package/skills/creative/baoyu-comic/references/tones/action.md +110 -0
  1047. package/skills/creative/baoyu-comic/references/tones/dramatic.md +95 -0
  1048. package/skills/creative/baoyu-comic/references/tones/energetic.md +105 -0
  1049. package/skills/creative/baoyu-comic/references/tones/neutral.md +63 -0
  1050. package/skills/creative/baoyu-comic/references/tones/romantic.md +100 -0
  1051. package/skills/creative/baoyu-comic/references/tones/vintage.md +104 -0
  1052. package/skills/creative/baoyu-comic/references/tones/warm.md +94 -0
  1053. package/skills/creative/baoyu-comic/references/workflow.md +401 -0
  1054. package/skills/creative/baoyu-infographic/PORT_NOTES.md +43 -0
  1055. package/skills/creative/baoyu-infographic/SKILL.md +237 -0
  1056. package/skills/creative/baoyu-infographic/references/analysis-framework.md +182 -0
  1057. package/skills/creative/baoyu-infographic/references/base-prompt.md +43 -0
  1058. package/skills/creative/baoyu-infographic/references/layouts/bento-grid.md +41 -0
  1059. package/skills/creative/baoyu-infographic/references/layouts/binary-comparison.md +48 -0
  1060. package/skills/creative/baoyu-infographic/references/layouts/bridge.md +41 -0
  1061. package/skills/creative/baoyu-infographic/references/layouts/circular-flow.md +41 -0
  1062. package/skills/creative/baoyu-infographic/references/layouts/comic-strip.md +41 -0
  1063. package/skills/creative/baoyu-infographic/references/layouts/comparison-matrix.md +41 -0
  1064. package/skills/creative/baoyu-infographic/references/layouts/dashboard.md +41 -0
  1065. package/skills/creative/baoyu-infographic/references/layouts/dense-modules.md +72 -0
  1066. package/skills/creative/baoyu-infographic/references/layouts/funnel.md +41 -0
  1067. package/skills/creative/baoyu-infographic/references/layouts/hierarchical-layers.md +48 -0
  1068. package/skills/creative/baoyu-infographic/references/layouts/hub-spoke.md +41 -0
  1069. package/skills/creative/baoyu-infographic/references/layouts/iceberg.md +41 -0
  1070. package/skills/creative/baoyu-infographic/references/layouts/isometric-map.md +41 -0
  1071. package/skills/creative/baoyu-infographic/references/layouts/jigsaw.md +41 -0
  1072. package/skills/creative/baoyu-infographic/references/layouts/linear-progression.md +48 -0
  1073. package/skills/creative/baoyu-infographic/references/layouts/periodic-table.md +41 -0
  1074. package/skills/creative/baoyu-infographic/references/layouts/story-mountain.md +41 -0
  1075. package/skills/creative/baoyu-infographic/references/layouts/structural-breakdown.md +48 -0
  1076. package/skills/creative/baoyu-infographic/references/layouts/tree-branching.md +41 -0
  1077. package/skills/creative/baoyu-infographic/references/layouts/venn-diagram.md +41 -0
  1078. package/skills/creative/baoyu-infographic/references/layouts/winding-roadmap.md +41 -0
  1079. package/skills/creative/baoyu-infographic/references/structured-content-template.md +244 -0
  1080. package/skills/creative/baoyu-infographic/references/styles/aged-academia.md +36 -0
  1081. package/skills/creative/baoyu-infographic/references/styles/bold-graphic.md +36 -0
  1082. package/skills/creative/baoyu-infographic/references/styles/chalkboard.md +61 -0
  1083. package/skills/creative/baoyu-infographic/references/styles/claymation.md +29 -0
  1084. package/skills/creative/baoyu-infographic/references/styles/corporate-memphis.md +29 -0
  1085. package/skills/creative/baoyu-infographic/references/styles/craft-handmade.md +44 -0
  1086. package/skills/creative/baoyu-infographic/references/styles/cyberpunk-neon.md +29 -0
  1087. package/skills/creative/baoyu-infographic/references/styles/hand-drawn-edu.md +63 -0
  1088. package/skills/creative/baoyu-infographic/references/styles/ikea-manual.md +29 -0
  1089. package/skills/creative/baoyu-infographic/references/styles/kawaii.md +29 -0
  1090. package/skills/creative/baoyu-infographic/references/styles/knolling.md +29 -0
  1091. package/skills/creative/baoyu-infographic/references/styles/lego-brick.md +29 -0
  1092. package/skills/creative/baoyu-infographic/references/styles/morandi-journal.md +60 -0
  1093. package/skills/creative/baoyu-infographic/references/styles/origami.md +29 -0
  1094. package/skills/creative/baoyu-infographic/references/styles/pixel-art.md +29 -0
  1095. package/skills/creative/baoyu-infographic/references/styles/pop-laboratory.md +48 -0
  1096. package/skills/creative/baoyu-infographic/references/styles/retro-pop-grid.md +47 -0
  1097. package/skills/creative/baoyu-infographic/references/styles/storybook-watercolor.md +29 -0
  1098. package/skills/creative/baoyu-infographic/references/styles/subway-map.md +29 -0
  1099. package/skills/creative/baoyu-infographic/references/styles/technical-schematic.md +36 -0
  1100. package/skills/creative/baoyu-infographic/references/styles/ui-wireframe.md +29 -0
  1101. package/skills/creative/claude-design/SKILL.md +591 -0
  1102. package/skills/creative/comfyui/SKILL.md +612 -0
  1103. package/skills/creative/comfyui/references/official-cli.md +255 -0
  1104. package/skills/creative/comfyui/references/rest-api.md +312 -0
  1105. package/skills/creative/comfyui/references/template-integrity.md +243 -0
  1106. package/skills/creative/comfyui/references/workflow-format.md +226 -0
  1107. package/skills/creative/comfyui/scripts/_common.py +835 -0
  1108. package/skills/creative/comfyui/scripts/auto_fix_deps.py +225 -0
  1109. package/skills/creative/comfyui/scripts/check_deps.py +437 -0
  1110. package/skills/creative/comfyui/scripts/comfyui_setup.sh +286 -0
  1111. package/skills/creative/comfyui/scripts/extract_schema.py +315 -0
  1112. package/skills/creative/comfyui/scripts/fetch_logs.py +158 -0
  1113. package/skills/creative/comfyui/scripts/hardware_check.py +497 -0
  1114. package/skills/creative/comfyui/scripts/health_check.py +223 -0
  1115. package/skills/creative/comfyui/scripts/run_batch.py +243 -0
  1116. package/skills/creative/comfyui/scripts/run_workflow.py +796 -0
  1117. package/skills/creative/comfyui/scripts/ws_monitor.py +267 -0
  1118. package/skills/creative/comfyui/tests/README.md +50 -0
  1119. package/skills/creative/comfyui/tests/conftest.py +64 -0
  1120. package/skills/creative/comfyui/tests/pytest.ini +5 -0
  1121. package/skills/creative/comfyui/tests/test_check_deps.py +68 -0
  1122. package/skills/creative/comfyui/tests/test_cloud_integration.py +95 -0
  1123. package/skills/creative/comfyui/tests/test_common.py +447 -0
  1124. package/skills/creative/comfyui/tests/test_extract_schema.py +185 -0
  1125. package/skills/creative/comfyui/tests/test_run_workflow.py +213 -0
  1126. package/skills/creative/comfyui/workflows/README.md +86 -0
  1127. package/skills/creative/comfyui/workflows/animatediff_video.json +64 -0
  1128. package/skills/creative/comfyui/workflows/flux_dev_txt2img.json +78 -0
  1129. package/skills/creative/comfyui/workflows/sd15_txt2img.json +49 -0
  1130. package/skills/creative/comfyui/workflows/sdxl_img2img.json +54 -0
  1131. package/skills/creative/comfyui/workflows/sdxl_inpaint.json +59 -0
  1132. package/skills/creative/comfyui/workflows/sdxl_txt2img.json +49 -0
  1133. package/skills/creative/comfyui/workflows/upscale_4x.json +27 -0
  1134. package/skills/creative/comfyui/workflows/wan_video_t2v.json +69 -0
  1135. package/skills/creative/creative-ideation/SKILL.md +152 -0
  1136. package/skills/creative/creative-ideation/references/full-prompt-library.md +110 -0
  1137. package/skills/creative/design-md/SKILL.md +199 -0
  1138. package/skills/creative/design-md/templates/starter.md +99 -0
  1139. package/skills/creative/excalidraw/SKILL.md +199 -0
  1140. package/skills/creative/excalidraw/references/colors.md +44 -0
  1141. package/skills/creative/excalidraw/references/dark-mode.md +68 -0
  1142. package/skills/creative/excalidraw/references/examples.md +141 -0
  1143. package/skills/creative/excalidraw/scripts/upload.py +133 -0
  1144. package/skills/creative/humanizer/LICENSE +21 -0
  1145. package/skills/creative/humanizer/SKILL.md +578 -0
  1146. package/skills/creative/manim-video/README.md +23 -0
  1147. package/skills/creative/manim-video/SKILL.md +269 -0
  1148. package/skills/creative/manim-video/references/animation-design-thinking.md +161 -0
  1149. package/skills/creative/manim-video/references/animations.md +282 -0
  1150. package/skills/creative/manim-video/references/camera-and-3d.md +135 -0
  1151. package/skills/creative/manim-video/references/decorations.md +202 -0
  1152. package/skills/creative/manim-video/references/equations.md +216 -0
  1153. package/skills/creative/manim-video/references/graphs-and-data.md +163 -0
  1154. package/skills/creative/manim-video/references/mobjects.md +333 -0
  1155. package/skills/creative/manim-video/references/paper-explainer.md +255 -0
  1156. package/skills/creative/manim-video/references/production-quality.md +190 -0
  1157. package/skills/creative/manim-video/references/rendering.md +185 -0
  1158. package/skills/creative/manim-video/references/scene-planning.md +118 -0
  1159. package/skills/creative/manim-video/references/troubleshooting.md +135 -0
  1160. package/skills/creative/manim-video/references/updaters-and-trackers.md +260 -0
  1161. package/skills/creative/manim-video/references/visual-design.md +124 -0
  1162. package/skills/creative/manim-video/scripts/setup.sh +14 -0
  1163. package/skills/creative/p5js/README.md +64 -0
  1164. package/skills/creative/p5js/SKILL.md +556 -0
  1165. package/skills/creative/p5js/references/animation.md +439 -0
  1166. package/skills/creative/p5js/references/color-systems.md +352 -0
  1167. package/skills/creative/p5js/references/core-api.md +410 -0
  1168. package/skills/creative/p5js/references/export-pipeline.md +566 -0
  1169. package/skills/creative/p5js/references/interaction.md +398 -0
  1170. package/skills/creative/p5js/references/shapes-and-geometry.md +300 -0
  1171. package/skills/creative/p5js/references/troubleshooting.md +532 -0
  1172. package/skills/creative/p5js/references/typography.md +302 -0
  1173. package/skills/creative/p5js/references/visual-effects.md +895 -0
  1174. package/skills/creative/p5js/references/webgl-and-3d.md +423 -0
  1175. package/skills/creative/p5js/scripts/export-frames.js +179 -0
  1176. package/skills/creative/p5js/scripts/render.sh +108 -0
  1177. package/skills/creative/p5js/scripts/serve.sh +28 -0
  1178. package/skills/creative/p5js/scripts/setup.sh +87 -0
  1179. package/skills/creative/p5js/templates/viewer.html +395 -0
  1180. package/skills/creative/pixel-art/ATTRIBUTION.md +54 -0
  1181. package/skills/creative/pixel-art/SKILL.md +218 -0
  1182. package/skills/creative/pixel-art/references/palettes.md +49 -0
  1183. package/skills/creative/pixel-art/scripts/__init__.py +0 -0
  1184. package/skills/creative/pixel-art/scripts/palettes.py +167 -0
  1185. package/skills/creative/pixel-art/scripts/pixel_art.py +162 -0
  1186. package/skills/creative/pixel-art/scripts/pixel_art_video.py +345 -0
  1187. package/skills/creative/popular-web-designs/SKILL.md +214 -0
  1188. package/skills/creative/popular-web-designs/templates/airbnb.md +259 -0
  1189. package/skills/creative/popular-web-designs/templates/airtable.md +102 -0
  1190. package/skills/creative/popular-web-designs/templates/apple.md +326 -0
  1191. package/skills/creative/popular-web-designs/templates/bmw.md +193 -0
  1192. package/skills/creative/popular-web-designs/templates/cal.md +272 -0
  1193. package/skills/creative/popular-web-designs/templates/claude.md +325 -0
  1194. package/skills/creative/popular-web-designs/templates/clay.md +317 -0
  1195. package/skills/creative/popular-web-designs/templates/clickhouse.md +294 -0
  1196. package/skills/creative/popular-web-designs/templates/cohere.md +279 -0
  1197. package/skills/creative/popular-web-designs/templates/coinbase.md +142 -0
  1198. package/skills/creative/popular-web-designs/templates/composio.md +320 -0
  1199. package/skills/creative/popular-web-designs/templates/cursor.md +322 -0
  1200. package/skills/creative/popular-web-designs/templates/elevenlabs.md +278 -0
  1201. package/skills/creative/popular-web-designs/templates/expo.md +294 -0
  1202. package/skills/creative/popular-web-designs/templates/figma.md +233 -0
  1203. package/skills/creative/popular-web-designs/templates/framer.md +259 -0
  1204. package/skills/creative/popular-web-designs/templates/hashicorp.md +291 -0
  1205. package/skills/creative/popular-web-designs/templates/ibm.md +345 -0
  1206. package/skills/creative/popular-web-designs/templates/intercom.md +159 -0
  1207. package/skills/creative/popular-web-designs/templates/kraken.md +138 -0
  1208. package/skills/creative/popular-web-designs/templates/linear.app.md +380 -0
  1209. package/skills/creative/popular-web-designs/templates/lovable.md +311 -0
  1210. package/skills/creative/popular-web-designs/templates/minimax.md +270 -0
  1211. package/skills/creative/popular-web-designs/templates/mintlify.md +339 -0
  1212. package/skills/creative/popular-web-designs/templates/miro.md +121 -0
  1213. package/skills/creative/popular-web-designs/templates/mistral.ai.md +274 -0
  1214. package/skills/creative/popular-web-designs/templates/mongodb.md +279 -0
  1215. package/skills/creative/popular-web-designs/templates/notion.md +322 -0
  1216. package/skills/creative/popular-web-designs/templates/nvidia.md +306 -0
  1217. package/skills/creative/popular-web-designs/templates/ollama.md +280 -0
  1218. package/skills/creative/popular-web-designs/templates/opencode.ai.md +294 -0
  1219. package/skills/creative/popular-web-designs/templates/pinterest.md +243 -0
  1220. package/skills/creative/popular-web-designs/templates/posthog.md +269 -0
  1221. package/skills/creative/popular-web-designs/templates/raycast.md +281 -0
  1222. package/skills/creative/popular-web-designs/templates/replicate.md +274 -0
  1223. package/skills/creative/popular-web-designs/templates/resend.md +316 -0
  1224. package/skills/creative/popular-web-designs/templates/revolut.md +198 -0
  1225. package/skills/creative/popular-web-designs/templates/runwayml.md +257 -0
  1226. package/skills/creative/popular-web-designs/templates/sanity.md +370 -0
  1227. package/skills/creative/popular-web-designs/templates/sentry.md +275 -0
  1228. package/skills/creative/popular-web-designs/templates/spacex.md +207 -0
  1229. package/skills/creative/popular-web-designs/templates/spotify.md +259 -0
  1230. package/skills/creative/popular-web-designs/templates/stripe.md +335 -0
  1231. package/skills/creative/popular-web-designs/templates/supabase.md +268 -0
  1232. package/skills/creative/popular-web-designs/templates/superhuman.md +265 -0
  1233. package/skills/creative/popular-web-designs/templates/together.ai.md +276 -0
  1234. package/skills/creative/popular-web-designs/templates/uber.md +308 -0
  1235. package/skills/creative/popular-web-designs/templates/vercel.md +323 -0
  1236. package/skills/creative/popular-web-designs/templates/voltagent.md +336 -0
  1237. package/skills/creative/popular-web-designs/templates/warp.md +266 -0
  1238. package/skills/creative/popular-web-designs/templates/webflow.md +105 -0
  1239. package/skills/creative/popular-web-designs/templates/wise.md +186 -0
  1240. package/skills/creative/popular-web-designs/templates/x.ai.md +270 -0
  1241. package/skills/creative/popular-web-designs/templates/zapier.md +341 -0
  1242. package/skills/creative/pretext/SKILL.md +220 -0
  1243. package/skills/creative/pretext/references/patterns.md +258 -0
  1244. package/skills/creative/pretext/templates/donut-orbit.html +1468 -0
  1245. package/skills/creative/pretext/templates/hello-orb-flow.html +95 -0
  1246. package/skills/creative/sketch/SKILL.md +218 -0
  1247. package/skills/creative/songwriting-and-ai-music/SKILL.md +287 -0
  1248. package/skills/creative/touchdesigner-mcp/SKILL.md +356 -0
  1249. package/skills/creative/touchdesigner-mcp/references/3d-scene.md +275 -0
  1250. package/skills/creative/touchdesigner-mcp/references/animation.md +221 -0
  1251. package/skills/creative/touchdesigner-mcp/references/audio-reactive.md +175 -0
  1252. package/skills/creative/touchdesigner-mcp/references/dat-scripting.md +352 -0
  1253. package/skills/creative/touchdesigner-mcp/references/external-data.md +322 -0
  1254. package/skills/creative/touchdesigner-mcp/references/geometry-comp.md +121 -0
  1255. package/skills/creative/touchdesigner-mcp/references/glsl.md +151 -0
  1256. package/skills/creative/touchdesigner-mcp/references/layout-compositor.md +131 -0
  1257. package/skills/creative/touchdesigner-mcp/references/mcp-tools.md +382 -0
  1258. package/skills/creative/touchdesigner-mcp/references/midi-osc.md +211 -0
  1259. package/skills/creative/touchdesigner-mcp/references/network-patterns.md +966 -0
  1260. package/skills/creative/touchdesigner-mcp/references/operator-tips.md +106 -0
  1261. package/skills/creative/touchdesigner-mcp/references/operators.md +239 -0
  1262. package/skills/creative/touchdesigner-mcp/references/panel-ui.md +281 -0
  1263. package/skills/creative/touchdesigner-mcp/references/particles.md +245 -0
  1264. package/skills/creative/touchdesigner-mcp/references/pitfalls.md +704 -0
  1265. package/skills/creative/touchdesigner-mcp/references/postfx.md +183 -0
  1266. package/skills/creative/touchdesigner-mcp/references/projection-mapping.md +211 -0
  1267. package/skills/creative/touchdesigner-mcp/references/python-api.md +463 -0
  1268. package/skills/creative/touchdesigner-mcp/references/replicator.md +198 -0
  1269. package/skills/creative/touchdesigner-mcp/references/troubleshooting.md +244 -0
  1270. package/skills/creative/touchdesigner-mcp/scripts/setup.sh +115 -0
  1271. package/skills/data-science/DESCRIPTION.md +3 -0
  1272. package/skills/data-science/jupyter-live-kernel/SKILL.md +167 -0
  1273. package/skills/devops/kanban-orchestrator/SKILL.md +189 -0
  1274. package/skills/devops/kanban-worker/SKILL.md +184 -0
  1275. package/skills/devops/webhook-subscriptions/SKILL.md +204 -0
  1276. package/skills/diagramming/DESCRIPTION.md +3 -0
  1277. package/skills/dogfood/SKILL.md +162 -0
  1278. package/skills/dogfood/references/issue-taxonomy.md +109 -0
  1279. package/skills/dogfood/templates/dogfood-report-template.md +86 -0
  1280. package/skills/domain/DESCRIPTION.md +24 -0
  1281. package/skills/email/DESCRIPTION.md +3 -0
  1282. package/skills/email/himalaya/SKILL.md +299 -0
  1283. package/skills/email/himalaya/references/configuration.md +227 -0
  1284. package/skills/email/himalaya/references/message-composition.md +199 -0
  1285. package/skills/gaming/DESCRIPTION.md +3 -0
  1286. package/skills/gaming/minecraft-modpack-server/SKILL.md +187 -0
  1287. package/skills/gaming/pokemon-player/SKILL.md +216 -0
  1288. package/skills/gifs/DESCRIPTION.md +3 -0
  1289. package/skills/github/DESCRIPTION.md +3 -0
  1290. package/skills/github/codebase-inspection/SKILL.md +116 -0
  1291. package/skills/github/github-auth/SKILL.md +247 -0
  1292. package/skills/github/github-auth/scripts/gh-env.sh +66 -0
  1293. package/skills/github/github-code-review/SKILL.md +481 -0
  1294. package/skills/github/github-code-review/references/review-output-template.md +74 -0
  1295. package/skills/github/github-issues/SKILL.md +370 -0
  1296. package/skills/github/github-issues/templates/bug-report.md +35 -0
  1297. package/skills/github/github-issues/templates/feature-request.md +31 -0
  1298. package/skills/github/github-pr-workflow/SKILL.md +367 -0
  1299. package/skills/github/github-pr-workflow/references/ci-troubleshooting.md +183 -0
  1300. package/skills/github/github-pr-workflow/references/conventional-commits.md +71 -0
  1301. package/skills/github/github-pr-workflow/templates/pr-body-bugfix.md +35 -0
  1302. package/skills/github/github-pr-workflow/templates/pr-body-feature.md +33 -0
  1303. package/skills/github/github-repo-management/SKILL.md +516 -0
  1304. package/skills/github/github-repo-management/references/github-api-cheatsheet.md +161 -0
  1305. package/skills/index-cache/anthropics_skills_skills_.json +1 -0
  1306. package/skills/index-cache/claude_marketplace_anthropics_skills.json +1 -0
  1307. package/skills/index-cache/lobehub_index.json +1 -0
  1308. package/skills/index-cache/openai_skills_skills_.json +1 -0
  1309. package/skills/inference-sh/DESCRIPTION.md +19 -0
  1310. package/skills/mcp/DESCRIPTION.md +3 -0
  1311. package/skills/mcp/native-mcp/SKILL.md +357 -0
  1312. package/skills/media/DESCRIPTION.md +3 -0
  1313. package/skills/media/gif-search/SKILL.md +91 -0
  1314. package/skills/media/heartmula/SKILL.md +171 -0
  1315. package/skills/media/songsee/SKILL.md +83 -0
  1316. package/skills/media/spotify/SKILL.md +135 -0
  1317. package/skills/media/youtube-content/SKILL.md +73 -0
  1318. package/skills/media/youtube-content/references/output-formats.md +56 -0
  1319. package/skills/media/youtube-content/scripts/fetch_transcript.py +124 -0
  1320. package/skills/mlops/DESCRIPTION.md +3 -0
  1321. package/skills/mlops/evaluation/DESCRIPTION.md +3 -0
  1322. package/skills/mlops/evaluation/lm-evaluation-harness/SKILL.md +498 -0
  1323. package/skills/mlops/evaluation/lm-evaluation-harness/references/api-evaluation.md +490 -0
  1324. package/skills/mlops/evaluation/lm-evaluation-harness/references/benchmark-guide.md +488 -0
  1325. package/skills/mlops/evaluation/lm-evaluation-harness/references/custom-tasks.md +602 -0
  1326. package/skills/mlops/evaluation/lm-evaluation-harness/references/distributed-eval.md +519 -0
  1327. package/skills/mlops/evaluation/weights-and-biases/SKILL.md +594 -0
  1328. package/skills/mlops/evaluation/weights-and-biases/references/artifacts.md +584 -0
  1329. package/skills/mlops/evaluation/weights-and-biases/references/integrations.md +700 -0
  1330. package/skills/mlops/evaluation/weights-and-biases/references/sweeps.md +847 -0
  1331. package/skills/mlops/huggingface-hub/SKILL.md +81 -0
  1332. package/skills/mlops/inference/DESCRIPTION.md +3 -0
  1333. package/skills/mlops/inference/llama-cpp/SKILL.md +249 -0
  1334. package/skills/mlops/inference/llama-cpp/references/advanced-usage.md +504 -0
  1335. package/skills/mlops/inference/llama-cpp/references/hub-discovery.md +168 -0
  1336. package/skills/mlops/inference/llama-cpp/references/optimization.md +89 -0
  1337. package/skills/mlops/inference/llama-cpp/references/quantization.md +243 -0
  1338. package/skills/mlops/inference/llama-cpp/references/server.md +150 -0
  1339. package/skills/mlops/inference/llama-cpp/references/troubleshooting.md +442 -0
  1340. package/skills/mlops/inference/obliteratus/SKILL.md +342 -0
  1341. package/skills/mlops/inference/obliteratus/references/analysis-modules.md +166 -0
  1342. package/skills/mlops/inference/obliteratus/references/methods-guide.md +141 -0
  1343. package/skills/mlops/inference/obliteratus/templates/abliteration-config.yaml +33 -0
  1344. package/skills/mlops/inference/obliteratus/templates/analysis-study.yaml +40 -0
  1345. package/skills/mlops/inference/obliteratus/templates/batch-abliteration.yaml +41 -0
  1346. package/skills/mlops/inference/vllm/SKILL.md +372 -0
  1347. package/skills/mlops/inference/vllm/references/optimization.md +226 -0
  1348. package/skills/mlops/inference/vllm/references/quantization.md +284 -0
  1349. package/skills/mlops/inference/vllm/references/server-deployment.md +255 -0
  1350. package/skills/mlops/inference/vllm/references/troubleshooting.md +447 -0
  1351. package/skills/mlops/models/DESCRIPTION.md +3 -0
  1352. package/skills/mlops/models/audiocraft/SKILL.md +568 -0
  1353. package/skills/mlops/models/audiocraft/references/advanced-usage.md +666 -0
  1354. package/skills/mlops/models/audiocraft/references/troubleshooting.md +504 -0
  1355. package/skills/mlops/models/segment-anything/SKILL.md +506 -0
  1356. package/skills/mlops/models/segment-anything/references/advanced-usage.md +589 -0
  1357. package/skills/mlops/models/segment-anything/references/troubleshooting.md +484 -0
  1358. package/skills/mlops/research/DESCRIPTION.md +3 -0
  1359. package/skills/mlops/research/dspy/SKILL.md +594 -0
  1360. package/skills/mlops/research/dspy/references/examples.md +663 -0
  1361. package/skills/mlops/research/dspy/references/modules.md +475 -0
  1362. package/skills/mlops/research/dspy/references/optimizers.md +566 -0
  1363. package/skills/mlops/training/DESCRIPTION.md +3 -0
  1364. package/skills/mlops/vector-databases/DESCRIPTION.md +3 -0
  1365. package/skills/note-taking/DESCRIPTION.md +3 -0
  1366. package/skills/note-taking/obsidian/SKILL.md +61 -0
  1367. package/skills/productivity/DESCRIPTION.md +3 -0
  1368. package/skills/productivity/airtable/SKILL.md +229 -0
  1369. package/skills/productivity/google-workspace/SKILL.md +335 -0
  1370. package/skills/productivity/google-workspace/references/gmail-search-syntax.md +63 -0
  1371. package/skills/productivity/google-workspace/scripts/_hermes_home.py +43 -0
  1372. package/skills/productivity/google-workspace/scripts/google_api.py +1221 -0
  1373. package/skills/productivity/google-workspace/scripts/gws_bridge.py +108 -0
  1374. package/skills/productivity/google-workspace/scripts/setup.py +454 -0
  1375. package/skills/productivity/linear/SKILL.md +380 -0
  1376. package/skills/productivity/linear/scripts/linear_api.py +445 -0
  1377. package/skills/productivity/maps/SKILL.md +195 -0
  1378. package/skills/productivity/maps/scripts/maps_client.py +1298 -0
  1379. package/skills/productivity/nano-pdf/SKILL.md +52 -0
  1380. package/skills/productivity/notion/SKILL.md +448 -0
  1381. package/skills/productivity/notion/references/block-types.md +112 -0
  1382. package/skills/productivity/ocr-and-documents/DESCRIPTION.md +3 -0
  1383. package/skills/productivity/ocr-and-documents/SKILL.md +172 -0
  1384. package/skills/productivity/ocr-and-documents/scripts/extract_marker.py +87 -0
  1385. package/skills/productivity/ocr-and-documents/scripts/extract_pymupdf.py +98 -0
  1386. package/skills/productivity/powerpoint/LICENSE.txt +30 -0
  1387. package/skills/productivity/powerpoint/SKILL.md +237 -0
  1388. package/skills/productivity/powerpoint/editing.md +205 -0
  1389. package/skills/productivity/powerpoint/pptxgenjs.md +420 -0
  1390. package/skills/productivity/powerpoint/scripts/__init__.py +0 -0
  1391. package/skills/productivity/powerpoint/scripts/add_slide.py +195 -0
  1392. package/skills/productivity/powerpoint/scripts/clean.py +286 -0
  1393. package/skills/productivity/powerpoint/scripts/office/helpers/__init__.py +0 -0
  1394. package/skills/productivity/powerpoint/scripts/office/helpers/merge_runs.py +199 -0
  1395. package/skills/productivity/powerpoint/scripts/office/helpers/simplify_redlines.py +197 -0
  1396. package/skills/productivity/powerpoint/scripts/office/pack.py +159 -0
  1397. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  1398. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  1399. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  1400. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  1401. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  1402. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  1403. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  1404. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  1405. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  1406. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  1407. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  1408. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  1409. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  1410. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  1411. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  1412. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  1413. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  1414. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  1415. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  1416. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  1417. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  1418. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  1419. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  1420. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  1421. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  1422. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  1423. package/skills/productivity/powerpoint/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  1424. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-contentTypes.xsd +42 -0
  1425. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-coreProperties.xsd +50 -0
  1426. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-digSig.xsd +49 -0
  1427. package/skills/productivity/powerpoint/scripts/office/schemas/ecma/fourth-edition/opc-relationships.xsd +33 -0
  1428. package/skills/productivity/powerpoint/scripts/office/schemas/mce/mc.xsd +75 -0
  1429. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  1430. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  1431. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  1432. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  1433. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  1434. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  1435. package/skills/productivity/powerpoint/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  1436. package/skills/productivity/teams-meeting-pipeline/SKILL.md +116 -0
  1437. package/skills/red-teaming/godmode/SKILL.md +404 -0
  1438. package/skills/red-teaming/godmode/references/jailbreak-templates.md +128 -0
  1439. package/skills/red-teaming/godmode/references/refusal-detection.md +142 -0
  1440. package/skills/red-teaming/godmode/scripts/auto_jailbreak.py +769 -0
  1441. package/skills/red-teaming/godmode/scripts/godmode_race.py +530 -0
  1442. package/skills/red-teaming/godmode/scripts/load_godmode.py +45 -0
  1443. package/skills/red-teaming/godmode/scripts/parseltongue.py +550 -0
  1444. package/skills/red-teaming/godmode/templates/prefill-subtle.json +10 -0
  1445. package/skills/red-teaming/godmode/templates/prefill.json +18 -0
  1446. package/skills/research/DESCRIPTION.md +3 -0
  1447. package/skills/research/arxiv/SKILL.md +282 -0
  1448. package/skills/research/arxiv/scripts/search_arxiv.py +114 -0
  1449. package/skills/research/blogwatcher/SKILL.md +137 -0
  1450. package/skills/research/llm-wiki/SKILL.md +507 -0
  1451. package/skills/research/polymarket/SKILL.md +77 -0
  1452. package/skills/research/polymarket/references/api-endpoints.md +220 -0
  1453. package/skills/research/polymarket/scripts/polymarket.py +284 -0
  1454. package/skills/research/research-paper-writing/SKILL.md +2377 -0
  1455. package/skills/research/research-paper-writing/references/autoreason-methodology.md +394 -0
  1456. package/skills/research/research-paper-writing/references/checklists.md +434 -0
  1457. package/skills/research/research-paper-writing/references/citation-workflow.md +564 -0
  1458. package/skills/research/research-paper-writing/references/experiment-patterns.md +728 -0
  1459. package/skills/research/research-paper-writing/references/human-evaluation.md +476 -0
  1460. package/skills/research/research-paper-writing/references/paper-types.md +481 -0
  1461. package/skills/research/research-paper-writing/references/reviewer-guidelines.md +433 -0
  1462. package/skills/research/research-paper-writing/references/sources.md +191 -0
  1463. package/skills/research/research-paper-writing/references/writing-guide.md +474 -0
  1464. package/skills/research/research-paper-writing/templates/README.md +251 -0
  1465. package/skills/research/research-paper-writing/templates/aaai2026/README.md +534 -0
  1466. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
  1467. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026-unified-template.tex +952 -0
  1468. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026.bib +111 -0
  1469. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026.bst +1493 -0
  1470. package/skills/research/research-paper-writing/templates/aaai2026/aaai2026.sty +315 -0
  1471. package/skills/research/research-paper-writing/templates/acl/README.md +50 -0
  1472. package/skills/research/research-paper-writing/templates/acl/acl.sty +312 -0
  1473. package/skills/research/research-paper-writing/templates/acl/acl_latex.tex +377 -0
  1474. package/skills/research/research-paper-writing/templates/acl/acl_lualatex.tex +101 -0
  1475. package/skills/research/research-paper-writing/templates/acl/acl_natbib.bst +1940 -0
  1476. package/skills/research/research-paper-writing/templates/acl/anthology.bib.txt +26 -0
  1477. package/skills/research/research-paper-writing/templates/acl/custom.bib +70 -0
  1478. package/skills/research/research-paper-writing/templates/acl/formatting.md +326 -0
  1479. package/skills/research/research-paper-writing/templates/colm2025/README.md +3 -0
  1480. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.bib +11 -0
  1481. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.bst +1440 -0
  1482. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.pdf +0 -0
  1483. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.sty +218 -0
  1484. package/skills/research/research-paper-writing/templates/colm2025/colm2025_conference.tex +305 -0
  1485. package/skills/research/research-paper-writing/templates/colm2025/fancyhdr.sty +485 -0
  1486. package/skills/research/research-paper-writing/templates/colm2025/math_commands.tex +508 -0
  1487. package/skills/research/research-paper-writing/templates/colm2025/natbib.sty +1246 -0
  1488. package/skills/research/research-paper-writing/templates/iclr2026/fancyhdr.sty +485 -0
  1489. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.bib +24 -0
  1490. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.bst +1440 -0
  1491. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.pdf +0 -0
  1492. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.sty +246 -0
  1493. package/skills/research/research-paper-writing/templates/iclr2026/iclr2026_conference.tex +414 -0
  1494. package/skills/research/research-paper-writing/templates/iclr2026/math_commands.tex +508 -0
  1495. package/skills/research/research-paper-writing/templates/iclr2026/natbib.sty +1246 -0
  1496. package/skills/research/research-paper-writing/templates/icml2026/algorithm.sty +79 -0
  1497. package/skills/research/research-paper-writing/templates/icml2026/algorithmic.sty +201 -0
  1498. package/skills/research/research-paper-writing/templates/icml2026/example_paper.bib +75 -0
  1499. package/skills/research/research-paper-writing/templates/icml2026/example_paper.pdf +0 -0
  1500. package/skills/research/research-paper-writing/templates/icml2026/example_paper.tex +662 -0
  1501. package/skills/research/research-paper-writing/templates/icml2026/fancyhdr.sty +864 -0
  1502. package/skills/research/research-paper-writing/templates/icml2026/icml2026.bst +1443 -0
  1503. package/skills/research/research-paper-writing/templates/icml2026/icml2026.sty +767 -0
  1504. package/skills/research/research-paper-writing/templates/icml2026/icml_numpapers.pdf +0 -0
  1505. package/skills/research/research-paper-writing/templates/neurips2025/Makefile +36 -0
  1506. package/skills/research/research-paper-writing/templates/neurips2025/extra_pkgs.tex +53 -0
  1507. package/skills/research/research-paper-writing/templates/neurips2025/main.tex +38 -0
  1508. package/skills/research/research-paper-writing/templates/neurips2025/neurips.sty +382 -0
  1509. package/skills/smart-home/DESCRIPTION.md +3 -0
  1510. package/skills/smart-home/openhue/SKILL.md +109 -0
  1511. package/skills/social-media/DESCRIPTION.md +3 -0
  1512. package/skills/social-media/xurl/SKILL.md +414 -0
  1513. package/skills/software-development/debugging-hermes-tui-commands/SKILL.md +152 -0
  1514. package/skills/software-development/hermes-agent-skill-authoring/SKILL.md +165 -0
  1515. package/skills/software-development/node-inspect-debugger/SKILL.md +319 -0
  1516. package/skills/software-development/plan/SKILL.md +58 -0
  1517. package/skills/software-development/python-debugpy/SKILL.md +375 -0
  1518. package/skills/software-development/requesting-code-review/SKILL.md +280 -0
  1519. package/skills/software-development/spike/SKILL.md +197 -0
  1520. package/skills/software-development/subagent-driven-development/SKILL.md +352 -0
  1521. package/skills/software-development/subagent-driven-development/references/context-budget-discipline.md +53 -0
  1522. package/skills/software-development/subagent-driven-development/references/gates-taxonomy.md +93 -0
  1523. package/skills/software-development/systematic-debugging/SKILL.md +367 -0
  1524. package/skills/software-development/test-driven-development/SKILL.md +343 -0
  1525. package/skills/software-development/writing-plans/SKILL.md +297 -0
  1526. package/skills/yuanbao/SKILL.md +108 -0
  1527. package/tools/__init__.py +25 -0
  1528. package/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  1529. package/tools/__pycache__/approval.cpython-312.pyc +0 -0
  1530. package/tools/__pycache__/binary_extensions.cpython-312.pyc +0 -0
  1531. package/tools/__pycache__/browser_camofox.cpython-312.pyc +0 -0
  1532. package/tools/__pycache__/browser_camofox_state.cpython-312.pyc +0 -0
  1533. package/tools/__pycache__/browser_cdp_tool.cpython-312.pyc +0 -0
  1534. package/tools/__pycache__/browser_dialog_tool.cpython-312.pyc +0 -0
  1535. package/tools/__pycache__/browser_supervisor.cpython-312.pyc +0 -0
  1536. package/tools/__pycache__/browser_tool.cpython-312.pyc +0 -0
  1537. package/tools/__pycache__/budget_config.cpython-312.pyc +0 -0
  1538. package/tools/__pycache__/checkpoint_manager.cpython-312.pyc +0 -0
  1539. package/tools/__pycache__/clarify_gateway.cpython-312.pyc +0 -0
  1540. package/tools/__pycache__/clarify_tool.cpython-312.pyc +0 -0
  1541. package/tools/__pycache__/code_execution_tool.cpython-312.pyc +0 -0
  1542. package/tools/__pycache__/computer_use_tool.cpython-312.pyc +0 -0
  1543. package/tools/__pycache__/cronjob_tools.cpython-312.pyc +0 -0
  1544. package/tools/__pycache__/debug_helpers.cpython-312.pyc +0 -0
  1545. package/tools/__pycache__/delegate_tool.cpython-312.pyc +0 -0
  1546. package/tools/__pycache__/discord_tool.cpython-312.pyc +0 -0
  1547. package/tools/__pycache__/feishu_doc_tool.cpython-312.pyc +0 -0
  1548. package/tools/__pycache__/feishu_drive_tool.cpython-312.pyc +0 -0
  1549. package/tools/__pycache__/file_operations.cpython-312.pyc +0 -0
  1550. package/tools/__pycache__/file_state.cpython-312.pyc +0 -0
  1551. package/tools/__pycache__/file_tools.cpython-312.pyc +0 -0
  1552. package/tools/__pycache__/homeassistant_tool.cpython-312.pyc +0 -0
  1553. package/tools/__pycache__/image_generation_tool.cpython-312.pyc +0 -0
  1554. package/tools/__pycache__/interrupt.cpython-312.pyc +0 -0
  1555. package/tools/__pycache__/kanban_tools.cpython-312.pyc +0 -0
  1556. package/tools/__pycache__/lazy_deps.cpython-312.pyc +0 -0
  1557. package/tools/__pycache__/managed_tool_gateway.cpython-312.pyc +0 -0
  1558. package/tools/__pycache__/mcp_tool.cpython-312.pyc +0 -0
  1559. package/tools/__pycache__/memory_tool.cpython-312.pyc +0 -0
  1560. package/tools/__pycache__/mixture_of_agents_tool.cpython-312.pyc +0 -0
  1561. package/tools/__pycache__/openrouter_client.cpython-312.pyc +0 -0
  1562. package/tools/__pycache__/process_registry.cpython-312.pyc +0 -0
  1563. package/tools/__pycache__/registry.cpython-312.pyc +0 -0
  1564. package/tools/__pycache__/schema_sanitizer.cpython-312.pyc +0 -0
  1565. package/tools/__pycache__/send_message_tool.cpython-312.pyc +0 -0
  1566. package/tools/__pycache__/session_search_tool.cpython-312.pyc +0 -0
  1567. package/tools/__pycache__/skill_manager_tool.cpython-312.pyc +0 -0
  1568. package/tools/__pycache__/skill_provenance.cpython-312.pyc +0 -0
  1569. package/tools/__pycache__/skill_usage.cpython-312.pyc +0 -0
  1570. package/tools/__pycache__/skills_guard.cpython-312.pyc +0 -0
  1571. package/tools/__pycache__/skills_sync.cpython-312.pyc +0 -0
  1572. package/tools/__pycache__/skills_tool.cpython-312.pyc +0 -0
  1573. package/tools/__pycache__/slash_confirm.cpython-312.pyc +0 -0
  1574. package/tools/__pycache__/terminal_tool.cpython-312.pyc +0 -0
  1575. package/tools/__pycache__/tirith_security.cpython-312.pyc +0 -0
  1576. package/tools/__pycache__/todo_tool.cpython-312.pyc +0 -0
  1577. package/tools/__pycache__/tool_backend_helpers.cpython-312.pyc +0 -0
  1578. package/tools/__pycache__/tool_result_storage.cpython-312.pyc +0 -0
  1579. package/tools/__pycache__/tts_tool.cpython-312.pyc +0 -0
  1580. package/tools/__pycache__/url_safety.cpython-312.pyc +0 -0
  1581. package/tools/__pycache__/video_generation_tool.cpython-312.pyc +0 -0
  1582. package/tools/__pycache__/vision_tools.cpython-312.pyc +0 -0
  1583. package/tools/__pycache__/voice_mode.cpython-312.pyc +0 -0
  1584. package/tools/__pycache__/web_tools.cpython-312.pyc +0 -0
  1585. package/tools/__pycache__/website_policy.cpython-312.pyc +0 -0
  1586. package/tools/__pycache__/x_search_tool.cpython-312.pyc +0 -0
  1587. package/tools/__pycache__/xai_http.cpython-312.pyc +0 -0
  1588. package/tools/__pycache__/yuanbao_tools.cpython-312.pyc +0 -0
  1589. package/tools/ansi_strip.py +44 -0
  1590. package/tools/approval.py +1392 -0
  1591. package/tools/binary_extensions.py +42 -0
  1592. package/tools/browser_camofox.py +700 -0
  1593. package/tools/browser_camofox_state.py +48 -0
  1594. package/tools/browser_cdp_tool.py +569 -0
  1595. package/tools/browser_dialog_tool.py +148 -0
  1596. package/tools/browser_providers/__init__.py +10 -0
  1597. package/tools/browser_providers/__pycache__/__init__.cpython-312.pyc +0 -0
  1598. package/tools/browser_providers/__pycache__/base.cpython-312.pyc +0 -0
  1599. package/tools/browser_providers/__pycache__/browser_use.cpython-312.pyc +0 -0
  1600. package/tools/browser_providers/__pycache__/browserbase.cpython-312.pyc +0 -0
  1601. package/tools/browser_providers/__pycache__/firecrawl.cpython-312.pyc +0 -0
  1602. package/tools/browser_providers/base.py +59 -0
  1603. package/tools/browser_providers/browser_use.py +225 -0
  1604. package/tools/browser_providers/browserbase.py +222 -0
  1605. package/tools/browser_providers/firecrawl.py +112 -0
  1606. package/tools/browser_supervisor.py +1457 -0
  1607. package/tools/browser_tool.py +3676 -0
  1608. package/tools/budget_config.py +51 -0
  1609. package/tools/checkpoint_manager.py +1639 -0
  1610. package/tools/clarify_gateway.py +278 -0
  1611. package/tools/clarify_tool.py +141 -0
  1612. package/tools/code_execution_tool.py +1782 -0
  1613. package/tools/computer_use/__init__.py +43 -0
  1614. package/tools/computer_use/__pycache__/__init__.cpython-312.pyc +0 -0
  1615. package/tools/computer_use/__pycache__/backend.cpython-312.pyc +0 -0
  1616. package/tools/computer_use/__pycache__/schema.cpython-312.pyc +0 -0
  1617. package/tools/computer_use/__pycache__/tool.cpython-312.pyc +0 -0
  1618. package/tools/computer_use/backend.py +150 -0
  1619. package/tools/computer_use/cua_backend.py +682 -0
  1620. package/tools/computer_use/schema.py +191 -0
  1621. package/tools/computer_use/tool.py +521 -0
  1622. package/tools/computer_use_tool.py +39 -0
  1623. package/tools/credential_files.py +437 -0
  1624. package/tools/cronjob_tools.py +719 -0
  1625. package/tools/debug_helpers.py +106 -0
  1626. package/tools/delegate_tool.py +2797 -0
  1627. package/tools/discord_tool.py +959 -0
  1628. package/tools/env_passthrough.py +145 -0
  1629. package/tools/environments/__init__.py +14 -0
  1630. package/tools/environments/__pycache__/__init__.cpython-312.pyc +0 -0
  1631. package/tools/environments/__pycache__/base.cpython-312.pyc +0 -0
  1632. package/tools/environments/__pycache__/docker.cpython-312.pyc +0 -0
  1633. package/tools/environments/__pycache__/file_sync.cpython-312.pyc +0 -0
  1634. package/tools/environments/__pycache__/local.cpython-312.pyc +0 -0
  1635. package/tools/environments/__pycache__/managed_modal.cpython-312.pyc +0 -0
  1636. package/tools/environments/__pycache__/modal.cpython-312.pyc +0 -0
  1637. package/tools/environments/__pycache__/modal_utils.cpython-312.pyc +0 -0
  1638. package/tools/environments/__pycache__/singularity.cpython-312.pyc +0 -0
  1639. package/tools/environments/__pycache__/ssh.cpython-312.pyc +0 -0
  1640. package/tools/environments/base.py +844 -0
  1641. package/tools/environments/daytona.py +270 -0
  1642. package/tools/environments/docker.py +656 -0
  1643. package/tools/environments/file_sync.py +400 -0
  1644. package/tools/environments/local.py +658 -0
  1645. package/tools/environments/managed_modal.py +282 -0
  1646. package/tools/environments/modal.py +479 -0
  1647. package/tools/environments/modal_utils.py +199 -0
  1648. package/tools/environments/singularity.py +263 -0
  1649. package/tools/environments/ssh.py +295 -0
  1650. package/tools/environments/vercel_sandbox.py +655 -0
  1651. package/tools/feishu_doc_tool.py +138 -0
  1652. package/tools/feishu_drive_tool.py +431 -0
  1653. package/tools/file_operations.py +1825 -0
  1654. package/tools/file_state.py +332 -0
  1655. package/tools/file_tools.py +1172 -0
  1656. package/tools/fuzzy_match.py +703 -0
  1657. package/tools/homeassistant_tool.py +513 -0
  1658. package/tools/image_generation_tool.py +1098 -0
  1659. package/tools/interrupt.py +98 -0
  1660. package/tools/kanban_tools.py +1139 -0
  1661. package/tools/lazy_deps.py +608 -0
  1662. package/tools/managed_tool_gateway.py +168 -0
  1663. package/tools/mcp_oauth.py +633 -0
  1664. package/tools/mcp_oauth_manager.py +607 -0
  1665. package/tools/mcp_tool.py +3483 -0
  1666. package/tools/memory_tool.py +584 -0
  1667. package/tools/microsoft_graph_auth.py +245 -0
  1668. package/tools/microsoft_graph_client.py +408 -0
  1669. package/tools/mixture_of_agents_tool.py +542 -0
  1670. package/tools/neutts_samples/jo.txt +1 -0
  1671. package/tools/neutts_samples/jo.wav +0 -0
  1672. package/tools/neutts_synth.py +104 -0
  1673. package/tools/openrouter_client.py +33 -0
  1674. package/tools/osv_check.py +155 -0
  1675. package/tools/patch_parser.py +592 -0
  1676. package/tools/path_security.py +43 -0
  1677. package/tools/process_registry.py +1534 -0
  1678. package/tools/registry.py +589 -0
  1679. package/tools/schema_sanitizer.py +370 -0
  1680. package/tools/send_message_tool.py +1900 -0
  1681. package/tools/session_search_tool.py +613 -0
  1682. package/tools/skill_manager_tool.py +932 -0
  1683. package/tools/skill_provenance.py +78 -0
  1684. package/tools/skill_usage.py +610 -0
  1685. package/tools/skills_guard.py +932 -0
  1686. package/tools/skills_hub.py +3263 -0
  1687. package/tools/skills_sync.py +432 -0
  1688. package/tools/skills_tool.py +1569 -0
  1689. package/tools/slash_confirm.py +167 -0
  1690. package/tools/terminal_tool.py +2376 -0
  1691. package/tools/tirith_security.py +775 -0
  1692. package/tools/todo_tool.py +277 -0
  1693. package/tools/tool_backend_helpers.py +144 -0
  1694. package/tools/tool_output_limits.py +92 -0
  1695. package/tools/tool_result_storage.py +232 -0
  1696. package/tools/transcription_tools.py +936 -0
  1697. package/tools/tts_tool.py +2285 -0
  1698. package/tools/url_safety.py +330 -0
  1699. package/tools/video_generation_tool.py +561 -0
  1700. package/tools/vision_tools.py +1422 -0
  1701. package/tools/voice_mode.py +1019 -0
  1702. package/tools/web_tools.py +1551 -0
  1703. package/tools/website_policy.py +283 -0
  1704. package/tools/x_search_tool.py +424 -0
  1705. package/tools/xai_http.py +83 -0
  1706. package/tools/yuanbao_tools.py +736 -0
  1707. package/toolset_distributions.py +364 -0
  1708. package/toolsets.py +866 -0
  1709. package/trajectory_compressor.py +1509 -0
  1710. package/tui_gateway/__init__.py +0 -0
  1711. package/tui_gateway/entry.py +251 -0
  1712. package/tui_gateway/event_publisher.py +126 -0
  1713. package/tui_gateway/render.py +49 -0
  1714. package/tui_gateway/server.py +6623 -0
  1715. package/tui_gateway/slash_worker.py +76 -0
  1716. package/tui_gateway/transport.py +219 -0
  1717. package/tui_gateway/ws.py +178 -0
  1718. package/utils.py +361 -0
@@ -0,0 +1,3789 @@
1
+ """
2
+ Canonical model catalogs and lightweight validation helpers.
3
+
4
+ Add, remove, or reorder entries here — both `hermes setup` and
5
+ `hermes` provider-selection will pick up the change automatically.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import urllib.request
13
+ import urllib.error
14
+ import time
15
+ from difflib import get_close_matches
16
+ from pathlib import Path
17
+ from typing import Any, NamedTuple, Optional
18
+
19
+ from hermes_cli import __version__ as _HERMES_VERSION
20
+
21
+ # Identify ourselves so endpoints fronted by Cloudflare's Browser Integrity
22
+ # Check (error 1010) don't reject the default ``Python-urllib/*`` signature.
23
+ _HERMES_USER_AGENT = f"hermes-cli/{_HERMES_VERSION}"
24
+
25
+ COPILOT_BASE_URL = "https://api.githubcopilot.com"
26
+ COPILOT_MODELS_URL = f"{COPILOT_BASE_URL}/models"
27
+ COPILOT_EDITOR_VERSION = "vscode/1.104.1"
28
+ COPILOT_REASONING_EFFORTS_GPT5 = ["minimal", "low", "medium", "high"]
29
+ COPILOT_REASONING_EFFORTS_O_SERIES = ["low", "medium", "high"]
30
+
31
+
32
+ # Fallback OpenRouter snapshot used when the live catalog is unavailable.
33
+ # (model_id, display description shown in menus)
34
+ OPENROUTER_MODELS: list[tuple[str, str]] = [
35
+ ("anthropic/claude-opus-4.7", ""),
36
+ ("anthropic/claude-opus-4.6", ""),
37
+ ("anthropic/claude-sonnet-4.6", ""),
38
+ ("moonshotai/kimi-k2.6", "recommended"),
39
+ ("openrouter/pareto-code", "auto-routes to cheapest coder meeting openrouter.min_coding_score"),
40
+ ("qwen/qwen3.6-plus", ""),
41
+ ("anthropic/claude-haiku-4.5", ""),
42
+ ("openai/gpt-5.5", ""),
43
+ ("openai/gpt-5.5-pro", ""),
44
+ ("openai/gpt-5.4-mini", ""),
45
+ ("openai/gpt-5.4-nano", ""),
46
+ ("openai/gpt-5.3-codex", ""),
47
+ ("xiaomi/mimo-v2.5-pro", ""),
48
+ ("tencent/hy3-preview", ""),
49
+ ("google/gemini-3-pro-image-preview", ""),
50
+ ("google/gemini-3-flash-preview", ""),
51
+ ("google/gemini-3.1-pro-preview", ""),
52
+ ("google/gemini-3.1-flash-lite-preview", ""),
53
+ ("qwen/qwen3.6-35b-a3b", ""),
54
+ ("stepfun/step-3.5-flash", ""),
55
+ ("minimax/minimax-m2.7", ""),
56
+ ("z-ai/glm-5.1", ""),
57
+ ("x-ai/grok-4.20", ""),
58
+ ("x-ai/grok-4.3", ""),
59
+ ("nvidia/nemotron-3-super-120b-a12b", ""),
60
+ ("deepseek/deepseek-v4-pro", ""),
61
+ # Free tier
62
+ ("openrouter/elephant-alpha", "free"),
63
+ ("openrouter/owl-alpha", "free"),
64
+ ("tencent/hy3-preview:free", "free"),
65
+ ("nvidia/nemotron-3-super-120b-a12b:free", "free"),
66
+ ("inclusionai/ring-2.6-1t:free", "free"),
67
+ ]
68
+
69
+ _openrouter_catalog_cache: list[tuple[str, str]] | None = None
70
+
71
+
72
+ # Fallback Vercel AI Gateway snapshot used when the live catalog is unavailable.
73
+ # OSS / open-weight models prioritized first, then closed-source by family.
74
+ # Slugs match Vercel's actual /v1/models catalog (e.g. alibaba/ for Qwen,
75
+ # zai/ and xai/ without hyphens).
76
+ VERCEL_AI_GATEWAY_MODELS: list[tuple[str, str]] = [
77
+ ("moonshotai/kimi-k2.6", "recommended"),
78
+ ("alibaba/qwen3.6-plus", ""),
79
+ ("zai/glm-5.1", ""),
80
+ ("minimax/minimax-m2.7", ""),
81
+ ("anthropic/claude-sonnet-4.6", ""),
82
+ ("anthropic/claude-opus-4.7", ""),
83
+ ("anthropic/claude-opus-4.6", ""),
84
+ ("anthropic/claude-haiku-4.5", ""),
85
+ ("openai/gpt-5.4", ""),
86
+ ("openai/gpt-5.4-mini", ""),
87
+ ("openai/gpt-5.3-codex", ""),
88
+ ("google/gemini-3.1-pro-preview", ""),
89
+ ("google/gemini-3-flash", ""),
90
+ ("google/gemini-3.1-flash-lite-preview", ""),
91
+ ("xai/grok-4.20-reasoning", ""),
92
+ ]
93
+
94
+ _ai_gateway_catalog_cache: list[tuple[str, str]] | None = None
95
+
96
+
97
+ def _codex_curated_models() -> list[str]:
98
+ """Derive the openai-codex curated list from codex_models.py.
99
+
100
+ Single source of truth: DEFAULT_CODEX_MODELS + forward-compat synthesis.
101
+ This keeps the gateway /model picker in sync with the CLI `hermes model`
102
+ flow without maintaining a separate static list.
103
+ """
104
+ from hermes_cli.codex_models import DEFAULT_CODEX_MODELS, _add_forward_compat_models
105
+ return _add_forward_compat_models(list(DEFAULT_CODEX_MODELS))
106
+
107
+
108
+ # Static fallback for xAI when the models.dev disk cache is empty (fresh
109
+ # install, offline first run, etc.). Mirrors the xAI-direct model IDs from
110
+ # $HERMES_HOME/models_dev_cache.json as of 2026-04-28. Whenever xAI renames
111
+ # or retires a model, the disk cache picks it up on the next refresh and the
112
+ # fallback here only matters until that refresh lands.
113
+ #
114
+ # Models retired by xAI on May 15, 2026 are excluded — see
115
+ # https://docs.x.ai/developers/migration/may-15-retirement
116
+ # (grok-4, grok-4-0709, grok-4-fast{,-reasoning,-non-reasoning},
117
+ # grok-4-1-fast{,-reasoning,-non-reasoning}, grok-code-fast-1 → grok-4.3).
118
+ _XAI_STATIC_FALLBACK: list[str] = [
119
+ "grok-4.3",
120
+ "grok-4.20-0309-reasoning",
121
+ "grok-4.20-0309-non-reasoning",
122
+ "grok-4.20-multi-agent-0309",
123
+ ]
124
+
125
+
126
+ _XAI_TOP_MODEL = "grok-4.3"
127
+
128
+
129
+ def _xai_promote_top(ids: list[str]) -> list[str]:
130
+ """Pin the headline xAI model to the top of the curated list."""
131
+ if _XAI_TOP_MODEL in ids:
132
+ return [_XAI_TOP_MODEL] + [m for m in ids if m != _XAI_TOP_MODEL]
133
+ return ids
134
+
135
+
136
+ def _xai_curated_models() -> list[str]:
137
+ """Derive the xAI-direct curated list from models.dev disk cache.
138
+
139
+ Reads $HERMES_HOME/models_dev_cache.json directly (no network) so this
140
+ runs at import time without blocking. Falls back to ``_XAI_STATIC_FALLBACK``
141
+ when the cache is empty or unreadable. Hermes refreshes the cache from
142
+ https://models.dev/api.json on normal use, so this list self-heals as
143
+ xAI renames models.
144
+
145
+ Mirrors ``_codex_curated_models()``'s role for openai-codex.
146
+ """
147
+ try:
148
+ from agent.models_dev import _load_disk_cache
149
+ data = _load_disk_cache()
150
+ xai = data.get("xai") if isinstance(data, dict) else None
151
+ models = xai.get("models") if isinstance(xai, dict) else None
152
+ if isinstance(models, dict) and models:
153
+ ids = [mid for mid in models.keys() if isinstance(mid, str)]
154
+ if ids:
155
+ return _xai_promote_top(sorted(ids))
156
+ except Exception:
157
+ # Any failure (missing file, malformed JSON, import error)
158
+ # falls through to the static list.
159
+ pass
160
+ return list(_XAI_STATIC_FALLBACK)
161
+
162
+
163
+ _PROVIDER_MODELS: dict[str, list[str]] = {
164
+ "nous": [
165
+ "anthropic/claude-opus-4.7",
166
+ "anthropic/claude-opus-4.6",
167
+ "anthropic/claude-sonnet-4.6",
168
+ "moonshotai/kimi-k2.6",
169
+ "qwen/qwen3.6-plus",
170
+ "anthropic/claude-haiku-4.5",
171
+ "openai/gpt-5.5",
172
+ "openai/gpt-5.5-pro",
173
+ "openai/gpt-5.4-mini",
174
+ "openai/gpt-5.4-nano",
175
+ "openai/gpt-5.3-codex",
176
+ "xiaomi/mimo-v2.5-pro",
177
+ "tencent/hy3-preview",
178
+ "google/gemini-3-pro-preview",
179
+ "google/gemini-3-flash-preview",
180
+ "google/gemini-3.1-pro-preview",
181
+ "google/gemini-3.1-flash-lite-preview",
182
+ "qwen/qwen3.6-35b-a3b",
183
+ "stepfun/step-3.5-flash",
184
+ "minimax/minimax-m2.7",
185
+ "z-ai/glm-5.1",
186
+ "x-ai/grok-4.3",
187
+ "nvidia/nemotron-3-super-120b-a12b",
188
+ "deepseek/deepseek-v4-pro",
189
+ ],
190
+ # Native OpenAI Chat Completions (api.openai.com). Used by /model counts and
191
+ # provider_model_ids fallback when /v1/models is unavailable.
192
+ "openai": [
193
+ "gpt-5.4",
194
+ "gpt-5.4-mini",
195
+ "gpt-5-mini",
196
+ "gpt-5.3-codex",
197
+ "gpt-5.2-codex",
198
+ "gpt-4.1",
199
+ "gpt-4o",
200
+ "gpt-4o-mini",
201
+ ],
202
+ "openai-codex": _codex_curated_models(),
203
+ "xai-oauth": _xai_curated_models(),
204
+ "copilot-acp": [
205
+ "copilot-acp",
206
+ ],
207
+ "copilot": [
208
+ "gpt-5.4",
209
+ "gpt-5.4-mini",
210
+ "gpt-5-mini",
211
+ "gpt-5.3-codex",
212
+ "gpt-5.2-codex",
213
+ "gpt-4.1",
214
+ "gpt-4o",
215
+ "gpt-4o-mini",
216
+ "claude-sonnet-4.6",
217
+ "claude-sonnet-4",
218
+ "claude-sonnet-4.5",
219
+ "claude-haiku-4.5",
220
+ "gemini-3.1-pro-preview",
221
+ "gemini-3-pro-preview",
222
+ "gemini-3-flash-preview",
223
+ "gemini-2.5-pro",
224
+ ],
225
+ "gemini": [
226
+ "gemini-3.1-pro-preview",
227
+ "gemini-3-pro-preview",
228
+ "gemini-3-flash-preview",
229
+ "gemini-3.1-flash-lite-preview",
230
+ ],
231
+ "google-gemini-cli": [
232
+ "gemini-3.1-pro-preview",
233
+ "gemini-3-pro-preview",
234
+ "gemini-3-flash-preview",
235
+ ],
236
+ "zai": [
237
+ "glm-5.1",
238
+ "glm-5",
239
+ "glm-5v-turbo",
240
+ "glm-5-turbo",
241
+ "glm-4.7",
242
+ "glm-4.5",
243
+ "glm-4.5-flash",
244
+ ],
245
+ "xai": _xai_curated_models(),
246
+ "nvidia": [
247
+ # NVIDIA flagship reasoning models
248
+ "nvidia/nemotron-3-super-120b-a12b",
249
+ "nvidia/nemotron-3-nano-30b-a3b",
250
+ "nvidia/llama-3.3-nemotron-super-49b-v1.5",
251
+ # Third-party agentic models hosted on build.nvidia.com
252
+ # (map to OpenRouter defaults — users get familiar picks on NIM)
253
+ "qwen/qwen3.5-397b-a17b",
254
+ "deepseek-ai/deepseek-v3.2",
255
+ "moonshotai/kimi-k2.6",
256
+ "minimaxai/minimax-m2.5",
257
+ "z-ai/glm5",
258
+ "openai/gpt-oss-120b",
259
+ ],
260
+ "kimi-coding": [
261
+ "kimi-k2.6",
262
+ "kimi-k2.5",
263
+ "kimi-for-coding",
264
+ "kimi-k2-thinking",
265
+ "kimi-k2-thinking-turbo",
266
+ "kimi-k2-turbo-preview",
267
+ "kimi-k2-0905-preview",
268
+ ],
269
+ "kimi-coding-cn": [
270
+ "kimi-k2.6",
271
+ "kimi-k2.5",
272
+ "kimi-k2-thinking",
273
+ "kimi-k2-turbo-preview",
274
+ "kimi-k2-0905-preview",
275
+ ],
276
+ "stepfun": [
277
+ "step-3.5-flash",
278
+ "step-3.5-flash-2603",
279
+ ],
280
+ "moonshot": [
281
+ "kimi-k2.6",
282
+ "kimi-k2.5",
283
+ "kimi-k2-thinking",
284
+ "kimi-k2-turbo-preview",
285
+ "kimi-k2-0905-preview",
286
+ ],
287
+ "minimax": [
288
+ "MiniMax-M2.7",
289
+ "MiniMax-M2.5",
290
+ "MiniMax-M2.1",
291
+ "MiniMax-M2",
292
+ ],
293
+ "minimax-oauth": [
294
+ "MiniMax-M2.7",
295
+ "MiniMax-M2.7-highspeed",
296
+ ],
297
+ "minimax-cn": [
298
+ "MiniMax-M2.7",
299
+ "MiniMax-M2.5",
300
+ "MiniMax-M2.1",
301
+ "MiniMax-M2",
302
+ ],
303
+ "anthropic": [
304
+ "claude-opus-4-7",
305
+ "claude-opus-4-6",
306
+ "claude-sonnet-4-6",
307
+ "claude-opus-4-5-20251101",
308
+ "claude-sonnet-4-5-20250929",
309
+ "claude-opus-4-20250514",
310
+ "claude-sonnet-4-20250514",
311
+ "claude-haiku-4-5-20251001",
312
+ ],
313
+ "deepseek": [
314
+ "deepseek-v4-pro",
315
+ "deepseek-v4-flash",
316
+ "deepseek-chat",
317
+ "deepseek-reasoner",
318
+ ],
319
+ "xiaomi": [
320
+ "mimo-v2.5-pro",
321
+ "mimo-v2.5",
322
+ "mimo-v2-pro",
323
+ "mimo-v2-omni",
324
+ "mimo-v2-flash",
325
+ ],
326
+ "tencent-tokenhub": [
327
+ "hy3-preview",
328
+ ],
329
+ "arcee": [
330
+ "trinity-large-thinking",
331
+ "trinity-large-preview",
332
+ "trinity-mini",
333
+ ],
334
+ "gmi": [
335
+ "zai-org/GLM-5.1-FP8",
336
+ "deepseek-ai/DeepSeek-V3.2",
337
+ "moonshotai/Kimi-K2.5",
338
+ "google/gemini-3.1-flash-lite-preview",
339
+ "anthropic/claude-sonnet-4.6",
340
+ "openai/gpt-5.4",
341
+ ],
342
+ "opencode-zen": [
343
+ "kimi-k2.5",
344
+ "gpt-5.4-pro",
345
+ "gpt-5.4",
346
+ "gpt-5.3-codex",
347
+ "gpt-5.2",
348
+ "gpt-5.2-codex",
349
+ "gpt-5.1",
350
+ "gpt-5.1-codex",
351
+ "gpt-5.1-codex-max",
352
+ "gpt-5.1-codex-mini",
353
+ "gpt-5",
354
+ "gpt-5-codex",
355
+ "gpt-5-nano",
356
+ "claude-opus-4-6",
357
+ "claude-opus-4-5",
358
+ "claude-opus-4-1",
359
+ "claude-sonnet-4-6",
360
+ "claude-sonnet-4-5",
361
+ "claude-sonnet-4",
362
+ "claude-haiku-4-5",
363
+ "claude-3-5-haiku",
364
+ "gemini-3.1-pro",
365
+ "gemini-3-pro",
366
+ "gemini-3-flash",
367
+ "minimax-m2.7",
368
+ "minimax-m2.5",
369
+ "minimax-m2.5-free",
370
+ "minimax-m2.1",
371
+ "glm-5",
372
+ "glm-4.7",
373
+ "glm-4.6",
374
+ "kimi-k2-thinking",
375
+ "kimi-k2",
376
+ "qwen3-coder",
377
+ "big-pickle",
378
+ ],
379
+ "opencode-go": [
380
+ "kimi-k2.6",
381
+ "kimi-k2.5",
382
+ "glm-5.1",
383
+ "glm-5",
384
+ "mimo-v2.5-pro",
385
+ "mimo-v2.5",
386
+ "mimo-v2-pro",
387
+ "mimo-v2-omni",
388
+ "minimax-m2.7",
389
+ "minimax-m2.5",
390
+ "qwen3.6-plus",
391
+ "qwen3.5-plus",
392
+ ],
393
+ "kilocode": [
394
+ "anthropic/claude-opus-4.6",
395
+ "anthropic/claude-sonnet-4.6",
396
+ "openai/gpt-5.4",
397
+ "google/gemini-3-pro-preview",
398
+ "google/gemini-3-flash-preview",
399
+ ],
400
+ # Alibaba DashScope Coding platform (coding-intl) — default endpoint.
401
+ # Supports Qwen models + third-party providers (GLM, Kimi, MiniMax).
402
+ # Users with classic DashScope keys should override DASHSCOPE_BASE_URL
403
+ # to https://dashscope-intl.aliyuncs.com/compatible-mode/v1 (OpenAI-compat)
404
+ # or https://dashscope-intl.aliyuncs.com/apps/anthropic (Anthropic-compat).
405
+ "alibaba": [
406
+ "qwen3.6-plus",
407
+ "kimi-k2.5",
408
+ "qwen3.5-plus",
409
+ "qwen3-coder-plus",
410
+ "qwen3-coder-next",
411
+ # Third-party models available on coding-intl
412
+ "glm-5",
413
+ "glm-4.7",
414
+ "MiniMax-M2.5",
415
+ ],
416
+ # Alibaba Coding Plan — same platform as alibaba (DashScope coding-intl),
417
+ # separate provider ID with its own base_url_env_var.
418
+ "alibaba-coding-plan": [
419
+ "qwen3.6-plus",
420
+ "qwen3.5-plus",
421
+ "qwen3-coder-plus",
422
+ "qwen3-coder-next",
423
+ "kimi-k2.5",
424
+ "glm-5",
425
+ "glm-4.7",
426
+ "MiniMax-M2.5",
427
+ ],
428
+ # Curated HF model list — only agentic models that map to OpenRouter defaults.
429
+ "huggingface": [
430
+ "moonshotai/Kimi-K2.5",
431
+ "Qwen/Qwen3.5-397B-A17B",
432
+ "Qwen/Qwen3.5-35B-A3B",
433
+ "deepseek-ai/DeepSeek-V3.2",
434
+ "MiniMaxAI/MiniMax-M2.5",
435
+ "zai-org/GLM-5",
436
+ "XiaomiMiMo/MiMo-V2-Flash",
437
+ "moonshotai/Kimi-K2-Thinking",
438
+ "moonshotai/Kimi-K2.6",
439
+ ],
440
+ # AWS Bedrock — static fallback list used when dynamic discovery is
441
+ # unavailable (no boto3, no credentials, or API error). The agent
442
+ # prefers live discovery via ListFoundationModels + ListInferenceProfiles.
443
+ # Use inference profile IDs (us.*) since most models require them.
444
+ "bedrock": [
445
+ "us.anthropic.claude-sonnet-4-6",
446
+ "us.anthropic.claude-opus-4-6-v1",
447
+ "us.anthropic.claude-haiku-4-5-20251001-v1:0",
448
+ "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
449
+ "us.amazon.nova-pro-v1:0",
450
+ "us.amazon.nova-lite-v1:0",
451
+ "us.amazon.nova-micro-v1:0",
452
+ "deepseek.v3.2",
453
+ "us.meta.llama4-maverick-17b-instruct-v1:0",
454
+ "us.meta.llama4-scout-17b-instruct-v1:0",
455
+ ],
456
+ # Azure Foundry: user-provided endpoint and model.
457
+ # Empty list because models depend on the endpoint configuration.
458
+ "azure-foundry": [],
459
+ "novita": [
460
+ "moonshotai/kimi-k2.5",
461
+ "minimax/minimax-m2.7",
462
+ "zai-org/glm-5",
463
+ "deepseek/deepseek-v3-0324",
464
+ "deepseek/deepseek-r1-0528",
465
+ "qwen/qwen3-235b-a22b-fp8",
466
+ ],
467
+ }
468
+
469
+ # Vercel AI Gateway: derive the bare-model-id catalog from the curated
470
+ # ``VERCEL_AI_GATEWAY_MODELS`` snapshot so both the picker (tuples with descriptions)
471
+ # and the static fallback catalog (bare ids) stay in sync from a single
472
+ # source of truth.
473
+ _PROVIDER_MODELS["ai-gateway"] = [mid for mid, _ in VERCEL_AI_GATEWAY_MODELS]
474
+
475
+ # ---------------------------------------------------------------------------
476
+ # Nous Portal free-model helper
477
+ # ---------------------------------------------------------------------------
478
+ # The Nous Portal models endpoint is the source of truth for which models
479
+ # are currently offered (free or paid). We trust whatever it returns and
480
+ # surface it to users as-is — no local allowlist filtering.
481
+
482
+
483
+ def _is_model_free(model_id: str, pricing: dict[str, dict[str, str]]) -> bool:
484
+ """Return True if *model_id* has zero-cost prompt AND completion pricing."""
485
+ p = pricing.get(model_id)
486
+ if not p:
487
+ return False
488
+ try:
489
+ return float(p.get("prompt", "1")) == 0 and float(p.get("completion", "1")) == 0
490
+ except (TypeError, ValueError):
491
+ return False
492
+
493
+
494
+ # ---------------------------------------------------------------------------
495
+ # Nous Portal account tier detection
496
+ # ---------------------------------------------------------------------------
497
+
498
+ def fetch_nous_account_tier(access_token: str, portal_base_url: str = "") -> dict[str, Any]:
499
+ """Fetch the user's Nous Portal account/subscription info.
500
+
501
+ Calls ``<portal>/api/oauth/account`` with the OAuth access token.
502
+
503
+ Returns the parsed JSON dict on success, e.g.::
504
+
505
+ {
506
+ "subscription": {
507
+ "plan": "Plus",
508
+ "tier": 2,
509
+ "monthly_charge": 20,
510
+ "credits_remaining": 1686.60,
511
+ ...
512
+ },
513
+ ...
514
+ }
515
+
516
+ Returns an empty dict on any failure (network, auth, parse).
517
+ """
518
+ base = (portal_base_url or "https://portal.nousresearch.com").rstrip("/")
519
+ url = f"{base}/api/oauth/account"
520
+ headers = {
521
+ "Authorization": f"Bearer {access_token}",
522
+ "Accept": "application/json",
523
+ }
524
+ try:
525
+ req = urllib.request.Request(url, headers=headers)
526
+ with urllib.request.urlopen(req, timeout=8) as resp:
527
+ return json.loads(resp.read().decode())
528
+ except Exception:
529
+ return {}
530
+
531
+
532
+ def is_nous_free_tier(account_info: dict[str, Any]) -> bool:
533
+ """Return True if the account info indicates a free (unpaid) tier.
534
+
535
+ Checks ``subscription.monthly_charge == 0``. Returns False when
536
+ the field is missing or unparseable (assumes paid — don't block users).
537
+ """
538
+ sub = account_info.get("subscription")
539
+ if not isinstance(sub, dict):
540
+ return False
541
+ charge = sub.get("monthly_charge")
542
+ if charge is None:
543
+ return False
544
+ try:
545
+ return float(charge) == 0
546
+ except (TypeError, ValueError):
547
+ return False
548
+
549
+
550
+ def partition_nous_models_by_tier(
551
+ model_ids: list[str],
552
+ pricing: dict[str, dict[str, str]],
553
+ free_tier: bool,
554
+ ) -> tuple[list[str], list[str]]:
555
+ """Split Nous models into (selectable, unavailable) based on user tier.
556
+
557
+ For paid-tier users: all models are selectable, none unavailable.
558
+
559
+ For free-tier users: only free models are selectable; paid models
560
+ are returned as unavailable (shown grayed out in the menu).
561
+ """
562
+ if not free_tier:
563
+ return (model_ids, [])
564
+
565
+ if not pricing:
566
+ return (model_ids, []) # can't determine, show everything
567
+
568
+ selectable: list[str] = []
569
+ unavailable: list[str] = []
570
+ for mid in model_ids:
571
+ if _is_model_free(mid, pricing):
572
+ selectable.append(mid)
573
+ else:
574
+ unavailable.append(mid)
575
+ return (selectable, unavailable)
576
+
577
+
578
+ def union_with_portal_free_recommendations(
579
+ curated_ids: list[str],
580
+ pricing: dict[str, dict[str, str]],
581
+ portal_base_url: str = "",
582
+ *,
583
+ force_refresh: bool = False,
584
+ ) -> tuple[list[str], dict[str, dict[str, str]]]:
585
+ """Augment curated list + pricing with the Portal's ``freeRecommendedModels``.
586
+
587
+ The Portal's ``/api/nous/recommended-models`` endpoint advertises which
588
+ models are free *right now* — independent of what the in-repo
589
+ ``_PROVIDER_MODELS["nous"]`` list happens to contain or whether the
590
+ docs-hosted catalog manifest has been rebuilt since the last release.
591
+
592
+ For free-tier users this is the source of truth: any model the Portal
593
+ flags as free should be selectable, even if the user is running an
594
+ older Hermes that doesn't ship that model in its hardcoded curated
595
+ list. This function returns an augmented ``(model_ids, pricing)``
596
+ pair where:
597
+
598
+ * Portal free recommendations missing from ``curated_ids`` are
599
+ appended at the front (so the picker shows them first).
600
+ * ``pricing`` gets a synthetic ``{"prompt": "0", "completion": "0"}``
601
+ entry for any free recommendation missing from the live pricing
602
+ map, so :func:`partition_nous_models_by_tier` keeps it.
603
+
604
+ Failures (network, parse, missing field) are silent and degrade to
605
+ returning the inputs unchanged.
606
+ """
607
+ try:
608
+ payload = fetch_nous_recommended_models(
609
+ portal_base_url, force_refresh=force_refresh
610
+ )
611
+ except Exception:
612
+ return (list(curated_ids), dict(pricing))
613
+
614
+ free_block = payload.get("freeRecommendedModels") if isinstance(payload, dict) else None
615
+ if not isinstance(free_block, list) or not free_block:
616
+ return (list(curated_ids), dict(pricing))
617
+
618
+ portal_free_ids: list[str] = []
619
+ for entry in free_block:
620
+ name = _extract_model_name(entry)
621
+ if name:
622
+ portal_free_ids.append(name)
623
+ if not portal_free_ids:
624
+ return (list(curated_ids), dict(pricing))
625
+
626
+ augmented_pricing = dict(pricing)
627
+ free_synthetic = {"prompt": "0", "completion": "0"}
628
+ for mid in portal_free_ids:
629
+ if mid not in augmented_pricing:
630
+ augmented_pricing[mid] = dict(free_synthetic)
631
+
632
+ augmented_ids = list(curated_ids)
633
+ seen = set(augmented_ids)
634
+ # Prepend Portal free recommendations that aren't already curated, so
635
+ # they appear first in the picker.
636
+ new_ones = [mid for mid in portal_free_ids if mid not in seen]
637
+ if new_ones:
638
+ augmented_ids = new_ones + augmented_ids
639
+
640
+ return (augmented_ids, augmented_pricing)
641
+
642
+
643
+ def union_with_portal_paid_recommendations(
644
+ curated_ids: list[str],
645
+ pricing: dict[str, dict[str, str]],
646
+ portal_base_url: str = "",
647
+ *,
648
+ force_refresh: bool = False,
649
+ ) -> tuple[list[str], dict[str, dict[str, str]]]:
650
+ """Augment curated list with the Portal's ``paidRecommendedModels``.
651
+
652
+ Mirror of :func:`union_with_portal_free_recommendations` for paid-tier
653
+ users. The Portal's ``/api/nous/recommended-models`` endpoint advertises
654
+ which paid models are blessed *right now* — independent of what the
655
+ in-repo ``_PROVIDER_MODELS["nous"]`` list happens to contain or whether
656
+ the docs-hosted catalog manifest has been rebuilt since the last release.
657
+
658
+ For paid-tier users this lets newly-launched paid models surface in the
659
+ picker even if the user is running an older Hermes that doesn't ship
660
+ them in its hardcoded curated list. This function returns an augmented
661
+ ``(model_ids, pricing)`` pair where:
662
+
663
+ * Portal paid recommendations missing from ``curated_ids`` are
664
+ appended at the front (so the picker shows them first).
665
+ * ``pricing`` is left untouched — we deliberately do NOT synthesize
666
+ pricing entries for paid models. Live pricing is fetched separately
667
+ via :func:`get_pricing_for_provider`; if the live endpoint hasn't
668
+ published pricing yet, the picker shows a blank price column rather
669
+ than fabricating numbers. (The free helper synthesizes ``$0`` so
670
+ :func:`partition_nous_models_by_tier` keeps free models selectable;
671
+ no equivalent gating applies on the paid side, so synthesis would
672
+ only mislead the user.)
673
+
674
+ Failures (network, parse, missing field) are silent and degrade to
675
+ returning the inputs unchanged — never block the picker on a
676
+ Portal-side hiccup.
677
+ """
678
+ try:
679
+ payload = fetch_nous_recommended_models(
680
+ portal_base_url, force_refresh=force_refresh
681
+ )
682
+ except Exception:
683
+ return (list(curated_ids), dict(pricing))
684
+
685
+ paid_block = payload.get("paidRecommendedModels") if isinstance(payload, dict) else None
686
+ if not isinstance(paid_block, list) or not paid_block:
687
+ return (list(curated_ids), dict(pricing))
688
+
689
+ portal_paid_ids: list[str] = []
690
+ for entry in paid_block:
691
+ name = _extract_model_name(entry)
692
+ if name:
693
+ portal_paid_ids.append(name)
694
+ if not portal_paid_ids:
695
+ return (list(curated_ids), dict(pricing))
696
+
697
+ augmented_ids = list(curated_ids)
698
+ seen = set(augmented_ids)
699
+ # Prepend Portal paid recommendations that aren't already curated, so
700
+ # the Portal-blessed picks surface first in the picker.
701
+ new_ones = [mid for mid in portal_paid_ids if mid not in seen]
702
+ if new_ones:
703
+ augmented_ids = new_ones + augmented_ids
704
+
705
+ return (augmented_ids, dict(pricing))
706
+
707
+
708
+ # ---------------------------------------------------------------------------
709
+ # TTL cache for free-tier detection — avoids repeated API calls within a
710
+ # session while still picking up upgrades quickly.
711
+ # ---------------------------------------------------------------------------
712
+ _FREE_TIER_CACHE_TTL: int = 180 # seconds (3 minutes)
713
+ _free_tier_cache: tuple[bool, float] | None = None # (result, timestamp)
714
+
715
+
716
+ def check_nous_free_tier() -> bool:
717
+ """Check if the current Nous Portal user is on a free (unpaid) tier.
718
+
719
+ Results are cached for ``_FREE_TIER_CACHE_TTL`` seconds to avoid
720
+ hitting the Portal API on every call. The cache is short-lived so
721
+ that an account upgrade is reflected within a few minutes.
722
+
723
+ Returns False (assume paid) on any error — never blocks paying users.
724
+ """
725
+ global _free_tier_cache
726
+ now = time.monotonic()
727
+ if _free_tier_cache is not None:
728
+ cached_result, cached_at = _free_tier_cache
729
+ if now - cached_at < _FREE_TIER_CACHE_TTL:
730
+ return cached_result
731
+
732
+ try:
733
+ from hermes_cli.auth import get_provider_auth_state, resolve_nous_runtime_credentials
734
+
735
+ # Ensure we have a fresh token (triggers refresh if needed)
736
+ resolve_nous_runtime_credentials(min_key_ttl_seconds=60)
737
+
738
+ state = get_provider_auth_state("nous")
739
+ if not state:
740
+ _free_tier_cache = (False, now)
741
+ return False
742
+ access_token = state.get("access_token", "")
743
+ portal_url = state.get("portal_base_url", "")
744
+ if not access_token:
745
+ _free_tier_cache = (False, now)
746
+ return False
747
+
748
+ account_info = fetch_nous_account_tier(access_token, portal_url)
749
+ result = is_nous_free_tier(account_info)
750
+ _free_tier_cache = (result, now)
751
+ return result
752
+ except Exception:
753
+ _free_tier_cache = (False, now)
754
+ return False # default to paid on error — don't block users
755
+
756
+
757
+ # ---------------------------------------------------------------------------
758
+ # Nous Portal recommended models
759
+ #
760
+ # The Portal publishes a curated list of suggested models (separated into
761
+ # paid and free tiers) plus dedicated recommendations for compaction (text
762
+ # summarisation / auxiliary) and vision tasks. We fetch it once per process
763
+ # with a TTL cache so callers can ask "what's the best aux model right now?"
764
+ # without hitting the network on every lookup.
765
+ #
766
+ # Shape of the response (fields we care about):
767
+ # {
768
+ # "paidRecommendedModels": [ {modelName, ...}, ... ],
769
+ # "freeRecommendedModels": [ {modelName, ...}, ... ],
770
+ # "paidRecommendedCompactionModel": {modelName, ...} | null,
771
+ # "paidRecommendedVisionModel": {modelName, ...} | null,
772
+ # "freeRecommendedCompactionModel": {modelName, ...} | null,
773
+ # "freeRecommendedVisionModel": {modelName, ...} | null,
774
+ # }
775
+ # ---------------------------------------------------------------------------
776
+
777
+ NOUS_RECOMMENDED_MODELS_PATH = "/api/nous/recommended-models"
778
+ _NOUS_RECOMMENDED_CACHE_TTL: int = 600 # seconds (10 minutes)
779
+ # (result_dict, timestamp) keyed by portal_base_url so staging vs prod don't collide.
780
+ _nous_recommended_cache: dict[str, tuple[dict[str, Any], float]] = {}
781
+
782
+
783
+ def fetch_nous_recommended_models(
784
+ portal_base_url: str = "",
785
+ timeout: float = 5.0,
786
+ *,
787
+ force_refresh: bool = False,
788
+ ) -> dict[str, Any]:
789
+ """Fetch the Nous Portal's curated recommended-models payload.
790
+
791
+ Hits ``<portal>/api/nous/recommended-models``. The endpoint is public —
792
+ no auth is required. Results are cached per portal URL for
793
+ ``_NOUS_RECOMMENDED_CACHE_TTL`` seconds; pass ``force_refresh=True`` to
794
+ bypass the cache.
795
+
796
+ Returns the parsed JSON dict on success, or ``{}`` on any failure
797
+ (network, parse, non-2xx). Callers must treat missing/null fields as
798
+ "no recommendation" and fall back to their own default.
799
+ """
800
+ base = (portal_base_url or "https://portal.nousresearch.com").rstrip("/")
801
+ now = time.monotonic()
802
+ cached = _nous_recommended_cache.get(base)
803
+ if not force_refresh and cached is not None:
804
+ payload, cached_at = cached
805
+ if now - cached_at < _NOUS_RECOMMENDED_CACHE_TTL:
806
+ return payload
807
+
808
+ url = f"{base}{NOUS_RECOMMENDED_MODELS_PATH}"
809
+ try:
810
+ req = urllib.request.Request(
811
+ url,
812
+ headers={"Accept": "application/json"},
813
+ )
814
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
815
+ data = json.loads(resp.read().decode())
816
+ if not isinstance(data, dict):
817
+ data = {}
818
+ except Exception:
819
+ data = {}
820
+
821
+ _nous_recommended_cache[base] = (data, now)
822
+ return data
823
+
824
+
825
+ def _resolve_nous_portal_url() -> str:
826
+ """Best-effort lookup of the Portal base URL the user is authed against."""
827
+ try:
828
+ from hermes_cli.auth import (
829
+ DEFAULT_NOUS_PORTAL_URL,
830
+ get_provider_auth_state,
831
+ )
832
+ state = get_provider_auth_state("nous") or {}
833
+ portal = str(state.get("portal_base_url") or "").strip()
834
+ if portal:
835
+ return portal.rstrip("/")
836
+ return str(DEFAULT_NOUS_PORTAL_URL).rstrip("/")
837
+ except Exception:
838
+ return "https://portal.nousresearch.com"
839
+
840
+
841
+ def _extract_model_name(entry: Any) -> Optional[str]:
842
+ """Pull the ``modelName`` field from a recommended-model entry, else None."""
843
+ if not isinstance(entry, dict):
844
+ return None
845
+ model_name = entry.get("modelName")
846
+ if isinstance(model_name, str) and model_name.strip():
847
+ return model_name.strip()
848
+ return None
849
+
850
+
851
+ def get_nous_recommended_aux_model(
852
+ *,
853
+ vision: bool = False,
854
+ free_tier: Optional[bool] = None,
855
+ portal_base_url: str = "",
856
+ force_refresh: bool = False,
857
+ ) -> Optional[str]:
858
+ """Return the Portal's recommended model name for an auxiliary task.
859
+
860
+ Picks the best field from the Portal's recommended-models payload:
861
+
862
+ * ``vision=True`` → ``paidRecommendedVisionModel`` (paid tier) or
863
+ ``freeRecommendedVisionModel`` (free tier)
864
+ * ``vision=False`` → ``paidRecommendedCompactionModel`` or
865
+ ``freeRecommendedCompactionModel``
866
+
867
+ When ``free_tier`` is ``None`` (default) the user's tier is auto-detected
868
+ via :func:`check_nous_free_tier`. Pass an explicit bool to bypass the
869
+ detection — useful for tests or when the caller already knows the tier.
870
+
871
+ For paid-tier users we prefer the paid recommendation but gracefully fall
872
+ back to the free recommendation if the Portal returned ``null`` for the
873
+ paid field (common during the staged rollout of new paid models).
874
+
875
+ Returns ``None`` when every candidate is missing, null, or the fetch
876
+ fails — callers should fall back to their own default (currently
877
+ ``google/gemini-3-flash-preview``).
878
+ """
879
+ base = portal_base_url or _resolve_nous_portal_url()
880
+ payload = fetch_nous_recommended_models(base, force_refresh=force_refresh)
881
+ if not payload:
882
+ return None
883
+
884
+ if free_tier is None:
885
+ try:
886
+ free_tier = check_nous_free_tier()
887
+ except Exception:
888
+ # On any detection error, assume paid — paid users see both fields
889
+ # anyway so this is a safe default that maximises model quality.
890
+ free_tier = False
891
+
892
+ if vision:
893
+ paid_key, free_key = "paidRecommendedVisionModel", "freeRecommendedVisionModel"
894
+ else:
895
+ paid_key, free_key = "paidRecommendedCompactionModel", "freeRecommendedCompactionModel"
896
+
897
+ # Preference order:
898
+ # free tier → free only
899
+ # paid tier → paid, then free (if paid field is null)
900
+ candidates = [free_key] if free_tier else [paid_key, free_key]
901
+ for key in candidates:
902
+ name = _extract_model_name(payload.get(key))
903
+ if name:
904
+ return name
905
+ return None
906
+
907
+
908
+ # ---------------------------------------------------------------------------
909
+ # Canonical provider list — single source of truth for provider identity.
910
+ # Every code path that lists, displays, or iterates providers derives from
911
+ # this list: hermes model, /model, list_authenticated_providers.
912
+ #
913
+ # Fields:
914
+ # slug — internal provider ID (used in config.yaml, --provider flag)
915
+ # label — short display name
916
+ # tui_desc — longer description for the `hermes model` interactive picker
917
+ # ---------------------------------------------------------------------------
918
+
919
+ class ProviderEntry(NamedTuple):
920
+ slug: str
921
+ label: str
922
+ tui_desc: str # detailed description for `hermes model` TUI
923
+
924
+ CANONICAL_PROVIDERS: list[ProviderEntry] = [
925
+ ProviderEntry("nous", "Nous Portal", "Nous Portal (Nous Research subscription)"),
926
+ ProviderEntry("openrouter", "OpenRouter", "OpenRouter (100+ models, pay-per-use)"),
927
+ ProviderEntry("novita", "NovitaAI", "NovitaAI (AI-native cloud: Model API, Agent Sandbox, GPU Cloud)"),
928
+ ProviderEntry("lmstudio", "LM Studio", "LM Studio (local desktop app with built-in model server)"),
929
+ ProviderEntry("anthropic", "Anthropic", "Anthropic (Claude models — API key or Claude Code)"),
930
+ ProviderEntry("openai-codex", "OpenAI Codex", "OpenAI Codex"),
931
+ ProviderEntry("alibaba", "Qwen Cloud", "Qwen Cloud / DashScope Coding (Qwen + multi-provider)"),
932
+ ProviderEntry("xai-oauth", "xAI Grok OAuth (SuperGrok Subscription)", "xAI Grok OAuth (SuperGrok Subscription)"),
933
+ ProviderEntry("xiaomi", "Xiaomi MiMo", "Xiaomi MiMo (MiMo-V2.5 and V2 models — pro, omni, flash)"),
934
+ ProviderEntry("tencent-tokenhub", "Tencent TokenHub", "Tencent TokenHub (Hy3 Preview — direct API via tokenhub.tencentmaas.com)"),
935
+ ProviderEntry("nvidia", "NVIDIA NIM", "NVIDIA NIM (Nemotron models — build.nvidia.com or local NIM)"),
936
+ ProviderEntry("copilot", "GitHub Copilot", "GitHub Copilot (uses GITHUB_TOKEN or gh auth token)"),
937
+ ProviderEntry("copilot-acp", "GitHub Copilot ACP", "GitHub Copilot ACP (spawns `copilot --acp --stdio`)"),
938
+ ProviderEntry("huggingface", "Hugging Face", "Hugging Face Inference Providers (20+ open models)"),
939
+ ProviderEntry("gemini", "Google AI Studio", "Google AI Studio (Gemini models — native Gemini API)"),
940
+ ProviderEntry("google-gemini-cli", "Google Gemini (OAuth)", "Google Gemini via OAuth + Code Assist (free tier supported; no API key needed)"),
941
+ ProviderEntry("deepseek", "DeepSeek", "DeepSeek (DeepSeek-V3, R1, coder — direct API)"),
942
+ ProviderEntry("xai", "xAI", "xAI (Grok models — direct API)"),
943
+ ProviderEntry("zai", "Z.AI / GLM", "Z.AI / GLM (Zhipu AI direct API)"),
944
+ ProviderEntry("kimi-coding", "Kimi / Kimi Coding Plan", "Kimi Coding Plan (api.kimi.com) & Moonshot API"),
945
+ ProviderEntry("kimi-coding-cn", "Kimi / Moonshot (China)", "Kimi / Moonshot China (Moonshot CN direct API)"),
946
+ ProviderEntry("stepfun", "StepFun Step Plan", "StepFun Step Plan (agent/coding models via Step Plan API)"),
947
+ ProviderEntry("minimax", "MiniMax", "MiniMax (global direct API)"),
948
+ ProviderEntry("minimax-oauth", "MiniMax (OAuth)", "MiniMax via OAuth browser login (Coding Plan, minimax.io)"),
949
+ ProviderEntry("minimax-cn", "MiniMax (China)", "MiniMax China (domestic direct API)"),
950
+ ProviderEntry("ollama-cloud", "Ollama Cloud", "Ollama Cloud (cloud-hosted open models — ollama.com)"),
951
+ ProviderEntry("arcee", "Arcee AI", "Arcee AI (Trinity models — direct API)"),
952
+ ProviderEntry("gmi", "GMI Cloud", "GMI Cloud (multi-model direct API)"),
953
+ ProviderEntry("kilocode", "Kilo Code", "Kilo Code (Kilo Gateway API)"),
954
+ ProviderEntry("opencode-zen", "OpenCode Zen", "OpenCode Zen (35+ curated models, pay-as-you-go)"),
955
+ ProviderEntry("opencode-go", "OpenCode Go", "OpenCode Go (open models, $10/month subscription)"),
956
+ ProviderEntry("bedrock", "AWS Bedrock", "AWS Bedrock (Claude, Nova, Llama, DeepSeek — IAM or API key)"),
957
+ ProviderEntry("azure-foundry", "Azure Foundry", "Azure Foundry (OpenAI-style or Anthropic-style endpoint — your Azure AI deployment)"),
958
+ ProviderEntry("ai-gateway", "Vercel AI Gateway", "Vercel AI Gateway"),
959
+ ProviderEntry("qwen-oauth", "Qwen OAuth (Portal)", "Qwen OAuth (reuses local Qwen CLI login)"),
960
+ ]
961
+
962
+ # Auto-extend CANONICAL_PROVIDERS with any provider registered in providers/
963
+ # that is not already in the list above. Adding plugins/model-providers/<name>/
964
+ # is sufficient to expose a new provider in the model picker, /model, and all
965
+ # downstream consumers — no edits to this file needed.
966
+ _canonical_slugs = {p.slug for p in CANONICAL_PROVIDERS}
967
+ try:
968
+ from providers import list_providers as _list_providers_for_canonical
969
+ for _pp in _list_providers_for_canonical():
970
+ if _pp.name in _canonical_slugs:
971
+ continue
972
+ if _pp.auth_type in {"oauth_device_code", "oauth_external", "external_process", "aws_sdk", "copilot"}:
973
+ continue # non-api-key flows need bespoke picker UX; skip auto-inject
974
+ _label = _pp.display_name or _pp.name
975
+ _desc = _pp.description or f"{_label} (direct API)"
976
+ CANONICAL_PROVIDERS.append(ProviderEntry(_pp.name, _label, _desc))
977
+ _canonical_slugs.add(_pp.name)
978
+ except Exception:
979
+ pass
980
+
981
+ # Derived dicts — used throughout the codebase
982
+ _PROVIDER_LABELS = {p.slug: p.label for p in CANONICAL_PROVIDERS}
983
+ _PROVIDER_LABELS["custom"] = "Custom endpoint" # special case: not a named provider
984
+
985
+
986
+ _PROVIDER_ALIASES = {
987
+ "glm": "zai",
988
+ "z-ai": "zai",
989
+ "z.ai": "zai",
990
+ "zhipu": "zai",
991
+ "github": "copilot",
992
+ "github-copilot": "copilot",
993
+ "github-models": "copilot",
994
+ "github-model": "copilot",
995
+ "github-copilot-acp": "copilot-acp",
996
+ "copilot-acp-agent": "copilot-acp",
997
+ "google": "gemini",
998
+ "google-gemini": "gemini",
999
+ "google-ai-studio": "gemini",
1000
+ "kimi": "kimi-coding",
1001
+ "moonshot": "kimi-coding",
1002
+ "kimi-cn": "kimi-coding-cn",
1003
+ "moonshot-cn": "kimi-coding-cn",
1004
+ "step": "stepfun",
1005
+ "stepfun-coding-plan": "stepfun",
1006
+ "arcee-ai": "arcee",
1007
+ "arceeai": "arcee",
1008
+ "gmi-cloud": "gmi",
1009
+ "gmicloud": "gmi",
1010
+ "minimax-china": "minimax-cn",
1011
+ "minimax_cn": "minimax-cn",
1012
+ "minimax-portal": "minimax-oauth",
1013
+ "minimax-global": "minimax-oauth",
1014
+ "minimax_oauth": "minimax-oauth",
1015
+ "claude": "anthropic",
1016
+ "claude-code": "anthropic",
1017
+ "deep-seek": "deepseek",
1018
+ "opencode": "opencode-zen",
1019
+ "zen": "opencode-zen",
1020
+ "go": "opencode-go",
1021
+ "opencode-go-sub": "opencode-go",
1022
+ "aigateway": "ai-gateway",
1023
+ "vercel": "ai-gateway",
1024
+ "vercel-ai-gateway": "ai-gateway",
1025
+ "kilo": "kilocode",
1026
+ "kilo-code": "kilocode",
1027
+ "kilo-gateway": "kilocode",
1028
+ "dashscope": "alibaba",
1029
+ "aliyun": "alibaba",
1030
+ "qwen": "alibaba",
1031
+ "alibaba-cloud": "alibaba",
1032
+ "qwen-portal": "qwen-oauth",
1033
+ "gemini-cli": "google-gemini-cli",
1034
+ "gemini-oauth": "google-gemini-cli",
1035
+ "hf": "huggingface",
1036
+ "hugging-face": "huggingface",
1037
+ "huggingface-hub": "huggingface",
1038
+ "novita-ai": "novita",
1039
+ "novitaai": "novita",
1040
+ "mimo": "xiaomi",
1041
+ "xiaomi-mimo": "xiaomi",
1042
+ "tencent": "tencent-tokenhub",
1043
+ "tokenhub": "tencent-tokenhub",
1044
+ "tencent-cloud": "tencent-tokenhub",
1045
+ "tencentmaas": "tencent-tokenhub",
1046
+ "aws": "bedrock",
1047
+ "aws-bedrock": "bedrock",
1048
+ "amazon-bedrock": "bedrock",
1049
+ "amazon": "bedrock",
1050
+ "grok": "xai",
1051
+ "grok-oauth": "xai-oauth",
1052
+ "xai-oauth": "xai-oauth",
1053
+ "x-ai-oauth": "xai-oauth",
1054
+ "xai-grok-oauth": "xai-oauth",
1055
+ "x-ai": "xai",
1056
+ "x.ai": "xai",
1057
+ "nim": "nvidia",
1058
+ "nvidia-nim": "nvidia",
1059
+ "build-nvidia": "nvidia",
1060
+ "nemotron": "nvidia",
1061
+ "lmstudio": "lmstudio",
1062
+ "lm-studio": "lmstudio",
1063
+ "lm_studio": "lmstudio",
1064
+ "ollama": "custom", # bare "ollama" = local; use "ollama-cloud" for cloud
1065
+ "ollama_cloud": "ollama-cloud",
1066
+ }
1067
+
1068
+
1069
+ def get_default_model_for_provider(provider: str) -> str:
1070
+ """Return the default model for a provider, or empty string if unknown.
1071
+
1072
+ Uses the first entry in _PROVIDER_MODELS as the default. This is the
1073
+ model a user would be offered first in the ``hermes model`` picker.
1074
+
1075
+ Used as a fallback when the user has configured a provider but never
1076
+ selected a model (e.g. ``hermes auth add openai-codex`` without
1077
+ ``hermes model``).
1078
+ """
1079
+ models = _PROVIDER_MODELS.get(provider, [])
1080
+ return models[0] if models else ""
1081
+
1082
+
1083
+ def _openrouter_model_is_free(pricing: Any) -> bool:
1084
+ """Return True when both prompt and completion pricing are zero."""
1085
+ if not isinstance(pricing, dict):
1086
+ return False
1087
+ try:
1088
+ return float(pricing.get("prompt", "0")) == 0 and float(pricing.get("completion", "0")) == 0
1089
+ except (TypeError, ValueError):
1090
+ return False
1091
+
1092
+
1093
+ def _openrouter_model_supports_tools(item: Any) -> bool:
1094
+ """Return True when the model's ``supported_parameters`` advertise tool calling.
1095
+
1096
+ hermes-agent is tool-calling-first — every provider path assumes the model
1097
+ can invoke tools. Models that don't advertise ``tools`` in their
1098
+ ``supported_parameters`` (e.g. image-only or completion-only models) cannot
1099
+ be driven by the agent loop and would fail at the first tool call.
1100
+
1101
+ **Permissive when the field is missing.** Some OpenRouter-compatible gateways
1102
+ (Nous Portal, private mirrors, older catalog snapshots) don't populate
1103
+ ``supported_parameters`` at all. Treat that as "unknown capability → allow"
1104
+ so the picker doesn't silently empty for those users. Only hide models
1105
+ whose ``supported_parameters`` is an explicit list that omits ``tools``.
1106
+
1107
+ Ported from Kilo-Org/kilocode#9068.
1108
+ """
1109
+ if not isinstance(item, dict):
1110
+ return True
1111
+ params = item.get("supported_parameters")
1112
+ if not isinstance(params, list):
1113
+ # Field absent / malformed / None — be permissive.
1114
+ return True
1115
+ return "tools" in params
1116
+
1117
+
1118
+ def fetch_openrouter_models(
1119
+ timeout: float = 8.0,
1120
+ *,
1121
+ force_refresh: bool = False,
1122
+ ) -> list[tuple[str, str]]:
1123
+ """Return the curated OpenRouter picker list, refreshed from the live catalog when possible."""
1124
+ global _openrouter_catalog_cache
1125
+
1126
+ if _openrouter_catalog_cache is not None and not force_refresh:
1127
+ return list(_openrouter_catalog_cache)
1128
+
1129
+ # Prefer the remotely-hosted catalog manifest; fall back to the in-repo
1130
+ # snapshot when the manifest is unreachable. Both are curated lists that
1131
+ # drive the picker; the OpenRouter live /v1/models filter (tool support,
1132
+ # free pricing) is applied on top either way.
1133
+ try:
1134
+ from hermes_cli.model_catalog import get_curated_openrouter_models
1135
+ remote = get_curated_openrouter_models()
1136
+ except Exception:
1137
+ remote = None
1138
+ fallback = list(remote) if remote else list(OPENROUTER_MODELS)
1139
+ preferred_ids = [mid for mid, _ in fallback]
1140
+
1141
+ try:
1142
+ req = urllib.request.Request(
1143
+ "https://openrouter.ai/api/v1/models",
1144
+ headers={"Accept": "application/json"},
1145
+ )
1146
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
1147
+ payload = json.loads(resp.read().decode())
1148
+ except Exception:
1149
+ return list(_openrouter_catalog_cache or fallback)
1150
+
1151
+ live_items = payload.get("data", [])
1152
+ if not isinstance(live_items, list):
1153
+ return list(_openrouter_catalog_cache or fallback)
1154
+
1155
+ live_by_id: dict[str, dict[str, Any]] = {}
1156
+ for item in live_items:
1157
+ if not isinstance(item, dict):
1158
+ continue
1159
+ mid = str(item.get("id") or "").strip()
1160
+ if not mid:
1161
+ continue
1162
+ live_by_id[mid] = item
1163
+
1164
+ curated: list[tuple[str, str]] = []
1165
+ for preferred_id in preferred_ids:
1166
+ live_item = live_by_id.get(preferred_id)
1167
+ if live_item is None:
1168
+ continue
1169
+ # Hide models that don't advertise tool-calling support — hermes-agent
1170
+ # requires it and surfacing them leads to immediate runtime failures
1171
+ # when the user selects them. Ported from Kilo-Org/kilocode#9068.
1172
+ if not _openrouter_model_supports_tools(live_item):
1173
+ continue
1174
+ desc = "free" if _openrouter_model_is_free(live_item.get("pricing")) else ""
1175
+ curated.append((preferred_id, desc))
1176
+
1177
+ if not curated:
1178
+ return list(_openrouter_catalog_cache or fallback)
1179
+
1180
+ first_id, _ = curated[0]
1181
+ curated[0] = (first_id, "recommended")
1182
+ _openrouter_catalog_cache = curated
1183
+ return list(curated)
1184
+
1185
+
1186
+ def model_ids(*, force_refresh: bool = False) -> list[str]:
1187
+ """Return just the OpenRouter model-id strings."""
1188
+ return [mid for mid, _ in fetch_openrouter_models(force_refresh=force_refresh)]
1189
+
1190
+
1191
+ def get_curated_nous_model_ids() -> list[str]:
1192
+ """Return the curated Nous Portal model-id list.
1193
+
1194
+ Prefers the remotely-hosted catalog manifest (published under
1195
+ ``website/static/api/model-catalog.json``); falls back to the in-repo
1196
+ snapshot in ``_PROVIDER_MODELS["nous"]`` when the manifest is
1197
+ unreachable. Always returns a list (never None).
1198
+ """
1199
+ try:
1200
+ from hermes_cli.model_catalog import get_curated_nous_models
1201
+ remote = get_curated_nous_models()
1202
+ except Exception:
1203
+ remote = None
1204
+ if remote:
1205
+ return list(remote)
1206
+ return list(_PROVIDER_MODELS.get("nous", []))
1207
+
1208
+
1209
+ def _ai_gateway_model_is_free(pricing: Any) -> bool:
1210
+ """Return True if an AI Gateway model has $0 input AND output pricing."""
1211
+ if not isinstance(pricing, dict):
1212
+ return False
1213
+ try:
1214
+ return float(pricing.get("input", "0")) == 0 and float(pricing.get("output", "0")) == 0
1215
+ except (TypeError, ValueError):
1216
+ return False
1217
+
1218
+
1219
+ def fetch_ai_gateway_models(
1220
+ timeout: float = 8.0,
1221
+ *,
1222
+ force_refresh: bool = False,
1223
+ ) -> list[tuple[str, str]]:
1224
+ """Return the curated AI Gateway picker list, refreshed from the live catalog when possible."""
1225
+ global _ai_gateway_catalog_cache
1226
+
1227
+ if _ai_gateway_catalog_cache is not None and not force_refresh:
1228
+ return list(_ai_gateway_catalog_cache)
1229
+
1230
+ from calvyn_constants import AI_GATEWAY_BASE_URL
1231
+
1232
+ fallback = list(VERCEL_AI_GATEWAY_MODELS)
1233
+ preferred_ids = [mid for mid, _ in fallback]
1234
+
1235
+ try:
1236
+ req = urllib.request.Request(
1237
+ f"{AI_GATEWAY_BASE_URL.rstrip('/')}/models",
1238
+ headers={"Accept": "application/json"},
1239
+ )
1240
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
1241
+ payload = json.loads(resp.read().decode())
1242
+ except Exception:
1243
+ return list(_ai_gateway_catalog_cache or fallback)
1244
+
1245
+ live_items = payload.get("data", [])
1246
+ if not isinstance(live_items, list):
1247
+ return list(_ai_gateway_catalog_cache or fallback)
1248
+
1249
+ live_by_id: dict[str, dict[str, Any]] = {}
1250
+ for item in live_items:
1251
+ if not isinstance(item, dict):
1252
+ continue
1253
+ mid = str(item.get("id") or "").strip()
1254
+ if not mid:
1255
+ continue
1256
+ live_by_id[mid] = item
1257
+
1258
+ curated: list[tuple[str, str]] = []
1259
+ for preferred_id in preferred_ids:
1260
+ live_item = live_by_id.get(preferred_id)
1261
+ if live_item is None:
1262
+ continue
1263
+ desc = "free" if _ai_gateway_model_is_free(live_item.get("pricing")) else ""
1264
+ curated.append((preferred_id, desc))
1265
+
1266
+ if not curated:
1267
+ return list(_ai_gateway_catalog_cache or fallback)
1268
+
1269
+ # If the live catalog offers a free Moonshot model, auto-promote it to
1270
+ # position #1 as "recommended" — dynamic discovery without a PR.
1271
+ free_moonshot = next(
1272
+ (
1273
+ mid
1274
+ for mid, item in live_by_id.items()
1275
+ if mid.startswith("moonshotai/")
1276
+ and _ai_gateway_model_is_free(item.get("pricing"))
1277
+ ),
1278
+ None,
1279
+ )
1280
+ if free_moonshot:
1281
+ curated = [(mid, desc) for mid, desc in curated if mid != free_moonshot]
1282
+ curated.insert(0, (free_moonshot, "recommended"))
1283
+ else:
1284
+ first_id, _ = curated[0]
1285
+ curated[0] = (first_id, "recommended")
1286
+
1287
+ _ai_gateway_catalog_cache = curated
1288
+ return list(curated)
1289
+
1290
+
1291
+ def ai_gateway_model_ids(*, force_refresh: bool = False) -> list[str]:
1292
+ """Return just the AI Gateway model-id strings."""
1293
+ return [mid for mid, _ in fetch_ai_gateway_models(force_refresh=force_refresh)]
1294
+
1295
+
1296
+
1297
+
1298
+ # ---------------------------------------------------------------------------
1299
+ # Pricing helpers — fetch live pricing from OpenRouter-compatible /v1/models
1300
+ # ---------------------------------------------------------------------------
1301
+
1302
+ # Cache: maps model_id → {"prompt": str, "completion": str} per endpoint
1303
+ _pricing_cache: dict[str, dict[str, dict[str, str]]] = {}
1304
+
1305
+
1306
+ def _format_price_per_mtok(per_token_str: str) -> str:
1307
+ """Convert a per-token price string to a human-friendly $/Mtok string.
1308
+
1309
+ Always uses 2 decimal places so that prices align vertically when
1310
+ right-justified in a column (the decimal point stays in the same position).
1311
+
1312
+ Examples:
1313
+ "0.000003" → "$3.00" (per million tokens)
1314
+ "0.00003" → "$30.00"
1315
+ "0.00000015" → "$0.15"
1316
+ "0.0000001" → "$0.10"
1317
+ "0.00018" → "$180.00"
1318
+ "0" → "free"
1319
+ """
1320
+ try:
1321
+ val = float(per_token_str)
1322
+ except (TypeError, ValueError):
1323
+ return "?"
1324
+ if val == 0:
1325
+ return "free"
1326
+ per_m = val * 1_000_000
1327
+ return f"${per_m:.2f}"
1328
+
1329
+
1330
+ def format_model_pricing_table(
1331
+ models: list[tuple[str, str]],
1332
+ pricing_map: dict[str, dict[str, str]],
1333
+ current_model: str = "",
1334
+ indent: str = " ",
1335
+ ) -> list[str]:
1336
+ """Build a column-aligned model+pricing table for terminal display.
1337
+
1338
+ Returns a list of pre-formatted lines ready to print.
1339
+ *models* is ``[(model_id, description), ...]``.
1340
+ """
1341
+ if not models:
1342
+ return []
1343
+
1344
+ # Build rows: (model_id, input_price, output_price, cache_price, is_current)
1345
+ rows: list[tuple[str, str, str, str, bool]] = []
1346
+ has_cache = False
1347
+ for mid, _desc in models:
1348
+ is_cur = mid == current_model
1349
+ p = pricing_map.get(mid)
1350
+ if p:
1351
+ inp = _format_price_per_mtok(p.get("prompt", ""))
1352
+ out = _format_price_per_mtok(p.get("completion", ""))
1353
+ cache_read = p.get("input_cache_read", "")
1354
+ cache = _format_price_per_mtok(cache_read) if cache_read else ""
1355
+ if cache:
1356
+ has_cache = True
1357
+ else:
1358
+ inp, out, cache = "", "", ""
1359
+ rows.append((mid, inp, out, cache, is_cur))
1360
+
1361
+ name_col = max(len(r[0]) for r in rows) + 2
1362
+ # Compute price column widths from the actual data so decimals align
1363
+ price_col = max(
1364
+ max((len(r[1]) for r in rows if r[1]), default=4),
1365
+ max((len(r[2]) for r in rows if r[2]), default=4),
1366
+ 3, # minimum: "In" / "Out" header
1367
+ )
1368
+ cache_col = max(
1369
+ max((len(r[3]) for r in rows if r[3]), default=4),
1370
+ 5, # minimum: "Cache" header
1371
+ ) if has_cache else 0
1372
+ lines: list[str] = []
1373
+
1374
+ # Header
1375
+ if has_cache:
1376
+ lines.append(f"{indent}{'Model':<{name_col}} {'In':>{price_col}} {'Out':>{price_col}} {'Cache':>{cache_col}} /Mtok")
1377
+ lines.append(f"{indent}{'-' * name_col} {'-' * price_col} {'-' * price_col} {'-' * cache_col}")
1378
+ else:
1379
+ lines.append(f"{indent}{'Model':<{name_col}} {'In':>{price_col}} {'Out':>{price_col}} /Mtok")
1380
+ lines.append(f"{indent}{'-' * name_col} {'-' * price_col} {'-' * price_col}")
1381
+
1382
+ for mid, inp, out, cache, is_cur in rows:
1383
+ marker = " ← current" if is_cur else ""
1384
+ if has_cache:
1385
+ lines.append(f"{indent}{mid:<{name_col}} {inp:>{price_col}} {out:>{price_col}} {cache:>{cache_col}}{marker}")
1386
+ else:
1387
+ lines.append(f"{indent}{mid:<{name_col}} {inp:>{price_col}} {out:>{price_col}}{marker}")
1388
+
1389
+ return lines
1390
+
1391
+
1392
+ def fetch_models_with_pricing(
1393
+ api_key: str | None = None,
1394
+ base_url: str = "https://openrouter.ai/api",
1395
+ timeout: float = 8.0,
1396
+ *,
1397
+ force_refresh: bool = False,
1398
+ ) -> dict[str, dict[str, str]]:
1399
+ """Fetch ``/v1/models`` and return ``{model_id: {prompt, completion}}`` pricing.
1400
+
1401
+ Results are cached per *base_url* so repeated calls are free.
1402
+ Works with any OpenRouter-compatible endpoint (OpenRouter, Nous Portal).
1403
+ """
1404
+ cache_key = (base_url or "").rstrip("/")
1405
+ if not force_refresh and cache_key in _pricing_cache:
1406
+ return _pricing_cache[cache_key]
1407
+
1408
+ url = cache_key.rstrip("/") + "/v1/models"
1409
+ headers: dict[str, str] = {
1410
+ "Accept": "application/json",
1411
+ "User-Agent": _HERMES_USER_AGENT,
1412
+ }
1413
+ if api_key:
1414
+ headers["Authorization"] = f"Bearer {api_key}"
1415
+
1416
+ try:
1417
+ req = urllib.request.Request(url, headers=headers)
1418
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
1419
+ payload = json.loads(resp.read().decode())
1420
+ except Exception:
1421
+ _pricing_cache[cache_key] = {}
1422
+ return {}
1423
+
1424
+ result: dict[str, dict[str, str]] = {}
1425
+ for item in payload.get("data", []):
1426
+ mid = item.get("id")
1427
+ pricing = item.get("pricing")
1428
+ if mid and isinstance(pricing, dict):
1429
+ entry: dict[str, str] = {
1430
+ "prompt": str(pricing.get("prompt", "")),
1431
+ "completion": str(pricing.get("completion", "")),
1432
+ }
1433
+ if pricing.get("input_cache_read"):
1434
+ entry["input_cache_read"] = str(pricing["input_cache_read"])
1435
+ if pricing.get("input_cache_write"):
1436
+ entry["input_cache_write"] = str(pricing["input_cache_write"])
1437
+ result[mid] = entry
1438
+
1439
+ _pricing_cache[cache_key] = result
1440
+ return result
1441
+
1442
+
1443
+ def fetch_ai_gateway_pricing(
1444
+ timeout: float = 8.0,
1445
+ *,
1446
+ force_refresh: bool = False,
1447
+ ) -> dict[str, dict[str, str]]:
1448
+ """Fetch Vercel AI Gateway /v1/models and return hermes-shaped pricing.
1449
+
1450
+ Vercel uses ``input`` / ``output`` field names; hermes's picker expects
1451
+ ``prompt`` / ``completion``. This translates. Cache read/write field names
1452
+ already match.
1453
+ """
1454
+ from calvyn_constants import AI_GATEWAY_BASE_URL
1455
+
1456
+ cache_key = AI_GATEWAY_BASE_URL.rstrip("/")
1457
+ if not force_refresh and cache_key in _pricing_cache:
1458
+ return _pricing_cache[cache_key]
1459
+
1460
+ try:
1461
+ req = urllib.request.Request(
1462
+ f"{cache_key}/models",
1463
+ headers={"Accept": "application/json"},
1464
+ )
1465
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
1466
+ payload = json.loads(resp.read().decode())
1467
+ except Exception:
1468
+ _pricing_cache[cache_key] = {}
1469
+ return {}
1470
+
1471
+ result: dict[str, dict[str, str]] = {}
1472
+ for item in payload.get("data", []):
1473
+ if not isinstance(item, dict):
1474
+ continue
1475
+ mid = item.get("id")
1476
+ pricing = item.get("pricing")
1477
+ if not (mid and isinstance(pricing, dict)):
1478
+ continue
1479
+ entry: dict[str, str] = {
1480
+ "prompt": str(pricing.get("input", "")),
1481
+ "completion": str(pricing.get("output", "")),
1482
+ }
1483
+ if pricing.get("input_cache_read"):
1484
+ entry["input_cache_read"] = str(pricing["input_cache_read"])
1485
+ if pricing.get("input_cache_write"):
1486
+ entry["input_cache_write"] = str(pricing["input_cache_write"])
1487
+ result[mid] = entry
1488
+
1489
+ _pricing_cache[cache_key] = result
1490
+ return result
1491
+
1492
+
1493
+ def _resolve_openrouter_api_key() -> str:
1494
+ """Best-effort OpenRouter API key for pricing fetch."""
1495
+ return os.getenv("OPENROUTER_API_KEY", "").strip()
1496
+
1497
+
1498
+ _DEFAULT_NOUS_INFERENCE_BASE = "https://inference-api.nousresearch.com"
1499
+
1500
+
1501
+ def _resolve_nous_pricing_credentials() -> tuple[str, str]:
1502
+ """Return ``(api_key, base_url)`` for Nous Portal pricing.
1503
+
1504
+ The Nous inference ``/v1/models`` endpoint exposes pricing without
1505
+ authentication, so the api_key is best-effort: when runtime credential
1506
+ resolution fails (expired refresh token, missing auth.json, etc.) we
1507
+ still return the default inference base URL so the picker keeps
1508
+ working with anonymous pricing data. Free-tier users in particular
1509
+ need this — pricing drives the free/paid partition, and silently
1510
+ returning empty pricing because of an auth blip makes the picker
1511
+ look broken ("No free models currently available").
1512
+ """
1513
+ try:
1514
+ from hermes_cli.auth import resolve_nous_runtime_credentials
1515
+ creds = resolve_nous_runtime_credentials()
1516
+ if creds:
1517
+ return (creds.get("api_key", ""), creds.get("base_url", ""))
1518
+ except Exception:
1519
+ pass
1520
+ return ("", _DEFAULT_NOUS_INFERENCE_BASE)
1521
+
1522
+
1523
+ def get_pricing_for_provider(provider: str, *, force_refresh: bool = False) -> dict[str, dict[str, str]]:
1524
+ """Return live pricing for providers that support it (openrouter, nous, ai-gateway, novita)."""
1525
+ normalized = normalize_provider(provider)
1526
+ if normalized == "openrouter":
1527
+ return fetch_models_with_pricing(
1528
+ api_key=_resolve_openrouter_api_key(),
1529
+ base_url="https://openrouter.ai/api",
1530
+ force_refresh=force_refresh,
1531
+ )
1532
+ if normalized == "ai-gateway":
1533
+ return fetch_ai_gateway_pricing(force_refresh=force_refresh)
1534
+ if normalized == "novita":
1535
+ return _fetch_novita_pricing(force_refresh=force_refresh)
1536
+ if normalized == "nous":
1537
+ api_key, base_url = _resolve_nous_pricing_credentials()
1538
+ if base_url:
1539
+ # Nous base_url typically looks like https://inference-api.nousresearch.com/v1
1540
+ # We need the part before /v1 for our fetch function
1541
+ stripped = base_url.rstrip("/")
1542
+ if stripped.endswith("/v1"):
1543
+ stripped = stripped[:-3]
1544
+ return fetch_models_with_pricing(
1545
+ api_key=api_key,
1546
+ base_url=stripped,
1547
+ force_refresh=force_refresh,
1548
+ )
1549
+ return {}
1550
+
1551
+
1552
+ def _fetch_novita_pricing(
1553
+ timeout: float = 8.0,
1554
+ *,
1555
+ force_refresh: bool = False,
1556
+ ) -> dict[str, dict[str, str]]:
1557
+ """Fetch pricing from NovitaAI /v1/models.
1558
+
1559
+ NovitaAI returns input/output prices per million tokens in units of
1560
+ 0.0001 USD. Convert them to the per-token strings used by the shared
1561
+ pricing formatter.
1562
+
1563
+ Results are cached in ``_pricing_cache`` keyed on the resolved base URL,
1564
+ matching the pattern used by ``fetch_ai_gateway_pricing`` — without this,
1565
+ every menu render or pricing lookup re-hits the network.
1566
+ """
1567
+ api_key = os.getenv("NOVITA_API_KEY", "").strip()
1568
+ if not api_key:
1569
+ return {}
1570
+
1571
+ base_url = os.getenv("NOVITA_BASE_URL", "").strip() or "https://api.novita.ai/openai/v1"
1572
+ cache_key = base_url.rstrip("/")
1573
+ if not force_refresh and cache_key in _pricing_cache:
1574
+ return _pricing_cache[cache_key]
1575
+
1576
+ url = cache_key + "/models"
1577
+ headers = {
1578
+ "Authorization": f"Bearer {api_key}",
1579
+ "Accept": "application/json",
1580
+ "User-Agent": _HERMES_USER_AGENT,
1581
+ }
1582
+
1583
+ try:
1584
+ req = urllib.request.Request(url, headers=headers)
1585
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
1586
+ payload = json.loads(resp.read().decode())
1587
+ except Exception:
1588
+ _pricing_cache[cache_key] = {}
1589
+ return {}
1590
+
1591
+ result: dict[str, dict[str, str]] = {}
1592
+ for item in payload.get("data", []):
1593
+ if not isinstance(item, dict):
1594
+ continue
1595
+ mid = item.get("id")
1596
+ if not mid:
1597
+ continue
1598
+ inp = item.get("input_token_price_per_m")
1599
+ out = item.get("output_token_price_per_m")
1600
+ if inp is None and out is None:
1601
+ continue
1602
+ result[str(mid)] = {
1603
+ "prompt": str(float(inp or 0) / 10_000 / 1_000_000),
1604
+ "completion": str(float(out or 0) / 10_000 / 1_000_000),
1605
+ }
1606
+
1607
+ _pricing_cache[cache_key] = result
1608
+ return result
1609
+
1610
+
1611
+ # All provider IDs and aliases that are valid for the provider:model syntax.
1612
+ _KNOWN_PROVIDER_NAMES: set[str] = (
1613
+ set(_PROVIDER_LABELS.keys())
1614
+ | set(_PROVIDER_ALIASES.keys())
1615
+ | {"openrouter", "custom"}
1616
+ )
1617
+
1618
+
1619
+ def list_available_providers() -> list[dict[str, str]]:
1620
+ """Return info about all providers the user could use with ``provider:model``.
1621
+
1622
+ Each dict has ``id``, ``label``, and ``aliases``.
1623
+ Checks which providers have valid credentials configured.
1624
+
1625
+ Derives the provider list from :data:`CANONICAL_PROVIDERS` (single
1626
+ source of truth shared with ``hermes model``, ``/model``, etc.).
1627
+ """
1628
+ # Derive display order from canonical list + custom
1629
+ provider_order = [p.slug for p in CANONICAL_PROVIDERS] + ["custom"]
1630
+
1631
+ # Build reverse alias map
1632
+ aliases_for: dict[str, list[str]] = {}
1633
+ for alias, canonical in _PROVIDER_ALIASES.items():
1634
+ aliases_for.setdefault(canonical, []).append(alias)
1635
+
1636
+ result = []
1637
+ for pid in provider_order:
1638
+ label = _PROVIDER_LABELS.get(pid, pid)
1639
+ alias_list = aliases_for.get(pid, [])
1640
+ # Check if this provider has credentials available
1641
+ has_creds = False
1642
+ try:
1643
+ from hermes_cli.auth import get_auth_status, has_usable_secret
1644
+ if pid == "custom":
1645
+ custom_base_url = _get_custom_base_url() or ""
1646
+ has_creds = bool(custom_base_url.strip())
1647
+ elif pid == "openrouter":
1648
+ has_creds = has_usable_secret(os.getenv("OPENROUTER_API_KEY", ""))
1649
+ else:
1650
+ status = get_auth_status(pid)
1651
+ has_creds = bool(status.get("logged_in") or status.get("configured"))
1652
+ except Exception:
1653
+ pass
1654
+ result.append({
1655
+ "id": pid,
1656
+ "label": label,
1657
+ "aliases": alias_list,
1658
+ "authenticated": has_creds,
1659
+ })
1660
+ return result
1661
+
1662
+
1663
+ def parse_model_input(raw: str, current_provider: str) -> tuple[str, str]:
1664
+ """Parse ``/model`` input into ``(provider, model)``.
1665
+
1666
+ Supports ``provider:model`` syntax to switch providers at runtime::
1667
+
1668
+ openrouter:anthropic/claude-sonnet-4.5 → ("openrouter", "anthropic/claude-sonnet-4.5")
1669
+ nous:hermes-3 → ("nous", "hermes-3")
1670
+ anthropic/claude-sonnet-4.5 → (current_provider, "anthropic/claude-sonnet-4.5")
1671
+ gpt-5.4 → (current_provider, "gpt-5.4")
1672
+
1673
+ The colon is only treated as a provider delimiter if the left side is a
1674
+ recognized provider name or alias. This avoids misinterpreting model names
1675
+ that happen to contain colons (e.g. ``anthropic/claude-3.5-sonnet:beta``).
1676
+
1677
+ Returns ``(provider, model)`` where *provider* is either the explicit
1678
+ provider from the input or *current_provider* if none was specified.
1679
+ """
1680
+ stripped = raw.strip()
1681
+ colon = stripped.find(":")
1682
+ if colon > 0:
1683
+ provider_part = stripped[:colon].strip().lower()
1684
+ model_part = stripped[colon + 1:].strip()
1685
+ if provider_part and model_part and provider_part in _KNOWN_PROVIDER_NAMES:
1686
+ # Support custom:name:model triple syntax for named custom
1687
+ # providers. ``custom:local:qwen`` → ("custom:local", "qwen").
1688
+ # Single colon ``custom:qwen`` → ("custom", "qwen") as before.
1689
+ if provider_part == "custom" and ":" in model_part:
1690
+ second_colon = model_part.find(":")
1691
+ custom_name = model_part[:second_colon].strip()
1692
+ actual_model = model_part[second_colon + 1:].strip()
1693
+ if custom_name and actual_model:
1694
+ return (f"custom:{custom_name}", actual_model)
1695
+ return (normalize_provider(provider_part), model_part)
1696
+ return (current_provider, stripped)
1697
+
1698
+
1699
+ def _get_custom_base_url() -> str:
1700
+ """Get the custom endpoint base_url from config.yaml."""
1701
+ try:
1702
+ from hermes_cli.config import load_config
1703
+ config = load_config()
1704
+ model_cfg = config.get("model", {})
1705
+ if isinstance(model_cfg, dict):
1706
+ return str(model_cfg.get("base_url", "")).strip()
1707
+ except Exception:
1708
+ pass
1709
+ return ""
1710
+
1711
+
1712
+ def curated_models_for_provider(
1713
+ provider: Optional[str],
1714
+ *,
1715
+ force_refresh: bool = False,
1716
+ ) -> list[tuple[str, str]]:
1717
+ """Return ``(model_id, description)`` tuples for a provider's model list.
1718
+
1719
+ Tries to fetch the live model list from the provider's API first,
1720
+ falling back to the static ``_PROVIDER_MODELS`` catalog if the API
1721
+ is unreachable.
1722
+ """
1723
+ normalized = normalize_provider(provider)
1724
+ if normalized == "openrouter":
1725
+ return fetch_openrouter_models(force_refresh=force_refresh)
1726
+
1727
+ # Try live API first (Codex, Nous, etc. all support /models)
1728
+ live = provider_model_ids(normalized)
1729
+ if live:
1730
+ return [(m, "") for m in live]
1731
+
1732
+ # Fallback to static catalog
1733
+ models = _PROVIDER_MODELS.get(normalized, [])
1734
+ return [(m, "") for m in models]
1735
+
1736
+
1737
+ def _provider_keys(provider: str) -> set[str]:
1738
+ key = (provider or "").strip().lower()
1739
+ normalized = normalize_provider(provider)
1740
+ return {k for k in (key, normalized) if k}
1741
+
1742
+
1743
+ def _model_in_provider_catalog(name_lower: str, providers: set[str]) -> bool:
1744
+ return any(
1745
+ name_lower == model.lower()
1746
+ for provider in providers
1747
+ for model in _PROVIDER_MODELS.get(provider, [])
1748
+ )
1749
+
1750
+
1751
+ _AGGREGATOR_PROVIDERS = frozenset(
1752
+ {"nous", "openrouter", "ai-gateway", "copilot", "kilocode"}
1753
+ )
1754
+
1755
+
1756
+ def _resolve_static_model_alias(
1757
+ name_lower: str,
1758
+ current_keys: set[str],
1759
+ ) -> Optional[tuple[str, str]]:
1760
+ """Resolve short aliases (e.g. sonnet/opus) using static catalogs only."""
1761
+ try:
1762
+ from hermes_cli.model_switch import MODEL_ALIASES
1763
+ except Exception:
1764
+ return None
1765
+
1766
+ identity = MODEL_ALIASES.get(name_lower)
1767
+ if identity is None:
1768
+ return None
1769
+
1770
+ vendor = identity.vendor
1771
+ family = identity.family
1772
+
1773
+ def _match(provider: str) -> Optional[str]:
1774
+ models = _PROVIDER_MODELS.get(provider, [])
1775
+ if not models:
1776
+ return None
1777
+ prefix = (
1778
+ f"{vendor}/{family}"
1779
+ if provider in _AGGREGATOR_PROVIDERS
1780
+ else family
1781
+ ).lower()
1782
+ for model in models:
1783
+ if model.lower().startswith(prefix):
1784
+ return model
1785
+ return None
1786
+
1787
+ for provider in current_keys:
1788
+ if matched := _match(provider):
1789
+ return provider, matched
1790
+
1791
+ for provider in _PROVIDER_MODELS:
1792
+ if provider in current_keys or provider in _AGGREGATOR_PROVIDERS:
1793
+ continue
1794
+ if matched := _match(provider):
1795
+ return provider, matched
1796
+
1797
+ for provider in _AGGREGATOR_PROVIDERS:
1798
+ if provider in current_keys and (matched := _match(provider)):
1799
+ return provider, matched
1800
+
1801
+ return None
1802
+
1803
+
1804
+ def detect_static_provider_for_model(
1805
+ model_name: str,
1806
+ current_provider: str,
1807
+ ) -> Optional[tuple[str, str]]:
1808
+ """Auto-detect a provider from static catalogs only.
1809
+
1810
+ Returns ``(provider_id, model_name)``. The model name may be remapped
1811
+ when a static alias or bare provider name resolves to a catalog default.
1812
+ Returns ``None`` when no confident match is found.
1813
+ """
1814
+ name = (model_name or "").strip()
1815
+ if not name:
1816
+ return None
1817
+
1818
+ name_lower = name.lower()
1819
+ current_keys = _provider_keys(current_provider)
1820
+
1821
+ alias_match = _resolve_static_model_alias(name_lower, current_keys)
1822
+ if alias_match:
1823
+ return alias_match
1824
+
1825
+ # --- Step 0: bare provider name typed as model ---
1826
+ # If someone types `/model nous` or `/model anthropic`, treat it as a
1827
+ # provider switch and pick the first model from that provider's catalog.
1828
+ # Skip "custom" and "openrouter" — custom has no model catalog, and
1829
+ # openrouter requires an explicit model name to be useful.
1830
+ resolved_provider = _PROVIDER_ALIASES.get(name_lower, name_lower)
1831
+ if resolved_provider not in {"custom", "openrouter"}:
1832
+ default_models = _PROVIDER_MODELS.get(resolved_provider, [])
1833
+ if (
1834
+ resolved_provider in _PROVIDER_LABELS
1835
+ and default_models
1836
+ and resolved_provider not in current_keys
1837
+ ):
1838
+ return (resolved_provider, default_models[0])
1839
+
1840
+ # Aggregators list other providers' models — never auto-switch TO them
1841
+ # If the model belongs to the current provider's catalog, don't suggest switching
1842
+ if _model_in_provider_catalog(name_lower, current_keys):
1843
+ return None
1844
+
1845
+ # --- Step 1: check static provider catalogs for a direct match ---
1846
+ for pid, models in _PROVIDER_MODELS.items():
1847
+ if pid in current_keys or pid in _AGGREGATOR_PROVIDERS:
1848
+ continue
1849
+ if any(name_lower == m.lower() for m in models):
1850
+ return (pid, name)
1851
+
1852
+ return None
1853
+
1854
+
1855
+ def detect_provider_for_model(
1856
+ model_name: str,
1857
+ current_provider: str,
1858
+ ) -> Optional[tuple[str, str]]:
1859
+ """Auto-detect the best provider for a model name.
1860
+
1861
+ Returns ``(provider_id, model_name)`` — the model name may be remapped
1862
+ (e.g. bare ``deepseek-chat`` → ``deepseek/deepseek-chat`` for OpenRouter).
1863
+ Returns ``None`` when no confident match is found.
1864
+
1865
+ Priority:
1866
+ 0. Bare provider name → switch to that provider's default model
1867
+ 1. Direct provider static catalog match
1868
+ 2. OpenRouter catalog match
1869
+ """
1870
+ name = (model_name or "").strip()
1871
+ if not name:
1872
+ return None
1873
+
1874
+ static_match = detect_static_provider_for_model(name, current_provider)
1875
+ if static_match:
1876
+ return static_match
1877
+ if _model_in_provider_catalog(name.lower(), _provider_keys(current_provider)):
1878
+ return None
1879
+
1880
+ # --- Step 2: check OpenRouter catalog ---
1881
+ # First try exact match (handles provider/model format)
1882
+ or_slug = _find_openrouter_slug(name)
1883
+ if or_slug:
1884
+ if current_provider != "openrouter":
1885
+ return ("openrouter", or_slug)
1886
+ # Already on openrouter, just return the resolved slug
1887
+ if or_slug != name:
1888
+ return ("openrouter", or_slug)
1889
+ return None # already on openrouter with matching name
1890
+
1891
+ return None
1892
+
1893
+
1894
+ def _find_openrouter_slug(model_name: str) -> Optional[str]:
1895
+ """Find the full OpenRouter model slug for a bare or partial model name.
1896
+
1897
+ Handles:
1898
+ - Exact match: ``anthropic/claude-opus-4.6`` → as-is
1899
+ - Bare name: ``deepseek-chat`` → ``deepseek/deepseek-chat``
1900
+ - Bare name: ``claude-opus-4.6`` → ``anthropic/claude-opus-4.6``
1901
+ """
1902
+ name_lower = model_name.strip().lower()
1903
+ if not name_lower:
1904
+ return None
1905
+
1906
+ # Exact match (already has provider/ prefix)
1907
+ for mid in model_ids():
1908
+ if name_lower == mid.lower():
1909
+ return mid
1910
+
1911
+ # Try matching just the model part (after the /)
1912
+ for mid in model_ids():
1913
+ if "/" in mid:
1914
+ _, model_part = mid.split("/", 1)
1915
+ if name_lower == model_part.lower():
1916
+ return mid
1917
+
1918
+ return None
1919
+
1920
+
1921
+ def normalize_provider(provider: Optional[str]) -> str:
1922
+ """Normalize provider aliases to Hermes' canonical provider ids.
1923
+
1924
+ Note: ``"auto"`` passes through unchanged — use
1925
+ ``hermes_cli.auth.resolve_provider()`` to resolve it to a concrete
1926
+ provider based on credentials and environment.
1927
+ """
1928
+ normalized = (provider or "openrouter").strip().lower()
1929
+ return _PROVIDER_ALIASES.get(normalized, normalized)
1930
+
1931
+
1932
+ def provider_label(provider: Optional[str]) -> str:
1933
+ """Return a human-friendly label for a provider id or alias."""
1934
+ original = (provider or "openrouter").strip()
1935
+ normalized = original.lower()
1936
+ if normalized == "auto":
1937
+ return "Auto"
1938
+ normalized = normalize_provider(normalized)
1939
+ return _PROVIDER_LABELS.get(normalized, original or "OpenRouter")
1940
+
1941
+
1942
+ # Models that support OpenAI Priority Processing (service_tier="priority").
1943
+ # See https://openai.com/api-priority-processing/ for the canonical list.
1944
+ #
1945
+ # Pattern-based matching — any OpenAI flagship model (gpt-*, o1*, o3*, o4*)
1946
+ # is assumed to support Priority Processing. service_tier=priority is silently
1947
+ # ignored by non-OpenAI endpoints (OpenRouter/Copilot/opencode-zen proxies
1948
+ # strip the field), so false positives are harmless. Codex-series models
1949
+ # (gpt-5-codex, gpt-5.3-codex, etc.) are excluded — they don't expose the
1950
+ # service_tier parameter through the Codex Responses API.
1951
+ _OPENAI_FAST_MODE_PREFIXES: tuple[str, ...] = (
1952
+ "gpt-",
1953
+ "o1",
1954
+ "o3",
1955
+ "o4",
1956
+ )
1957
+
1958
+
1959
+ def _is_openai_fast_model(model_id: Optional[str]) -> bool:
1960
+ """Return True if the model is an OpenAI flagship eligible for Priority Processing."""
1961
+ raw = _strip_vendor_prefix(str(model_id or ""))
1962
+ base = raw.split(":")[0]
1963
+ if not base:
1964
+ return False
1965
+ # Exclude Codex-series — they route through the Codex Responses API
1966
+ # which doesn't accept service_tier.
1967
+ if "codex" in base:
1968
+ return False
1969
+ return any(base.startswith(prefix) for prefix in _OPENAI_FAST_MODE_PREFIXES)
1970
+
1971
+
1972
+ # Models that support Anthropic Fast Mode (speed="fast").
1973
+ # See https://platform.claude.com/docs/en/build-with-claude/fast-mode
1974
+ #
1975
+ # Pattern-based matching — any claude-* model is eligible. The anthropic
1976
+ # adapter gates speed=fast on native Anthropic endpoints only (see
1977
+ # _is_third_party_anthropic_endpoint in agent/anthropic_adapter.py), so
1978
+ # third-party proxies that would reject the beta header are protected.
1979
+
1980
+
1981
+ def _strip_vendor_prefix(model_id: str) -> str:
1982
+ """Strip vendor/ prefix from a model ID (e.g. 'anthropic/claude-opus-4-6' -> 'claude-opus-4-6')."""
1983
+ raw = str(model_id or "").strip().lower()
1984
+ if "/" in raw:
1985
+ raw = raw.split("/", 1)[1]
1986
+ return raw
1987
+
1988
+
1989
+ def model_supports_fast_mode(model_id: Optional[str]) -> bool:
1990
+ """Return whether Hermes should expose the /fast toggle for this model."""
1991
+ return _is_anthropic_fast_model(model_id) or _is_openai_fast_model(model_id)
1992
+
1993
+
1994
+ def _is_anthropic_fast_model(model_id: Optional[str]) -> bool:
1995
+ """Return True if the model is a Claude model eligible for Anthropic Fast Mode.
1996
+
1997
+ Fast mode is currently supported on Claude Opus 4.6 only. Per Anthropic's
1998
+ docs (https://platform.claude.com/docs/en/build-with-claude/fast-mode):
1999
+ "Fast mode is currently supported on Opus 4.6 only. Sending speed: fast
2000
+ with an unsupported model returns an error." Opus 4.7 explicitly rejects
2001
+ the ``speed`` parameter with HTTP 400.
2002
+ """
2003
+ raw = _strip_vendor_prefix(str(model_id or ""))
2004
+ base = raw.split(":")[0]
2005
+ if not base.startswith("claude-"):
2006
+ return False
2007
+ # Only Opus 4.6 supports fast mode at present.
2008
+ return "opus-4-6" in base or "opus-4.6" in base
2009
+
2010
+
2011
+ def resolve_fast_mode_overrides(model_id: Optional[str]) -> dict[str, Any] | None:
2012
+ """Return request_overrides for fast/priority mode, or None if unsupported.
2013
+
2014
+ Returns provider-appropriate overrides:
2015
+ - OpenAI models: ``{"service_tier": "priority"}`` (Priority Processing)
2016
+ - Anthropic models: ``{"speed": "fast"}`` (Anthropic Fast Mode beta)
2017
+
2018
+ The overrides are injected into the API request kwargs by
2019
+ ``_build_api_kwargs`` in run_agent.py — each API path handles its own
2020
+ keys (service_tier for OpenAI/Codex, speed for Anthropic Messages).
2021
+ """
2022
+ if not model_supports_fast_mode(model_id):
2023
+ return None
2024
+ if _is_anthropic_fast_model(model_id):
2025
+ return {"speed": "fast"}
2026
+ return {"service_tier": "priority"}
2027
+
2028
+
2029
+ def _resolve_copilot_catalog_api_key() -> str:
2030
+ """Best-effort GitHub token for fetching the Copilot model catalog.
2031
+
2032
+ Resolution order:
2033
+ 1. ``resolve_api_key_provider_credentials("copilot")`` — env vars
2034
+ (``COPILOT_GITHUB_TOKEN`` / ``GH_TOKEN`` / ``GITHUB_TOKEN``) plus
2035
+ the ``gh auth token`` CLI fallback.
2036
+ 2. ``read_credential_pool("copilot")`` — a token (typically a
2037
+ ``gho_*`` from device-code login, or a fine-grained PAT) stored in
2038
+ ``auth.json`` under ``credential_pool.copilot[]``. The pool is
2039
+ populated by ``hermes auth add copilot`` and by ``_seed_from_env``
2040
+ when the env var is set in ``~/.hermes/.env``.
2041
+
2042
+ Without (2), users whose only Copilot credential is in the pool see
2043
+ the ``/model`` picker fall back to a stale hardcoded list because the
2044
+ live catalog fetch silently 401s. To avoid wedging on a malformed pool
2045
+ entry, each candidate is exchanged via ``exchange_copilot_token`` —
2046
+ only entries that actually exchange successfully are returned, so a
2047
+ later valid entry is reachable when an earlier one is unsupported.
2048
+ """
2049
+ try:
2050
+ from hermes_cli.auth import resolve_api_key_provider_credentials
2051
+
2052
+ creds = resolve_api_key_provider_credentials("copilot")
2053
+ api_key = str(creds.get("api_key") or "").strip()
2054
+ if api_key:
2055
+ return api_key
2056
+ except Exception:
2057
+ pass
2058
+
2059
+ try:
2060
+ from hermes_cli.auth import read_credential_pool
2061
+ from hermes_cli.copilot_auth import (
2062
+ exchange_copilot_token,
2063
+ validate_copilot_token,
2064
+ )
2065
+
2066
+ for entry in read_credential_pool("copilot"):
2067
+ if not isinstance(entry, dict):
2068
+ continue
2069
+ raw = str(entry.get("access_token") or "").strip()
2070
+ if not raw:
2071
+ continue
2072
+ valid, _ = validate_copilot_token(raw)
2073
+ if not valid:
2074
+ continue
2075
+ try:
2076
+ api_token, _expires_at = exchange_copilot_token(raw)
2077
+ except Exception:
2078
+ continue
2079
+ if api_token:
2080
+ return api_token
2081
+ except Exception:
2082
+ pass
2083
+
2084
+ return ""
2085
+
2086
+
2087
+ # Providers where models.dev is treated as authoritative: curated static
2088
+ # lists are kept only as an offline fallback and to capture custom additions
2089
+ # the registry doesn't publish yet. Adding a provider here causes its
2090
+ # curated list to be merged with fresh models.dev entries (fresh first, any
2091
+ # curated-only names appended) for both the CLI and the gateway /model picker.
2092
+ #
2093
+ # DELIBERATELY EXCLUDED:
2094
+ # - "openrouter": curated list is already a hand-picked agentic subset of
2095
+ # OpenRouter's 400+ catalog. Blindly merging would dump everything.
2096
+ # - "nous": curated list and Portal /models endpoint are the source of
2097
+ # truth for the subscription tier.
2098
+ # Also excluded: providers that already have dedicated live-endpoint
2099
+ # branches below (copilot, anthropic, ai-gateway, ollama-cloud, custom,
2100
+ # stepfun, openai-codex) — those paths handle freshness themselves.
2101
+ _MODELS_DEV_PREFERRED: frozenset[str] = frozenset({
2102
+ "opencode-go",
2103
+ "opencode-zen",
2104
+ "deepseek",
2105
+ "kilocode",
2106
+ "fireworks",
2107
+ "mistral",
2108
+ "togetherai",
2109
+ "cohere",
2110
+ "perplexity",
2111
+ "groq",
2112
+ "nvidia",
2113
+ "huggingface",
2114
+ "zai",
2115
+ "gemini",
2116
+ "google",
2117
+ })
2118
+
2119
+
2120
+ def _merge_with_models_dev(provider: str, curated: list[str]) -> list[str]:
2121
+ """Merge curated list with fresh models.dev entries for a preferred provider.
2122
+
2123
+ Returns models.dev entries first (in models.dev order), then any
2124
+ curated-only entries appended. Preserves case for curated fallbacks
2125
+ (e.g. ``MiniMax-M2.7``) while trusting models.dev for newer variants.
2126
+
2127
+ If models.dev is unreachable or returns nothing, the curated list is
2128
+ returned unchanged — this is the offline/CI fallback path.
2129
+ """
2130
+ try:
2131
+ from agent.models_dev import list_agentic_models
2132
+ mdev = list_agentic_models(provider)
2133
+ except Exception:
2134
+ mdev = []
2135
+
2136
+ if not mdev:
2137
+ return list(curated)
2138
+
2139
+ # Case-insensitive dedup while preserving order and curated casing.
2140
+ seen_lower: set[str] = set()
2141
+ merged: list[str] = []
2142
+ for mid in mdev:
2143
+ key = str(mid).lower()
2144
+ if key in seen_lower:
2145
+ continue
2146
+ seen_lower.add(key)
2147
+ merged.append(mid)
2148
+ for mid in curated:
2149
+ key = str(mid).lower()
2150
+ if key in seen_lower:
2151
+ continue
2152
+ seen_lower.add(key)
2153
+ merged.append(mid)
2154
+ return merged
2155
+
2156
+
2157
+ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False) -> list[str]:
2158
+ """Return the best known model catalog for a provider.
2159
+
2160
+ Tries live API endpoints for providers that support them (Codex, Nous),
2161
+ falling back to static lists. For providers in ``_MODELS_DEV_PREFERRED``
2162
+ (opencode-go/zen, xiaomi, deepseek, smaller inference providers, etc.),
2163
+ models.dev entries are merged on top of curated so new models released
2164
+ on the platform appear in ``/model`` without a Hermes release.
2165
+ """
2166
+ normalized = normalize_provider(provider)
2167
+ if normalized == "openrouter":
2168
+ return model_ids(force_refresh=force_refresh)
2169
+ if normalized == "openai-codex":
2170
+ from hermes_cli.codex_models import get_codex_model_ids
2171
+
2172
+ # Pass the live OAuth access token so the picker matches whatever
2173
+ # ChatGPT lists for this account right now (new models appear without
2174
+ # a Hermes release). Falls back to the hardcoded catalog if no token
2175
+ # or the endpoint is unreachable.
2176
+ access_token = None
2177
+ try:
2178
+ from hermes_cli.auth import resolve_codex_runtime_credentials
2179
+
2180
+ creds = resolve_codex_runtime_credentials(refresh_if_expiring=True)
2181
+ access_token = creds.get("api_key")
2182
+ except Exception:
2183
+ access_token = None
2184
+ return get_codex_model_ids(access_token=access_token)
2185
+ if normalized == "xai-oauth":
2186
+ return list(_PROVIDER_MODELS.get("xai-oauth", _PROVIDER_MODELS.get("xai", [])))
2187
+ if normalized in {"copilot", "copilot-acp"}:
2188
+ try:
2189
+ live = _fetch_github_models(_resolve_copilot_catalog_api_key())
2190
+ if live:
2191
+ return live
2192
+ except Exception:
2193
+ pass
2194
+ if normalized == "copilot-acp":
2195
+ return list(_PROVIDER_MODELS.get("copilot", []))
2196
+ if normalized == "nous":
2197
+ # Try live Nous Portal /models endpoint
2198
+ try:
2199
+ from hermes_cli.auth import fetch_nous_models, resolve_nous_runtime_credentials
2200
+ creds = resolve_nous_runtime_credentials()
2201
+ if creds:
2202
+ live = fetch_nous_models(api_key=creds.get("api_key", ""), inference_base_url=creds.get("base_url", ""))
2203
+ if live:
2204
+ return live
2205
+ except Exception:
2206
+ pass
2207
+ if normalized == "stepfun":
2208
+ try:
2209
+ from hermes_cli.auth import resolve_api_key_provider_credentials
2210
+
2211
+ creds = resolve_api_key_provider_credentials("stepfun")
2212
+ api_key = str(creds.get("api_key") or "").strip()
2213
+ base_url = str(creds.get("base_url") or "").strip()
2214
+ if api_key and base_url:
2215
+ live = fetch_api_models(api_key, base_url)
2216
+ if live:
2217
+ return live
2218
+ except Exception:
2219
+ pass
2220
+ if normalized == "anthropic":
2221
+ live = _fetch_anthropic_models()
2222
+ if live:
2223
+ return live
2224
+ if normalized == "ai-gateway":
2225
+ live = _fetch_ai_gateway_models()
2226
+ if live:
2227
+ return live
2228
+ if normalized == "ollama-cloud":
2229
+ live = fetch_ollama_cloud_models(force_refresh=force_refresh)
2230
+ if live:
2231
+ return live
2232
+ if normalized == "openai":
2233
+ api_key = os.getenv("OPENAI_API_KEY", "").strip()
2234
+ if api_key:
2235
+ base_raw = os.getenv("OPENAI_BASE_URL", "").strip().rstrip("/")
2236
+ base = base_raw or "https://api.openai.com/v1"
2237
+ try:
2238
+ live = fetch_api_models(api_key, base)
2239
+ if live:
2240
+ return live
2241
+ except Exception:
2242
+ pass
2243
+ if normalized == "gmi":
2244
+ try:
2245
+ from hermes_cli.auth import resolve_api_key_provider_credentials
2246
+
2247
+ creds = resolve_api_key_provider_credentials("gmi")
2248
+ api_key = str(creds.get("api_key") or "").strip()
2249
+ base_url = str(creds.get("base_url") or "").strip()
2250
+ if api_key and base_url:
2251
+ live = fetch_api_models(api_key, base_url)
2252
+ if live:
2253
+ return live
2254
+ except Exception:
2255
+ pass
2256
+ if normalized == "custom":
2257
+ base_url = _get_custom_base_url()
2258
+ if base_url:
2259
+ # Try common API key env vars for custom endpoints
2260
+ api_key = (
2261
+ os.getenv("CUSTOM_API_KEY", "")
2262
+ or os.getenv("OPENAI_API_KEY", "")
2263
+ or os.getenv("OPENROUTER_API_KEY", "")
2264
+ )
2265
+ live = fetch_api_models(api_key, base_url)
2266
+ if live:
2267
+ return live
2268
+ # Bedrock uses live discovery keyed by the resolved AWS region so that
2269
+ # EU/AP users see eu.*/ap.* model IDs instead of the static us.* list.
2270
+ # Note: early return intentionally skips _MODELS_DEV_PREFERRED merge
2271
+ # below — bedrock is not expected to appear in that table.
2272
+ if normalized == "bedrock":
2273
+ try:
2274
+ from agent.bedrock_adapter import bedrock_model_ids_or_none
2275
+ ids = bedrock_model_ids_or_none()
2276
+ if ids is not None:
2277
+ return ids
2278
+ except Exception:
2279
+ pass
2280
+
2281
+ # ── Profile-based generic live fetch (all simple api-key providers) ──
2282
+ # Handles any provider registered in providers/ with auth_type="api_key".
2283
+ # Replaces per-provider copy-paste blocks (stepfun, gmi, zai, etc.).
2284
+ try:
2285
+ from providers import get_provider_profile
2286
+ from hermes_cli.auth import resolve_api_key_provider_credentials
2287
+
2288
+ _p = get_provider_profile(normalized)
2289
+ if _p and _p.auth_type == "api_key" and _p.base_url:
2290
+ try:
2291
+ creds = resolve_api_key_provider_credentials(normalized)
2292
+ api_key = str(creds.get("api_key") or "").strip()
2293
+ base_url = str(creds.get("base_url") or "").strip()
2294
+ except Exception:
2295
+ api_key, base_url = "", _p.base_url
2296
+ if not base_url:
2297
+ base_url = _p.base_url
2298
+ if api_key:
2299
+ live = _p.fetch_models(api_key=api_key)
2300
+ if live:
2301
+ return live
2302
+ # Use profile's fallback_models if defined
2303
+ if _p.fallback_models:
2304
+ return list(_p.fallback_models)
2305
+ except Exception:
2306
+ pass
2307
+
2308
+ curated_static = list(_PROVIDER_MODELS.get(normalized, []))
2309
+ if normalized in _MODELS_DEV_PREFERRED:
2310
+ return _merge_with_models_dev(normalized, curated_static)
2311
+ return curated_static
2312
+
2313
+
2314
+ def _fetch_anthropic_models(timeout: float = 5.0) -> Optional[list[str]]:
2315
+ """Fetch available models from the Anthropic /v1/models endpoint.
2316
+
2317
+ Uses resolve_anthropic_token() to find credentials (env vars or
2318
+ Claude Code auto-discovery). Returns sorted model IDs or None.
2319
+ """
2320
+ try:
2321
+ from agent.anthropic_adapter import resolve_anthropic_token, _is_oauth_token
2322
+ except ImportError:
2323
+ return None
2324
+
2325
+ token = resolve_anthropic_token()
2326
+ if not token:
2327
+ return None
2328
+
2329
+ headers: dict[str, str] = {"anthropic-version": "2023-06-01"}
2330
+ is_oauth = _is_oauth_token(token)
2331
+ if is_oauth:
2332
+ headers["Authorization"] = f"Bearer {token}"
2333
+ from agent.anthropic_adapter import _COMMON_BETAS, _OAUTH_ONLY_BETAS, _CONTEXT_1M_BETA
2334
+ headers["anthropic-beta"] = ",".join(_COMMON_BETAS + _OAUTH_ONLY_BETAS)
2335
+ else:
2336
+ headers["x-api-key"] = token
2337
+
2338
+ def _do_request(h: dict[str, str]):
2339
+ req = urllib.request.Request(
2340
+ "https://api.anthropic.com/v1/models",
2341
+ headers=h,
2342
+ )
2343
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
2344
+ return json.loads(resp.read().decode())
2345
+
2346
+ try:
2347
+ try:
2348
+ data = _do_request(headers)
2349
+ except urllib.error.HTTPError as http_err:
2350
+ # Reactive recovery for OAuth subscriptions that reject the 1M
2351
+ # context beta with 400 "long context beta is not yet available
2352
+ # for this subscription". Retry once without the beta; re-raise
2353
+ # anything else so the outer except logs it.
2354
+ if (
2355
+ is_oauth
2356
+ and http_err.code == 400
2357
+ ):
2358
+ try:
2359
+ body_text = http_err.read().decode(errors="ignore").lower()
2360
+ except Exception:
2361
+ body_text = ""
2362
+ if "long context beta" in body_text and "not yet available" in body_text:
2363
+ headers["anthropic-beta"] = ",".join(
2364
+ [b for b in _COMMON_BETAS if b != _CONTEXT_1M_BETA]
2365
+ + list(_OAUTH_ONLY_BETAS)
2366
+ )
2367
+ data = _do_request(headers)
2368
+ else:
2369
+ raise
2370
+ else:
2371
+ raise
2372
+ models = [m["id"] for m in data.get("data", []) if m.get("id")]
2373
+ # Sort: latest/largest first (opus > sonnet > haiku, higher version first)
2374
+ return sorted(models, key=lambda m: (
2375
+ "opus" not in m, # opus first
2376
+ "sonnet" not in m, # then sonnet
2377
+ "haiku" not in m, # then haiku
2378
+ m, # alphabetical within tier
2379
+ ))
2380
+ except Exception as e:
2381
+ import logging
2382
+ logging.getLogger(__name__).debug("Failed to fetch Anthropic models: %s", e)
2383
+ return None
2384
+
2385
+
2386
+ def _payload_items(payload: Any) -> list[dict[str, Any]]:
2387
+ if isinstance(payload, list):
2388
+ return [item for item in payload if isinstance(item, dict)]
2389
+ if isinstance(payload, dict):
2390
+ data = payload.get("data", [])
2391
+ if isinstance(data, list):
2392
+ return [item for item in data if isinstance(item, dict)]
2393
+ return []
2394
+
2395
+
2396
+ def copilot_default_headers() -> dict[str, str]:
2397
+ """Standard headers for Copilot API requests.
2398
+
2399
+ Includes Openai-Intent and x-initiator headers that opencode and the
2400
+ Copilot CLI send on every request.
2401
+ """
2402
+ try:
2403
+ from hermes_cli.copilot_auth import copilot_request_headers
2404
+ return copilot_request_headers(is_agent_turn=True)
2405
+ except ImportError:
2406
+ return {
2407
+ "Editor-Version": COPILOT_EDITOR_VERSION,
2408
+ "User-Agent": "HermesAgent/1.0",
2409
+ "Openai-Intent": "conversation-edits",
2410
+ "x-initiator": "agent",
2411
+ }
2412
+
2413
+
2414
+ def _copilot_catalog_item_is_text_model(item: dict[str, Any]) -> bool:
2415
+ model_id = str(item.get("id") or "").strip()
2416
+ if not model_id:
2417
+ return False
2418
+
2419
+ if item.get("model_picker_enabled") is False:
2420
+ return False
2421
+
2422
+ capabilities = item.get("capabilities")
2423
+ if isinstance(capabilities, dict):
2424
+ model_type = str(capabilities.get("type") or "").strip().lower()
2425
+ if model_type and model_type != "chat":
2426
+ return False
2427
+
2428
+ supported_endpoints = item.get("supported_endpoints")
2429
+ if isinstance(supported_endpoints, list):
2430
+ normalized_endpoints = {
2431
+ str(endpoint).strip()
2432
+ for endpoint in supported_endpoints
2433
+ if str(endpoint).strip()
2434
+ }
2435
+ if normalized_endpoints and not normalized_endpoints.intersection(
2436
+ {"/chat/completions", "/responses", "/v1/messages"}
2437
+ ):
2438
+ return False
2439
+
2440
+ return True
2441
+
2442
+
2443
+ def fetch_github_model_catalog(
2444
+ api_key: Optional[str] = None, timeout: float = 5.0
2445
+ ) -> Optional[list[dict[str, Any]]]:
2446
+ """Fetch the live GitHub Copilot model catalog for this account."""
2447
+ attempts: list[dict[str, str]] = []
2448
+ if api_key:
2449
+ attempts.append({
2450
+ **copilot_default_headers(),
2451
+ "Authorization": f"Bearer {api_key}",
2452
+ })
2453
+ attempts.append(copilot_default_headers())
2454
+
2455
+ for headers in attempts:
2456
+ req = urllib.request.Request(COPILOT_MODELS_URL, headers=headers)
2457
+ try:
2458
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
2459
+ data = json.loads(resp.read().decode())
2460
+ items = _payload_items(data)
2461
+ models: list[dict[str, Any]] = []
2462
+ seen_ids: set[str] = set()
2463
+ for item in items:
2464
+ if not _copilot_catalog_item_is_text_model(item):
2465
+ continue
2466
+ model_id = str(item.get("id") or "").strip()
2467
+ if not model_id or model_id in seen_ids:
2468
+ continue
2469
+ seen_ids.add(model_id)
2470
+ models.append(item)
2471
+ if models:
2472
+ return models
2473
+ except Exception:
2474
+ continue
2475
+ return None
2476
+
2477
+
2478
+ # ─── Copilot catalog context-window helpers ─────────────────────────────────
2479
+
2480
+ # Module-level cache: {model_id: max_prompt_tokens}
2481
+ _copilot_context_cache: dict[str, int] = {}
2482
+ _copilot_context_cache_time: float = 0.0
2483
+ _COPILOT_CONTEXT_CACHE_TTL = 3600 # 1 hour
2484
+
2485
+
2486
+ def get_copilot_model_context(model_id: str, api_key: Optional[str] = None) -> Optional[int]:
2487
+ """Look up max_prompt_tokens for a Copilot model from the live /models API.
2488
+
2489
+ Results are cached in-process for 1 hour to avoid repeated API calls.
2490
+ Returns the token limit or None if not found.
2491
+ """
2492
+ global _copilot_context_cache, _copilot_context_cache_time
2493
+
2494
+ # Serve from cache if fresh
2495
+ if _copilot_context_cache and (time.time() - _copilot_context_cache_time < _COPILOT_CONTEXT_CACHE_TTL):
2496
+ if model_id in _copilot_context_cache:
2497
+ return _copilot_context_cache[model_id]
2498
+ # Cache is fresh but model not in it — don't re-fetch
2499
+ return None
2500
+
2501
+ # Fetch and populate cache
2502
+ catalog = fetch_github_model_catalog(api_key=api_key)
2503
+ if not catalog:
2504
+ return None
2505
+
2506
+ cache: dict[str, int] = {}
2507
+ for item in catalog:
2508
+ mid = str(item.get("id") or "").strip()
2509
+ if not mid:
2510
+ continue
2511
+ caps = item.get("capabilities") or {}
2512
+ limits = caps.get("limits") or {}
2513
+ max_prompt = limits.get("max_prompt_tokens")
2514
+ if isinstance(max_prompt, int) and max_prompt > 0:
2515
+ cache[mid] = max_prompt
2516
+
2517
+ _copilot_context_cache = cache
2518
+ _copilot_context_cache_time = time.time()
2519
+
2520
+ return cache.get(model_id)
2521
+
2522
+
2523
+ def _is_github_models_base_url(base_url: Optional[str]) -> bool:
2524
+ normalized = (base_url or "").strip().rstrip("/").lower()
2525
+ return (
2526
+ normalized.startswith(COPILOT_BASE_URL)
2527
+ or normalized.startswith("https://models.github.ai/inference")
2528
+ or normalized.startswith("https://models.inference.ai.azure.com")
2529
+ )
2530
+
2531
+
2532
+ def _lmstudio_server_root(base_url: Optional[str]) -> Optional[str]:
2533
+ """Strip ``/v1`` suffix from an LM Studio base URL to get the native API root.
2534
+
2535
+ Returns ``None`` when the base URL is empty/invalid.
2536
+ """
2537
+ root = (base_url or "").strip().rstrip("/")
2538
+ if root.endswith("/v1"):
2539
+ root = root[:-3].rstrip("/")
2540
+ return root or None
2541
+
2542
+
2543
+ def _lmstudio_request_headers(api_key: Optional[str] = None) -> dict:
2544
+ """Build HTTP headers for LM Studio native API requests."""
2545
+ headers = {"User-Agent": _HERMES_USER_AGENT}
2546
+ token = str(api_key or "").strip()
2547
+ if token:
2548
+ headers["Authorization"] = f"Bearer {token}"
2549
+ return headers
2550
+
2551
+
2552
+ def _lmstudio_fetch_raw_models(
2553
+ api_key: Optional[str] = None,
2554
+ base_url: Optional[str] = None,
2555
+ timeout: float = 5.0,
2556
+ ) -> Optional[list[dict]]:
2557
+ """Fetch the raw model list from LM Studio's ``/api/v1/models``.
2558
+
2559
+ Returns the ``models`` list of dicts on success, ``None`` on network
2560
+ errors or malformed responses. Raises ``AuthError`` on HTTP 401/403.
2561
+ """
2562
+ server_root = _lmstudio_server_root(base_url)
2563
+ if not server_root:
2564
+ return None
2565
+
2566
+ headers = _lmstudio_request_headers(api_key)
2567
+ request = urllib.request.Request(server_root + "/api/v1/models", headers=headers)
2568
+ try:
2569
+ with urllib.request.urlopen(request, timeout=timeout) as resp:
2570
+ payload = json.loads(resp.read().decode())
2571
+ except urllib.error.HTTPError as exc:
2572
+ if exc.code in {401, 403}:
2573
+ from hermes_cli.auth import AuthError
2574
+ raise AuthError(
2575
+ f"LM Studio rejected the request with HTTP {exc.code}.",
2576
+ provider="lmstudio",
2577
+ code="auth_rejected",
2578
+ ) from exc
2579
+ import logging
2580
+ logging.getLogger(__name__).debug(
2581
+ "LM Studio probe at %s failed with HTTP %s", server_root, exc.code,
2582
+ )
2583
+ return None
2584
+ except Exception as exc:
2585
+ import logging
2586
+ logging.getLogger(__name__).debug(
2587
+ "LM Studio probe at %s failed: %s", server_root, exc,
2588
+ )
2589
+ return None
2590
+
2591
+ raw_models = payload.get("models") if isinstance(payload, dict) else None
2592
+ if not isinstance(raw_models, list):
2593
+ import logging
2594
+ logging.getLogger(__name__).debug(
2595
+ "LM Studio probe at %s returned malformed payload (no `models` list)",
2596
+ server_root,
2597
+ )
2598
+ return None
2599
+ return raw_models
2600
+
2601
+
2602
+ def probe_lmstudio_models(
2603
+ api_key: Optional[str] = None,
2604
+ base_url: Optional[str] = None,
2605
+ timeout: float = 5.0,
2606
+ ) -> Optional[list[str]]:
2607
+ """Probe LM Studio's model listing.
2608
+
2609
+ Returns chat-capable model keys on success, including the valid empty-list
2610
+ case when the server is reachable but has no non-embedding models.
2611
+ Returns ``None`` on network errors, malformed responses, or empty/invalid
2612
+ base URLs.
2613
+
2614
+ Raises ``AuthError`` on HTTP 401/403 so callers can surface token issues
2615
+ separately from reachability problems.
2616
+ """
2617
+ raw_models = _lmstudio_fetch_raw_models(api_key=api_key, base_url=base_url, timeout=timeout)
2618
+ if raw_models is None:
2619
+ return None
2620
+
2621
+ keys: list[str] = []
2622
+ for raw in raw_models:
2623
+ if not isinstance(raw, dict):
2624
+ continue
2625
+ if str(raw.get("type") or "").strip().lower() == "embedding":
2626
+ continue
2627
+ key = str(raw.get("key") or raw.get("id") or "").strip()
2628
+ if key and key not in keys:
2629
+ keys.append(key)
2630
+ return keys
2631
+
2632
+
2633
+ def fetch_lmstudio_models(
2634
+ api_key: Optional[str] = None,
2635
+ base_url: Optional[str] = None,
2636
+ timeout: float = 5.0,
2637
+ ) -> list[str]:
2638
+ """Fetch LM Studio chat-capable model keys from native ``/api/v1/models``.
2639
+
2640
+ Returns a list of model keys (e.g. ``publisher/model-name``) with embedding
2641
+ models filtered out. Returns an empty list on network errors, malformed
2642
+ responses, or empty/invalid base URLs.
2643
+
2644
+ Raises ``AuthError`` on HTTP 401/403 so callers can distinguish a missing
2645
+ or wrong ``LM_API_KEY`` from an unreachable server — the most common
2646
+ LM Studio support case once auth-enabled mode is turned on.
2647
+ """
2648
+ models = probe_lmstudio_models(api_key=api_key, base_url=base_url, timeout=timeout)
2649
+ return models or []
2650
+
2651
+
2652
+ def ensure_lmstudio_model_loaded(
2653
+ model: str,
2654
+ base_url: Optional[str],
2655
+ api_key: Optional[str],
2656
+ target_context_length: int,
2657
+ timeout: float = 120.0,
2658
+ ) -> Optional[int]:
2659
+ """Ensure LM Studio has ``model`` loaded with at least ``target_context_length``.
2660
+
2661
+ No-op when an instance is already loaded with sufficient context. Otherwise
2662
+ POSTs ``/api/v1/models/load`` to (re)load with the target context, capped
2663
+ at the model's ``max_context_length``. Returns the resolved loaded context
2664
+ length, or ``None`` when the probe / load failed.
2665
+ """
2666
+ server_root = _lmstudio_server_root(base_url)
2667
+ if not server_root:
2668
+ return None
2669
+
2670
+ headers = _lmstudio_request_headers(api_key)
2671
+
2672
+ try:
2673
+ raw_models = _lmstudio_fetch_raw_models(api_key=api_key, base_url=base_url, timeout=10)
2674
+ except Exception:
2675
+ raw_models = None
2676
+ if raw_models is None:
2677
+ return None
2678
+
2679
+ target_entry = None
2680
+ for raw in raw_models:
2681
+ if not isinstance(raw, dict):
2682
+ continue
2683
+ if raw.get("key") == model or raw.get("id") == model:
2684
+ target_entry = raw
2685
+ break
2686
+ if target_entry is None:
2687
+ return None
2688
+
2689
+ max_ctx = target_entry.get("max_context_length")
2690
+ if isinstance(max_ctx, int) and max_ctx > 0:
2691
+ target_context_length = min(target_context_length, max_ctx)
2692
+
2693
+ for inst in target_entry.get("loaded_instances") or []:
2694
+ cfg = inst.get("config") if isinstance(inst, dict) else None
2695
+ loaded_ctx = cfg.get("context_length") if isinstance(cfg, dict) else None
2696
+ if isinstance(loaded_ctx, int) and loaded_ctx >= target_context_length:
2697
+ return loaded_ctx
2698
+
2699
+ body = json.dumps({
2700
+ "model": model,
2701
+ "context_length": target_context_length,
2702
+ }).encode()
2703
+ load_headers = dict(headers)
2704
+ load_headers["Content-Type"] = "application/json"
2705
+ try:
2706
+ with urllib.request.urlopen(
2707
+ urllib.request.Request(
2708
+ server_root + "/api/v1/models/load",
2709
+ data=body,
2710
+ headers=load_headers,
2711
+ method="POST",
2712
+ ),
2713
+ timeout=timeout,
2714
+ ) as resp:
2715
+ resp.read()
2716
+ except Exception:
2717
+ return None
2718
+ return target_context_length
2719
+
2720
+
2721
+ def lmstudio_model_reasoning_options(
2722
+ model: str,
2723
+ base_url: Optional[str],
2724
+ api_key: Optional[str] = None,
2725
+ timeout: float = 5.0,
2726
+ ) -> list[str]:
2727
+ """Return the reasoning ``allowed_options`` LM Studio publishes for ``model``.
2728
+
2729
+ Pulls ``capabilities.reasoning.allowed_options`` from ``/api/v1/models``.
2730
+ Returns ``[]`` when the model is unknown, the endpoint is unreachable,
2731
+ or the model does not declare a reasoning capability.
2732
+ """
2733
+ try:
2734
+ raw_models = _lmstudio_fetch_raw_models(api_key=api_key, base_url=base_url, timeout=timeout)
2735
+ except Exception:
2736
+ raw_models = None
2737
+ if not raw_models:
2738
+ return []
2739
+
2740
+ for raw in raw_models:
2741
+ if not isinstance(raw, dict):
2742
+ continue
2743
+ if raw.get("key") != model and raw.get("id") != model:
2744
+ continue
2745
+ caps = raw.get("capabilities")
2746
+ reasoning = caps.get("reasoning") if isinstance(caps, dict) else None
2747
+ opts = reasoning.get("allowed_options") if isinstance(reasoning, dict) else None
2748
+ if isinstance(opts, list):
2749
+ return [str(o).strip().lower() for o in opts if isinstance(o, str)]
2750
+ return []
2751
+ return []
2752
+
2753
+
2754
+ def _fetch_github_models(api_key: Optional[str] = None, timeout: float = 5.0) -> Optional[list[str]]:
2755
+ catalog = fetch_github_model_catalog(api_key=api_key, timeout=timeout)
2756
+ if not catalog:
2757
+ return None
2758
+ return [item.get("id", "") for item in catalog if item.get("id")]
2759
+
2760
+
2761
+ _COPILOT_MODEL_ALIASES = {
2762
+ "openai/gpt-5": "gpt-5-mini",
2763
+ "openai/gpt-5-chat": "gpt-5-mini",
2764
+ "openai/gpt-5-mini": "gpt-5-mini",
2765
+ "openai/gpt-5-nano": "gpt-5-mini",
2766
+ "openai/gpt-4.1": "gpt-4.1",
2767
+ "openai/gpt-4.1-mini": "gpt-4.1",
2768
+ "openai/gpt-4.1-nano": "gpt-4.1",
2769
+ "openai/gpt-4o": "gpt-4o",
2770
+ "openai/gpt-4o-mini": "gpt-4o-mini",
2771
+ "openai/o1": "gpt-5.2",
2772
+ "openai/o1-mini": "gpt-5-mini",
2773
+ "openai/o1-preview": "gpt-5.2",
2774
+ "openai/o3": "gpt-5.3-codex",
2775
+ "openai/o3-mini": "gpt-5-mini",
2776
+ "openai/o4-mini": "gpt-5-mini",
2777
+ "anthropic/claude-opus-4.6": "claude-opus-4.6",
2778
+ "anthropic/claude-sonnet-4.6": "claude-sonnet-4.6",
2779
+ "anthropic/claude-sonnet-4": "claude-sonnet-4",
2780
+ "anthropic/claude-sonnet-4.5": "claude-sonnet-4.5",
2781
+ "anthropic/claude-haiku-4.5": "claude-haiku-4.5",
2782
+ # Dash-notation fallbacks: Hermes' default Claude IDs elsewhere use
2783
+ # hyphens (anthropic native format), but Copilot's API only accepts
2784
+ # dot-notation. Accept both so users who configure copilot + a
2785
+ # default hyphenated Claude model don't hit HTTP 400
2786
+ # "model_not_supported". See issue #6879.
2787
+ "claude-opus-4-6": "claude-opus-4.6",
2788
+ "claude-sonnet-4-6": "claude-sonnet-4.6",
2789
+ "claude-sonnet-4-0": "claude-sonnet-4",
2790
+ "claude-sonnet-4-5": "claude-sonnet-4.5",
2791
+ "claude-haiku-4-5": "claude-haiku-4.5",
2792
+ "anthropic/claude-opus-4-6": "claude-opus-4.6",
2793
+ "anthropic/claude-sonnet-4-6": "claude-sonnet-4.6",
2794
+ "anthropic/claude-sonnet-4-0": "claude-sonnet-4",
2795
+ "anthropic/claude-sonnet-4-5": "claude-sonnet-4.5",
2796
+ "anthropic/claude-haiku-4-5": "claude-haiku-4.5",
2797
+ }
2798
+
2799
+
2800
+ def _copilot_catalog_ids(
2801
+ catalog: Optional[list[dict[str, Any]]] = None,
2802
+ api_key: Optional[str] = None,
2803
+ ) -> set[str]:
2804
+ if catalog is None and api_key:
2805
+ catalog = fetch_github_model_catalog(api_key=api_key)
2806
+ if not catalog:
2807
+ return set()
2808
+ return {
2809
+ str(item.get("id") or "").strip()
2810
+ for item in catalog
2811
+ if str(item.get("id") or "").strip()
2812
+ }
2813
+
2814
+
2815
+ def normalize_copilot_model_id(
2816
+ model_id: Optional[str],
2817
+ *,
2818
+ catalog: Optional[list[dict[str, Any]]] = None,
2819
+ api_key: Optional[str] = None,
2820
+ ) -> str:
2821
+ raw = str(model_id or "").strip()
2822
+ if not raw:
2823
+ return ""
2824
+
2825
+ catalog_ids = _copilot_catalog_ids(catalog=catalog, api_key=api_key)
2826
+ alias = _COPILOT_MODEL_ALIASES.get(raw)
2827
+ if alias:
2828
+ return alias
2829
+
2830
+ candidates = [raw]
2831
+ if "/" in raw:
2832
+ candidates.append(raw.split("/", 1)[1].strip())
2833
+
2834
+ if raw.endswith("-mini"):
2835
+ candidates.append(raw[:-5])
2836
+ if raw.endswith("-nano"):
2837
+ candidates.append(raw[:-5])
2838
+ if raw.endswith("-chat"):
2839
+ candidates.append(raw[:-5])
2840
+
2841
+ seen: set[str] = set()
2842
+ for candidate in candidates:
2843
+ if not candidate or candidate in seen:
2844
+ continue
2845
+ seen.add(candidate)
2846
+ if candidate in _COPILOT_MODEL_ALIASES:
2847
+ return _COPILOT_MODEL_ALIASES[candidate]
2848
+ if candidate in catalog_ids:
2849
+ return candidate
2850
+
2851
+ if "/" in raw:
2852
+ return raw.split("/", 1)[1].strip()
2853
+ return raw
2854
+
2855
+
2856
+ def _github_reasoning_efforts_for_model_id(model_id: str) -> list[str]:
2857
+ raw = (model_id or "").strip().lower()
2858
+ if raw.startswith(("openai/o1", "openai/o3", "openai/o4", "o1", "o3", "o4")):
2859
+ return list(COPILOT_REASONING_EFFORTS_O_SERIES)
2860
+ normalized = normalize_copilot_model_id(model_id).lower()
2861
+ if normalized.startswith("gpt-5"):
2862
+ return list(COPILOT_REASONING_EFFORTS_GPT5)
2863
+ return []
2864
+
2865
+
2866
+ def _should_use_copilot_responses_api(model_id: str) -> bool:
2867
+ """Decide whether a Copilot model should use the Responses API.
2868
+
2869
+ Replicates opencode's ``shouldUseCopilotResponsesApi`` logic:
2870
+ GPT-5+ models use Responses API, except ``gpt-5-mini`` which uses
2871
+ Chat Completions. All non-GPT models (Claude, Gemini, etc.) use
2872
+ Chat Completions.
2873
+ """
2874
+ import re
2875
+
2876
+ match = re.match(r"^gpt-(\d+)", model_id)
2877
+ if not match:
2878
+ return False
2879
+ major = int(match.group(1))
2880
+ return major >= 5 and not model_id.startswith("gpt-5-mini")
2881
+
2882
+
2883
+ def copilot_model_api_mode(
2884
+ model_id: Optional[str],
2885
+ *,
2886
+ catalog: Optional[list[dict[str, Any]]] = None,
2887
+ api_key: Optional[str] = None,
2888
+ ) -> str:
2889
+ """Determine the API mode for a Copilot model.
2890
+
2891
+ Uses the model ID pattern (matching opencode's approach) as the
2892
+ primary signal. Falls back to the catalog's ``supported_endpoints``
2893
+ only for models not covered by the pattern check.
2894
+ """
2895
+ # Fetch the catalog once so normalize + endpoint check share it
2896
+ # (avoids two redundant network calls for non-GPT-5 models).
2897
+ if catalog is None and api_key:
2898
+ catalog = fetch_github_model_catalog(api_key=api_key)
2899
+
2900
+ normalized = normalize_copilot_model_id(model_id, catalog=catalog, api_key=api_key)
2901
+ if not normalized:
2902
+ return "chat_completions"
2903
+
2904
+ # Primary: model ID pattern (matches opencode's shouldUseCopilotResponsesApi)
2905
+ if _should_use_copilot_responses_api(normalized):
2906
+ return "codex_responses"
2907
+
2908
+ # Secondary: check catalog for non-GPT-5 models (Claude via /v1/messages, etc.)
2909
+ if catalog:
2910
+ catalog_entry = next((item for item in catalog if item.get("id") == normalized), None)
2911
+ if isinstance(catalog_entry, dict):
2912
+ supported_endpoints = {
2913
+ str(endpoint).strip()
2914
+ for endpoint in (catalog_entry.get("supported_endpoints") or [])
2915
+ if str(endpoint).strip()
2916
+ }
2917
+ # For non-GPT-5 models, check if they only support messages API
2918
+ if "/v1/messages" in supported_endpoints and "/chat/completions" not in supported_endpoints:
2919
+ return "anthropic_messages"
2920
+
2921
+ return "chat_completions"
2922
+
2923
+
2924
+ # Azure Foundry model families that require the Responses API. Azure
2925
+ # rejects /chat/completions against these deployments with
2926
+ # ``400 "The requested operation is unsupported."`` — the same payload Bob
2927
+ # Dobolina hit in April 2026 on ``gpt-5.3-codex`` while ``gpt-4o-pure`` on
2928
+ # the same endpoint worked fine. Keep the patterns broad enough to cover
2929
+ # vendor-renamed deployments (e.g. ``gpt-5.3-codex``, ``gpt-5-codex``,
2930
+ # ``gpt-5.4``, ``o1-preview``) but tight enough to leave GPT-4 / 3.5 / Llama /
2931
+ # Mistral / Grok deployments on chat completions.
2932
+ _AZURE_FOUNDRY_RESPONSES_PREFIXES = (
2933
+ "codex", # codex-*, codex-mini
2934
+ "gpt-5", # gpt-5, gpt-5.x, gpt-5-codex, gpt-5.x-codex
2935
+ "o1", # o1, o1-preview, o1-mini
2936
+ "o3", # o3, o3-mini
2937
+ "o4", # o4, o4-mini
2938
+ )
2939
+
2940
+
2941
+ def azure_foundry_model_api_mode(model_name: Optional[str]) -> Optional[str]:
2942
+ """Infer Azure Foundry api_mode from a deployment/model name.
2943
+
2944
+ Returns ``"codex_responses"`` when the model name matches a family that
2945
+ only accepts the Responses API on Azure Foundry (GPT-5.x, codex, o1/o3/o4
2946
+ reasoning models). Returns ``None`` otherwise — the caller should fall
2947
+ back to the configured/default api_mode (typically ``chat_completions``)
2948
+ so GPT-4o, GPT-4 Turbo, Llama, Mistral, etc. keep working.
2949
+
2950
+ Intentionally does NOT return ``anthropic_messages``; Anthropic-style
2951
+ Azure endpoints are disambiguated by URL (``/anthropic`` suffix) in
2952
+ ``runtime_provider._detect_api_mode_for_url`` and by the user setting
2953
+ ``model.api_mode: anthropic_messages`` explicitly.
2954
+ """
2955
+ raw = str(model_name or "").strip().lower()
2956
+ if not raw:
2957
+ return None
2958
+ # Strip any vendor/ prefix a user may have copied from OpenRouter / Copilot.
2959
+ if "/" in raw:
2960
+ raw = raw.rsplit("/", 1)[-1]
2961
+ # gpt-5-mini speaks chat completions on Copilot but Azure Foundry deploys
2962
+ # the full gpt-5 family uniformly on Responses API — don't carve an
2963
+ # exception here.
2964
+ for prefix in _AZURE_FOUNDRY_RESPONSES_PREFIXES:
2965
+ if raw.startswith(prefix):
2966
+ return "codex_responses"
2967
+ return None
2968
+
2969
+
2970
+ def normalize_opencode_model_id(provider_id: Optional[str], model_id: Optional[str]) -> str:
2971
+ """Normalize OpenCode config IDs to the bare model slug used in API requests."""
2972
+ provider = normalize_provider(provider_id)
2973
+ current = str(model_id or "").strip()
2974
+ if not current or provider not in {"opencode-zen", "opencode-go"}:
2975
+ return current
2976
+
2977
+ prefix = f"{provider}/"
2978
+ if current.lower().startswith(prefix):
2979
+ return current[len(prefix):]
2980
+ return current
2981
+
2982
+
2983
+ def opencode_model_api_mode(provider_id: Optional[str], model_id: Optional[str]) -> str:
2984
+ """Determine the API mode for an OpenCode Zen / Go model.
2985
+
2986
+ OpenCode routes different models behind different API surfaces:
2987
+
2988
+ - GPT-5 / Codex models on Zen use ``/v1/responses``
2989
+ - Claude models on Zen use ``/v1/messages``
2990
+ - MiniMax models on Go use ``/v1/messages``
2991
+ - GLM / Kimi on Go use ``/v1/chat/completions``
2992
+ - Other Zen models (Gemini, GLM, Kimi, MiniMax, Qwen, etc.) use
2993
+ ``/v1/chat/completions``
2994
+
2995
+ This follows the published OpenCode docs for Zen and Go endpoints.
2996
+ """
2997
+ provider = normalize_provider(provider_id)
2998
+ normalized = normalize_opencode_model_id(provider_id, model_id).lower()
2999
+ if not normalized:
3000
+ return "chat_completions"
3001
+
3002
+ if provider == "opencode-go":
3003
+ if normalized.startswith("minimax-"):
3004
+ return "anthropic_messages"
3005
+ return "chat_completions"
3006
+
3007
+ if provider == "opencode-zen":
3008
+ if normalized.startswith("claude-"):
3009
+ return "anthropic_messages"
3010
+ if normalized.startswith("gpt-"):
3011
+ return "codex_responses"
3012
+ return "chat_completions"
3013
+
3014
+ return "chat_completions"
3015
+
3016
+
3017
+ def github_model_reasoning_efforts(
3018
+ model_id: Optional[str],
3019
+ *,
3020
+ catalog: Optional[list[dict[str, Any]]] = None,
3021
+ api_key: Optional[str] = None,
3022
+ ) -> list[str]:
3023
+ """Return supported reasoning-effort levels for a Copilot-visible model."""
3024
+ normalized = normalize_copilot_model_id(model_id, catalog=catalog, api_key=api_key)
3025
+ if not normalized:
3026
+ return []
3027
+
3028
+ catalog_entry = None
3029
+ if catalog is not None:
3030
+ catalog_entry = next((item for item in catalog if item.get("id") == normalized), None)
3031
+ elif api_key:
3032
+ fetched_catalog = fetch_github_model_catalog(api_key=api_key)
3033
+ if fetched_catalog:
3034
+ catalog_entry = next((item for item in fetched_catalog if item.get("id") == normalized), None)
3035
+
3036
+ if catalog_entry is not None:
3037
+ capabilities = catalog_entry.get("capabilities")
3038
+ if isinstance(capabilities, dict):
3039
+ supports = capabilities.get("supports")
3040
+ if isinstance(supports, dict):
3041
+ efforts = supports.get("reasoning_effort")
3042
+ if isinstance(efforts, list):
3043
+ normalized_efforts = [
3044
+ str(effort).strip().lower()
3045
+ for effort in efforts
3046
+ if str(effort).strip()
3047
+ ]
3048
+ return list(dict.fromkeys(normalized_efforts))
3049
+ return []
3050
+ legacy_capabilities = {
3051
+ str(capability).strip().lower()
3052
+ for capability in catalog_entry.get("capabilities", [])
3053
+ if str(capability).strip()
3054
+ }
3055
+ if "reasoning" not in legacy_capabilities:
3056
+ return []
3057
+
3058
+ return _github_reasoning_efforts_for_model_id(str(model_id or normalized))
3059
+
3060
+
3061
+ def probe_api_models(
3062
+ api_key: Optional[str],
3063
+ base_url: Optional[str],
3064
+ timeout: float = 5.0,
3065
+ api_mode: Optional[str] = None,
3066
+ ) -> dict[str, Any]:
3067
+ """Probe a ``/models`` endpoint with light URL heuristics.
3068
+
3069
+ For ``anthropic_messages`` mode, uses ``x-api-key`` and
3070
+ ``anthropic-version`` headers (Anthropic's native auth) instead of
3071
+ ``Authorization: Bearer``. The response shape (``data[].id``) is
3072
+ identical, so the same parser works for both.
3073
+ """
3074
+ normalized = (base_url or "").strip().rstrip("/")
3075
+ if not normalized:
3076
+ return {
3077
+ "models": None,
3078
+ "probed_url": None,
3079
+ "resolved_base_url": "",
3080
+ "suggested_base_url": None,
3081
+ "used_fallback": False,
3082
+ }
3083
+
3084
+ if _is_github_models_base_url(normalized):
3085
+ models = _fetch_github_models(api_key=api_key, timeout=timeout)
3086
+ return {
3087
+ "models": models,
3088
+ "probed_url": COPILOT_MODELS_URL,
3089
+ "resolved_base_url": COPILOT_BASE_URL,
3090
+ "suggested_base_url": None,
3091
+ "used_fallback": False,
3092
+ }
3093
+
3094
+ if normalized.endswith("/v1"):
3095
+ alternate_base = normalized[:-3].rstrip("/")
3096
+ else:
3097
+ alternate_base = normalized + "/v1"
3098
+
3099
+ candidates: list[tuple[str, bool]] = [(normalized, False)]
3100
+ if alternate_base and alternate_base != normalized:
3101
+ candidates.append((alternate_base, True))
3102
+
3103
+ tried: list[str] = []
3104
+ headers: dict[str, str] = {"User-Agent": _HERMES_USER_AGENT}
3105
+ if api_key and api_mode == "anthropic_messages":
3106
+ headers["x-api-key"] = api_key
3107
+ headers["anthropic-version"] = "2023-06-01"
3108
+ elif api_key:
3109
+ headers["Authorization"] = f"Bearer {api_key}"
3110
+ if normalized.startswith(COPILOT_BASE_URL):
3111
+ headers.update(copilot_default_headers())
3112
+
3113
+ for candidate_base, is_fallback in candidates:
3114
+ url = candidate_base.rstrip("/") + "/models"
3115
+ tried.append(url)
3116
+ req = urllib.request.Request(url, headers=headers)
3117
+ try:
3118
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
3119
+ data = json.loads(resp.read().decode())
3120
+ return {
3121
+ "models": [m.get("id", "") for m in data.get("data", [])],
3122
+ "probed_url": url,
3123
+ "resolved_base_url": candidate_base.rstrip("/"),
3124
+ "suggested_base_url": alternate_base if alternate_base != candidate_base else normalized,
3125
+ "used_fallback": is_fallback,
3126
+ }
3127
+ except Exception:
3128
+ continue
3129
+
3130
+ return {
3131
+ "models": None,
3132
+ "probed_url": tried[0] if tried else normalized.rstrip("/") + "/models",
3133
+ "resolved_base_url": normalized,
3134
+ "suggested_base_url": alternate_base if alternate_base != normalized else None,
3135
+ "used_fallback": False,
3136
+ }
3137
+
3138
+
3139
+ def _fetch_ai_gateway_models(timeout: float = 5.0) -> Optional[list[str]]:
3140
+ """Fetch available language models with tool-use from AI Gateway."""
3141
+ api_key = os.getenv("AI_GATEWAY_API_KEY", "").strip()
3142
+ if not api_key:
3143
+ return None
3144
+ base_url = os.getenv("AI_GATEWAY_BASE_URL", "").strip()
3145
+ if not base_url:
3146
+ from calvyn_constants import AI_GATEWAY_BASE_URL
3147
+ base_url = AI_GATEWAY_BASE_URL
3148
+
3149
+ url = base_url.rstrip("/") + "/models"
3150
+ headers: dict[str, str] = {
3151
+ "Authorization": f"Bearer {api_key}",
3152
+ "User-Agent": _HERMES_USER_AGENT,
3153
+ }
3154
+ req = urllib.request.Request(url, headers=headers)
3155
+ try:
3156
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
3157
+ data = json.loads(resp.read().decode())
3158
+ return [
3159
+ m["id"]
3160
+ for m in data.get("data", [])
3161
+ if m.get("id")
3162
+ and m.get("type") == "language"
3163
+ and "tool-use" in (m.get("tags") or [])
3164
+ ]
3165
+ except Exception:
3166
+ return None
3167
+
3168
+
3169
+ def fetch_api_models(
3170
+ api_key: Optional[str],
3171
+ base_url: Optional[str],
3172
+ timeout: float = 5.0,
3173
+ api_mode: Optional[str] = None,
3174
+ ) -> Optional[list[str]]:
3175
+ """Fetch the list of available model IDs from the provider's ``/models`` endpoint.
3176
+
3177
+ Returns a list of model ID strings, or ``None`` if the endpoint could not
3178
+ be reached (network error, timeout, auth failure, etc.).
3179
+ """
3180
+ return probe_api_models(api_key, base_url, timeout=timeout, api_mode=api_mode).get("models")
3181
+
3182
+
3183
+ # ---------------------------------------------------------------------------
3184
+ # Ollama Cloud — merged model discovery with disk cache
3185
+ # ---------------------------------------------------------------------------
3186
+
3187
+
3188
+
3189
+ _OLLAMA_CLOUD_CACHE_TTL = 3600 # 1 hour
3190
+
3191
+
3192
+ def _strip_ollama_cloud_suffix(model_id: str) -> str:
3193
+ """Strip :cloud / -cloud suffixes that models.dev appends to Ollama Cloud IDs.
3194
+
3195
+ The live API uses clean IDs (e.g. 'kimi-k2.6') while models.dev sometimes
3196
+ returns them as 'kimi-k2.6:cloud'. Normalising before the dedup merge
3197
+ prevents duplicate entries in the merged model list.
3198
+ """
3199
+ for suffix in (":cloud", "-cloud"):
3200
+ if model_id.endswith(suffix):
3201
+ return model_id[: -len(suffix)]
3202
+ return model_id
3203
+
3204
+
3205
+ def _ollama_cloud_cache_path() -> Path:
3206
+ """Return the path for the Ollama Cloud model cache."""
3207
+ from calvyn_constants import get_hermes_home
3208
+ return get_hermes_home() / "ollama_cloud_models_cache.json"
3209
+
3210
+
3211
+ def _load_ollama_cloud_cache(*, ignore_ttl: bool = False) -> Optional[dict]:
3212
+ """Load cached Ollama Cloud models from disk.
3213
+
3214
+ Args:
3215
+ ignore_ttl: If True, return data even if the TTL has expired (stale fallback).
3216
+ """
3217
+ try:
3218
+ cache_path = _ollama_cloud_cache_path()
3219
+ if not cache_path.exists():
3220
+ return None
3221
+ with open(cache_path, encoding="utf-8") as f:
3222
+ data = json.load(f)
3223
+ if not isinstance(data, dict):
3224
+ return None
3225
+ models = data.get("models")
3226
+ if not (isinstance(models, list) and models):
3227
+ return None
3228
+ if not ignore_ttl:
3229
+ cached_at = data.get("cached_at", 0)
3230
+ if (time.time() - cached_at) > _OLLAMA_CLOUD_CACHE_TTL:
3231
+ return None # stale
3232
+ return data
3233
+ except Exception:
3234
+ pass
3235
+ return None
3236
+
3237
+
3238
+ def _save_ollama_cloud_cache(models: list[str]) -> None:
3239
+ """Persist the merged Ollama Cloud model list to disk."""
3240
+ try:
3241
+ from utils import atomic_json_write
3242
+ cache_path = _ollama_cloud_cache_path()
3243
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
3244
+ atomic_json_write(cache_path, {"models": models, "cached_at": time.time()}, indent=None)
3245
+ except Exception:
3246
+ pass
3247
+
3248
+
3249
+ def fetch_ollama_cloud_models(
3250
+ api_key: Optional[str] = None,
3251
+ base_url: Optional[str] = None,
3252
+ *,
3253
+ force_refresh: bool = False,
3254
+ ) -> list[str]:
3255
+ """Fetch Ollama Cloud models by merging live API + models.dev, with disk cache.
3256
+
3257
+ Resolution order:
3258
+ 1. Disk cache (if fresh, < 1 hour, and not force_refresh)
3259
+ 2. Live ``/v1/models`` endpoint (primary — freshest source)
3260
+ 3. models.dev registry (secondary — fills gaps for unlisted models)
3261
+ 4. Merge: live models first, then models.dev additions (deduped)
3262
+
3263
+ Returns a list of model IDs (never None — empty list on total failure).
3264
+ """
3265
+ # 1. Check disk cache
3266
+ if not force_refresh:
3267
+ cached = _load_ollama_cloud_cache()
3268
+ if cached is not None:
3269
+ return cached["models"]
3270
+
3271
+ # 2. Live API probe
3272
+ if not api_key:
3273
+ api_key = os.getenv("OLLAMA_API_KEY", "")
3274
+ if not base_url:
3275
+ base_url = os.getenv("OLLAMA_BASE_URL", "") or "https://ollama.com/v1"
3276
+
3277
+ live_models: list[str] = []
3278
+ if api_key:
3279
+ result = fetch_api_models(api_key, base_url, timeout=8.0)
3280
+ if result:
3281
+ live_models = result
3282
+
3283
+ # 3. models.dev registry
3284
+ mdev_models: list[str] = []
3285
+ try:
3286
+ from agent.models_dev import list_agentic_models
3287
+ mdev_models = list_agentic_models("ollama-cloud")
3288
+ except Exception:
3289
+ pass
3290
+
3291
+ # 4. Merge: live first, then models.dev additions (deduped, order-preserving)
3292
+ if live_models or mdev_models:
3293
+ seen: set[str] = set()
3294
+ merged: list[str] = []
3295
+ for m in live_models:
3296
+ if m and m not in seen:
3297
+ seen.add(m)
3298
+ merged.append(m)
3299
+ for m in mdev_models:
3300
+ normalized = _strip_ollama_cloud_suffix(m)
3301
+ if normalized and normalized not in seen:
3302
+ seen.add(normalized)
3303
+ merged.append(normalized)
3304
+ if merged:
3305
+ _save_ollama_cloud_cache(merged)
3306
+ return merged
3307
+
3308
+ # Total failure — return stale cache if available (ignore TTL)
3309
+ stale = _load_ollama_cloud_cache(ignore_ttl=True)
3310
+ if stale is not None:
3311
+ return stale["models"]
3312
+
3313
+ return []
3314
+
3315
+
3316
+ def validate_requested_model(
3317
+ model_name: str,
3318
+ provider: Optional[str],
3319
+ *,
3320
+ api_key: Optional[str] = None,
3321
+ base_url: Optional[str] = None,
3322
+ api_mode: Optional[str] = None,
3323
+ ) -> dict[str, Any]:
3324
+ """
3325
+ Validate a ``/model`` value for the active provider.
3326
+
3327
+ Performs format checks first, then probes the live API to confirm
3328
+ the model actually exists.
3329
+
3330
+ Returns a dict with:
3331
+ - accepted: whether the CLI should switch to the requested model now
3332
+ - persist: whether it is safe to save to config
3333
+ - recognized: whether it matched a known provider catalog
3334
+ - message: optional warning / guidance for the user
3335
+ """
3336
+ requested = (model_name or "").strip()
3337
+ normalized = normalize_provider(provider)
3338
+ if normalized == "openrouter" and base_url and "openrouter.ai" not in base_url:
3339
+ normalized = "custom"
3340
+ requested_for_lookup = requested
3341
+ if normalized == "copilot":
3342
+ requested_for_lookup = normalize_copilot_model_id(
3343
+ requested,
3344
+ api_key=api_key,
3345
+ ) or requested
3346
+
3347
+ if not requested:
3348
+ return {
3349
+ "accepted": False,
3350
+ "persist": False,
3351
+ "recognized": False,
3352
+ "message": "Model name cannot be empty.",
3353
+ }
3354
+
3355
+ if any(ch.isspace() for ch in requested):
3356
+ return {
3357
+ "accepted": False,
3358
+ "persist": False,
3359
+ "recognized": False,
3360
+ "message": "Model names cannot contain spaces.",
3361
+ }
3362
+
3363
+ if normalized == "lmstudio":
3364
+ from hermes_cli.auth import AuthError
3365
+ # Use probe_lmstudio_models so we can distinguish None (unreachable
3366
+ # / malformed response) from [] (reachable, but no chat-capable models
3367
+ # are loaded). fetch_lmstudio_models collapses both to [].
3368
+ try:
3369
+ models = probe_lmstudio_models(api_key=api_key, base_url=base_url)
3370
+ except AuthError as exc:
3371
+ return {
3372
+ "accepted": False, "persist": False, "recognized": False,
3373
+ "message": (
3374
+ f"{exc} Set `LM_API_KEY` (or update it) to match the server's bearer token."
3375
+ ),
3376
+ }
3377
+ if models is None:
3378
+ return {
3379
+ "accepted": False, "persist": False, "recognized": False,
3380
+ "message": f"Could not reach LM Studio's `/api/v1/models` to validate `{requested}`.",
3381
+ }
3382
+ if not models:
3383
+ return {
3384
+ "accepted": False, "persist": False, "recognized": False,
3385
+ "message": (
3386
+ f"LM Studio is reachable but no chat-capable models are loaded. "
3387
+ f"Load `{requested}` in LM Studio (Developer tab → Load Model) and try again."
3388
+ ),
3389
+ }
3390
+ if requested_for_lookup in set(models):
3391
+ return {"accepted": True, "persist": True, "recognized": True, "message": None}
3392
+ return {
3393
+ "accepted": False, "persist": False, "recognized": False,
3394
+ "message": f"Model `{requested}` was not found in LM Studio's model listing.",
3395
+ }
3396
+
3397
+ if normalized == "custom" or normalized.startswith("custom:"):
3398
+ # Try probing with correct auth for the api_mode.
3399
+ if api_mode == "anthropic_messages":
3400
+ probe = probe_api_models(api_key, base_url, api_mode=api_mode)
3401
+ else:
3402
+ probe = probe_api_models(api_key, base_url)
3403
+ api_models = probe.get("models")
3404
+ if api_models is not None:
3405
+ if requested_for_lookup in set(api_models):
3406
+ return {
3407
+ "accepted": True,
3408
+ "persist": True,
3409
+ "recognized": True,
3410
+ "message": None,
3411
+ }
3412
+
3413
+ # Auto-correct if the top match is very similar (e.g. typo)
3414
+ auto = get_close_matches(requested_for_lookup, api_models, n=1, cutoff=0.9)
3415
+ if auto:
3416
+ return {
3417
+ "accepted": True,
3418
+ "persist": True,
3419
+ "recognized": True,
3420
+ "corrected_model": auto[0],
3421
+ "message": f"Auto-corrected `{requested}` → `{auto[0]}`",
3422
+ }
3423
+
3424
+ suggestions = get_close_matches(requested, api_models, n=3, cutoff=0.5)
3425
+ suggestion_text = ""
3426
+ if suggestions:
3427
+ suggestion_text = "\n Similar models: " + ", ".join(f"`{s}`" for s in suggestions)
3428
+
3429
+ message = (
3430
+ f"Note: `{requested}` was not found in this custom endpoint's model listing "
3431
+ f"({probe.get('probed_url')}). It may still work if the server supports hidden or aliased models."
3432
+ f"{suggestion_text}"
3433
+ )
3434
+ if probe.get("used_fallback"):
3435
+ message += (
3436
+ f"\n Endpoint verification succeeded after trying `{probe.get('resolved_base_url')}`. "
3437
+ f"Consider saving that as your base URL."
3438
+ )
3439
+
3440
+ return {
3441
+ "accepted": True,
3442
+ "persist": True,
3443
+ "recognized": False,
3444
+ "message": message,
3445
+ }
3446
+
3447
+ message = (
3448
+ f"Note: could not reach this custom endpoint's model listing at `{probe.get('probed_url')}`. "
3449
+ f"Hermes will still save `{requested}`, but the endpoint should expose `/models` for verification."
3450
+ )
3451
+ if api_mode == "anthropic_messages":
3452
+ message += (
3453
+ "\n Many Anthropic-compatible proxies do not implement the Models API "
3454
+ "(GET /v1/models). The model name has been accepted without verification."
3455
+ )
3456
+ if probe.get("suggested_base_url"):
3457
+ message += f"\n If this server expects `/v1`, try base URL: `{probe.get('suggested_base_url')}`"
3458
+
3459
+ return {
3460
+ "accepted": api_mode == "anthropic_messages",
3461
+ "persist": True,
3462
+ "recognized": False,
3463
+ "message": message,
3464
+ }
3465
+
3466
+ # Providers with non-standard catalog validation — /v1/models probing is not the right path.
3467
+ if normalized in {"openai-codex", "xai-oauth"}:
3468
+ try:
3469
+ catalog_models = provider_model_ids(normalized)
3470
+ except Exception:
3471
+ catalog_models = []
3472
+ if catalog_models:
3473
+ if requested_for_lookup in set(catalog_models):
3474
+ return {
3475
+ "accepted": True,
3476
+ "persist": True,
3477
+ "recognized": True,
3478
+ "message": None,
3479
+ }
3480
+ # Auto-correct if the top match is very similar (e.g. typo)
3481
+ auto = get_close_matches(requested_for_lookup, catalog_models, n=1, cutoff=0.9)
3482
+ if auto:
3483
+ return {
3484
+ "accepted": True,
3485
+ "persist": True,
3486
+ "recognized": True,
3487
+ "corrected_model": auto[0],
3488
+ "message": f"Auto-corrected `{requested}` → `{auto[0]}`",
3489
+ }
3490
+ suggestions = get_close_matches(requested_for_lookup, catalog_models, n=3, cutoff=0.5)
3491
+ suggestion_text = ""
3492
+ if suggestions:
3493
+ suggestion_text = "\n Similar models: " + ", ".join(f"`{s}`" for s in suggestions)
3494
+ provider_label = "OpenAI Codex" if normalized == "openai-codex" else "xAI Grok OAuth (SuperGrok Subscription)"
3495
+ return {
3496
+ "accepted": True,
3497
+ "persist": True,
3498
+ "recognized": False,
3499
+ "message": (
3500
+ f"Note: `{requested}` was not found in the {provider_label} model listing. "
3501
+ "It may still work if your account has access to a newer or hidden model ID."
3502
+ f"{suggestion_text}"
3503
+ ),
3504
+ }
3505
+
3506
+ # MiniMax providers don't expose a /models endpoint — validate against
3507
+ # the static catalog instead, similar to openai-codex.
3508
+ if normalized in {"minimax", "minimax-cn"}:
3509
+ try:
3510
+ catalog_models = provider_model_ids(normalized)
3511
+ except Exception:
3512
+ catalog_models = []
3513
+ if catalog_models:
3514
+ # Case-insensitive lookup (catalog uses mixed case like MiniMax-M2.7)
3515
+ catalog_lower = {m.lower(): m for m in catalog_models}
3516
+ if requested_for_lookup.lower() in catalog_lower:
3517
+ return {
3518
+ "accepted": True,
3519
+ "persist": True,
3520
+ "recognized": True,
3521
+ "message": None,
3522
+ }
3523
+ # Auto-correct close matches (case-insensitive)
3524
+ catalog_lower_list = list(catalog_lower.keys())
3525
+ auto = get_close_matches(requested_for_lookup.lower(), catalog_lower_list, n=1, cutoff=0.9)
3526
+ if auto:
3527
+ corrected = catalog_lower[auto[0]]
3528
+ return {
3529
+ "accepted": True,
3530
+ "persist": True,
3531
+ "recognized": True,
3532
+ "corrected_model": corrected,
3533
+ "message": f"Auto-corrected `{requested}` → `{corrected}`",
3534
+ }
3535
+ suggestions = get_close_matches(requested_for_lookup.lower(), catalog_lower_list, n=3, cutoff=0.5)
3536
+ suggestion_text = ""
3537
+ if suggestions:
3538
+ suggestion_text = "\n Similar models: " + ", ".join(f"`{catalog_lower[s]}`" for s in suggestions)
3539
+ return {
3540
+ "accepted": True,
3541
+ "persist": True,
3542
+ "recognized": False,
3543
+ "message": (
3544
+ f"Note: `{requested}` was not found in the MiniMax catalog."
3545
+ f"{suggestion_text}"
3546
+ "\n MiniMax does not expose a /models endpoint, so Hermes cannot verify the model name."
3547
+ "\n The model may still work if it exists on the server."
3548
+ ),
3549
+ }
3550
+
3551
+ # Native Anthropic provider: /v1/models requires x-api-key (or Bearer for
3552
+ # OAuth) plus anthropic-version headers. The generic OpenAI-style probe
3553
+ # below uses plain Bearer auth and 401s against Anthropic, so dispatch to
3554
+ # the native fetcher which handles both API keys and Claude-Code OAuth
3555
+ # tokens. (The api_mode=="anthropic_messages" branch below handles the
3556
+ # Messages-API transport case separately.)
3557
+ if normalized == "anthropic":
3558
+ anthropic_models = _fetch_anthropic_models()
3559
+ if anthropic_models is not None:
3560
+ if requested_for_lookup in set(anthropic_models):
3561
+ return {
3562
+ "accepted": True,
3563
+ "persist": True,
3564
+ "recognized": True,
3565
+ "message": None,
3566
+ }
3567
+ auto = get_close_matches(requested_for_lookup, anthropic_models, n=1, cutoff=0.9)
3568
+ if auto:
3569
+ return {
3570
+ "accepted": True,
3571
+ "persist": True,
3572
+ "recognized": True,
3573
+ "corrected_model": auto[0],
3574
+ "message": f"Auto-corrected `{requested}` → `{auto[0]}`",
3575
+ }
3576
+ suggestions = get_close_matches(requested, anthropic_models, n=3, cutoff=0.5)
3577
+ suggestion_text = ""
3578
+ if suggestions:
3579
+ suggestion_text = "\n Similar models: " + ", ".join(f"`{s}`" for s in suggestions)
3580
+ # Accept anyway — Anthropic sometimes gates newer/preview models
3581
+ # (e.g. snapshot IDs, early-access releases) behind accounts
3582
+ # even though they aren't listed on /v1/models.
3583
+ return {
3584
+ "accepted": True,
3585
+ "persist": True,
3586
+ "recognized": False,
3587
+ "message": (
3588
+ f"Note: `{requested}` was not found in Anthropic's /v1/models listing. "
3589
+ f"It may still work if you have early-access or snapshot IDs."
3590
+ f"{suggestion_text}"
3591
+ ),
3592
+ }
3593
+ # _fetch_anthropic_models returned None — no token resolvable or
3594
+ # network failure. Fall through to the generic warning below.
3595
+
3596
+ # Anthropic Messages API: many proxies don't implement /v1/models.
3597
+ # Try probing with correct auth; if it fails, accept with a warning.
3598
+ if api_mode == "anthropic_messages":
3599
+ api_models = fetch_api_models(api_key, base_url, api_mode=api_mode)
3600
+ if api_models is not None:
3601
+ if requested_for_lookup in set(api_models):
3602
+ return {
3603
+ "accepted": True,
3604
+ "persist": True,
3605
+ "recognized": True,
3606
+ "message": None,
3607
+ }
3608
+ auto = get_close_matches(requested_for_lookup, api_models, n=1, cutoff=0.9)
3609
+ if auto:
3610
+ return {
3611
+ "accepted": True,
3612
+ "persist": True,
3613
+ "recognized": True,
3614
+ "corrected_model": auto[0],
3615
+ "message": f"Auto-corrected `{requested}` → `{auto[0]}`",
3616
+ }
3617
+ # Probe failed or model not found — accept anyway (proxy likely
3618
+ # doesn't implement the Anthropic Models API).
3619
+ return {
3620
+ "accepted": True,
3621
+ "persist": True,
3622
+ "recognized": False,
3623
+ "message": (
3624
+ f"Note: could not verify `{requested}` against this endpoint's "
3625
+ f"model listing. Many Anthropic-compatible proxies do not "
3626
+ f"implement GET /v1/models. The model name has been accepted "
3627
+ f"without verification."
3628
+ ),
3629
+ }
3630
+
3631
+ # Probe the live API to check if the model actually exists
3632
+ api_models = fetch_api_models(api_key, base_url)
3633
+
3634
+ if api_models is not None:
3635
+ # Gemini's OpenAI-compat /v1beta/openai/models endpoint returns IDs
3636
+ # prefixed with "models/" (e.g. "models/gemini-2.5-flash") — native
3637
+ # Gemini-API convention. Our curated list and user input both use
3638
+ # the bare ID, so a direct set-membership check drops every known
3639
+ # Gemini model. Strip the prefix before comparison. See #12532.
3640
+ if normalized == "gemini":
3641
+ api_models = [
3642
+ m[len("models/"):] if isinstance(m, str) and m.startswith("models/") else m
3643
+ for m in api_models
3644
+ ]
3645
+ if requested_for_lookup in set(api_models):
3646
+ # API confirmed the model exists
3647
+ return {
3648
+ "accepted": True,
3649
+ "persist": True,
3650
+ "recognized": True,
3651
+ "message": None,
3652
+ }
3653
+ else:
3654
+ # API responded but model is not listed. Accept anyway —
3655
+ # the user may have access to models not shown in the public
3656
+ # listing (e.g. Z.AI Pro/Max plans can use glm-5 on coding
3657
+ # endpoints even though it's not in /models). Warn but allow.
3658
+
3659
+ # Auto-correct if the top match is very similar (e.g. typo)
3660
+ auto = get_close_matches(requested_for_lookup, api_models, n=1, cutoff=0.9)
3661
+ if auto:
3662
+ return {
3663
+ "accepted": True,
3664
+ "persist": True,
3665
+ "recognized": True,
3666
+ "corrected_model": auto[0],
3667
+ "message": f"Auto-corrected `{requested}` → `{auto[0]}`",
3668
+ }
3669
+
3670
+ suggestions = get_close_matches(requested, api_models, n=3, cutoff=0.5)
3671
+ suggestion_text = ""
3672
+ if suggestions:
3673
+ suggestion_text = "\n Similar models: " + ", ".join(f"`{s}`" for s in suggestions)
3674
+
3675
+ return {
3676
+ "accepted": False,
3677
+ "persist": False,
3678
+ "recognized": False,
3679
+ "message": (
3680
+ f"Model `{requested}` was not found in this provider's model listing."
3681
+ f"{suggestion_text}"
3682
+ ),
3683
+ }
3684
+
3685
+ # api_models is None — couldn't reach API. Accept and persist,
3686
+ # but warn so typos don't silently break things.
3687
+
3688
+ # Bedrock: use our own discovery instead of HTTP /models endpoint.
3689
+ # Bedrock's bedrock-runtime URL doesn't support /models — it uses the
3690
+ # AWS SDK control plane (ListFoundationModels + ListInferenceProfiles).
3691
+ if normalized == "bedrock":
3692
+ try:
3693
+ from agent.bedrock_adapter import discover_bedrock_models, resolve_bedrock_region
3694
+ region = resolve_bedrock_region()
3695
+ discovered = discover_bedrock_models(region)
3696
+ discovered_ids = {m["id"] for m in discovered}
3697
+ if requested in discovered_ids:
3698
+ return {
3699
+ "accepted": True,
3700
+ "persist": True,
3701
+ "recognized": True,
3702
+ "message": None,
3703
+ }
3704
+ # Not in discovered list — still accept (user may have custom
3705
+ # inference profiles or cross-account access), but warn.
3706
+ suggestions = get_close_matches(requested, list(discovered_ids), n=3, cutoff=0.4)
3707
+ suggestion_text = ""
3708
+ if suggestions:
3709
+ suggestion_text = "\n Similar models: " + ", ".join(f"`{s}`" for s in suggestions)
3710
+ return {
3711
+ "accepted": True,
3712
+ "persist": True,
3713
+ "recognized": False,
3714
+ "message": (
3715
+ f"Note: `{requested}` was not found in Bedrock model discovery for {region}. "
3716
+ f"It may still work with custom inference profiles or cross-account access."
3717
+ f"{suggestion_text}"
3718
+ ),
3719
+ }
3720
+ except Exception:
3721
+ pass # Fall through to generic warning
3722
+
3723
+ # Static-catalog fallback: when the /models probe was unreachable,
3724
+ # validate against the curated list from provider_model_ids() — same
3725
+ # pattern as the openai-codex and minimax branches above. This keeps
3726
+ # /model switches working in the gateway for providers whose /models
3727
+ # endpoint is temporarily unreachable or returns a non-JSON payload.
3728
+ # Without this block, validate_requested_model would reject every model
3729
+ # on such providers, switch_model() would return success=False, and
3730
+ # the gateway would never write to _session_model_overrides.
3731
+ provider_label = _PROVIDER_LABELS.get(normalized, normalized)
3732
+ try:
3733
+ catalog_models = provider_model_ids(normalized)
3734
+ except Exception:
3735
+ catalog_models = []
3736
+
3737
+ if catalog_models:
3738
+ catalog_lower = {m.lower(): m for m in catalog_models}
3739
+ if requested_for_lookup.lower() in catalog_lower:
3740
+ return {
3741
+ "accepted": True,
3742
+ "persist": True,
3743
+ "recognized": True,
3744
+ "message": None,
3745
+ }
3746
+ catalog_lower_list = list(catalog_lower.keys())
3747
+ auto = get_close_matches(
3748
+ requested_for_lookup.lower(), catalog_lower_list, n=1, cutoff=0.9
3749
+ )
3750
+ if auto:
3751
+ corrected = catalog_lower[auto[0]]
3752
+ return {
3753
+ "accepted": True,
3754
+ "persist": True,
3755
+ "recognized": True,
3756
+ "corrected_model": corrected,
3757
+ "message": f"Auto-corrected `{requested}` → `{corrected}`",
3758
+ }
3759
+ suggestions = get_close_matches(
3760
+ requested_for_lookup.lower(), catalog_lower_list, n=3, cutoff=0.5
3761
+ )
3762
+ suggestion_text = ""
3763
+ if suggestions:
3764
+ suggestion_text = "\n Similar models: " + ", ".join(
3765
+ f"`{catalog_lower[s]}`" for s in suggestions
3766
+ )
3767
+ return {
3768
+ "accepted": True,
3769
+ "persist": True,
3770
+ "recognized": False,
3771
+ "message": (
3772
+ f"Note: `{requested}` was not found in the {provider_label} curated catalog "
3773
+ f"and the /models endpoint was unreachable.{suggestion_text}"
3774
+ f"\n The model may still work if it exists on the provider."
3775
+ ),
3776
+ }
3777
+
3778
+ # No catalog available — accept with a warning, matching the comment's
3779
+ # stated intent ("Accept and persist, but warn").
3780
+ return {
3781
+ "accepted": True,
3782
+ "persist": True,
3783
+ "recognized": False,
3784
+ "message": (
3785
+ f"Note: could not reach the {provider_label} API to validate `{requested}`. "
3786
+ f"If the service isn't down, this model may not be valid."
3787
+ ),
3788
+ }
3789
+