126 lines
4.1 KiB
Python
126 lines
4.1 KiB
Python
"""Model fallback routing.
|
|
|
|
Zero third-party dependencies; standard library only.
|
|
|
|
This module decides *whether* a failed model attempt may fall back to
|
|
the next model in a priority list, and *which* model to try next. It
|
|
never runs a model turn itself and never talks to any provider.
|
|
"""
|
|
|
|
FALLBACKABLE = {404, 429, 500, 502, 503, 504, "timeout"}
|
|
|
|
|
|
def _is_fallbackable_code(value):
|
|
"""Return True for a single status code / error token."""
|
|
if isinstance(value, bool):
|
|
return False
|
|
if isinstance(value, int):
|
|
if value in FALLBACKABLE:
|
|
return True
|
|
# Any other 5xx is a server-side failure of the same kind.
|
|
return 500 <= value <= 599
|
|
if isinstance(value, str):
|
|
text = value.strip().lower()
|
|
if text in FALLBACKABLE:
|
|
return True
|
|
if text.isdigit():
|
|
return _is_fallbackable_code(int(text))
|
|
return False
|
|
return False
|
|
|
|
|
|
def is_fallbackable(error) -> bool:
|
|
"""Return True if ``error`` may trigger a model fallback.
|
|
|
|
``error`` may be:
|
|
|
|
* an int HTTP status code (404, 429, 5xx -> True; 401, 400 -> False);
|
|
* the string ``"timeout"``;
|
|
* an object / exception carrying a ``status`` or ``code`` attribute
|
|
holding one of the above.
|
|
|
|
Authentication failures (401) and bad-request errors (400) are
|
|
deliberately *not* fallbackable: retrying them on another model
|
|
cannot fix bad credentials or a bad request.
|
|
"""
|
|
if error is None:
|
|
return False
|
|
if isinstance(error, (int, str)) and not isinstance(error, bool):
|
|
return _is_fallbackable_code(error)
|
|
for attr in ("status", "code"):
|
|
value = getattr(error, attr, None)
|
|
if value is not None:
|
|
return _is_fallbackable_code(value)
|
|
return False
|
|
|
|
|
|
class ModelRouter:
|
|
"""Walk a priority list of models, one fallback at a time."""
|
|
|
|
def __init__(self, priority_list):
|
|
if not priority_list:
|
|
raise ValueError("priority_list must not be empty")
|
|
self.priority_list = list(priority_list)
|
|
|
|
def first(self):
|
|
"""Return the first (highest-priority) model."""
|
|
return self.priority_list[0]
|
|
|
|
def next_after(self, current_model, error):
|
|
"""Return the model after ``current_model``, or None.
|
|
|
|
Returns None when ``error`` is not fallbackable, when
|
|
``current_model`` is not in the priority list, or when there
|
|
is no model left after ``current_model``.
|
|
"""
|
|
if not is_fallbackable(error):
|
|
return None
|
|
try:
|
|
index = self.priority_list.index(current_model)
|
|
except ValueError:
|
|
return None
|
|
if index + 1 < len(self.priority_list):
|
|
return self.priority_list[index + 1]
|
|
return None
|
|
|
|
def route(self, attempts):
|
|
"""Return the next model to try given ``attempts``, or None.
|
|
|
|
``attempts`` is a list of ``(model, error)`` pairs in the order
|
|
they were tried. With no attempts yet, the next model to try
|
|
is the first in the priority list. A non-fallbackable error
|
|
anywhere in the chain stops routing (returns None), as does
|
|
exhausting the priority list.
|
|
"""
|
|
if not attempts:
|
|
return self.first()
|
|
last_model = None
|
|
for model, error in attempts:
|
|
if not is_fallbackable(error):
|
|
return None
|
|
last_model = model
|
|
return self.next_after(last_model, attempts[-1][1])
|
|
|
|
|
|
def record_fallback(task_record, from_model, to_model, reason) -> dict:
|
|
"""Record a model fallback on ``task_record`` and return the event.
|
|
|
|
Fallback must be recorded as an event, never silently downgraded:
|
|
the returned event is appended to ``task_record["events"]``
|
|
(created if absent) so the downgrade is always visible in the
|
|
task's history.
|
|
"""
|
|
event = {
|
|
"type": "model_fallback",
|
|
"task_id": task_record.get("id"),
|
|
"from_model": from_model,
|
|
"to_model": to_model,
|
|
"reason": reason,
|
|
}
|
|
events = task_record.get("events")
|
|
if not isinstance(events, list):
|
|
events = []
|
|
task_record["events"] = events
|
|
events.append(event)
|
|
return event
|