-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Steak fallback in llm #7274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Steak fallback in llm #7274
Changes from all commits
a6c5af7
69360e8
b20a12b
59d3c10
db1ec30
26645f5
9d630cd
0db42d2
1a12608
7dd7f16
0e78589
686ac84
1c732f4
2bb278c
23aeeeb
3252691
a9225fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,7 @@ def __init__( | |
|
|
||
| self._llm_instances = llm | ||
| self._attempt_timeout = attempt_timeout | ||
| self._current_llm_index: int = 0 | ||
| self._max_retry_per_llm = max_retry_per_llm | ||
| self._retry_interval = retry_interval | ||
| self._retry_on_chunk_sent = retry_on_chunk_sent | ||
|
|
@@ -84,6 +85,59 @@ def model(self) -> str: | |
| def provider(self) -> str: | ||
| return "livekit" | ||
|
|
||
| def switch_to_next(self, *, only_if_available: bool = True) -> bool: | ||
| """ | ||
| Move pointer to next LLM in order. | ||
|
|
||
| Args: | ||
| only_if_available: if True, skip unavailable LLMs | ||
|
|
||
| Returns: | ||
| True if switched, False otherwise | ||
| """ | ||
|
|
||
| n = len(self._llm_instances) | ||
| start = self._current_llm_index | ||
|
|
||
| for offset in range(1, n + 1): | ||
| i = (start + offset) % n | ||
| status = self._status[i] | ||
|
|
||
| if not only_if_available or status.available: | ||
| prev = self._current_llm_index | ||
| self._current_llm_index = i | ||
|
|
||
| logger.info( | ||
| f"Manual switch LLM from " | ||
| f"{self._llm_instances[prev].label} " | ||
| f"to {self._llm_instances[i].label}" | ||
| ) | ||
|
|
||
| return True | ||
|
|
||
| return False | ||
|
|
||
| def _ordered_indices(self) -> list[int]: | ||
| n = len(self._llm_instances) | ||
| return ( | ||
| [self._current_llm_index] | ||
| + list(range(self._current_llm_index + 1, n)) | ||
| + list(range(0, self._current_llm_index)) | ||
| ) | ||
|
|
||
| def _mark_failed(self, idx: int) -> None: | ||
| self._status[idx].available = False | ||
|
|
||
| for i in self._ordered_indices()[1:]: | ||
| if self._status[i].available: | ||
| self._current_llm_index = i | ||
| logger.info( | ||
| f"Auto switch LLM from " | ||
| f"{self._llm_instances[idx].label} " | ||
| f"to {self._llm_instances[i].label}" | ||
| ) | ||
| return | ||
|
Comment on lines
+131
to
+139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Late failures displace healthy providers When an older request fails after the pointer moves, Learn moreThe adapter-wide current pointer is shared by concurrent requests. Example: Request A starts on provider 0. Another failure moves the current pointer to healthy provider 1. When request A later reports provider 0 failed, Recommended fix: Always mark Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| def chat( | ||
| self, | ||
| *, | ||
|
|
@@ -239,12 +293,17 @@ async def _recover_llm_task(llm: LLM) -> None: | |
| async def _run(self) -> None: | ||
| start_time = time.time() | ||
|
|
||
| all_failed = all(not llm_status.available for llm_status in self._fallback_adapter._status) | ||
| adapter = self._fallback_adapter | ||
|
|
||
| all_failed = all(not llm_status.available for llm_status in adapter._status) | ||
| if all_failed: | ||
| logger.error("all LLMs are unavailable, retrying..") | ||
|
|
||
| for i, llm in enumerate(self._fallback_adapter._llm_instances): | ||
| llm_status = self._fallback_adapter._status[i] | ||
| indices = range(len(adapter._llm_instances)) if all_failed else adapter._ordered_indices() | ||
|
|
||
| for i in indices: | ||
| llm = adapter._llm_instances[i] | ||
| llm_status = adapter._status[i] | ||
| if llm_status.available or all_failed: | ||
| text_sent: str = "" | ||
| tool_calls_sent: list[str] = [] | ||
|
|
@@ -261,8 +320,8 @@ async def _run(self) -> None: | |
| return | ||
| except Exception: # exceptions already logged inside _try_generate | ||
| if llm_status.available: | ||
| llm_status.available = False | ||
| self._fallback_adapter.emit( | ||
| adapter._mark_failed(i) | ||
| adapter.emit( | ||
| "llm_availability_changed", | ||
| AvailabilityChangedEvent(llm=llm, available=False), | ||
| ) | ||
|
|
@@ -282,7 +341,8 @@ async def _run(self) -> None: | |
| extra=extra, | ||
| ) | ||
|
|
||
| self._try_recovery(llm) | ||
| if not llm_status.available: | ||
| self._try_recovery(llm) | ||
|
|
||
| raise APIConnectionError( | ||
| f"all LLMs failed ({[llm.label for llm in self._fallback_adapter._llm_instances]}) after {time.time() - start_time} seconds" # noqa: E501 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 No-op provider switches report success
switch_to_nexteventually reselects the starting provider. It returnsTruewhen no alternative is available, although the pointer never changes.Learn more
The loop includes
offset == n, which wrapsiback tostart. If the current provider is available, that final iteration always succeeds even though no movement occurred. The STT implementation has the same contract violation in its manual switch.Example: An adapter has one provider, or provider 0 is current while every other provider is unavailable. Calling
switch_to_next()logs a switch from provider 0 to itself and returnsTrue; callers cannot detect that failover was impossible.Recommended fix: Iterate offsets
1..n-1only. ReturnFalsewhen no distinct eligible provider exists.Was this helpful? React with 👍 or 👎 to provide feedback.