hanzo 0.3.21__py3-none-any.whl → 0.3.23__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 hanzo might be problematic. Click here for more details.
- hanzo/base_agent.py +517 -0
- hanzo/batch_orchestrator.py +988 -0
- hanzo/cli.py +1 -1
- hanzo/commands/repl.py +5 -2
- hanzo/dev.py +463 -261
- hanzo/fallback_handler.py +78 -52
- hanzo/memory_manager.py +145 -122
- hanzo/model_registry.py +399 -0
- hanzo/rate_limiter.py +59 -74
- hanzo/streaming.py +91 -70
- {hanzo-0.3.21.dist-info → hanzo-0.3.23.dist-info}/METADATA +1 -1
- {hanzo-0.3.21.dist-info → hanzo-0.3.23.dist-info}/RECORD +14 -11
- {hanzo-0.3.21.dist-info → hanzo-0.3.23.dist-info}/WHEEL +0 -0
- {hanzo-0.3.21.dist-info → hanzo-0.3.23.dist-info}/entry_points.txt +0 -0
hanzo/rate_limiter.py
CHANGED
|
@@ -4,17 +4,18 @@ Prevents API overuse and handles failures gracefully.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import time
|
|
7
|
+
import random
|
|
7
8
|
import asyncio
|
|
8
|
-
from typing import
|
|
9
|
-
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Dict, Callable, Optional
|
|
10
10
|
from datetime import datetime, timedelta
|
|
11
11
|
from collections import deque
|
|
12
|
-
import
|
|
12
|
+
from dataclasses import field, dataclass
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@dataclass
|
|
16
16
|
class RateLimitConfig:
|
|
17
17
|
"""Configuration for rate limiting."""
|
|
18
|
+
|
|
18
19
|
requests_per_minute: int = 20
|
|
19
20
|
requests_per_hour: int = 100
|
|
20
21
|
burst_size: int = 5
|
|
@@ -27,6 +28,7 @@ class RateLimitConfig:
|
|
|
27
28
|
@dataclass
|
|
28
29
|
class RateLimitState:
|
|
29
30
|
"""Current state of rate limiter."""
|
|
31
|
+
|
|
30
32
|
minute_requests: deque = field(default_factory=lambda: deque(maxlen=60))
|
|
31
33
|
hour_requests: deque = field(default_factory=lambda: deque(maxlen=3600))
|
|
32
34
|
last_request: Optional[datetime] = None
|
|
@@ -39,18 +41,18 @@ class RateLimitState:
|
|
|
39
41
|
|
|
40
42
|
class RateLimiter:
|
|
41
43
|
"""Rate limiter with error recovery."""
|
|
42
|
-
|
|
44
|
+
|
|
43
45
|
def __init__(self, config: RateLimitConfig = None):
|
|
44
46
|
"""Initialize rate limiter."""
|
|
45
47
|
self.config = config or RateLimitConfig()
|
|
46
48
|
self.states: Dict[str, RateLimitState] = {}
|
|
47
|
-
|
|
49
|
+
|
|
48
50
|
def get_state(self, key: str = "default") -> RateLimitState:
|
|
49
51
|
"""Get or create state for a key."""
|
|
50
52
|
if key not in self.states:
|
|
51
53
|
self.states[key] = RateLimitState()
|
|
52
54
|
return self.states[key]
|
|
53
|
-
|
|
55
|
+
|
|
54
56
|
async def check_rate_limit(self, key: str = "default") -> tuple[bool, float]:
|
|
55
57
|
"""
|
|
56
58
|
Check if request is allowed.
|
|
@@ -58,7 +60,7 @@ class RateLimiter:
|
|
|
58
60
|
"""
|
|
59
61
|
state = self.get_state(key)
|
|
60
62
|
now = datetime.now()
|
|
61
|
-
|
|
63
|
+
|
|
62
64
|
# Check if throttled
|
|
63
65
|
if state.is_throttled and state.throttle_until:
|
|
64
66
|
if now < state.throttle_until:
|
|
@@ -68,41 +70,41 @@ class RateLimiter:
|
|
|
68
70
|
# Throttle period ended
|
|
69
71
|
state.is_throttled = False
|
|
70
72
|
state.throttle_until = None
|
|
71
|
-
|
|
73
|
+
|
|
72
74
|
# Clean old requests
|
|
73
75
|
minute_ago = now - timedelta(minutes=1)
|
|
74
76
|
hour_ago = now - timedelta(hours=1)
|
|
75
|
-
|
|
77
|
+
|
|
76
78
|
# Remove old requests from queues
|
|
77
79
|
while state.minute_requests and state.minute_requests[0] < minute_ago:
|
|
78
80
|
state.minute_requests.popleft()
|
|
79
|
-
|
|
81
|
+
|
|
80
82
|
while state.hour_requests and state.hour_requests[0] < hour_ago:
|
|
81
83
|
state.hour_requests.popleft()
|
|
82
|
-
|
|
84
|
+
|
|
83
85
|
# Check minute limit
|
|
84
86
|
if len(state.minute_requests) >= self.config.requests_per_minute:
|
|
85
87
|
# Calculate wait time
|
|
86
88
|
oldest = state.minute_requests[0]
|
|
87
89
|
wait_seconds = (oldest + timedelta(minutes=1) - now).total_seconds()
|
|
88
90
|
return False, max(0, wait_seconds)
|
|
89
|
-
|
|
91
|
+
|
|
90
92
|
# Check hour limit
|
|
91
93
|
if len(state.hour_requests) >= self.config.requests_per_hour:
|
|
92
94
|
# Calculate wait time
|
|
93
95
|
oldest = state.hour_requests[0]
|
|
94
96
|
wait_seconds = (oldest + timedelta(hours=1) - now).total_seconds()
|
|
95
97
|
return False, max(0, wait_seconds)
|
|
96
|
-
|
|
98
|
+
|
|
97
99
|
# Check burst limit
|
|
98
100
|
if state.last_request:
|
|
99
101
|
time_since_last = (now - state.last_request).total_seconds()
|
|
100
102
|
if time_since_last < 1.0 / self.config.burst_size:
|
|
101
103
|
wait_seconds = (1.0 / self.config.burst_size) - time_since_last
|
|
102
104
|
return False, wait_seconds
|
|
103
|
-
|
|
105
|
+
|
|
104
106
|
return True, 0
|
|
105
|
-
|
|
107
|
+
|
|
106
108
|
async def acquire(self, key: str = "default") -> bool:
|
|
107
109
|
"""
|
|
108
110
|
Acquire a rate limit slot.
|
|
@@ -110,7 +112,7 @@ class RateLimiter:
|
|
|
110
112
|
"""
|
|
111
113
|
while True:
|
|
112
114
|
allowed, wait_seconds = await self.check_rate_limit(key)
|
|
113
|
-
|
|
115
|
+
|
|
114
116
|
if allowed:
|
|
115
117
|
# Record request
|
|
116
118
|
state = self.get_state(key)
|
|
@@ -120,37 +122,37 @@ class RateLimiter:
|
|
|
120
122
|
state.last_request = now
|
|
121
123
|
state.total_requests += 1
|
|
122
124
|
return True
|
|
123
|
-
|
|
125
|
+
|
|
124
126
|
# Wait before retrying
|
|
125
127
|
if wait_seconds > 0:
|
|
126
128
|
await asyncio.sleep(min(wait_seconds, 5)) # Check every 5 seconds max
|
|
127
|
-
|
|
129
|
+
|
|
128
130
|
def record_error(self, key: str = "default", error: Exception = None):
|
|
129
131
|
"""Record an error for the key."""
|
|
130
132
|
state = self.get_state(key)
|
|
131
133
|
state.consecutive_errors += 1
|
|
132
134
|
state.total_errors += 1
|
|
133
|
-
|
|
135
|
+
|
|
134
136
|
# Implement exponential backoff on errors
|
|
135
137
|
if state.consecutive_errors >= 3:
|
|
136
138
|
# Throttle for increasing periods
|
|
137
139
|
backoff_minutes = min(
|
|
138
140
|
self.config.backoff_base ** (state.consecutive_errors - 2),
|
|
139
|
-
60 # Max 1 hour
|
|
141
|
+
60, # Max 1 hour
|
|
140
142
|
)
|
|
141
143
|
state.is_throttled = True
|
|
142
144
|
state.throttle_until = datetime.now() + timedelta(minutes=backoff_minutes)
|
|
143
|
-
|
|
145
|
+
|
|
144
146
|
def record_success(self, key: str = "default"):
|
|
145
147
|
"""Record a successful request."""
|
|
146
148
|
state = self.get_state(key)
|
|
147
149
|
state.consecutive_errors = 0
|
|
148
|
-
|
|
150
|
+
|
|
149
151
|
def get_status(self, key: str = "default") -> Dict[str, Any]:
|
|
150
152
|
"""Get current status for monitoring."""
|
|
151
153
|
state = self.get_state(key)
|
|
152
154
|
now = datetime.now()
|
|
153
|
-
|
|
155
|
+
|
|
154
156
|
return {
|
|
155
157
|
"requests_last_minute": len(state.minute_requests),
|
|
156
158
|
"requests_last_hour": len(state.hour_requests),
|
|
@@ -170,47 +172,47 @@ class RateLimiter:
|
|
|
170
172
|
|
|
171
173
|
class ErrorRecovery:
|
|
172
174
|
"""Error recovery with retries and fallback."""
|
|
173
|
-
|
|
175
|
+
|
|
174
176
|
def __init__(self, rate_limiter: RateLimiter = None):
|
|
175
177
|
"""Initialize error recovery."""
|
|
176
178
|
self.rate_limiter = rate_limiter or RateLimiter()
|
|
177
179
|
self.fallback_handlers: Dict[type, Callable] = {}
|
|
178
|
-
|
|
180
|
+
|
|
179
181
|
def register_fallback(self, error_type: type, handler: Callable):
|
|
180
182
|
"""Register a fallback handler for an error type."""
|
|
181
183
|
self.fallback_handlers[error_type] = handler
|
|
182
|
-
|
|
184
|
+
|
|
183
185
|
async def with_retry(
|
|
184
186
|
self,
|
|
185
187
|
func: Callable,
|
|
186
188
|
*args,
|
|
187
189
|
key: str = "default",
|
|
188
190
|
max_retries: Optional[int] = None,
|
|
189
|
-
**kwargs
|
|
191
|
+
**kwargs,
|
|
190
192
|
) -> Any:
|
|
191
193
|
"""
|
|
192
194
|
Execute function with retry logic.
|
|
193
195
|
"""
|
|
194
196
|
max_retries = max_retries or self.rate_limiter.config.max_retries
|
|
195
197
|
last_error = None
|
|
196
|
-
|
|
198
|
+
|
|
197
199
|
for attempt in range(max_retries):
|
|
198
200
|
try:
|
|
199
201
|
# Check rate limit
|
|
200
202
|
await self.rate_limiter.acquire(key)
|
|
201
|
-
|
|
203
|
+
|
|
202
204
|
# Execute function
|
|
203
205
|
result = await func(*args, **kwargs)
|
|
204
|
-
|
|
206
|
+
|
|
205
207
|
# Record success
|
|
206
208
|
self.rate_limiter.record_success(key)
|
|
207
|
-
|
|
209
|
+
|
|
208
210
|
return result
|
|
209
|
-
|
|
211
|
+
|
|
210
212
|
except Exception as e:
|
|
211
213
|
last_error = e
|
|
212
214
|
self.rate_limiter.record_error(key, e)
|
|
213
|
-
|
|
215
|
+
|
|
214
216
|
# Check for fallback handler
|
|
215
217
|
for error_type, handler in self.fallback_handlers.items():
|
|
216
218
|
if isinstance(e, error_type):
|
|
@@ -218,20 +220,20 @@ class ErrorRecovery:
|
|
|
218
220
|
return await handler(*args, **kwargs)
|
|
219
221
|
except:
|
|
220
222
|
pass # Fallback failed, continue with retry
|
|
221
|
-
|
|
223
|
+
|
|
222
224
|
# Calculate backoff
|
|
223
225
|
if attempt < max_retries - 1:
|
|
224
|
-
backoff = self.rate_limiter.config.backoff_base
|
|
225
|
-
|
|
226
|
+
backoff = self.rate_limiter.config.backoff_base**attempt
|
|
227
|
+
|
|
226
228
|
# Add jitter if configured
|
|
227
229
|
if self.rate_limiter.config.jitter:
|
|
228
|
-
backoff *=
|
|
229
|
-
|
|
230
|
+
backoff *= 0.5 + random.random()
|
|
231
|
+
|
|
230
232
|
await asyncio.sleep(min(backoff, 60)) # Max 60 seconds
|
|
231
|
-
|
|
233
|
+
|
|
232
234
|
# All retries failed
|
|
233
235
|
raise last_error or Exception("All retry attempts failed")
|
|
234
|
-
|
|
236
|
+
|
|
235
237
|
async def with_circuit_breaker(
|
|
236
238
|
self,
|
|
237
239
|
func: Callable,
|
|
@@ -239,21 +241,21 @@ class ErrorRecovery:
|
|
|
239
241
|
key: str = "default",
|
|
240
242
|
threshold: int = 5,
|
|
241
243
|
timeout: int = 60,
|
|
242
|
-
**kwargs
|
|
244
|
+
**kwargs,
|
|
243
245
|
) -> Any:
|
|
244
246
|
"""
|
|
245
247
|
Execute function with circuit breaker pattern.
|
|
246
248
|
"""
|
|
247
249
|
state = self.rate_limiter.get_state(key)
|
|
248
|
-
|
|
250
|
+
|
|
249
251
|
# Check if circuit is open
|
|
250
252
|
if state.is_throttled:
|
|
251
253
|
raise Exception(f"Circuit breaker open for {key}")
|
|
252
|
-
|
|
254
|
+
|
|
253
255
|
try:
|
|
254
256
|
result = await self.with_retry(func, *args, key=key, **kwargs)
|
|
255
257
|
return result
|
|
256
|
-
|
|
258
|
+
|
|
257
259
|
except Exception as e:
|
|
258
260
|
# Check if we should open the circuit
|
|
259
261
|
if state.consecutive_errors >= threshold:
|
|
@@ -265,61 +267,44 @@ class ErrorRecovery:
|
|
|
265
267
|
|
|
266
268
|
class SmartRateLimiter:
|
|
267
269
|
"""Smart rate limiter that adapts to API responses."""
|
|
268
|
-
|
|
270
|
+
|
|
269
271
|
def __init__(self):
|
|
270
272
|
"""Initialize smart rate limiter."""
|
|
271
273
|
self.limiters: Dict[str, RateLimiter] = {}
|
|
272
274
|
self.recovery = ErrorRecovery()
|
|
273
|
-
|
|
275
|
+
|
|
274
276
|
# Default configs for known APIs
|
|
275
277
|
self.configs = {
|
|
276
278
|
"openai": RateLimitConfig(
|
|
277
|
-
requests_per_minute=60,
|
|
278
|
-
requests_per_hour=1000,
|
|
279
|
-
burst_size=10
|
|
279
|
+
requests_per_minute=60, requests_per_hour=1000, burst_size=10
|
|
280
280
|
),
|
|
281
281
|
"anthropic": RateLimitConfig(
|
|
282
|
-
requests_per_minute=50,
|
|
283
|
-
requests_per_hour=1000,
|
|
284
|
-
burst_size=5
|
|
282
|
+
requests_per_minute=50, requests_per_hour=1000, burst_size=5
|
|
285
283
|
),
|
|
286
284
|
"local": RateLimitConfig(
|
|
287
|
-
requests_per_minute=100,
|
|
288
|
-
requests_per_hour=10000,
|
|
289
|
-
burst_size=20
|
|
285
|
+
requests_per_minute=100, requests_per_hour=10000, burst_size=20
|
|
290
286
|
),
|
|
291
287
|
"free": RateLimitConfig(
|
|
292
|
-
requests_per_minute=10,
|
|
293
|
-
requests_per_hour=100,
|
|
294
|
-
burst_size=2
|
|
288
|
+
requests_per_minute=10, requests_per_hour=100, burst_size=2
|
|
295
289
|
),
|
|
296
290
|
}
|
|
297
|
-
|
|
291
|
+
|
|
298
292
|
def get_limiter(self, api_type: str) -> RateLimiter:
|
|
299
293
|
"""Get or create limiter for API type."""
|
|
300
294
|
if api_type not in self.limiters:
|
|
301
295
|
config = self.configs.get(api_type, RateLimitConfig())
|
|
302
296
|
self.limiters[api_type] = RateLimiter(config)
|
|
303
297
|
return self.limiters[api_type]
|
|
304
|
-
|
|
298
|
+
|
|
305
299
|
async def execute_with_limit(
|
|
306
|
-
self,
|
|
307
|
-
api_type: str,
|
|
308
|
-
func: Callable,
|
|
309
|
-
*args,
|
|
310
|
-
**kwargs
|
|
300
|
+
self, api_type: str, func: Callable, *args, **kwargs
|
|
311
301
|
) -> Any:
|
|
312
302
|
"""Execute function with appropriate rate limiting."""
|
|
313
303
|
limiter = self.get_limiter(api_type)
|
|
314
304
|
recovery = ErrorRecovery(limiter)
|
|
315
|
-
|
|
316
|
-
return await recovery.with_retry(
|
|
317
|
-
|
|
318
|
-
*args,
|
|
319
|
-
key=api_type,
|
|
320
|
-
**kwargs
|
|
321
|
-
)
|
|
322
|
-
|
|
305
|
+
|
|
306
|
+
return await recovery.with_retry(func, *args, key=api_type, **kwargs)
|
|
307
|
+
|
|
323
308
|
def get_all_status(self) -> Dict[str, Dict[str, Any]]:
|
|
324
309
|
"""Get status of all limiters."""
|
|
325
310
|
return {
|
|
@@ -329,4 +314,4 @@ class SmartRateLimiter:
|
|
|
329
314
|
|
|
330
315
|
|
|
331
316
|
# Global instance for easy use
|
|
332
|
-
smart_limiter = SmartRateLimiter()
|
|
317
|
+
smart_limiter = SmartRateLimiter()
|