hud-python 0.4.45__py3-none-any.whl → 0.5.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. hud/__init__.py +27 -7
  2. hud/agents/__init__.py +70 -5
  3. hud/agents/base.py +238 -500
  4. hud/agents/claude.py +236 -247
  5. hud/agents/gateway.py +42 -0
  6. hud/agents/gemini.py +264 -0
  7. hud/agents/gemini_cua.py +324 -0
  8. hud/agents/grounded_openai.py +98 -100
  9. hud/agents/misc/integration_test_agent.py +51 -20
  10. hud/agents/misc/response_agent.py +48 -36
  11. hud/agents/openai.py +282 -296
  12. hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
  13. hud/agents/operator.py +199 -0
  14. hud/agents/resolver.py +70 -0
  15. hud/agents/tests/conftest.py +133 -0
  16. hud/agents/tests/test_base.py +300 -622
  17. hud/agents/tests/test_base_runtime.py +233 -0
  18. hud/agents/tests/test_claude.py +381 -214
  19. hud/agents/tests/test_client.py +9 -10
  20. hud/agents/tests/test_gemini.py +369 -0
  21. hud/agents/tests/test_grounded_openai_agent.py +65 -50
  22. hud/agents/tests/test_openai.py +377 -140
  23. hud/agents/tests/test_operator.py +362 -0
  24. hud/agents/tests/test_resolver.py +192 -0
  25. hud/agents/tests/test_run_eval.py +179 -0
  26. hud/agents/types.py +148 -0
  27. hud/cli/__init__.py +493 -546
  28. hud/cli/analyze.py +43 -5
  29. hud/cli/build.py +699 -113
  30. hud/cli/debug.py +8 -5
  31. hud/cli/dev.py +889 -732
  32. hud/cli/eval.py +793 -667
  33. hud/cli/flows/dev.py +167 -0
  34. hud/cli/flows/init.py +191 -0
  35. hud/cli/flows/tasks.py +153 -56
  36. hud/cli/flows/templates.py +151 -0
  37. hud/cli/flows/tests/__init__.py +1 -0
  38. hud/cli/flows/tests/test_dev.py +126 -0
  39. hud/cli/init.py +60 -58
  40. hud/cli/pull.py +1 -1
  41. hud/cli/push.py +38 -13
  42. hud/cli/rft.py +311 -0
  43. hud/cli/rft_status.py +145 -0
  44. hud/cli/tests/test_analyze.py +5 -5
  45. hud/cli/tests/test_analyze_metadata.py +3 -2
  46. hud/cli/tests/test_analyze_module.py +120 -0
  47. hud/cli/tests/test_build.py +110 -8
  48. hud/cli/tests/test_build_failure.py +41 -0
  49. hud/cli/tests/test_build_module.py +50 -0
  50. hud/cli/tests/test_cli_init.py +6 -1
  51. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  52. hud/cli/tests/test_cli_root.py +140 -0
  53. hud/cli/tests/test_convert.py +361 -0
  54. hud/cli/tests/test_debug.py +12 -10
  55. hud/cli/tests/test_dev.py +197 -0
  56. hud/cli/tests/test_eval.py +251 -0
  57. hud/cli/tests/test_eval_bedrock.py +51 -0
  58. hud/cli/tests/test_init.py +124 -0
  59. hud/cli/tests/test_main_module.py +11 -5
  60. hud/cli/tests/test_mcp_server.py +12 -100
  61. hud/cli/tests/test_push.py +1 -1
  62. hud/cli/tests/test_push_happy.py +74 -0
  63. hud/cli/tests/test_push_wrapper.py +23 -0
  64. hud/cli/tests/test_registry.py +1 -1
  65. hud/cli/tests/test_utils.py +1 -1
  66. hud/cli/{rl → utils}/celebrate.py +14 -12
  67. hud/cli/utils/config.py +18 -1
  68. hud/cli/utils/docker.py +130 -4
  69. hud/cli/utils/env_check.py +9 -9
  70. hud/cli/utils/git.py +136 -0
  71. hud/cli/utils/interactive.py +39 -5
  72. hud/cli/utils/metadata.py +70 -1
  73. hud/cli/utils/runner.py +1 -1
  74. hud/cli/utils/server.py +2 -2
  75. hud/cli/utils/source_hash.py +3 -3
  76. hud/cli/utils/tasks.py +4 -1
  77. hud/cli/utils/tests/__init__.py +0 -0
  78. hud/cli/utils/tests/test_config.py +58 -0
  79. hud/cli/utils/tests/test_docker.py +93 -0
  80. hud/cli/utils/tests/test_docker_hints.py +71 -0
  81. hud/cli/utils/tests/test_env_check.py +74 -0
  82. hud/cli/utils/tests/test_environment.py +42 -0
  83. hud/cli/utils/tests/test_git.py +142 -0
  84. hud/cli/utils/tests/test_interactive_module.py +60 -0
  85. hud/cli/utils/tests/test_local_runner.py +50 -0
  86. hud/cli/utils/tests/test_logging_utils.py +23 -0
  87. hud/cli/utils/tests/test_metadata.py +49 -0
  88. hud/cli/utils/tests/test_package_runner.py +35 -0
  89. hud/cli/utils/tests/test_registry_utils.py +49 -0
  90. hud/cli/utils/tests/test_remote_runner.py +25 -0
  91. hud/cli/utils/tests/test_runner_modules.py +52 -0
  92. hud/cli/utils/tests/test_source_hash.py +36 -0
  93. hud/cli/utils/tests/test_tasks.py +80 -0
  94. hud/cli/utils/version_check.py +258 -0
  95. hud/cli/{rl → utils}/viewer.py +2 -2
  96. hud/clients/README.md +12 -11
  97. hud/clients/__init__.py +4 -3
  98. hud/clients/base.py +166 -26
  99. hud/clients/environment.py +51 -0
  100. hud/clients/fastmcp.py +13 -6
  101. hud/clients/mcp_use.py +45 -15
  102. hud/clients/tests/test_analyze_scenarios.py +206 -0
  103. hud/clients/tests/test_protocol.py +9 -3
  104. hud/datasets/__init__.py +23 -20
  105. hud/datasets/loader.py +326 -0
  106. hud/datasets/runner.py +198 -105
  107. hud/datasets/tests/__init__.py +0 -0
  108. hud/datasets/tests/test_loader.py +221 -0
  109. hud/datasets/tests/test_utils.py +315 -0
  110. hud/datasets/utils.py +270 -90
  111. hud/environment/__init__.py +52 -0
  112. hud/environment/connection.py +258 -0
  113. hud/environment/connectors/__init__.py +33 -0
  114. hud/environment/connectors/base.py +68 -0
  115. hud/environment/connectors/local.py +177 -0
  116. hud/environment/connectors/mcp_config.py +137 -0
  117. hud/environment/connectors/openai.py +101 -0
  118. hud/environment/connectors/remote.py +172 -0
  119. hud/environment/environment.py +835 -0
  120. hud/environment/integrations/__init__.py +45 -0
  121. hud/environment/integrations/adk.py +67 -0
  122. hud/environment/integrations/anthropic.py +196 -0
  123. hud/environment/integrations/gemini.py +92 -0
  124. hud/environment/integrations/langchain.py +82 -0
  125. hud/environment/integrations/llamaindex.py +68 -0
  126. hud/environment/integrations/openai.py +238 -0
  127. hud/environment/mock.py +306 -0
  128. hud/environment/router.py +263 -0
  129. hud/environment/scenarios.py +620 -0
  130. hud/environment/tests/__init__.py +1 -0
  131. hud/environment/tests/test_connection.py +317 -0
  132. hud/environment/tests/test_connectors.py +205 -0
  133. hud/environment/tests/test_environment.py +593 -0
  134. hud/environment/tests/test_integrations.py +257 -0
  135. hud/environment/tests/test_local_connectors.py +242 -0
  136. hud/environment/tests/test_scenarios.py +1086 -0
  137. hud/environment/tests/test_tools.py +208 -0
  138. hud/environment/types.py +23 -0
  139. hud/environment/utils/__init__.py +35 -0
  140. hud/environment/utils/formats.py +215 -0
  141. hud/environment/utils/schema.py +171 -0
  142. hud/environment/utils/tool_wrappers.py +113 -0
  143. hud/eval/__init__.py +67 -0
  144. hud/eval/context.py +727 -0
  145. hud/eval/display.py +299 -0
  146. hud/eval/instrument.py +187 -0
  147. hud/eval/manager.py +533 -0
  148. hud/eval/parallel.py +268 -0
  149. hud/eval/task.py +372 -0
  150. hud/eval/tests/__init__.py +1 -0
  151. hud/eval/tests/test_context.py +178 -0
  152. hud/eval/tests/test_eval.py +210 -0
  153. hud/eval/tests/test_manager.py +152 -0
  154. hud/eval/tests/test_parallel.py +168 -0
  155. hud/eval/tests/test_task.py +291 -0
  156. hud/eval/types.py +65 -0
  157. hud/eval/utils.py +194 -0
  158. hud/patches/__init__.py +19 -0
  159. hud/patches/mcp_patches.py +308 -0
  160. hud/patches/warnings.py +54 -0
  161. hud/samples/browser.py +4 -4
  162. hud/server/__init__.py +2 -1
  163. hud/server/low_level.py +2 -1
  164. hud/server/router.py +164 -0
  165. hud/server/server.py +567 -80
  166. hud/server/tests/test_mcp_server_integration.py +11 -11
  167. hud/server/tests/test_mcp_server_more.py +1 -1
  168. hud/server/tests/test_server_extra.py +2 -0
  169. hud/settings.py +45 -3
  170. hud/shared/exceptions.py +36 -10
  171. hud/shared/hints.py +26 -1
  172. hud/shared/requests.py +15 -3
  173. hud/shared/tests/test_exceptions.py +40 -31
  174. hud/shared/tests/test_hints.py +167 -0
  175. hud/telemetry/__init__.py +20 -19
  176. hud/telemetry/exporter.py +201 -0
  177. hud/telemetry/instrument.py +165 -253
  178. hud/telemetry/tests/test_eval_telemetry.py +356 -0
  179. hud/telemetry/tests/test_exporter.py +258 -0
  180. hud/telemetry/tests/test_instrument.py +401 -0
  181. hud/tools/__init__.py +18 -2
  182. hud/tools/agent.py +223 -0
  183. hud/tools/apply_patch.py +639 -0
  184. hud/tools/base.py +54 -4
  185. hud/tools/bash.py +2 -2
  186. hud/tools/computer/__init__.py +36 -3
  187. hud/tools/computer/anthropic.py +2 -2
  188. hud/tools/computer/gemini.py +385 -0
  189. hud/tools/computer/hud.py +23 -6
  190. hud/tools/computer/openai.py +20 -21
  191. hud/tools/computer/qwen.py +434 -0
  192. hud/tools/computer/settings.py +37 -0
  193. hud/tools/edit.py +3 -7
  194. hud/tools/executors/base.py +4 -2
  195. hud/tools/executors/pyautogui.py +1 -1
  196. hud/tools/grounding/grounded_tool.py +13 -18
  197. hud/tools/grounding/grounder.py +10 -31
  198. hud/tools/grounding/tests/test_grounded_tool.py +26 -44
  199. hud/tools/jupyter.py +330 -0
  200. hud/tools/playwright.py +18 -3
  201. hud/tools/shell.py +308 -0
  202. hud/tools/tests/test_agent_tool.py +355 -0
  203. hud/tools/tests/test_apply_patch.py +718 -0
  204. hud/tools/tests/test_computer.py +4 -9
  205. hud/tools/tests/test_computer_actions.py +24 -2
  206. hud/tools/tests/test_jupyter_tool.py +181 -0
  207. hud/tools/tests/test_shell.py +596 -0
  208. hud/tools/tests/test_submit.py +85 -0
  209. hud/tools/tests/test_types.py +193 -0
  210. hud/tools/types.py +21 -1
  211. hud/types.py +194 -56
  212. hud/utils/__init__.py +2 -0
  213. hud/utils/env.py +67 -0
  214. hud/utils/hud_console.py +89 -18
  215. hud/utils/mcp.py +15 -58
  216. hud/utils/strict_schema.py +162 -0
  217. hud/utils/tests/test_init.py +1 -2
  218. hud/utils/tests/test_mcp.py +1 -28
  219. hud/utils/tests/test_pretty_errors.py +186 -0
  220. hud/utils/tests/test_tool_shorthand.py +154 -0
  221. hud/utils/tests/test_version.py +1 -1
  222. hud/utils/types.py +20 -0
  223. hud/version.py +1 -1
  224. hud_python-0.5.13.dist-info/METADATA +264 -0
  225. hud_python-0.5.13.dist-info/RECORD +305 -0
  226. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
  227. hud/agents/langchain.py +0 -261
  228. hud/agents/lite_llm.py +0 -72
  229. hud/cli/rl/__init__.py +0 -180
  230. hud/cli/rl/config.py +0 -101
  231. hud/cli/rl/display.py +0 -133
  232. hud/cli/rl/gpu.py +0 -63
  233. hud/cli/rl/gpu_utils.py +0 -321
  234. hud/cli/rl/local_runner.py +0 -595
  235. hud/cli/rl/presets.py +0 -96
  236. hud/cli/rl/remote_runner.py +0 -463
  237. hud/cli/rl/rl_api.py +0 -150
  238. hud/cli/rl/vllm.py +0 -177
  239. hud/cli/rl/wait_utils.py +0 -89
  240. hud/datasets/parallel.py +0 -687
  241. hud/misc/__init__.py +0 -1
  242. hud/misc/claude_plays_pokemon.py +0 -292
  243. hud/otel/__init__.py +0 -35
  244. hud/otel/collector.py +0 -142
  245. hud/otel/config.py +0 -181
  246. hud/otel/context.py +0 -570
  247. hud/otel/exporters.py +0 -369
  248. hud/otel/instrumentation.py +0 -135
  249. hud/otel/processors.py +0 -121
  250. hud/otel/tests/__init__.py +0 -1
  251. hud/otel/tests/test_processors.py +0 -197
  252. hud/rl/README.md +0 -30
  253. hud/rl/__init__.py +0 -1
  254. hud/rl/actor.py +0 -176
  255. hud/rl/buffer.py +0 -405
  256. hud/rl/chat_template.jinja +0 -101
  257. hud/rl/config.py +0 -192
  258. hud/rl/distributed.py +0 -132
  259. hud/rl/learner.py +0 -637
  260. hud/rl/tests/__init__.py +0 -1
  261. hud/rl/tests/test_learner.py +0 -186
  262. hud/rl/train.py +0 -382
  263. hud/rl/types.py +0 -101
  264. hud/rl/utils/start_vllm_server.sh +0 -30
  265. hud/rl/utils.py +0 -524
  266. hud/rl/vllm_adapter.py +0 -143
  267. hud/telemetry/job.py +0 -352
  268. hud/telemetry/replay.py +0 -74
  269. hud/telemetry/tests/test_replay.py +0 -40
  270. hud/telemetry/tests/test_trace.py +0 -63
  271. hud/telemetry/trace.py +0 -158
  272. hud/utils/agent_factories.py +0 -86
  273. hud/utils/async_utils.py +0 -65
  274. hud/utils/group_eval.py +0 -223
  275. hud/utils/progress.py +0 -149
  276. hud/utils/tasks.py +0 -127
  277. hud/utils/tests/test_async_utils.py +0 -173
  278. hud/utils/tests/test_progress.py +0 -261
  279. hud_python-0.4.45.dist-info/METADATA +0 -552
  280. hud_python-0.4.45.dist-info/RECORD +0 -228
  281. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
  282. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/licenses/LICENSE +0 -0
@@ -84,7 +84,7 @@ async def test_initialize_runs_once_and_tools_work() -> None:
84
84
 
85
85
  async def connect_and_check() -> None:
86
86
  cfg = {"srv": {"url": f"http://127.0.0.1:{port}/mcp"}}
87
- client = MCPClient(mcp_config=cfg, auto_trace=False, verbose=False)
87
+ client = MCPClient(mcp_config=cfg, verbose=False)
88
88
  await client.initialize()
89
89
  tools = await client.list_tools()
90
90
  names = sorted(t.name for t in tools)
@@ -123,7 +123,7 @@ async def test_shutdown_handler_only_on_sigterm_flag() -> None:
123
123
  try:
124
124
  # sanity connect so lifespan actually ran
125
125
  cfg = {"srv": {"url": f"http://127.0.0.1:{port}/mcp"}}
126
- c = MCPClient(mcp_config=cfg, auto_trace=False, verbose=False)
126
+ c = MCPClient(mcp_config=cfg, verbose=False)
127
127
  await c.initialize()
128
128
  await c.shutdown()
129
129
  finally:
@@ -140,7 +140,7 @@ async def test_shutdown_handler_only_on_sigterm_flag() -> None:
140
140
  server_task2 = await _start_http_server(mcp, port=port2)
141
141
  try:
142
142
  cfg = {"srv": {"url": f"http://127.0.0.1:{port2}/mcp"}}
143
- c = MCPClient(mcp_config=cfg, auto_trace=False, verbose=False)
143
+ c = MCPClient(mcp_config=cfg, verbose=False)
144
144
  await c.initialize()
145
145
  await c.shutdown()
146
146
 
@@ -170,7 +170,7 @@ async def test_initializer_exception_propagates_to_client() -> None:
170
170
  server_task = await _start_http_server(mcp, port)
171
171
 
172
172
  cfg = {"srv": {"url": f"http://127.0.0.1:{port}/mcp"}}
173
- client = MCPClient(mcp_config=cfg, auto_trace=False, verbose=False)
173
+ client = MCPClient(mcp_config=cfg, verbose=False)
174
174
 
175
175
  try:
176
176
  with pytest.raises(Exception):
@@ -211,7 +211,7 @@ async def test_init_after_tools_preserves_handlers_and_runs_once() -> None:
211
211
 
212
212
  async def connect_and_check() -> None:
213
213
  cfg = {"srv": {"url": f"http://127.0.0.1:{port}/mcp"}}
214
- c = MCPClient(mcp_config=cfg, auto_trace=False, verbose=False)
214
+ c = MCPClient(mcp_config=cfg, verbose=False)
215
215
  await c.initialize()
216
216
  tools = await c.list_tools()
217
217
  names = sorted(t.name for t in tools)
@@ -244,7 +244,7 @@ async def test_tool_default_argument_used_when_omitted() -> None:
244
244
  server_task = await _start_http_server(mcp, port)
245
245
  try:
246
246
  cfg = {"srv": {"url": f"http://127.0.0.1:{port}/mcp"}}
247
- c = MCPClient(mcp_config=cfg, auto_trace=False, verbose=False)
247
+ c = MCPClient(mcp_config=cfg, verbose=False)
248
248
  await c.initialize()
249
249
  # Call with no args → default should kick in
250
250
  res = await c.call_tool(name="echo", arguments={})
@@ -273,7 +273,7 @@ async def test_shutdown_handler_runs_once_when_both_paths_fire() -> None:
273
273
  try:
274
274
  # Ensure lifespan started
275
275
  cfg = {"srv": {"url": f"http://127.0.0.1:{port}/mcp"}}
276
- c = MCPClient(mcp_config=cfg, auto_trace=False, verbose=False)
276
+ c = MCPClient(mcp_config=cfg, verbose=False)
277
277
  await c.initialize()
278
278
  await c.shutdown()
279
279
 
@@ -315,7 +315,7 @@ async def test_initialize_ctx_exposes_client_info() -> None:
315
315
  server_task = await _start_http_server(mcp, port)
316
316
  try:
317
317
  cfg = {"srv": {"url": f"http://127.0.0.1:{port}/mcp"}}
318
- c = MCPClient(mcp_config=cfg, auto_trace=False, verbose=False)
318
+ c = MCPClient(mcp_config=cfg, verbose=False)
319
319
  await c.initialize()
320
320
  await c.shutdown()
321
321
  finally:
@@ -344,7 +344,7 @@ async def test_initialize_redirects_stdout_to_stderr(capsys) -> None:
344
344
 
345
345
  try:
346
346
  cfg = {"srv": {"url": f"http://127.0.0.1:{port}/mcp"}}
347
- c = MCPClient(mcp_config=cfg, auto_trace=False, verbose=False)
347
+ c = MCPClient(mcp_config=cfg, verbose=False)
348
348
  await c.initialize()
349
349
  await c.shutdown()
350
350
  finally:
@@ -373,11 +373,11 @@ async def test_initialize_callable_form_runs_once() -> None:
373
373
  server_task = await _start_http_server(mcp, port)
374
374
  try:
375
375
  cfg = {"srv": {"url": f"http://127.0.0.1:{port}/mcp"}}
376
- c1 = MCPClient(mcp_config=cfg, auto_trace=False, verbose=False)
376
+ c1 = MCPClient(mcp_config=cfg, verbose=False)
377
377
  await c1.initialize()
378
378
  await c1.shutdown()
379
379
 
380
- c2 = MCPClient(mcp_config=cfg, auto_trace=False, verbose=False)
380
+ c2 = MCPClient(mcp_config=cfg, verbose=False)
381
381
  await c2.initialize()
382
382
  await c2.shutdown()
383
383
  finally:
@@ -142,7 +142,7 @@ async def test_last_initialize_handler_wins_and_ctx_shape_exists() -> None:
142
142
 
143
143
  try:
144
144
  cfg = {"srv": {"url": f"http://127.0.0.1:{port}/mcp"}}
145
- c = MCPClient(mcp_config=cfg, auto_trace=False, verbose=False)
145
+ c = MCPClient(mcp_config=cfg, verbose=False)
146
146
  await c.initialize()
147
147
 
148
148
  # Call a tool to ensure init didn't break anything
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import asyncio
5
+ import sys
5
6
  from contextlib import asynccontextmanager, suppress
6
7
 
7
8
  import anyio
@@ -98,6 +99,7 @@ async def test_last_shutdown_handler_wins(patch_stdio):
98
99
  server_mod._sigterm_received = False # type: ignore[attr-defined]
99
100
 
100
101
 
102
+ @pytest.mark.skipif(sys.platform == "win32", reason="asyncio.add_signal_handler is Unix-only")
101
103
  def test__run_with_sigterm_registers_handlers_when_enabled(monkeypatch: pytest.MonkeyPatch):
102
104
  """
103
105
  Verify that _run_with_sigterm attempts to register SIGTERM/SIGINT handlers
hud/settings.py CHANGED
@@ -53,23 +53,35 @@ class Settings(BaseSettings):
53
53
  )
54
54
 
55
55
  hud_telemetry_url: str = Field(
56
- default="https://telemetry.hud.so/v3/api",
56
+ default="https://telemetry.hud.ai/v3/api",
57
57
  description="Base URL for the HUD API",
58
58
  validation_alias="HUD_TELEMETRY_URL",
59
59
  )
60
60
 
61
61
  hud_mcp_url: str = Field(
62
- default="https://mcp.hud.so/v3/mcp",
62
+ default="https://mcp.hud.ai/v3/mcp",
63
63
  description="Base URL for the MCP Server",
64
64
  validation_alias="HUD_MCP_URL",
65
65
  )
66
66
 
67
67
  hud_rl_url: str = Field(
68
- default="https://rl.hud.so/v1",
68
+ default="https://rl.hud.ai/v1",
69
69
  description="Base URL for the HUD RL API server",
70
70
  validation_alias="HUD_RL_URL",
71
71
  )
72
72
 
73
+ hud_api_url: str = Field(
74
+ default="https://api.hud.ai",
75
+ description="Base URL for the HUD API server",
76
+ validation_alias="HUD_API_URL",
77
+ )
78
+
79
+ hud_gateway_url: str = Field(
80
+ default="https://inference.hud.ai",
81
+ description="Base URL for the HUD inference gateway",
82
+ validation_alias="HUD_GATEWAY_URL",
83
+ )
84
+
73
85
  api_key: str | None = Field(
74
86
  default=None,
75
87
  description="API key for authentication with the HUD API",
@@ -82,12 +94,36 @@ class Settings(BaseSettings):
82
94
  validation_alias="ANTHROPIC_API_KEY",
83
95
  )
84
96
 
97
+ aws_access_key_id: str | None = Field(
98
+ default=None,
99
+ description="AWS access key ID for Bedrock",
100
+ validation_alias="AWS_ACCESS_KEY_ID",
101
+ )
102
+
103
+ aws_secret_access_key: str | None = Field(
104
+ default=None,
105
+ description="AWS secret access key for Bedrock",
106
+ validation_alias="AWS_SECRET_ACCESS_KEY",
107
+ )
108
+
109
+ aws_region: str | None = Field(
110
+ default=None,
111
+ description="AWS region for Bedrock (e.g., us-east-1)",
112
+ validation_alias="AWS_REGION",
113
+ )
114
+
85
115
  openai_api_key: str | None = Field(
86
116
  default=None,
87
117
  description="API key for OpenAI models",
88
118
  validation_alias="OPENAI_API_KEY",
89
119
  )
90
120
 
121
+ gemini_api_key: str | None = Field(
122
+ default=None,
123
+ description="API key for Google Gemini models",
124
+ validation_alias="GEMINI_API_KEY",
125
+ )
126
+
91
127
  openrouter_api_key: str | None = Field(
92
128
  default=None,
93
129
  description="API key for OpenRouter models",
@@ -124,6 +160,12 @@ class Settings(BaseSettings):
124
160
  validation_alias="HUD_LOG_STREAM",
125
161
  )
126
162
 
163
+ client_timeout: int = Field(
164
+ default=900,
165
+ description="Timeout in seconds for MCP client operations (default: 900 = 15 minutes)",
166
+ validation_alias="HUD_CLIENT_TIMEOUT",
167
+ )
168
+
127
169
 
128
170
  # Create a singleton instance
129
171
  settings = Settings()
hud/shared/exceptions.py CHANGED
@@ -69,11 +69,6 @@ class HudException(Exception):
69
69
  elif isinstance(exc_value, Exception):
70
70
  # Try to convert to a specific HudException
71
71
  result = cls._analyze_exception(exc_value, message or str(exc_value))
72
- # If we couldn't categorize it (still base HudException),
73
- # just re-raise the original exception
74
- if type(result) is HudException:
75
- # Re-raise the original exception unchanged
76
- raise exc_value from None
77
72
  return result
78
73
 
79
74
  # Normal creation
@@ -136,7 +131,7 @@ class HudException(Exception):
136
131
  ),
137
132
  (
138
133
  lambda: ("api key" in error_msg or "authorization" in error_msg)
139
- and ("hud" in error_msg or "mcp.hud.so" in error_msg),
134
+ and ("hud" in error_msg or "mcp.hud.ai" in error_msg),
140
135
  HudAuthenticationError,
141
136
  ),
142
137
  (
@@ -190,11 +185,42 @@ class HudRequestError(HudException):
190
185
  self.response_text = response_text
191
186
  self.response_headers = response_headers
192
187
  # Compute default hints from status code if none provided
193
- if hints is None and status_code in (401, 403, 429):
188
+ if hints is None and status_code in (401, 402, 403, 429):
194
189
  try:
195
- from hud.shared.hints import HUD_API_KEY_MISSING, RATE_LIMIT_HIT # type: ignore
196
-
197
- if status_code in (401, 403):
190
+ from hud.shared.hints import ( # type: ignore
191
+ CREDITS_EXHAUSTED,
192
+ HUD_API_KEY_MISSING,
193
+ PRO_PLAN_REQUIRED,
194
+ RATE_LIMIT_HIT,
195
+ )
196
+
197
+ if status_code == 402:
198
+ hints = [CREDITS_EXHAUSTED]
199
+ elif status_code == 403:
200
+ # Default 403 to auth unless the message clearly indicates Pro plan
201
+ combined_text = (message or "").lower()
202
+ try:
203
+ if response_text:
204
+ combined_text += "\n" + str(response_text).lower()
205
+ except Exception: # noqa: S110
206
+ pass
207
+ try:
208
+ if response_json and isinstance(response_json, dict):
209
+ detail = response_json.get("detail")
210
+ if isinstance(detail, str):
211
+ combined_text += "\n" + detail.lower()
212
+ except Exception: # noqa: S110
213
+ pass
214
+
215
+ mentions_pro = (
216
+ "pro plan" in combined_text
217
+ or "requires pro" in combined_text
218
+ or "pro mode" in combined_text
219
+ or combined_text.strip().startswith("pro ")
220
+ )
221
+
222
+ hints = [PRO_PLAN_REQUIRED] if mentions_pro else [HUD_API_KEY_MISSING]
223
+ elif status_code == 401:
198
224
  hints = [HUD_API_KEY_MISSING]
199
225
  elif status_code == 429:
200
226
  hints = [RATE_LIMIT_HIT]
hud/shared/hints.py CHANGED
@@ -38,7 +38,7 @@ HUD_API_KEY_MISSING = Hint(
38
38
  message="Missing or invalid HUD_API_KEY.",
39
39
  tips=[
40
40
  "Set HUD_API_KEY in your environment or run: hud set HUD_API_KEY=your-key-here",
41
- "Get a key at https://hud.so",
41
+ "Get a key at https://hud.ai",
42
42
  "Check for whitespace or truncation",
43
43
  ],
44
44
  docs_url=None,
@@ -61,6 +61,31 @@ RATE_LIMIT_HIT = Hint(
61
61
  context=["network"],
62
62
  )
63
63
 
64
+ # Billing / plan upgrade
65
+ PRO_PLAN_REQUIRED = Hint(
66
+ title="Pro plan required",
67
+ message="This feature requires Pro.",
68
+ tips=[
69
+ "Upgrade your plan to continue",
70
+ ],
71
+ docs_url="https://hud.ai/project/billing",
72
+ command_examples=None,
73
+ code="PRO_PLAN_REQUIRED",
74
+ context=["billing", "plan"],
75
+ )
76
+
77
+ CREDITS_EXHAUSTED = Hint(
78
+ title="Credits exhausted",
79
+ message="Your credits are exhausted.",
80
+ tips=[
81
+ "Top up credits or upgrade your plan",
82
+ ],
83
+ docs_url="https://hud.ai/project/billing",
84
+ command_examples=None,
85
+ code="CREDITS_EXHAUSTED",
86
+ context=["billing", "credits"],
87
+ )
88
+
64
89
  TOOL_NOT_FOUND = Hint(
65
90
  title="Tool not found",
66
91
  message="Requested tool doesn't exist.",
hud/shared/requests.py CHANGED
@@ -18,7 +18,11 @@ from hud.shared.exceptions import (
18
18
  HudRequestError,
19
19
  HudTimeoutError,
20
20
  )
21
- from hud.shared.hints import HUD_API_KEY_MISSING, RATE_LIMIT_HIT
21
+ from hud.shared.hints import (
22
+ CREDITS_EXHAUSTED,
23
+ HUD_API_KEY_MISSING,
24
+ RATE_LIMIT_HIT,
25
+ )
22
26
 
23
27
  # Set up logger
24
28
  logger = logging.getLogger("hud.http")
@@ -137,9 +141,13 @@ async def make_request(
137
141
  raise HudTimeoutError(f"Request timed out: {e!s}") from None
138
142
  except httpx.HTTPStatusError as e:
139
143
  err = HudRequestError.from_httpx_error(e)
140
- if getattr(err, "status_code", None) == 429 and RATE_LIMIT_HIT not in err.hints:
144
+ code = getattr(err, "status_code", None)
145
+ if code == 429 and RATE_LIMIT_HIT not in err.hints:
141
146
  logger.debug("Attaching RATE_LIMIT hint to 429 error")
142
147
  err.hints.append(RATE_LIMIT_HIT)
148
+ elif code == 402 and CREDITS_EXHAUSTED not in err.hints:
149
+ logger.debug("Attaching CREDITS_EXHAUSTED hint to 402 error")
150
+ err.hints.append(CREDITS_EXHAUSTED)
143
151
  raise err from None
144
152
  except httpx.RequestError as e:
145
153
  if attempt <= max_retries:
@@ -234,9 +242,13 @@ def make_request_sync(
234
242
  raise HudTimeoutError(f"Request timed out: {e!s}") from None
235
243
  except httpx.HTTPStatusError as e:
236
244
  err = HudRequestError.from_httpx_error(e)
237
- if getattr(err, "status_code", None) == 429 and RATE_LIMIT_HIT not in err.hints:
245
+ code = getattr(err, "status_code", None)
246
+ if code == 429 and RATE_LIMIT_HIT not in err.hints:
238
247
  logger.debug("Attaching RATE_LIMIT hint to 429 error")
239
248
  err.hints.append(RATE_LIMIT_HIT)
249
+ elif code == 402 and CREDITS_EXHAUSTED not in err.hints:
250
+ logger.debug("Attaching CREDITS_EXHAUSTED hint to 402 error")
251
+ err.hints.append(CREDITS_EXHAUSTED)
240
252
  raise err from None
241
253
  except httpx.RequestError as e:
242
254
  if attempt <= max_retries:
@@ -7,7 +7,7 @@ classification and helpful hints for users.
7
7
  from __future__ import annotations
8
8
 
9
9
  import json
10
- from unittest.mock import Mock, patch
10
+ from unittest.mock import Mock
11
11
 
12
12
  import httpx
13
13
  import pytest
@@ -26,6 +26,7 @@ from hud.shared.hints import (
26
26
  CLIENT_NOT_INITIALIZED,
27
27
  HUD_API_KEY_MISSING,
28
28
  INVALID_CONFIG,
29
+ PRO_PLAN_REQUIRED,
29
30
  RATE_LIMIT_HIT,
30
31
  TOOL_NOT_FOUND,
31
32
  )
@@ -98,7 +99,7 @@ class TestHudExceptionAutoConversion:
98
99
  def test_hud_api_key_error(self):
99
100
  """Test that HUD API key errors become HudAuthenticationError."""
100
101
  try:
101
- raise ValueError("API key missing for mcp.hud.so")
102
+ raise ValueError("API key missing for mcp.hud.ai")
102
103
  except Exception as e:
103
104
  with pytest.raises(HudAuthenticationError) as exc_info:
104
105
  raise HudException from e
@@ -156,25 +157,23 @@ class TestHudExceptionAutoConversion:
156
157
  assert str(exc_info.value) == "Async operation timed out"
157
158
 
158
159
  def test_generic_error_remains_hudexception(self):
159
- """Test that unmatched errors remain as base HudException."""
160
+ """Uncategorized errors become base HudException with original message."""
160
161
  try:
161
162
  raise ValueError("Some random error")
162
163
  except Exception as e:
163
164
  with pytest.raises(HudException) as exc_info:
164
165
  raise HudException from e
165
-
166
- # Should be base HudException, not a subclass
166
+ # Should be base HudException, not subclass
167
167
  assert type(exc_info.value) is HudException
168
- assert exc_info.value.hints == []
168
+ assert str(exc_info.value) == "Some random error"
169
169
 
170
170
  def test_custom_message_override(self):
171
- """Test that custom message overrides the original."""
171
+ """Custom message should be used for categorized errors."""
172
172
  try:
173
- raise ValueError("Original error")
173
+ raise ValueError("Client not initialized - call initialize() first")
174
174
  except Exception as e:
175
- with pytest.raises(HudException) as exc_info:
175
+ with pytest.raises(HudClientError) as exc_info:
176
176
  raise HudException("Custom error message") from e
177
-
178
177
  assert str(exc_info.value) == "Custom error message"
179
178
 
180
179
  def test_already_hud_exception_passthrough(self):
@@ -204,6 +203,22 @@ class TestHudRequestError:
204
203
  error = HudRequestError("Forbidden", status_code=403)
205
204
  assert HUD_API_KEY_MISSING in error.hints
206
205
 
206
+ def test_403_pro_plan_message_sets_pro_hint(self):
207
+ """403 with Pro wording should map to PRO_PLAN_REQUIRED, not auth."""
208
+ error = HudRequestError("Feature requires Pro plan", status_code=403)
209
+ assert PRO_PLAN_REQUIRED in error.hints
210
+ assert HUD_API_KEY_MISSING not in error.hints
211
+
212
+ def test_403_pro_plan_detail_sets_pro_hint(self):
213
+ """403 with detail indicating Pro should map to PRO_PLAN_REQUIRED."""
214
+ error = HudRequestError(
215
+ "Forbidden",
216
+ status_code=403,
217
+ response_json={"detail": "Requires Pro plan"},
218
+ )
219
+ assert PRO_PLAN_REQUIRED in error.hints
220
+ assert HUD_API_KEY_MISSING not in error.hints
221
+
207
222
  def test_429_adds_rate_limit_hint(self):
208
223
  """Test that 429 status adds rate limit hint."""
209
224
  error = HudRequestError("Too Many Requests", status_code=429)
@@ -243,23 +258,19 @@ class TestMCPErrorHandling:
243
258
  @pytest.mark.asyncio
244
259
  async def test_mcp_error_handling(self):
245
260
  """Test that McpError is handled appropriately."""
246
- # Since McpError is imported dynamically, we'll mock it
247
- with patch("hud.clients.mcp_use.McpError") as MockMcpError:
248
- MockMcpError.side_effect = Exception
261
+ # Create a dynamic class named "McpError" to trigger name-based detection
262
+ McpError = type("McpError", (Exception,), {})
249
263
 
250
- # Create a mock MCP error
251
- mcp_error = Exception("MCP protocol error: Unknown method")
252
- mcp_error.__class__.__name__ = "McpError"
253
-
254
- try:
255
- raise mcp_error
256
- except Exception as e:
257
- # This would typically be caught in the client code
258
- # and re-raised as HudException
259
- with pytest.raises(HudException) as exc_info:
260
- raise HudException from e
264
+ try:
265
+ raise McpError("MCP protocol error: Unknown method")
266
+ except Exception as e:
267
+ # This would typically be caught in the client code
268
+ # and re-raised as HudException
269
+ with pytest.raises(HudException) as exc_info:
270
+ raise HudException from e
261
271
 
262
- assert "MCP protocol error" in str(exc_info.value)
272
+ assert "MCP protocol error" in str(exc_info.value)
273
+ assert "MCP protocol error" in str(exc_info.value)
263
274
 
264
275
  def test_mcp_tool_error_result(self):
265
276
  """Test handling of MCP tool execution errors (isError: true)."""
@@ -352,7 +363,8 @@ class TestExceptionRendering:
352
363
  assert len(error.hints) == 1
353
364
  assert error.hints[0] == HUD_API_KEY_MISSING
354
365
  assert error.hints[0].title == "HUD API key required"
355
- assert "Set HUD_API_KEY environment variable" in error.hints[0].tips[0]
366
+ # Hint copy evolved; keep the assertion robust to minor copy changes
367
+ assert "Set HUD_API_KEY" in error.hints[0].tips[0]
356
368
 
357
369
  def test_exception_type_preservation(self):
358
370
  """Test that exception types are preserved through conversion."""
@@ -396,16 +408,13 @@ class TestEdgeCases:
396
408
  assert type(error) is HudException
397
409
 
398
410
  def test_empty_error_message(self):
399
- """Test handling of empty error messages."""
411
+ """Empty message still results in a HudException instance."""
400
412
  try:
401
413
  raise ValueError("")
402
414
  except Exception as e:
403
- with pytest.raises(HudException) as exc_info:
415
+ with pytest.raises(HudException):
404
416
  raise HudException from e
405
417
 
406
- # Should still have some message
407
- assert str(exc_info.value) != ""
408
-
409
418
  def test_circular_exception_chain(self):
410
419
  """Test that we don't create circular exception chains."""
411
420
  original = HudAuthenticationError("Original")
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ from hud.shared.hints import (
6
+ CLIENT_NOT_INITIALIZED,
7
+ ENV_VAR_MISSING,
8
+ HUD_API_KEY_MISSING,
9
+ INVALID_CONFIG,
10
+ MCP_SERVER_ERROR,
11
+ RATE_LIMIT_HIT,
12
+ TOOL_NOT_FOUND,
13
+ Hint,
14
+ render_hints,
15
+ )
16
+
17
+
18
+ def test_hint_objects_basic():
19
+ assert HUD_API_KEY_MISSING.title and isinstance(HUD_API_KEY_MISSING.tips, list)
20
+ assert RATE_LIMIT_HIT.code == "RATE_LIMIT"
21
+ assert TOOL_NOT_FOUND.title.startswith("Tool")
22
+ assert CLIENT_NOT_INITIALIZED.message
23
+ assert ENV_VAR_MISSING.command_examples is not None
24
+
25
+
26
+ def test_all_hint_constants():
27
+ """Test that all predefined hint constants have required fields."""
28
+ hints = [
29
+ HUD_API_KEY_MISSING,
30
+ RATE_LIMIT_HIT,
31
+ TOOL_NOT_FOUND,
32
+ CLIENT_NOT_INITIALIZED,
33
+ INVALID_CONFIG,
34
+ ENV_VAR_MISSING,
35
+ MCP_SERVER_ERROR,
36
+ ]
37
+
38
+ for hint in hints:
39
+ assert hint.title
40
+ assert hint.message
41
+ assert hint.code
42
+
43
+
44
+ def test_hint_creation():
45
+ """Test creating a custom Hint."""
46
+ hint = Hint(
47
+ title="Test Hint",
48
+ message="This is a test",
49
+ tips=["Tip 1", "Tip 2"],
50
+ docs_url="https://example.com",
51
+ command_examples=["command 1"],
52
+ code="TEST_CODE",
53
+ context=["test", "custom"],
54
+ )
55
+
56
+ assert hint.title == "Test Hint"
57
+ assert hint.message == "This is a test"
58
+ assert hint.tips and len(hint.tips) == 2
59
+ assert hint.docs_url == "https://example.com"
60
+ assert hint.command_examples and len(hint.command_examples) == 1
61
+ assert hint.code == "TEST_CODE"
62
+ assert hint.context and "test" in hint.context
63
+
64
+
65
+ def test_hint_minimal():
66
+ """Test creating a minimal Hint with only required fields."""
67
+ hint = Hint(title="Minimal", message="Just basics")
68
+
69
+ assert hint.title == "Minimal"
70
+ assert hint.message == "Just basics"
71
+ assert hint.tips is None
72
+ assert hint.docs_url is None
73
+ assert hint.command_examples is None
74
+ assert hint.code is None
75
+ assert hint.context is None
76
+
77
+
78
+ def test_render_hints_none():
79
+ """Test that render_hints handles None gracefully."""
80
+ # Should not raise
81
+ render_hints(None)
82
+
83
+
84
+ def test_render_hints_empty_list():
85
+ """Test that render_hints handles empty list gracefully."""
86
+ # Should not raise
87
+ render_hints([])
88
+
89
+
90
+ @patch("hud.utils.hud_console.hud_console")
91
+ def test_render_hints_with_tips(mock_console):
92
+ """Test rendering hints with tips."""
93
+ render_hints([HUD_API_KEY_MISSING])
94
+
95
+ # Should call warning for title/message
96
+ mock_console.warning.assert_called()
97
+ # Should call info for tips
98
+ assert mock_console.info.call_count >= 1
99
+
100
+
101
+ @patch("hud.utils.hud_console.hud_console")
102
+ def test_render_hints_with_command_examples(mock_console):
103
+ """Test rendering hints with command examples."""
104
+ render_hints([ENV_VAR_MISSING])
105
+
106
+ # Should call command_example
107
+ mock_console.command_example.assert_called()
108
+
109
+
110
+ @patch("hud.utils.hud_console.hud_console")
111
+ def test_render_hints_with_docs_url(mock_console):
112
+ """Test rendering hints with documentation URL."""
113
+ hint = Hint(
114
+ title="Test",
115
+ message="Test message",
116
+ docs_url="https://docs.example.com",
117
+ )
118
+
119
+ render_hints([hint])
120
+
121
+ # Should call link for docs URL
122
+ mock_console.link.assert_called_with("https://docs.example.com")
123
+
124
+
125
+ @patch("hud.utils.hud_console.hud_console")
126
+ def test_render_hints_same_title_and_message(mock_console):
127
+ """Test rendering hints when title equals message."""
128
+ hint = Hint(title="Same", message="Same")
129
+
130
+ render_hints([hint])
131
+
132
+ # Should only call warning once with just the message
133
+ mock_console.warning.assert_called_once_with("Same")
134
+
135
+
136
+ @patch("hud.utils.hud_console.hud_console")
137
+ def test_render_hints_different_title_and_message(mock_console):
138
+ """Test rendering hints when title differs from message."""
139
+ hint = Hint(title="Title", message="Different message")
140
+
141
+ render_hints([hint])
142
+
143
+ # Should call warning with both title and message
144
+ mock_console.warning.assert_called_once()
145
+ call_args = mock_console.warning.call_args[0][0]
146
+ assert "Title" in call_args
147
+ assert "Different message" in call_args
148
+
149
+
150
+ def test_render_hints_with_custom_design():
151
+ """Test rendering hints with custom design object."""
152
+ custom_design = MagicMock()
153
+
154
+ hint = Hint(title="Test", message="Message")
155
+ # Should not raise when custom design is provided
156
+ render_hints([hint], design=custom_design)
157
+
158
+
159
+ @patch("hud.utils.hud_console.hud_console")
160
+ def test_render_hints_handles_exception(mock_console):
161
+ """Test that render_hints handles exceptions gracefully."""
162
+ mock_console.warning.side_effect = Exception("Test error")
163
+
164
+ hint = Hint(title="Test", message="Message")
165
+
166
+ # Should not raise, just log warning
167
+ render_hints([hint])