Make CoderAI broker queue hold time configurable; bump to 0.99.83

When a coderai broker provider's worker is offline, requests are held and
retried while it warms up. The hold window was hardcoded (10s x 3 = 30s).
Expose it per provider via coderai_config:
- broker_queue_timeout_seconds: total hold time (0 disables holding)
- broker_warmup_wait_seconds: poll interval between retries
- broker_max_warmup_waits: explicit retry count (when no queue timeout)

Defaults preserve prior behavior. Applied to both warm-up retry loops.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 5dff11f4
...@@ -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, get_max_completion_tokens_for_model from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model, get_max_completion_tokens_for_model
__version__ = "0.99.82" __version__ = "0.99.83"
__all__ = [ __all__ = [
# Config # Config
"config", "config",
......
...@@ -26,6 +26,7 @@ from __future__ import annotations ...@@ -26,6 +26,7 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
import logging import logging
import math
import time import time
from typing import Any, AsyncIterator, Dict, List, Optional, Tuple, Union from typing import Any, AsyncIterator, Dict, List, Optional, Tuple, Union
import base64 import base64
...@@ -45,9 +46,18 @@ logger = logging.getLogger(__name__) ...@@ -45,9 +46,18 @@ logger = logging.getLogger(__name__)
# CoderAI workers often run on small/edge hardware that drops its broker session # 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 # 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 # transient "warming up" condition, not a provider fault: hold the request and retry
# a bounded number of times instead of failing. This is applied for ALL request # while the worker is offline instead of failing immediately. This is applied for ALL
# paths (rotation, autoselect and direct) and ONLY for the broker-session error. # request paths (rotation, autoselect and direct) and ONLY for the broker-session error.
#
# The hold window is bounded so a request can't queue forever. Both the poll interval
# and the total hold time are configurable per provider via coderai_config:
# - broker_warmup_wait_seconds: seconds between retries (default below)
# - broker_queue_timeout_seconds: total time to hold a request while the worker is
# offline; 0 disables holding (fail immediately).
# When set, it derives the retry count from the
# poll interval. Takes precedence over the count.
# - broker_max_warmup_waits: explicit retry count (used when no queue timeout)
CODERAI_WARMUP_WAIT_SECONDS: float = 10.0 CODERAI_WARMUP_WAIT_SECONDS: float = 10.0
CODERAI_MAX_WARMUP_WAITS: int = 3 CODERAI_MAX_WARMUP_WAITS: int = 3
_CODERAI_WARMUP_MARKER = "No active CoderAI broker session" _CODERAI_WARMUP_MARKER = "No active CoderAI broker session"
...@@ -86,6 +96,7 @@ class CoderAIProviderHandler(BaseProviderHandler): ...@@ -86,6 +96,7 @@ class CoderAIProviderHandler(BaseProviderHandler):
self._broker_enabled = bool(self._coderai_config.get("broker_enabled", True)) self._broker_enabled = bool(self._coderai_config.get("broker_enabled", True))
self._broker_mode = bool(self._coderai_config.get("broker_mode", False)) self._broker_mode = bool(self._coderai_config.get("broker_mode", False))
self._broker_preferred = bool(self._coderai_config.get("broker_preferred", False)) self._broker_preferred = bool(self._coderai_config.get("broker_preferred", False))
self._broker_warmup_wait, self._broker_max_warmup_waits = self._resolve_warmup_limits()
if self._broker_mode: if self._broker_mode:
self._transport = "broker" self._transport = "broker"
self._base_endpoint = self._normalize_http_base(self._raw_endpoint) self._base_endpoint = self._normalize_http_base(self._raw_endpoint)
...@@ -131,6 +142,40 @@ class CoderAIProviderHandler(BaseProviderHandler): ...@@ -131,6 +142,40 @@ class CoderAIProviderHandler(BaseProviderHandler):
def _usage_cache_key(self) -> str: def _usage_cache_key(self) -> str:
return f"coderai:{self.provider_id}" return f"coderai:{self.provider_id}"
def _resolve_warmup_limits(self) -> Tuple[float, int]:
"""Resolve the broker warm-up poll interval and max retry count.
When ``broker_queue_timeout_seconds`` is configured it defines the total
time a request may be held while the worker is offline, and the retry count
is derived from it (timeout / poll interval). Otherwise the explicit
``broker_max_warmup_waits`` count is used. Both fall back to module defaults.
"""
cfg = self._coderai_config
def _as_float(value, default):
try:
if value is None:
return default
return float(value)
except (TypeError, ValueError):
return default
wait = _as_float(cfg.get("broker_warmup_wait_seconds"), CODERAI_WARMUP_WAIT_SECONDS)
if wait <= 0:
wait = CODERAI_WARMUP_WAIT_SECONDS
if cfg.get("broker_queue_timeout_seconds") is not None:
timeout = _as_float(cfg.get("broker_queue_timeout_seconds"), 0.0)
max_waits = max(0, math.ceil(timeout / wait)) if timeout > 0 else 0
else:
try:
max_waits = int(cfg.get("broker_max_warmup_waits"))
except (TypeError, ValueError):
max_waits = CODERAI_MAX_WARMUP_WAITS
max_waits = max(0, max_waits)
return wait, max_waits
def _resolve_coderai_config(self) -> Dict[str, Any]: def _resolve_coderai_config(self) -> Dict[str, Any]:
if isinstance(self.provider_config, dict): if isinstance(self.provider_config, dict):
raw = self.provider_config.get("coderai_config") or {} raw = self.provider_config.get("coderai_config") or {}
...@@ -246,21 +291,22 @@ class CoderAIProviderHandler(BaseProviderHandler): ...@@ -246,21 +291,22 @@ class CoderAIProviderHandler(BaseProviderHandler):
"""Like _broker_request, but tolerant of a CoderAI worker that is warming up. """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; 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 hold the request and retry while it is offline, up to the configured queue
times, instead of surfacing the error. Used for chat requests on every path. time limit (see _resolve_warmup_limits), instead of surfacing the error.
Used for chat requests on every path.
""" """
attempt = 0 attempt = 0
while True: while True:
try: try:
return await self._broker_request(op, payload, timeout=timeout) return await self._broker_request(op, payload, timeout=timeout)
except Exception as e: except Exception as e:
if _is_coderai_warmup_error(e) and attempt < CODERAI_MAX_WARMUP_WAITS: if _is_coderai_warmup_error(e) and attempt < self._broker_max_warmup_waits:
attempt += 1 attempt += 1
logger.warning( logger.warning(
f"[{self.provider_id}] CoderAI broker warming up (no session); waiting " 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})" f"{self._broker_warmup_wait:.0f}s and retrying ({attempt}/{self._broker_max_warmup_waits})"
) )
await asyncio.sleep(CODERAI_WARMUP_WAIT_SECONDS) await asyncio.sleep(self._broker_warmup_wait)
continue continue
raise raise
...@@ -560,13 +606,13 @@ class CoderAIProviderHandler(BaseProviderHandler): ...@@ -560,13 +606,13 @@ class CoderAIProviderHandler(BaseProviderHandler):
return response return response
except Exception as e: except Exception as e:
if _is_coderai_warmup_error(e): if _is_coderai_warmup_error(e):
if warmup_attempts < CODERAI_MAX_WARMUP_WAITS: if warmup_attempts < self._broker_max_warmup_waits:
warmup_attempts += 1 warmup_attempts += 1
logger.warning( logger.warning(
f"[{self.provider_id}] CoderAI broker warming up (no session); waiting " 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})" f"{self._broker_warmup_wait:.0f}s and retrying ({warmup_attempts}/{self._broker_max_warmup_waits})"
) )
await asyncio.sleep(CODERAI_WARMUP_WAIT_SECONDS) await asyncio.sleep(self._broker_warmup_wait)
continue continue
# Still warming up after all waits — surface the error WITHOUT # Still warming up after all waits — surface the error WITHOUT
# recording a failure (don't disable the provider for a cooldown). # recording a failure (don't disable the provider for a cooldown).
......
...@@ -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.82" version = "0.99.83"
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.82", version="0.99.83",
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