hud-python 0.4.19__py3-none-any.whl → 0.4.21__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.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/__init__.py +7 -0
- hud/agents/base.py +40 -10
- hud/agents/claude.py +44 -25
- hud/agents/tests/test_client.py +6 -27
- hud/cli/__init__.py +50 -20
- hud/cli/build.py +3 -44
- hud/cli/eval.py +25 -6
- hud/cli/init.py +4 -4
- hud/cli/push.py +3 -1
- hud/cli/tests/test_push.py +6 -6
- hud/clients/__init__.py +3 -2
- hud/clients/base.py +25 -26
- hud/clients/mcp_use.py +44 -22
- hud/datasets/task.py +11 -2
- hud/native/__init__.py +6 -0
- hud/native/comparator.py +546 -0
- hud/native/tests/__init__.py +1 -0
- hud/native/tests/test_comparator.py +539 -0
- hud/native/tests/test_native_init.py +79 -0
- hud/otel/instrumentation.py +0 -2
- hud/server/server.py +9 -2
- hud/shared/exceptions.py +204 -31
- hud/shared/hints.py +177 -0
- hud/shared/requests.py +15 -3
- hud/shared/tests/test_exceptions.py +385 -144
- hud/tools/__init__.py +2 -0
- hud/tools/playwright.py +1 -1
- hud/tools/submit.py +66 -0
- hud/types.py +33 -5
- hud/utils/design.py +57 -0
- hud/utils/mcp.py +6 -0
- hud/utils/pretty_errors.py +68 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.19.dist-info → hud_python-0.4.21.dist-info}/METADATA +2 -4
- {hud_python-0.4.19.dist-info → hud_python-0.4.21.dist-info}/RECORD +39 -31
- {hud_python-0.4.19.dist-info → hud_python-0.4.21.dist-info}/WHEEL +0 -0
- {hud_python-0.4.19.dist-info → hud_python-0.4.21.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.19.dist-info → hud_python-0.4.21.dist-info}/licenses/LICENSE +0 -0
hud/shared/exceptions.py
CHANGED
|
@@ -1,36 +1,179 @@
|
|
|
1
|
+
"""HUD SDK Exception System.
|
|
2
|
+
|
|
3
|
+
This module provides intelligent exception handling with automatic error
|
|
4
|
+
classification and helpful hints for users.
|
|
5
|
+
|
|
6
|
+
Key Features:
|
|
7
|
+
- Auto-converts generic exceptions to specific HUD exceptions
|
|
8
|
+
- Attaches contextual hints based on error type
|
|
9
|
+
- Clean chaining syntax: raise HudException() from e
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
try:
|
|
13
|
+
client.call_tool("missing")
|
|
14
|
+
except Exception as e:
|
|
15
|
+
raise HudException() from e # Becomes HudToolNotFoundError with hints
|
|
16
|
+
"""
|
|
17
|
+
|
|
1
18
|
from __future__ import annotations
|
|
2
19
|
|
|
20
|
+
import asyncio
|
|
21
|
+
import json
|
|
3
22
|
import logging
|
|
4
|
-
from typing import TYPE_CHECKING, Any
|
|
23
|
+
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
|
|
5
24
|
|
|
6
25
|
if TYPE_CHECKING:
|
|
7
26
|
from typing import Self
|
|
8
27
|
|
|
9
28
|
import httpx
|
|
10
29
|
|
|
30
|
+
from hud.shared.hints import (
|
|
31
|
+
CLIENT_NOT_INITIALIZED,
|
|
32
|
+
ENV_VAR_MISSING,
|
|
33
|
+
HUD_API_KEY_MISSING,
|
|
34
|
+
INVALID_CONFIG,
|
|
35
|
+
MCP_SERVER_ERROR,
|
|
36
|
+
RATE_LIMIT_HIT,
|
|
37
|
+
TOOL_NOT_FOUND,
|
|
38
|
+
Hint,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
T = TypeVar("T", bound="HudException")
|
|
42
|
+
|
|
11
43
|
logger = logging.getLogger(__name__)
|
|
12
44
|
|
|
13
45
|
|
|
14
46
|
class HudException(Exception):
|
|
15
47
|
"""Base exception class for all HUD SDK errors.
|
|
16
48
|
|
|
17
|
-
|
|
18
|
-
|
|
49
|
+
Usage:
|
|
50
|
+
raise HudException() from e # Auto-converts to appropriate subclass
|
|
51
|
+
raise HudException("Custom message") from e # With custom message
|
|
19
52
|
"""
|
|
20
53
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
54
|
+
def __new__(cls, message: str = "", *args: Any, **kwargs: Any) -> Any:
|
|
55
|
+
"""Auto-convert generic exceptions to specific HUD exceptions when chained."""
|
|
56
|
+
import sys
|
|
57
|
+
|
|
58
|
+
# Only intercept for base HudException, not subclasses
|
|
59
|
+
if cls is not HudException:
|
|
60
|
+
return super().__new__(cls)
|
|
61
|
+
|
|
62
|
+
# Check if we're in a 'raise...from' context
|
|
63
|
+
exc_type, exc_value, _ = sys.exc_info()
|
|
64
|
+
if exc_type and exc_value:
|
|
65
|
+
# If it's already a HudException, return it as-is
|
|
66
|
+
if isinstance(exc_value, HudException):
|
|
67
|
+
return exc_value
|
|
68
|
+
# Otherwise analyze if it's a regular Exception
|
|
69
|
+
elif isinstance(exc_value, Exception):
|
|
70
|
+
# Try to convert to a specific HudException
|
|
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
|
+
return result
|
|
78
|
+
|
|
79
|
+
# Normal creation
|
|
80
|
+
return super().__new__(cls)
|
|
81
|
+
|
|
82
|
+
# Subclasses can override this class attribute
|
|
83
|
+
default_hints: ClassVar[list[Hint]] = []
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
message: str = "",
|
|
88
|
+
response_json: dict[str, Any] | None = None,
|
|
89
|
+
*,
|
|
90
|
+
hints: list[Hint] | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
# If we already have args set (from _analyze_exception), don't override them
|
|
93
|
+
if not self.args:
|
|
94
|
+
# Pass the message to the base Exception class
|
|
95
|
+
super().__init__(message)
|
|
96
|
+
self.message = message or (self.args[0] if self.args else "")
|
|
24
97
|
self.response_json = response_json
|
|
98
|
+
# If hints not provided, use defaults defined by subclass
|
|
99
|
+
self.hints: list[Hint] = hints if hints is not None else list(self.default_hints)
|
|
25
100
|
|
|
26
101
|
def __str__(self) -> str:
|
|
27
|
-
|
|
102
|
+
# Get the message from the exception
|
|
103
|
+
# First check if we have args (standard Exception message storage)
|
|
104
|
+
msg = str(self.args[0]) if self.args and self.args[0] else ""
|
|
105
|
+
|
|
106
|
+
# Add response JSON if available
|
|
28
107
|
if self.response_json:
|
|
29
|
-
|
|
30
|
-
|
|
108
|
+
if msg:
|
|
109
|
+
return f"{msg} | Response: {self.response_json}"
|
|
110
|
+
else:
|
|
111
|
+
return f"Response: {self.response_json}"
|
|
31
112
|
|
|
113
|
+
return msg
|
|
32
114
|
|
|
33
|
-
|
|
115
|
+
@classmethod
|
|
116
|
+
def _analyze_exception(cls, e: Exception, message: str = "") -> HudException:
|
|
117
|
+
"""Convert generic exceptions to specific HUD exceptions based on content."""
|
|
118
|
+
error_msg = str(e).lower()
|
|
119
|
+
final_msg = message or str(e)
|
|
120
|
+
|
|
121
|
+
# Map error patterns to exception types
|
|
122
|
+
patterns = [
|
|
123
|
+
# (condition_func, exception_class)
|
|
124
|
+
(
|
|
125
|
+
lambda: "not initialized" in error_msg or "not connected" in error_msg,
|
|
126
|
+
HudClientError,
|
|
127
|
+
),
|
|
128
|
+
(
|
|
129
|
+
lambda: "invalid json" in error_msg or "config" in error_msg or "json" in error_msg,
|
|
130
|
+
HudConfigError,
|
|
131
|
+
),
|
|
132
|
+
(
|
|
133
|
+
lambda: "tool" in error_msg
|
|
134
|
+
and ("not found" in error_msg or "not exist" in error_msg),
|
|
135
|
+
HudToolNotFoundError,
|
|
136
|
+
),
|
|
137
|
+
(
|
|
138
|
+
lambda: ("api key" in error_msg or "authorization" in error_msg)
|
|
139
|
+
and ("hud" in error_msg or "mcp.hud.so" in error_msg),
|
|
140
|
+
HudAuthenticationError,
|
|
141
|
+
),
|
|
142
|
+
(
|
|
143
|
+
lambda: "rate limit" in error_msg or "too many request" in error_msg,
|
|
144
|
+
HudRateLimitError,
|
|
145
|
+
),
|
|
146
|
+
(lambda: isinstance(e, (TimeoutError | asyncio.TimeoutError)), HudTimeoutError),
|
|
147
|
+
(lambda: isinstance(e, json.JSONDecodeError), HudConfigError),
|
|
148
|
+
(
|
|
149
|
+
lambda: "environment variable" in error_msg and "required" in error_msg,
|
|
150
|
+
HudEnvVarError,
|
|
151
|
+
),
|
|
152
|
+
(lambda: "event loop" in error_msg and "closed" in error_msg, HudClientError),
|
|
153
|
+
(
|
|
154
|
+
lambda: type(e).__name__ == "McpError", # Check by name to avoid import issues
|
|
155
|
+
HudMCPError,
|
|
156
|
+
),
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
# Find first matching pattern
|
|
160
|
+
for condition, exception_class in patterns:
|
|
161
|
+
if condition():
|
|
162
|
+
# Create instance directly using Exception.__new__ to bypass our custom __new__
|
|
163
|
+
instance = Exception.__new__(exception_class)
|
|
164
|
+
# Manually set args before calling __init__ to ensure proper Exception behavior
|
|
165
|
+
instance.args = (final_msg,)
|
|
166
|
+
instance.__init__(final_msg)
|
|
167
|
+
return instance
|
|
168
|
+
|
|
169
|
+
# No pattern matched - return base exception instance
|
|
170
|
+
instance = Exception.__new__(HudException)
|
|
171
|
+
instance.args = (final_msg,)
|
|
172
|
+
instance.__init__(final_msg)
|
|
173
|
+
return instance
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class HudRequestError(HudException):
|
|
34
177
|
"""Any request to the HUD API can raise this exception."""
|
|
35
178
|
|
|
36
179
|
def __init__(
|
|
@@ -40,13 +183,24 @@ class HudRequestError(Exception):
|
|
|
40
183
|
response_text: str | None = None,
|
|
41
184
|
response_json: dict[str, Any] | None = None,
|
|
42
185
|
response_headers: dict[str, str] | None = None,
|
|
186
|
+
*,
|
|
187
|
+
hints: list[Hint] | None = None,
|
|
43
188
|
) -> None:
|
|
44
|
-
self.message = message
|
|
45
189
|
self.status_code = status_code
|
|
46
190
|
self.response_text = response_text
|
|
47
|
-
self.response_json = response_json
|
|
48
191
|
self.response_headers = response_headers
|
|
49
|
-
|
|
192
|
+
# Compute default hints from status code if none provided
|
|
193
|
+
if hints is None and status_code in (401, 403, 429):
|
|
194
|
+
try:
|
|
195
|
+
from hud.shared.hints import HUD_API_KEY_MISSING, RATE_LIMIT_HIT # type: ignore
|
|
196
|
+
|
|
197
|
+
if status_code in (401, 403):
|
|
198
|
+
hints = [HUD_API_KEY_MISSING]
|
|
199
|
+
elif status_code == 429:
|
|
200
|
+
hints = [RATE_LIMIT_HIT]
|
|
201
|
+
except Exception as import_error:
|
|
202
|
+
logger.debug("Failed to attach structured hints: %s", import_error)
|
|
203
|
+
super().__init__(message, response_json, hints=hints)
|
|
50
204
|
|
|
51
205
|
def __str__(self) -> str:
|
|
52
206
|
parts = [self.message]
|
|
@@ -110,13 +264,14 @@ class HudRequestError(Exception):
|
|
|
110
264
|
response_text[:500],
|
|
111
265
|
"..." if len(response_text) > 500 else "",
|
|
112
266
|
)
|
|
113
|
-
|
|
267
|
+
inst = cls(
|
|
114
268
|
message=message,
|
|
115
269
|
status_code=status_code,
|
|
116
270
|
response_text=response_text,
|
|
117
271
|
response_json=response_json,
|
|
118
272
|
response_headers=response_headers,
|
|
119
273
|
)
|
|
274
|
+
return inst
|
|
120
275
|
|
|
121
276
|
|
|
122
277
|
class HudResponseError(HudException):
|
|
@@ -148,35 +303,53 @@ class HudResponseError(HudException):
|
|
|
148
303
|
|
|
149
304
|
|
|
150
305
|
class HudAuthenticationError(HudException):
|
|
151
|
-
"""
|
|
306
|
+
"""Missing or invalid HUD API key."""
|
|
152
307
|
|
|
153
|
-
|
|
154
|
-
has insufficient permissions for the requested operation.
|
|
155
|
-
"""
|
|
308
|
+
default_hints: ClassVar[list[Hint]] = [HUD_API_KEY_MISSING]
|
|
156
309
|
|
|
157
310
|
|
|
158
311
|
class HudRateLimitError(HudException):
|
|
159
|
-
"""
|
|
312
|
+
"""Too many requests to the API."""
|
|
160
313
|
|
|
161
|
-
|
|
162
|
-
short period of time.
|
|
163
|
-
"""
|
|
314
|
+
default_hints: ClassVar[list[Hint]] = [RATE_LIMIT_HIT]
|
|
164
315
|
|
|
165
316
|
|
|
166
317
|
class HudTimeoutError(HudException):
|
|
167
|
-
"""
|
|
168
|
-
|
|
169
|
-
This exception is raised when a request takes longer than the
|
|
170
|
-
configured timeout period.
|
|
171
|
-
"""
|
|
318
|
+
"""Request timed out."""
|
|
172
319
|
|
|
173
320
|
|
|
174
321
|
class HudNetworkError(HudException):
|
|
175
|
-
"""
|
|
322
|
+
"""Network connection issue."""
|
|
176
323
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"""
|
|
324
|
+
|
|
325
|
+
class HudClientError(HudException):
|
|
326
|
+
"""MCP client not initialized."""
|
|
327
|
+
|
|
328
|
+
default_hints: ClassVar[list[Hint]] = [CLIENT_NOT_INITIALIZED]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class HudConfigError(HudException):
|
|
332
|
+
"""Invalid or missing configuration."""
|
|
333
|
+
|
|
334
|
+
default_hints: ClassVar[list[Hint]] = [INVALID_CONFIG]
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class HudEnvVarError(HudException):
|
|
338
|
+
"""Missing required environment variables."""
|
|
339
|
+
|
|
340
|
+
default_hints: ClassVar[list[Hint]] = [ENV_VAR_MISSING]
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class HudToolNotFoundError(HudException):
|
|
344
|
+
"""Requested tool not found."""
|
|
345
|
+
|
|
346
|
+
default_hints: ClassVar[list[Hint]] = [TOOL_NOT_FOUND]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class HudMCPError(HudException):
|
|
350
|
+
"""MCP protocol or server error."""
|
|
351
|
+
|
|
352
|
+
default_hints: ClassVar[list[Hint]] = [MCP_SERVER_ERROR]
|
|
180
353
|
|
|
181
354
|
|
|
182
355
|
class GymMakeException(HudException):
|
hud/shared/hints.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Hint:
|
|
14
|
+
"""Structured hint for user guidance.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
title: Short title describing the hint.
|
|
18
|
+
message: Main explanatory message.
|
|
19
|
+
tips: Optional list of short actionable tips.
|
|
20
|
+
docs_url: Optional URL for documentation.
|
|
21
|
+
command_examples: Optional list of command examples to show.
|
|
22
|
+
code: Optional machine-readable code (e.g., "AUTH_API_KEY_MISSING").
|
|
23
|
+
context: Optional context tags (e.g., ["auth", "docker", "mcp"]).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
title: str
|
|
27
|
+
message: str
|
|
28
|
+
tips: list[str] | None = None
|
|
29
|
+
docs_url: str | None = None
|
|
30
|
+
command_examples: list[str] | None = None
|
|
31
|
+
code: str | None = None
|
|
32
|
+
context: list[str] | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Common, reusable hints
|
|
36
|
+
HUD_API_KEY_MISSING = Hint(
|
|
37
|
+
title="HUD API key required",
|
|
38
|
+
message="Missing or invalid HUD_API_KEY.",
|
|
39
|
+
tips=[
|
|
40
|
+
"Set HUD_API_KEY environment variable",
|
|
41
|
+
"Get a key at https://app.hud.so",
|
|
42
|
+
"Check for whitespace or truncation",
|
|
43
|
+
],
|
|
44
|
+
docs_url=None,
|
|
45
|
+
command_examples=None,
|
|
46
|
+
code="HUD_AUTH_MISSING",
|
|
47
|
+
context=["auth", "hud"],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
RATE_LIMIT_HIT = Hint(
|
|
51
|
+
title="Rate limit reached",
|
|
52
|
+
message="Too many requests.",
|
|
53
|
+
tips=[
|
|
54
|
+
"Lower --max-concurrent",
|
|
55
|
+
"Add retry delay",
|
|
56
|
+
"Check API quotas",
|
|
57
|
+
],
|
|
58
|
+
docs_url=None,
|
|
59
|
+
command_examples=None,
|
|
60
|
+
code="RATE_LIMIT",
|
|
61
|
+
context=["network"],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
TOOL_NOT_FOUND = Hint(
|
|
65
|
+
title="Tool not found",
|
|
66
|
+
message="Requested tool doesn't exist.",
|
|
67
|
+
tips=[
|
|
68
|
+
"Check tool name spelling",
|
|
69
|
+
"Run: hud analyze --live <image>",
|
|
70
|
+
"Verify server implements tool",
|
|
71
|
+
],
|
|
72
|
+
docs_url=None,
|
|
73
|
+
command_examples=None,
|
|
74
|
+
code="TOOL_NOT_FOUND",
|
|
75
|
+
context=["mcp", "tools"],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
CLIENT_NOT_INITIALIZED = Hint(
|
|
79
|
+
title="Client not initialized",
|
|
80
|
+
message="MCP client must be initialized before use.",
|
|
81
|
+
tips=[
|
|
82
|
+
"Call client.initialize() first",
|
|
83
|
+
"Or use async with client:",
|
|
84
|
+
"Check connection succeeded",
|
|
85
|
+
],
|
|
86
|
+
docs_url=None,
|
|
87
|
+
command_examples=None,
|
|
88
|
+
code="CLIENT_NOT_INIT",
|
|
89
|
+
context=["mcp", "client"],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
INVALID_CONFIG = Hint(
|
|
93
|
+
title="Invalid configuration",
|
|
94
|
+
message="Configuration is missing or malformed.",
|
|
95
|
+
tips=[
|
|
96
|
+
"Check JSON syntax",
|
|
97
|
+
"Verify required fields",
|
|
98
|
+
"See examples in docs",
|
|
99
|
+
],
|
|
100
|
+
docs_url=None,
|
|
101
|
+
command_examples=None,
|
|
102
|
+
code="INVALID_CONFIG",
|
|
103
|
+
context=["config"],
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
ENV_VAR_MISSING = Hint(
|
|
107
|
+
title="Environment variable required",
|
|
108
|
+
message="Required environment variables are missing.",
|
|
109
|
+
tips=[
|
|
110
|
+
"Set required environment variables",
|
|
111
|
+
"Use -e flag: hud build . -e VAR_NAME=value",
|
|
112
|
+
"Check Dockerfile for ENV requirements",
|
|
113
|
+
"Run hud debug . --build for detailed logs",
|
|
114
|
+
],
|
|
115
|
+
docs_url=None,
|
|
116
|
+
command_examples=["hud build . -e BROWSER_PROVIDER=anchorbrowser"],
|
|
117
|
+
code="ENV_VAR_MISSING",
|
|
118
|
+
context=["env", "config"],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
MCP_SERVER_ERROR = Hint(
|
|
122
|
+
title="MCP server error",
|
|
123
|
+
message="The MCP server encountered an error.",
|
|
124
|
+
tips=[
|
|
125
|
+
"Check server logs for details",
|
|
126
|
+
"Verify server configuration",
|
|
127
|
+
"Ensure all dependencies are installed",
|
|
128
|
+
"Run hud debug to see detailed output",
|
|
129
|
+
],
|
|
130
|
+
docs_url=None,
|
|
131
|
+
command_examples=["hud debug", "hud dev --verbose"],
|
|
132
|
+
code="MCP_SERVER_ERROR",
|
|
133
|
+
context=["mcp", "server"],
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def render_hints(hints: Iterable[Hint] | None, *, design: Any | None = None) -> None:
|
|
138
|
+
"""Render a collection of hints using the HUD design system if available.
|
|
139
|
+
|
|
140
|
+
If design is not provided, this is a no-op to keep library use headless.
|
|
141
|
+
"""
|
|
142
|
+
if not hints:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
if design is None:
|
|
147
|
+
from hud.utils.design import design as default_design # lazy import
|
|
148
|
+
|
|
149
|
+
design = default_design
|
|
150
|
+
except Exception:
|
|
151
|
+
# If design is unavailable (non-CLI contexts), silently skip rendering
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
for hint in hints:
|
|
155
|
+
try:
|
|
156
|
+
# Compact rendering - skip title if same as message
|
|
157
|
+
if hint.title and hint.title != hint.message:
|
|
158
|
+
design.warning(f"{hint.title}: {hint.message}")
|
|
159
|
+
else:
|
|
160
|
+
design.warning(hint.message)
|
|
161
|
+
|
|
162
|
+
# Tips as bullet points
|
|
163
|
+
if hint.tips:
|
|
164
|
+
for tip in hint.tips:
|
|
165
|
+
design.info(f" • {tip}")
|
|
166
|
+
|
|
167
|
+
# Only show command examples if provided
|
|
168
|
+
if hint.command_examples:
|
|
169
|
+
for cmd in hint.command_examples:
|
|
170
|
+
design.command_example(cmd)
|
|
171
|
+
|
|
172
|
+
# Only show docs URL if provided
|
|
173
|
+
if hint.docs_url:
|
|
174
|
+
design.link(hint.docs_url)
|
|
175
|
+
except Exception:
|
|
176
|
+
logger.warning("Failed to render hint: %s", hint)
|
|
177
|
+
continue
|
hud/shared/requests.py
CHANGED
|
@@ -18,6 +18,7 @@ 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
22
|
|
|
22
23
|
# Set up logger
|
|
23
24
|
logger = logging.getLogger("hud.http")
|
|
@@ -97,7 +98,10 @@ async def make_request(
|
|
|
97
98
|
HudTimeoutError: If the request times out.
|
|
98
99
|
"""
|
|
99
100
|
if not api_key:
|
|
100
|
-
raise HudAuthenticationError(
|
|
101
|
+
raise HudAuthenticationError(
|
|
102
|
+
"API key is required but not provided",
|
|
103
|
+
hints=[HUD_API_KEY_MISSING],
|
|
104
|
+
)
|
|
101
105
|
|
|
102
106
|
headers = {"Authorization": f"Bearer {api_key}"}
|
|
103
107
|
retry_status_codes = [502, 503, 504]
|
|
@@ -132,7 +136,11 @@ async def make_request(
|
|
|
132
136
|
except httpx.TimeoutException as e:
|
|
133
137
|
raise HudTimeoutError(f"Request timed out: {e!s}") from None
|
|
134
138
|
except httpx.HTTPStatusError as e:
|
|
135
|
-
|
|
139
|
+
err = HudRequestError.from_httpx_error(e)
|
|
140
|
+
if getattr(err, "status_code", None) == 429 and RATE_LIMIT_HIT not in err.hints:
|
|
141
|
+
logger.debug("Attaching RATE_LIMIT hint to 429 error")
|
|
142
|
+
err.hints.append(RATE_LIMIT_HIT)
|
|
143
|
+
raise err from None
|
|
136
144
|
except httpx.RequestError as e:
|
|
137
145
|
if attempt <= max_retries:
|
|
138
146
|
await _handle_retry(
|
|
@@ -225,7 +233,11 @@ def make_request_sync(
|
|
|
225
233
|
except httpx.TimeoutException as e:
|
|
226
234
|
raise HudTimeoutError(f"Request timed out: {e!s}") from None
|
|
227
235
|
except httpx.HTTPStatusError as e:
|
|
228
|
-
|
|
236
|
+
err = HudRequestError.from_httpx_error(e)
|
|
237
|
+
if getattr(err, "status_code", None) == 429 and RATE_LIMIT_HIT not in err.hints:
|
|
238
|
+
logger.debug("Attaching RATE_LIMIT hint to 429 error")
|
|
239
|
+
err.hints.append(RATE_LIMIT_HIT)
|
|
240
|
+
raise err from None
|
|
229
241
|
except httpx.RequestError as e:
|
|
230
242
|
if attempt <= max_retries:
|
|
231
243
|
retry_time = retry_delay * (2 ** (attempt - 1))
|