Commit 08187ec4 authored by Stefy Lanza (nextime / spora )'s avatar Stefy Lanza (nextime / spora )

Merge branch 'fix/request-scoped-errors-no-cooldown'

parents 28e9643d fae38ea0
...@@ -39,6 +39,7 @@ from fastapi import HTTPException, Request ...@@ -39,6 +39,7 @@ from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse, StreamingResponse, Response from fastapi.responses import JSONResponse, StreamingResponse, Response
from .models import ChatCompletionRequest, ChatCompletionResponse from .models import ChatCompletionRequest, ChatCompletionResponse
from .providers import get_provider_handler, RateLimitError from .providers import get_provider_handler, RateLimitError
from .providers.base import is_request_scoped_error
from .config import config from .config import config
from .studio import infer_model_capabilities from .studio import infer_model_capabilities
from .studio_adapters import effective_studio_adapter, infer_studio_adapter_profile, adapt_studio_payload_with_profile from .studio_adapters import effective_studio_adapter, infer_studio_adapter_profile, adapt_studio_payload_with_profile
...@@ -97,8 +98,18 @@ def _is_upstream_rate_limit(error) -> bool: ...@@ -97,8 +98,18 @@ def _is_upstream_rate_limit(error) -> bool:
def _should_record_failure(error) -> bool: def _should_record_failure(error) -> bool:
"""Whether an exception should count against the provider's failure budget.""" """Whether an exception should count against the provider's failure budget.
return not _is_coderai_warmup_error(error) and not _is_upstream_rate_limit(error)
Excludes coderai warm-up errors, upstream rate limits, and request-scoped
errors (bad input / unsupported capability such as a 404 "No endpoints found
that support image input") — none of which indicate the provider is unhealthy,
so they must not trip the cooldown that disables it for all traffic.
"""
return (
not _is_coderai_warmup_error(error)
and not _is_upstream_rate_limit(error)
and not is_request_scoped_error(error)
)
def _raise_if_provider_unavailable(handler, provider_id: str) -> None: def _raise_if_provider_unavailable(handler, provider_id: str) -> None:
......
...@@ -193,6 +193,53 @@ class RateLimitError(Exception): ...@@ -193,6 +193,53 @@ class RateLimitError(Exception):
self.status_code = status_code self.status_code = status_code
# HTTP statuses that describe a problem with THIS request — bad input, an
# unsupported capability for the chosen model, or a rate limit — rather than the
# provider being unhealthy. They must not count toward the consecutive-failure
# budget that disables the whole provider: otherwise a handful of request-scoped
# errors (e.g. a model that lacks an image endpoint returning 404 "No endpoints
# found that support image input", or a bad image returning 400) take the whole
# provider offline for every request for the cooldown window.
_REQUEST_SCOPED_STATUSES = frozenset({400, 404, 413, 415, 422, 429})
def is_request_scoped_status(status) -> bool:
"""True when an HTTP status code is about the request, not provider health."""
try:
return int(status) in _REQUEST_SCOPED_STATUSES
except (TypeError, ValueError):
return False
def extract_http_status(error) -> Optional[int]:
"""Best-effort HTTP status code from a provider exception."""
status = getattr(error, 'status_code', None)
if status is None:
status = getattr(getattr(error, 'response', None), 'status_code', None)
try:
return int(status) if status is not None else None
except (TypeError, ValueError):
return None
def is_request_scoped_error(error) -> bool:
"""True when the error is about the specific request (bad input / unsupported
capability / rate limit) rather than the provider's health, so it must not
count toward the failure cooldown that disables the whole provider.
Falls back to message sniffing for handlers that wrap the upstream error in a
plain Exception without preserving the status code (e.g. the streaming path).
"""
if is_request_scoped_status(extract_http_status(error)):
return True
text = str(error or '')
return (
'No endpoints found that support image input' in text
or 'Cannot decode visions' in text
or '429 Too Many Requests' in text
)
class AnthropicFormatConverter: class AnthropicFormatConverter:
""" """
Shared utility class for converting between OpenAI and Anthropic message formats. Shared utility class for converting between OpenAI and Anthropic message formats.
......
...@@ -411,8 +411,19 @@ class KiloProviderHandler(BaseProviderHandler): ...@@ -411,8 +411,19 @@ class KiloProviderHandler(BaseProviderHandler):
return response return response
except Exception as e: except Exception as e:
import logging import logging
from .base import is_request_scoped_error, extract_http_status
logging.error(f"KiloProviderHandler: Error: {str(e)}", exc_info=True) logging.error(f"KiloProviderHandler: Error: {str(e)}", exc_info=True)
self.record_failure() # A per-request error (bad input, or the chosen model lacking an image
# endpoint → 404 "No endpoints found that support image input", or a
# 429 rate limit) says nothing about provider health. Counting it toward
# the three-strike cooldown would take kilo offline for every request.
if is_request_scoped_error(e):
logging.warning(
f"KiloProviderHandler: request-scoped error (status={extract_http_status(e)}) "
f"not counted toward the provider failure budget"
)
else:
self.record_failure()
raise e raise e
async def _handle_streaming_request(self, request_params: Dict, token: str, model: str): async def _handle_streaming_request(self, request_params: Dict, token: str, model: str):
...@@ -462,8 +473,13 @@ class KiloProviderHandler(BaseProviderHandler): ...@@ -462,8 +473,13 @@ class KiloProviderHandler(BaseProviderHandler):
error_json if 'error_json' in locals() else error_message, error_json if 'error_json' in locals() else error_message,
dict(response.headers) dict(response.headers)
) )
self.record_failure() # Request-scoped statuses (bad input, unsupported capability, rate
# limit) must not trip the provider failure cooldown — see
# is_request_scoped_status. Only genuine provider faults count.
from .base import is_request_scoped_status
if not is_request_scoped_status(response.status_code):
self.record_failure()
raise Exception(f"Kilo API streaming error ({response.status_code}): {error_message}") raise Exception(f"Kilo API streaming error ({response.status_code}): {error_message}")
except Exception: except Exception:
await streaming_client.aclose() await streaming_client.aclose()
......
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