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
from .handlers import RequestHandler, RotationHandler, AutoselectHandler
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__ = [
# Config
"config",
......
......@@ -65,13 +65,16 @@ from .prompt_analysis import analyze_prompt_payload
_autoselect_result_cache: dict = {}
_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"
# state: wait CODERAI_WARMUP_WAIT_SECONDS and retry the same provider, up to
# CODERAI_MAX_WARMUP_WAITS times, instead of recording a failure (which would
# disable the provider for a long cooldown).
CODERAI_WARMUP_WAIT_SECONDS: float = 10.0
CODERAI_MAX_WARMUP_WAITS: int = 3
def _is_coderai_warmup_error(error) -> bool:
"""True when the error is just a missing CoderAI broker session (worker warming up).
CoderAI workers often run on small/edge hardware that drops the broker session
while cooling down. That is transient, not a provider fault: callers must not
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.
# Values: {"type": "broker", "provider_id": str, "path": str}
......@@ -942,6 +945,8 @@ class RequestHandler:
logger.info(f"=== RequestHandler.handle_chat_completion END ===")
return response
except Exception as e:
# 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
......@@ -1666,6 +1671,8 @@ class RequestHandler:
logger.warning(f"Analytics recording for streaming request failed: {analytics_error}")
except Exception as e:
# 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
......@@ -2162,6 +2169,7 @@ class RequestHandler:
logger.warning(f"Market settlement failed for audio transcription {provider_id}: {market_error}")
return result
except Exception as e:
if not _is_coderai_warmup_error(e):
handler.record_failure()
raise HTTPException(status_code=500, detail=str(e))
......@@ -2196,6 +2204,7 @@ class RequestHandler:
logger.warning(f"Market settlement failed for text to speech {provider_id}: {market_error}")
return result
except Exception as e:
if not _is_coderai_warmup_error(e):
handler.record_failure()
raise HTTPException(status_code=500, detail=str(e))
......@@ -2240,6 +2249,7 @@ class RequestHandler:
logger.warning(f"Market settlement failed for image generation {provider_id}: {market_error}")
return result
except Exception as e:
if not _is_coderai_warmup_error(e):
handler.record_failure()
raise HTTPException(status_code=500, detail=str(e))
......@@ -2274,6 +2284,7 @@ class RequestHandler:
logger.warning(f"Market settlement failed for embeddings {provider_id}: {market_error}")
return result
except Exception as e:
if not _is_coderai_warmup_error(e):
handler.record_failure()
raise HTTPException(status_code=500, detail=str(e))
......@@ -3489,7 +3500,6 @@ class RotationHandler:
max_retries = 5
tried_models = [] # Track which models have been tried
model_retry_counts = {} # Track retry count per model
coderai_warmup_counts = {} # Track CoderAI "warming up" waits per model
last_error = None
successful_model = None
successful_handler = None
......@@ -3909,26 +3919,15 @@ class RotationHandler:
last_error = str(e)
# CoderAI "warming up": the broker session is temporarily gone while
# the (often small/edge) worker cools down. This is NOT a provider
# fault — do not record a failure (which would disable the provider for
# a long cooldown). Instead wait a few seconds and retry the same
# provider, up to a bounded number of times, fully transparent to the
# client.
# the (often small/edge) worker cools down. The provider handler
# already waited and retried internally; if it still failed, this is
# NOT a provider fault — do not record a failure (which would disable
# the provider for a long cooldown), just fail over to the next
# provider, fully transparent to the client.
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(
f"CoderAI provider {provider_id} still warming up after "
f"{warmup_count} wait(s); failing over to the next provider "
f"(still not recorded as a failure)"
f"CoderAI provider {provider_id} still warming up after internal retries; "
f"failing over to the next provider (not recorded as a failure)"
)
tried_models.append(current_model)
continue
......@@ -5028,6 +5027,8 @@ class RotationHandler:
error_dict = {"error": str(e)}
yield f"data: {json.dumps(error_dict)}\n\n".encode('utf-8')
except Exception as e:
# 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
self._record_dashboard_proxy_event(
......
......@@ -43,6 +43,24 @@ from .base import AISBF_DEBUG, BaseProviderHandler
logger = logging.getLogger(__name__)
# CoderAI workers often run on small/edge hardware that drops its broker session
# while cooling down to avoid overheating. A missing broker session is therefore a
# transient "warming up" condition, not a provider fault: wait and retry the request
# a bounded number of times instead of failing. This is applied for ALL request
# paths (rotation, autoselect and direct) and ONLY for the broker-session error.
CODERAI_WARMUP_WAIT_SECONDS: float = 10.0
CODERAI_MAX_WARMUP_WAITS: int = 3
_CODERAI_WARMUP_MARKER = "No active CoderAI broker session"
# CoderAI requests can be very long-running on modest hardware, so default to a
# multi-hour request timeout (overridable via coderai_config.request_timeout).
CODERAI_DEFAULT_REQUEST_TIMEOUT: float = 10800.0 # 3 hours
def _is_coderai_warmup_error(error) -> bool:
"""True when the error is just a missing CoderAI broker session (warming up)."""
return _CODERAI_WARMUP_MARKER in str(error or "")
class CoderAIProviderHandler(BaseProviderHandler):
"""Provider for CoderAI local servers over HTTP or WebSocket bridge."""
......@@ -58,7 +76,7 @@ class CoderAIProviderHandler(BaseProviderHandler):
self._bridge_path = str(self._coderai_config.get("bridge_path") or "/coderai/ws").strip() or "/coderai/ws"
self._registration_path = str(self._coderai_config.get("registration_path") or "/coderai/register").strip() or "/coderai/register"
self._broker_ws_path = str(self._coderai_config.get("broker_ws_path") or "/api/coderai/wss").strip() or "/api/coderai/wss"
self._request_timeout = float(self._coderai_config.get("request_timeout") or 300.0)
self._request_timeout = float(self._coderai_config.get("request_timeout") or CODERAI_DEFAULT_REQUEST_TIMEOUT)
self._model_timeout = float(self._coderai_config.get("model_timeout") or 30.0)
self._websocket_enabled = bool(self._coderai_config.get("websocket_enabled", True))
self._http_enabled = bool(self._coderai_config.get("http_enabled", True))
......@@ -73,7 +91,7 @@ class CoderAIProviderHandler(BaseProviderHandler):
self._base_endpoint = self._normalize_http_base(self._raw_endpoint)
self._ws_endpoint = self._normalize_ws_endpoint(self._raw_endpoint)
self._apply_provider_defaults()
self.client = OpenAI(base_url=f"{self._base_endpoint}/v1", api_key=self._effective_api_key())
self.client = OpenAI(base_url=f"{self._base_endpoint}/v1", api_key=self._effective_api_key(), timeout=self._request_timeout)
def _get_provider_value(self, key: str, default: Any = None) -> Any:
if isinstance(self.provider_config, dict):
......@@ -224,8 +242,30 @@ class CoderAIProviderHandler(BaseProviderHandler):
extra=extra,
)
async def _broker_request_with_warmup(self, op: str, payload: Dict[str, Any], timeout: float) -> Dict[str, Any]:
"""Like _broker_request, but tolerant of a CoderAI worker that is warming up.
A missing broker session means the (often small/edge) worker is cooling down;
wait CODERAI_WARMUP_WAIT_SECONDS and retry, up to CODERAI_MAX_WARMUP_WAITS
times, instead of surfacing the error. Used for chat requests on every path.
"""
attempt = 0
while True:
try:
return await self._broker_request(op, payload, timeout=timeout)
except Exception as e:
if _is_coderai_warmup_error(e) and attempt < CODERAI_MAX_WARMUP_WAITS:
attempt += 1
logger.warning(
f"[{self.provider_id}] CoderAI broker warming up (no session); waiting "
f"{CODERAI_WARMUP_WAIT_SECONDS:.0f}s and retrying ({attempt}/{CODERAI_MAX_WARMUP_WAITS})"
)
await asyncio.sleep(CODERAI_WARMUP_WAIT_SECONDS)
continue
raise
async def _broker_stream(self, op: str, payload: Dict[str, Any], timeout: float) -> AsyncIterator[bytes]:
message = await self._broker_request(op, payload, timeout=timeout)
message = await self._broker_request_with_warmup(op, payload, timeout=timeout)
status = message.get("status") or "ok"
if status == "error":
raise Exception(message.get("error") or "CoderAI broker bridge error")
......@@ -475,6 +515,15 @@ class CoderAIProviderHandler(BaseProviderHandler):
await self.apply_rate_limit()
payload = self._build_chat_payload(model, messages, max_tokens, temperature, stream, tools, tool_choice)
# Warm-up retry loop: a missing broker session means the CoderAI worker
# (often small/edge hardware) is cooling down to avoid overheating. That is
# a transient condition, not a provider fault, so we wait and retry the same
# provider a bounded number of times instead of failing. This applies on
# every request path (rotation, autoselect and direct) and only for the
# broker-session error. For streaming the broker call is lazy, so its warm-up
# retry lives inside _broker_stream (via _broker_request_with_warmup).
warmup_attempts = 0
while True:
try:
if await self._use_broker():
if stream:
......@@ -503,12 +552,19 @@ class CoderAIProviderHandler(BaseProviderHandler):
self.record_success()
return response
except Exception as e:
# A missing broker session means the CoderAI worker (often small/edge
# hardware) is warming up / cooling down to avoid overheating — a
# transient condition, not a provider fault. Don't record a failure for
# it (that would count toward disabling the provider for a long
# cooldown); the rotation handler waits a few seconds and retries.
if 'No active CoderAI broker session' not in str(e):
if _is_coderai_warmup_error(e):
if warmup_attempts < CODERAI_MAX_WARMUP_WAITS:
warmup_attempts += 1
logger.warning(
f"[{self.provider_id}] CoderAI broker warming up (no session); waiting "
f"{CODERAI_WARMUP_WAIT_SECONDS:.0f}s and retrying ({warmup_attempts}/{CODERAI_MAX_WARMUP_WAITS})"
)
await asyncio.sleep(CODERAI_WARMUP_WAIT_SECONDS)
continue
# Still warming up after all waits — surface the error WITHOUT
# recording a failure (don't disable the provider for a cooldown).
raise
# Any other error is a genuine provider fault.
self.record_failure()
raise
......@@ -606,7 +662,7 @@ class CoderAIProviderHandler(BaseProviderHandler):
async for chunk in self._broker_stream("proxy", payload, timeout=self._request_timeout):
chunks.append(chunk)
return 200, {"stream_chunks": [base64.b64encode(chunk).decode("ascii") for chunk in chunks], "stream_encoding": "base64"}
message = await self._broker_request("proxy", payload, timeout=self._request_timeout)
message = await self._broker_request_with_warmup("proxy", payload, timeout=self._request_timeout)
if (message.get("status") or "ok") == "error":
raise Exception(message.get("error") or "CoderAI broker proxy request failed")
envelope = message.get("payload") or {}
......
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
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"
readme = "README.md"
license = "GPL-3.0-or-later"
......
......@@ -106,7 +106,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.99.76",
version="0.99.77",
author="AISBF Contributors",
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",
......
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