cloud: production control plane (API, auth/RBAC, queue, sandbox, memory/KB, fallback/cost, webhooks)
This commit is contained in:
1 parent
b97c34465f
commit
b74d99e959
17 files changed
+3420
No files matched your search
+125
@@ -0,0 +1,125 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user