Fix rotation providers never degrading: don't record success on lazy streams

For streaming requests providers returned a lazy generator and immediately
called record_success(), resetting the failure counter to 0 BEFORE the
rotation handler primed the stream and the upstream returned 400/429. Every
attempt did success(reset->0) then failure(->1), so the disable threshold (3)
was never reached and a consistently-failing provider was never disabled.

Guard the premature record_success() so it does not fire for streaming
(codex, openai, runpod, coderai). The caller already records success after
the stream is primed/consumed, so the failure counter now accumulates and a
failing provider is correctly disabled/cooled-down. Bump to 0.99.74.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 1d726a93
...@@ -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.73" __version__ = "0.99.74"
__all__ = [ __all__ = [
# Config # Config
"config", "config",
......
...@@ -496,7 +496,11 @@ class CoderAIProviderHandler(BaseProviderHandler): ...@@ -496,7 +496,11 @@ class CoderAIProviderHandler(BaseProviderHandler):
raise RuntimeError(f"[{self.provider_id}] No active CoderAI broker session; direct fallback not allowed with broker_preferred=True") raise RuntimeError(f"[{self.provider_id}] No active CoderAI broker session; direct fallback not allowed with broker_preferred=True")
response = self.client.chat.completions.create(**payload) response = self.client.chat.completions.create(**payload)
self.record_success() # Streaming returns a lazy iterator; recording success here would
# prematurely reset the failure counter before the stream is consumed.
# The caller records success after priming/consuming the stream.
if not stream:
self.record_success()
return response return response
except Exception: except Exception:
self.record_failure() self.record_failure()
......
...@@ -858,7 +858,14 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -858,7 +858,14 @@ class CodexProviderHandler(BaseProviderHandler):
) )
logger.info(f"CodexProviderHandler: Response received") logger.info(f"CodexProviderHandler: Response received")
self.record_success() # For streaming requests the response is a lazy async generator: the
# upstream HTTP request has not run yet, so we cannot know if it
# succeeded. Recording success here would prematurely reset the
# failure counter before priming surfaces an immediate provider error
# (e.g. 400/429), preventing the provider from ever being disabled.
# The caller records success after the stream is primed/consumed.
if not stream:
self.record_success()
if AISBF_DEBUG: if AISBF_DEBUG:
logger.info(f"=== RAW CODEX RESPONSE ===") logger.info(f"=== RAW CODEX RESPONSE ===")
......
...@@ -169,7 +169,11 @@ class OpenAIProviderHandler(BaseProviderHandler): ...@@ -169,7 +169,11 @@ class OpenAIProviderHandler(BaseProviderHandler):
response = self.client.chat.completions.create(**request_params) response = self.client.chat.completions.create(**request_params)
logging.info(f"OpenAIProviderHandler: Response received: {response}") logging.info(f"OpenAIProviderHandler: Response received: {response}")
self.record_success() # Streaming returns a lazy iterator; the upstream call has not been
# consumed yet, so recording success here would prematurely reset the
# failure counter. The caller records success after priming/consuming.
if not stream:
self.record_success()
# Dump raw response if AISBF_DEBUG is enabled # Dump raw response if AISBF_DEBUG is enabled
if AISBF_DEBUG: if AISBF_DEBUG:
......
...@@ -86,7 +86,11 @@ class RunpodProviderHandler(BaseProviderHandler): ...@@ -86,7 +86,11 @@ class RunpodProviderHandler(BaseProviderHandler):
await self._ensure_non_public_resource_ready() await self._ensure_non_public_resource_ready()
delegate = self._build_delegate_handler(self._wrapper_mode) delegate = self._build_delegate_handler(self._wrapper_mode)
response = await delegate.handle_request(model, messages, max_tokens=max_tokens, temperature=temperature, stream=stream, tools=tools, tool_choice=tool_choice) response = await delegate.handle_request(model, messages, max_tokens=max_tokens, temperature=temperature, stream=stream, tools=tools, tool_choice=tool_choice)
self.record_success() # For streaming the delegate returns a lazy iterator; recording success
# here would prematurely reset the failure counter before the stream is
# consumed. The caller records success after priming/consuming the stream.
if not stream:
self.record_success()
return response return response
async def get_models(self) -> List[Model]: async def get_models(self) -> List[Model]:
......
...@@ -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.73" version = "0.99.74"
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.73", version="0.99.74",
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