CoderAI: warm-up wait-and-retry on all paths + multi-hour request timeout

Move the CoderAI broker warm-up handling into the provider so it applies on
every request path (rotation, autoselect and direct), and only for the
broker-session "cooling down" error:

- coderai.py: handle_request now wraps the broker decision in a warm-up retry
  loop — on "No active CoderAI broker session" it waits CODERAI_WARMUP_WAIT_
  SECONDS (10s) and retries the same provider up to CODERAI_MAX_WARMUP_WAITS
  (3) times, then surfaces the error WITHOUT recording a failure. Streaming
  and native-proxy paths get the same treatment via _broker_request_with_warmup
  (lazy generators retry on first broker contact). Other errors still record a
  failure as before.
- handlers.py: every place that could disable a provider on a caught error now
  skips record_failure() for the CoderAI warm-up condition (direct chat, direct
  streaming, rotation streaming, audio/TTS/image/embeddings, and the rotation
  retry loop), via the shared _is_coderai_warmup_error() helper. The rotation
  loop no longer sleeps itself (the provider already waited) — it just fails
  over without recording a failure.
- Default CoderAI request timeout raised from 5 min to 3 hours
  (CODERAI_DEFAULT_REQUEST_TIMEOUT, overridable via coderai_config.request_
  timeout); the timeout is also applied to the direct OpenAI client.

Bump version to 0.99.77.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 578eeb9d
...@@ -55,7 +55,7 @@ from .auth.qwen import QwenOAuth2 ...@@ -55,7 +55,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.99.76" __version__ = "0.99.77"
__all__ = [ __all__ = [
# Config # Config
"config", "config",
......
...@@ -65,13 +65,16 @@ from .prompt_analysis import analyze_prompt_payload ...@@ -65,13 +65,16 @@ from .prompt_analysis import analyze_prompt_payload
_autoselect_result_cache: dict = {} _autoselect_result_cache: dict = {}
_autoselect_result_cache_ttl: int = 3600 # seconds _autoselect_result_cache_ttl: int = 3600 # seconds
# CoderAI providers often run on small/edge hardware that drops its broker session
# while cooling down. Treat a missing broker session as a transient "warming up" def _is_coderai_warmup_error(error) -> bool:
# state: wait CODERAI_WARMUP_WAIT_SECONDS and retry the same provider, up to """True when the error is just a missing CoderAI broker session (worker warming up).
# CODERAI_MAX_WARMUP_WAITS times, instead of recording a failure (which would
# disable the provider for a long cooldown). CoderAI workers often run on small/edge hardware that drops the broker session
CODERAI_WARMUP_WAIT_SECONDS: float = 10.0 while cooling down. That is transient, not a provider fault: callers must not
CODERAI_MAX_WARMUP_WAITS: int = 3 record a failure for it (which would disable the provider for a long cooldown).
The marker string is CoderAI-specific, so this is safe to check on any path.
"""
return 'No active CoderAI broker session' in str(error or '')
# Registry for proxied content — shared across all RequestHandler instances. # Registry for proxied content — shared across all RequestHandler instances.
# Values: {"type": "broker", "provider_id": str, "path": str} # Values: {"type": "broker", "provider_id": str, "path": str}
...@@ -942,8 +945,10 @@ class RequestHandler: ...@@ -942,8 +945,10 @@ class RequestHandler:
logger.info(f"=== RequestHandler.handle_chat_completion END ===") logger.info(f"=== RequestHandler.handle_chat_completion END ===")
return response return response
except Exception as e: except Exception as e:
handler.record_failure() # CoderAI warm-up (no broker session) is transient — don't disable it.
if not _is_coderai_warmup_error(e):
handler.record_failure()
# Record failed request analytics # Record failed request analytics
try: try:
analytics = get_analytics() analytics = get_analytics()
...@@ -1666,8 +1671,10 @@ class RequestHandler: ...@@ -1666,8 +1671,10 @@ class RequestHandler:
logger.warning(f"Analytics recording for streaming request failed: {analytics_error}") logger.warning(f"Analytics recording for streaming request failed: {analytics_error}")
except Exception as e: except Exception as e:
handler.record_failure() # CoderAI warm-up (no broker session) is transient — don't disable it.
if not _is_coderai_warmup_error(e):
handler.record_failure()
# Record analytics for failed streaming request # Record analytics for failed streaming request
try: try:
analytics = get_analytics() analytics = get_analytics()
...@@ -2162,7 +2169,8 @@ class RequestHandler: ...@@ -2162,7 +2169,8 @@ class RequestHandler:
logger.warning(f"Market settlement failed for audio transcription {provider_id}: {market_error}") logger.warning(f"Market settlement failed for audio transcription {provider_id}: {market_error}")
return result return result
except Exception as e: except Exception as e:
handler.record_failure() if not _is_coderai_warmup_error(e):
handler.record_failure()
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
async def handle_text_to_speech(self, request: Request, provider_id: str, request_data: Dict) -> StreamingResponse: async def handle_text_to_speech(self, request: Request, provider_id: str, request_data: Dict) -> StreamingResponse:
...@@ -2196,7 +2204,8 @@ class RequestHandler: ...@@ -2196,7 +2204,8 @@ class RequestHandler:
logger.warning(f"Market settlement failed for text to speech {provider_id}: {market_error}") logger.warning(f"Market settlement failed for text to speech {provider_id}: {market_error}")
return result return result
except Exception as e: except Exception as e:
handler.record_failure() if not _is_coderai_warmup_error(e):
handler.record_failure()
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
async def handle_image_generation(self, request: Request, provider_id: str, request_data: Dict) -> Dict: async def handle_image_generation(self, request: Request, provider_id: str, request_data: Dict) -> Dict:
...@@ -2240,7 +2249,8 @@ class RequestHandler: ...@@ -2240,7 +2249,8 @@ class RequestHandler:
logger.warning(f"Market settlement failed for image generation {provider_id}: {market_error}") logger.warning(f"Market settlement failed for image generation {provider_id}: {market_error}")
return result return result
except Exception as e: except Exception as e:
handler.record_failure() if not _is_coderai_warmup_error(e):
handler.record_failure()
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
async def handle_embeddings(self, request: Request, provider_id: str, request_data: Dict) -> Dict: async def handle_embeddings(self, request: Request, provider_id: str, request_data: Dict) -> Dict:
...@@ -2274,7 +2284,8 @@ class RequestHandler: ...@@ -2274,7 +2284,8 @@ class RequestHandler:
logger.warning(f"Market settlement failed for embeddings {provider_id}: {market_error}") logger.warning(f"Market settlement failed for embeddings {provider_id}: {market_error}")
return result return result
except Exception as e: except Exception as e:
handler.record_failure() if not _is_coderai_warmup_error(e):
handler.record_failure()
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
def _rewrite_coderai_file_urls(self, body_str: str, provider_id: str, request: Request, *, via_broker: bool) -> str: def _rewrite_coderai_file_urls(self, body_str: str, provider_id: str, request: Request, *, via_broker: bool) -> str:
...@@ -3489,7 +3500,6 @@ class RotationHandler: ...@@ -3489,7 +3500,6 @@ class RotationHandler:
max_retries = 5 max_retries = 5
tried_models = [] # Track which models have been tried tried_models = [] # Track which models have been tried
model_retry_counts = {} # Track retry count per model model_retry_counts = {} # Track retry count per model
coderai_warmup_counts = {} # Track CoderAI "warming up" waits per model
last_error = None last_error = None
successful_model = None successful_model = None
successful_handler = None successful_handler = None
...@@ -3909,26 +3919,15 @@ class RotationHandler: ...@@ -3909,26 +3919,15 @@ class RotationHandler:
last_error = str(e) last_error = str(e)
# CoderAI "warming up": the broker session is temporarily gone while # CoderAI "warming up": the broker session is temporarily gone while
# the (often small/edge) worker cools down. This is NOT a provider # the (often small/edge) worker cools down. The provider handler
# fault — do not record a failure (which would disable the provider for # already waited and retried internally; if it still failed, this is
# a long cooldown). Instead wait a few seconds and retry the same # NOT a provider fault — do not record a failure (which would disable
# provider, up to a bounded number of times, fully transparent to the # the provider for a long cooldown), just fail over to the next
# client. # provider, fully transparent to the client.
if self._is_coderai_warming_error(e, provider_id): if self._is_coderai_warming_error(e, provider_id):
warmup_count = coderai_warmup_counts.get(model_key, 0)
if warmup_count < CODERAI_MAX_WARMUP_WAITS:
coderai_warmup_counts[model_key] = warmup_count + 1
logger.warning(
f"CoderAI provider {provider_id} is warming up (no broker session); "
f"waiting {CODERAI_WARMUP_WAIT_SECONDS:.0f}s and retrying "
f"(warmup {warmup_count + 1}/{CODERAI_MAX_WARMUP_WAITS}) — not recorded as a failure"
)
await asyncio.sleep(CODERAI_WARMUP_WAIT_SECONDS)
continue
logger.warning( logger.warning(
f"CoderAI provider {provider_id} still warming up after " f"CoderAI provider {provider_id} still warming up after internal retries; "
f"{warmup_count} wait(s); failing over to the next provider " f"failing over to the next provider (not recorded as a failure)"
f"(still not recorded as a failure)"
) )
tried_models.append(current_model) tried_models.append(current_model)
continue continue
...@@ -5028,7 +5027,9 @@ class RotationHandler: ...@@ -5028,7 +5027,9 @@ class RotationHandler:
error_dict = {"error": str(e)} error_dict = {"error": str(e)}
yield f"data: {json.dumps(error_dict)}\n\n".encode('utf-8') yield f"data: {json.dumps(error_dict)}\n\n".encode('utf-8')
except Exception as e: except Exception as e:
handler.record_failure() # CoderAI warm-up (no broker session) is transient — don't disable it.
if not _is_coderai_warmup_error(e):
handler.record_failure()
latency_ms = (time.time() - request_start_time) * 1000 if request_start_time else 0 latency_ms = (time.time() - request_start_time) * 1000 if request_start_time else 0
self._record_dashboard_proxy_event( self._record_dashboard_proxy_event(
None, None,
......
This diff is collapsed.
...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" ...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aisbf" name = "aisbf"
version = "0.99.76" version = "0.99.77"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations" description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md" readme = "README.md"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
......
...@@ -106,7 +106,7 @@ class InstallCommand(_install): ...@@ -106,7 +106,7 @@ class InstallCommand(_install):
setup( setup(
name="aisbf", name="aisbf",
version="0.99.76", version="0.99.77",
author="AISBF Contributors", author="AISBF Contributors",
author_email="stefy@nexlab.net", author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations", description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment