Spaces:
Paused
Paused
Merge branch 'Antigravity'
Browse files
src/rotator_library/client.py
CHANGED
|
@@ -620,8 +620,9 @@ class RotatingClient:
|
|
| 620 |
litellm.ServiceUnavailableError,
|
| 621 |
litellm.InternalServerError,
|
| 622 |
APIConnectionError,
|
|
|
|
| 623 |
) as e:
|
| 624 |
-
# This is a critical, typed error from litellm that signals a key failure.
|
| 625 |
# We do not try to parse it here. We wrap it and raise it immediately
|
| 626 |
# for the outer retry loop to handle.
|
| 627 |
lib_logger.warning(
|
|
@@ -1065,7 +1066,10 @@ class RotatingClient:
|
|
| 1065 |
)
|
| 1066 |
|
| 1067 |
# Only trigger provider-wide cooldown for rate limits, not quota issues
|
| 1068 |
-
if
|
|
|
|
|
|
|
|
|
|
| 1069 |
cooldown_duration = classified_error.retry_after or 60
|
| 1070 |
await self.cooldown_manager.start_cooldown(
|
| 1071 |
provider, cooldown_duration
|
|
@@ -1225,9 +1229,9 @@ class RotatingClient:
|
|
| 1225 |
|
| 1226 |
# Handle rate limits with cooldown (exclude quota_exceeded from provider-wide cooldown)
|
| 1227 |
if (
|
| 1228 |
-
|
| 1229 |
-
|
| 1230 |
-
):
|
| 1231 |
cooldown_duration = classified_error.retry_after or 60
|
| 1232 |
await self.cooldown_manager.start_cooldown(
|
| 1233 |
provider, cooldown_duration
|
|
@@ -1494,7 +1498,7 @@ class RotatingClient:
|
|
| 1494 |
lib_logger.info(
|
| 1495 |
f"Attempting stream with credential {mask_credential(current_cred)} (Attempt {attempt + 1}/{self.max_retries})"
|
| 1496 |
)
|
| 1497 |
-
|
| 1498 |
if pre_request_callback:
|
| 1499 |
try:
|
| 1500 |
await pre_request_callback(
|
|
@@ -1973,9 +1977,9 @@ class RotatingClient:
|
|
| 1973 |
|
| 1974 |
# Handle rate limits with cooldown (exclude quota_exceeded)
|
| 1975 |
if (
|
| 1976 |
-
|
| 1977 |
-
|
| 1978 |
-
):
|
| 1979 |
cooldown_duration = classified_error.retry_after or 60
|
| 1980 |
await self.cooldown_manager.start_cooldown(
|
| 1981 |
provider, cooldown_duration
|
|
|
|
| 620 |
litellm.ServiceUnavailableError,
|
| 621 |
litellm.InternalServerError,
|
| 622 |
APIConnectionError,
|
| 623 |
+
httpx.HTTPStatusError,
|
| 624 |
) as e:
|
| 625 |
+
# This is a critical, typed error from litellm or httpx that signals a key failure.
|
| 626 |
# We do not try to parse it here. We wrap it and raise it immediately
|
| 627 |
# for the outer retry loop to handle.
|
| 628 |
lib_logger.warning(
|
|
|
|
| 1066 |
)
|
| 1067 |
|
| 1068 |
# Only trigger provider-wide cooldown for rate limits, not quota issues
|
| 1069 |
+
if (
|
| 1070 |
+
classified_error.status_code == 429
|
| 1071 |
+
and classified_error.error_type != "quota_exceeded"
|
| 1072 |
+
):
|
| 1073 |
cooldown_duration = classified_error.retry_after or 60
|
| 1074 |
await self.cooldown_manager.start_cooldown(
|
| 1075 |
provider, cooldown_duration
|
|
|
|
| 1229 |
|
| 1230 |
# Handle rate limits with cooldown (exclude quota_exceeded from provider-wide cooldown)
|
| 1231 |
if (
|
| 1232 |
+
classified_error.status_code == 429
|
| 1233 |
+
and classified_error.error_type != "quota_exceeded"
|
| 1234 |
+
) or classified_error.error_type == "rate_limit":
|
| 1235 |
cooldown_duration = classified_error.retry_after or 60
|
| 1236 |
await self.cooldown_manager.start_cooldown(
|
| 1237 |
provider, cooldown_duration
|
|
|
|
| 1498 |
lib_logger.info(
|
| 1499 |
f"Attempting stream with credential {mask_credential(current_cred)} (Attempt {attempt + 1}/{self.max_retries})"
|
| 1500 |
)
|
| 1501 |
+
|
| 1502 |
if pre_request_callback:
|
| 1503 |
try:
|
| 1504 |
await pre_request_callback(
|
|
|
|
| 1977 |
|
| 1978 |
# Handle rate limits with cooldown (exclude quota_exceeded)
|
| 1979 |
if (
|
| 1980 |
+
classified_error.status_code == 429
|
| 1981 |
+
and classified_error.error_type != "quota_exceeded"
|
| 1982 |
+
) or classified_error.error_type == "rate_limit":
|
| 1983 |
cooldown_duration = classified_error.retry_after or 60
|
| 1984 |
await self.cooldown_manager.start_cooldown(
|
| 1985 |
provider, cooldown_duration
|
src/rotator_library/error_handler.py
CHANGED
|
@@ -18,12 +18,60 @@ from litellm.exceptions import (
|
|
| 18 |
)
|
| 19 |
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
def extract_retry_after_from_body(error_body: Optional[str]) -> Optional[int]:
|
| 22 |
"""
|
| 23 |
Extract the retry-after time from an API error response body.
|
| 24 |
|
| 25 |
Handles various error formats including:
|
| 26 |
- Gemini CLI: "Your quota will reset after 39s."
|
|
|
|
| 27 |
- Generic: "quota will reset after 120s", "retry after 60s"
|
| 28 |
|
| 29 |
Args:
|
|
@@ -35,21 +83,21 @@ def extract_retry_after_from_body(error_body: Optional[str]) -> Optional[int]:
|
|
| 35 |
if not error_body:
|
| 36 |
return None
|
| 37 |
|
| 38 |
-
# Pattern to match various "reset after
|
| 39 |
patterns = [
|
| 40 |
-
r"quota will reset after\s*(\
|
| 41 |
-
r"reset after\s*(\
|
| 42 |
-
r"retry after\s*(\
|
| 43 |
r"try again in\s*(\d+)\s*seconds?",
|
| 44 |
]
|
| 45 |
|
| 46 |
for pattern in patterns:
|
| 47 |
match = re.search(pattern, error_body, re.IGNORECASE)
|
| 48 |
if match:
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
|
| 54 |
return None
|
| 55 |
|
|
@@ -306,14 +354,91 @@ class ClassifiedError:
|
|
| 306 |
return f"ClassifiedError(type={self.error_type}, status={self.status_code}, retry_after={self.retry_after}, original_exc={self.original_exception})"
|
| 307 |
|
| 308 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
def get_retry_after(error: Exception) -> Optional[int]:
|
| 310 |
"""
|
| 311 |
Extracts the 'retry-after' duration in seconds from an exception message.
|
| 312 |
Handles both integer and string representations of the duration, as well as JSON bodies.
|
| 313 |
Also checks HTTP response headers for httpx.HTTPStatusError instances.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
"""
|
| 315 |
-
# 0. For httpx errors, check response
|
| 316 |
if isinstance(error, httpx.HTTPStatusError):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
headers = error.response.headers
|
| 318 |
# Check standard Retry-After header (case-insensitive)
|
| 319 |
retry_header = headers.get("retry-after") or headers.get("Retry-After")
|
|
@@ -339,81 +464,51 @@ def get_retry_after(error: Exception) -> Optional[int]:
|
|
| 339 |
except (ValueError, TypeError):
|
| 340 |
pass
|
| 341 |
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
-
#
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
json_match = re.search(r"(\{.*\})", error_str, re.DOTALL)
|
| 348 |
-
if json_match:
|
| 349 |
-
error_json = json.loads(json_match.group(1))
|
| 350 |
-
retry_info = error_json.get("error", {}).get("details", [{}])[0]
|
| 351 |
-
if retry_info.get("@type") == "type.googleapis.com/google.rpc.RetryInfo":
|
| 352 |
-
delay_str = retry_info.get("retryDelay", {}).get("seconds")
|
| 353 |
-
if delay_str:
|
| 354 |
-
return int(delay_str)
|
| 355 |
-
# Fallback for the other format
|
| 356 |
-
delay_str = retry_info.get("retryDelay")
|
| 357 |
-
if isinstance(delay_str, str) and delay_str.endswith("s"):
|
| 358 |
-
return int(delay_str[:-1])
|
| 359 |
-
|
| 360 |
-
except (json.JSONDecodeError, IndexError, KeyError, TypeError):
|
| 361 |
-
pass # If JSON parsing fails, proceed to regex and attribute checks
|
| 362 |
-
|
| 363 |
-
# 2. Common regex patterns for 'retry-after' (with duration format support)
|
| 364 |
patterns = [
|
| 365 |
r"retry[-_\s]after:?\s*(\d+)", # Matches: retry-after, retry_after, retry after
|
| 366 |
r"retry in\s*(\d+)\s*seconds?",
|
| 367 |
r"wait for\s*(\d+)\s*seconds?",
|
| 368 |
-
r'"
|
| 369 |
r"x-ratelimit-reset:?\s*(\d+)",
|
| 370 |
-
|
| 371 |
-
r"reset after\s*(\
|
|
|
|
|
|
|
| 372 |
]
|
| 373 |
|
| 374 |
for pattern in patterns:
|
| 375 |
-
match = re.search(pattern,
|
| 376 |
if match:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
try:
|
| 378 |
-
return int(
|
| 379 |
except (ValueError, IndexError):
|
| 380 |
continue
|
| 381 |
|
| 382 |
-
# 3. Handle
|
| 383 |
-
duration_match = re.search(r"(\d+)\s*([smh])", error_str)
|
| 384 |
-
if duration_match:
|
| 385 |
-
try:
|
| 386 |
-
value = int(duration_match.group(1))
|
| 387 |
-
unit = duration_match.group(2)
|
| 388 |
-
if unit == "s":
|
| 389 |
-
return value
|
| 390 |
-
elif unit == "m":
|
| 391 |
-
return value * 60
|
| 392 |
-
elif unit == "h":
|
| 393 |
-
return value * 3600
|
| 394 |
-
except (ValueError, IndexError):
|
| 395 |
-
pass
|
| 396 |
-
|
| 397 |
-
# 4. Handle cases where the error object itself has the attribute
|
| 398 |
if hasattr(error, "retry_after"):
|
| 399 |
value = getattr(error, "retry_after")
|
| 400 |
if isinstance(value, int):
|
| 401 |
return value
|
| 402 |
if isinstance(value, str):
|
| 403 |
-
|
| 404 |
-
if
|
| 405 |
-
return
|
| 406 |
-
# Handle "60s", "2m" format in attribute
|
| 407 |
-
duration_match = re.search(r"(\d+)\s*([smh])", value.lower())
|
| 408 |
-
if duration_match:
|
| 409 |
-
val = int(duration_match.group(1))
|
| 410 |
-
unit = duration_match.group(2)
|
| 411 |
-
if unit == "s":
|
| 412 |
-
return val
|
| 413 |
-
elif unit == "m":
|
| 414 |
-
return val * 60
|
| 415 |
-
elif unit == "h":
|
| 416 |
-
return val * 3600
|
| 417 |
|
| 418 |
return None
|
| 419 |
|
|
|
|
| 18 |
)
|
| 19 |
|
| 20 |
|
| 21 |
+
def _parse_duration_string(duration_str: str) -> Optional[int]:
|
| 22 |
+
"""
|
| 23 |
+
Parse duration strings in various formats to total seconds.
|
| 24 |
+
|
| 25 |
+
Handles:
|
| 26 |
+
- Compound durations: '156h14m36.752463453s', '2h30m', '45m30s'
|
| 27 |
+
- Simple durations: '562476.752463453s', '3600s', '60m', '2h'
|
| 28 |
+
- Plain seconds (no unit): '562476'
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
duration_str: Duration string to parse
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
Total seconds as integer, or None if parsing fails
|
| 35 |
+
"""
|
| 36 |
+
if not duration_str:
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
total_seconds = 0
|
| 40 |
+
remaining = duration_str.strip().lower()
|
| 41 |
+
|
| 42 |
+
# Try parsing as plain number first (no units)
|
| 43 |
+
try:
|
| 44 |
+
return int(float(remaining))
|
| 45 |
+
except ValueError:
|
| 46 |
+
pass
|
| 47 |
+
|
| 48 |
+
# Parse hours component
|
| 49 |
+
hour_match = re.match(r"(\d+)h", remaining)
|
| 50 |
+
if hour_match:
|
| 51 |
+
total_seconds += int(hour_match.group(1)) * 3600
|
| 52 |
+
remaining = remaining[hour_match.end() :]
|
| 53 |
+
|
| 54 |
+
# Parse minutes component
|
| 55 |
+
min_match = re.match(r"(\d+)m", remaining)
|
| 56 |
+
if min_match:
|
| 57 |
+
total_seconds += int(min_match.group(1)) * 60
|
| 58 |
+
remaining = remaining[min_match.end() :]
|
| 59 |
+
|
| 60 |
+
# Parse seconds component (including decimals like 36.752463453s)
|
| 61 |
+
sec_match = re.match(r"([\d.]+)s", remaining)
|
| 62 |
+
if sec_match:
|
| 63 |
+
total_seconds += int(float(sec_match.group(1)))
|
| 64 |
+
|
| 65 |
+
return total_seconds if total_seconds > 0 else None
|
| 66 |
+
|
| 67 |
+
|
| 68 |
def extract_retry_after_from_body(error_body: Optional[str]) -> Optional[int]:
|
| 69 |
"""
|
| 70 |
Extract the retry-after time from an API error response body.
|
| 71 |
|
| 72 |
Handles various error formats including:
|
| 73 |
- Gemini CLI: "Your quota will reset after 39s."
|
| 74 |
+
- Antigravity: "quota will reset after 156h14m36s"
|
| 75 |
- Generic: "quota will reset after 120s", "retry after 60s"
|
| 76 |
|
| 77 |
Args:
|
|
|
|
| 83 |
if not error_body:
|
| 84 |
return None
|
| 85 |
|
| 86 |
+
# Pattern to match various "reset after" formats - capture the full duration string
|
| 87 |
patterns = [
|
| 88 |
+
r"quota will reset after\s*([\dhmso.]+)", # Matches compound: 156h14m36s or 120s
|
| 89 |
+
r"reset after\s*([\dhmso.]+)",
|
| 90 |
+
r"retry after\s*([\dhmso.]+)",
|
| 91 |
r"try again in\s*(\d+)\s*seconds?",
|
| 92 |
]
|
| 93 |
|
| 94 |
for pattern in patterns:
|
| 95 |
match = re.search(pattern, error_body, re.IGNORECASE)
|
| 96 |
if match:
|
| 97 |
+
duration_str = match.group(1)
|
| 98 |
+
result = _parse_duration_string(duration_str)
|
| 99 |
+
if result is not None:
|
| 100 |
+
return result
|
| 101 |
|
| 102 |
return None
|
| 103 |
|
|
|
|
| 354 |
return f"ClassifiedError(type={self.error_type}, status={self.status_code}, retry_after={self.retry_after}, original_exc={self.original_exception})"
|
| 355 |
|
| 356 |
|
| 357 |
+
def _extract_retry_from_json_body(json_text: str) -> Optional[int]:
|
| 358 |
+
"""
|
| 359 |
+
Extract retry delay from a JSON error response body.
|
| 360 |
+
|
| 361 |
+
Handles Antigravity/Google API error formats with details array containing:
|
| 362 |
+
- RetryInfo with retryDelay: "562476.752463453s"
|
| 363 |
+
- ErrorInfo metadata with quotaResetDelay: "156h14m36.752463453s"
|
| 364 |
+
|
| 365 |
+
Args:
|
| 366 |
+
json_text: JSON string (original case, not lowercased)
|
| 367 |
+
|
| 368 |
+
Returns:
|
| 369 |
+
Retry delay in seconds, or None if not found
|
| 370 |
+
"""
|
| 371 |
+
try:
|
| 372 |
+
# Find JSON object in the text
|
| 373 |
+
json_match = re.search(r"(\{.*\})", json_text, re.DOTALL)
|
| 374 |
+
if not json_match:
|
| 375 |
+
return None
|
| 376 |
+
|
| 377 |
+
error_json = json.loads(json_match.group(1))
|
| 378 |
+
details = error_json.get("error", {}).get("details", [])
|
| 379 |
+
|
| 380 |
+
# Iterate through ALL details items (not just index 0)
|
| 381 |
+
for detail in details:
|
| 382 |
+
detail_type = detail.get("@type", "")
|
| 383 |
+
|
| 384 |
+
# Check RetryInfo for retryDelay (most authoritative)
|
| 385 |
+
# Note: Case-sensitive key names as returned by API
|
| 386 |
+
if "google.rpc.RetryInfo" in detail_type:
|
| 387 |
+
delay_str = detail.get("retryDelay")
|
| 388 |
+
if delay_str:
|
| 389 |
+
# Handle both {"seconds": "123"} format and "123.456s" string format
|
| 390 |
+
if isinstance(delay_str, dict):
|
| 391 |
+
seconds = delay_str.get("seconds")
|
| 392 |
+
if seconds:
|
| 393 |
+
return int(float(seconds))
|
| 394 |
+
elif isinstance(delay_str, str):
|
| 395 |
+
result = _parse_duration_string(delay_str)
|
| 396 |
+
if result is not None:
|
| 397 |
+
return result
|
| 398 |
+
|
| 399 |
+
# Check ErrorInfo metadata for quotaResetDelay (Antigravity-specific)
|
| 400 |
+
if "google.rpc.ErrorInfo" in detail_type:
|
| 401 |
+
metadata = detail.get("metadata", {})
|
| 402 |
+
# Try both camelCase and lowercase variants
|
| 403 |
+
quota_reset_delay = metadata.get("quotaResetDelay") or metadata.get(
|
| 404 |
+
"quotaresetdelay"
|
| 405 |
+
)
|
| 406 |
+
if quota_reset_delay:
|
| 407 |
+
result = _parse_duration_string(quota_reset_delay)
|
| 408 |
+
if result is not None:
|
| 409 |
+
return result
|
| 410 |
+
|
| 411 |
+
except (json.JSONDecodeError, IndexError, KeyError, TypeError):
|
| 412 |
+
pass
|
| 413 |
+
|
| 414 |
+
return None
|
| 415 |
+
|
| 416 |
+
|
| 417 |
def get_retry_after(error: Exception) -> Optional[int]:
|
| 418 |
"""
|
| 419 |
Extracts the 'retry-after' duration in seconds from an exception message.
|
| 420 |
Handles both integer and string representations of the duration, as well as JSON bodies.
|
| 421 |
Also checks HTTP response headers for httpx.HTTPStatusError instances.
|
| 422 |
+
|
| 423 |
+
Supports Antigravity/Google API error formats:
|
| 424 |
+
- RetryInfo with retryDelay: "562476.752463453s"
|
| 425 |
+
- ErrorInfo metadata with quotaResetDelay: "156h14m36.752463453s"
|
| 426 |
+
- Human-readable message: "quota will reset after 156h14m36s"
|
| 427 |
"""
|
| 428 |
+
# 0. For httpx errors, check response body and headers
|
| 429 |
if isinstance(error, httpx.HTTPStatusError):
|
| 430 |
+
# First, try to parse the response body JSON (contains retryDelay/quotaResetDelay)
|
| 431 |
+
# This is where Antigravity puts the retry information
|
| 432 |
+
try:
|
| 433 |
+
response_text = error.response.text
|
| 434 |
+
if response_text:
|
| 435 |
+
result = _extract_retry_from_json_body(response_text)
|
| 436 |
+
if result is not None:
|
| 437 |
+
return result
|
| 438 |
+
except Exception:
|
| 439 |
+
pass # Response body may not be available
|
| 440 |
+
|
| 441 |
+
# Fallback to HTTP headers
|
| 442 |
headers = error.response.headers
|
| 443 |
# Check standard Retry-After header (case-insensitive)
|
| 444 |
retry_header = headers.get("retry-after") or headers.get("Retry-After")
|
|
|
|
| 464 |
except (ValueError, TypeError):
|
| 465 |
pass
|
| 466 |
|
| 467 |
+
# 1. Try to parse JSON from the error string representation
|
| 468 |
+
# Some exceptions embed JSON in their string representation
|
| 469 |
+
error_str = str(error)
|
| 470 |
+
result = _extract_retry_from_json_body(error_str)
|
| 471 |
+
if result is not None:
|
| 472 |
+
return result
|
| 473 |
|
| 474 |
+
# 2. Common regex patterns for 'retry-after' (with compound duration support)
|
| 475 |
+
# Use lowercase for pattern matching
|
| 476 |
+
error_str_lower = error_str.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
patterns = [
|
| 478 |
r"retry[-_\s]after:?\s*(\d+)", # Matches: retry-after, retry_after, retry after
|
| 479 |
r"retry in\s*(\d+)\s*seconds?",
|
| 480 |
r"wait for\s*(\d+)\s*seconds?",
|
| 481 |
+
r'"retrydelay":\s*"([\d.]+)s?"', # retryDelay in JSON (lowercased)
|
| 482 |
r"x-ratelimit-reset:?\s*(\d+)",
|
| 483 |
+
# Compound duration patterns (Antigravity format)
|
| 484 |
+
r"quota will reset after\s*([\dhms.]+)", # e.g., "156h14m36s" or "120s"
|
| 485 |
+
r"reset after\s*([\dhms.]+)",
|
| 486 |
+
r'"quotaresetdelay":\s*"([\dhms.]+)"', # quotaResetDelay in JSON (lowercased)
|
| 487 |
]
|
| 488 |
|
| 489 |
for pattern in patterns:
|
| 490 |
+
match = re.search(pattern, error_str_lower)
|
| 491 |
if match:
|
| 492 |
+
duration_str = match.group(1)
|
| 493 |
+
# Try parsing as compound duration first
|
| 494 |
+
result = _parse_duration_string(duration_str)
|
| 495 |
+
if result is not None:
|
| 496 |
+
return result
|
| 497 |
+
# Fallback to simple integer
|
| 498 |
try:
|
| 499 |
+
return int(duration_str)
|
| 500 |
except (ValueError, IndexError):
|
| 501 |
continue
|
| 502 |
|
| 503 |
+
# 3. Handle cases where the error object itself has the attribute
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
if hasattr(error, "retry_after"):
|
| 505 |
value = getattr(error, "retry_after")
|
| 506 |
if isinstance(value, int):
|
| 507 |
return value
|
| 508 |
if isinstance(value, str):
|
| 509 |
+
result = _parse_duration_string(value)
|
| 510 |
+
if result is not None:
|
| 511 |
+
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
|
| 513 |
return None
|
| 514 |
|
src/rotator_library/providers/antigravity_provider.py
CHANGED
|
@@ -1919,15 +1919,18 @@ class AntigravityProvider(AntigravityAuthBase, ProviderInterface):
|
|
| 1919 |
system_instruction = None
|
| 1920 |
gemini_contents = []
|
| 1921 |
|
| 1922 |
-
# Extract system
|
| 1923 |
-
|
|
|
|
| 1924 |
system_content = messages.pop(0).get("content", "")
|
| 1925 |
if system_content:
|
| 1926 |
-
|
| 1927 |
system_content, _strip_cache_control=True
|
| 1928 |
)
|
| 1929 |
-
|
| 1930 |
-
|
|
|
|
|
|
|
| 1931 |
|
| 1932 |
# Build tool_call_id → name mapping
|
| 1933 |
tool_id_to_name = {}
|
|
|
|
| 1919 |
system_instruction = None
|
| 1920 |
gemini_contents = []
|
| 1921 |
|
| 1922 |
+
# Extract system prompts (handle multiple consecutive system messages)
|
| 1923 |
+
system_parts = []
|
| 1924 |
+
while messages and messages[0].get("role") == "system":
|
| 1925 |
system_content = messages.pop(0).get("content", "")
|
| 1926 |
if system_content:
|
| 1927 |
+
new_parts = self._parse_content_parts(
|
| 1928 |
system_content, _strip_cache_control=True
|
| 1929 |
)
|
| 1930 |
+
system_parts.extend(new_parts)
|
| 1931 |
+
|
| 1932 |
+
if system_parts:
|
| 1933 |
+
system_instruction = {"role": "user", "parts": system_parts}
|
| 1934 |
|
| 1935 |
# Build tool_call_id → name mapping
|
| 1936 |
tool_id_to_name = {}
|