-
Notifications
You must be signed in to change notification settings - Fork 0
fix(ai): restore omniroute failover and alert on gateway failures #4534
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
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
7563379
fix(litellm): drop qwen35-2b from the omniroute fallback chain
Tanguille bce5119
feat(omniroute)!: track next for the LKGP failover fix
Tanguille 6d7016a
feat(litellm): alert on gateway failures and fallback-chain health
Tanguille f0b9a02
refactor(litellm): let the increase() window be the only debounce
Tanguille aae4da5
feat(litellm): watch the fallback backend structurally, not via traffic
Tanguille 9c20470
Merge branch 'main' into fix/omniroute-failover
Tanguille 6c08696
docs(ai): trim the incident forensics out of the new comments
Tanguille File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,3 +9,4 @@ resources: | |
| - models.yaml | ||
| - servicemonitor.yaml | ||
| - grafanadashboard.yaml | ||
| - prometheusrule.yaml | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| --- | ||
| # yaml-language-server: $schema=https://k8s-schemas.home-operations.com/monitoring.coreos.com/prometheusrule_v1.json | ||
| apiVersion: monitoring.coreos.com/v1 | ||
| kind: PrometheusRule | ||
| metadata: | ||
| name: litellm-rules | ||
| spec: | ||
| groups: | ||
| - name: litellm.rules | ||
| # increase() over rate(): traffic is bursty and tiny (single digits | ||
| # overnight, dozens during a Renovate burst), so a per-second rate rounds | ||
| # to noise. It also rides out the counter resets from litellm's pod rolls. | ||
| # The window is the debounce, so `for` only has to survive one bad | ||
| # evaluation -- setting it comparable to the window adds the two together | ||
| # and hides the real firing delay from anyone reading the threshold. | ||
| rules: | ||
| # A caller got an error back. Everything else here is early warning; | ||
| # this is the outage itself. | ||
| - alert: LiteLLMRequestsFailing | ||
| expr: |- | ||
| sum by (requested_model, exception_status) ( | ||
| increase(litellm_proxy_failed_requests_metric_total[15m]) | ||
| ) > 2 | ||
| for: 10m | ||
| annotations: | ||
| summary: >- | ||
| litellm is returning {{ $labels.exception_status }} for | ||
| {{ $labels.requested_model }} — the fallback chain is not covering it | ||
| labels: | ||
| severity: critical | ||
|
|
||
| # The only structural rule here: every other one needs a real request to | ||
| # fail first, which at this traffic volume means hours of lag. This | ||
| # fires on zero traffic and enforces the invariant proxy.yaml's chain | ||
| # depends on -- qwen-3.8-fast must point at a live deployment. absent() | ||
| # covers the target being retired out from under the chain, == 0 covers | ||
| # scaled-to-0, crashloop and unschedulable. | ||
| # Deployment name is hardcoded: nothing derives it from proxy.yaml's | ||
| # fallback target, so the two move together or this silently stops | ||
| # watching. Critical, not warning -- qwen-3.8 shares this backend, so a | ||
| # hit means hermes has no local model either. | ||
| - alert: LiteLLMFallbackBackendDown | ||
| expr: |- | ||
| kube_deployment_status_replicas_available{namespace="ai", deployment="qwen38-27b-vllm"} == 0 | ||
| or | ||
| absent(kube_deployment_status_replicas_available{namespace="ai", deployment="qwen38-27b-vllm"}) | ||
| # replicas: 1 on a single dGPU node, so every pod roll dips to 0. | ||
| for: 10m | ||
| annotations: | ||
| summary: >- | ||
| qwen38-27b-vllm has no available replica — litellm's only fallback | ||
| target is gone and hermes has no local model | ||
| labels: | ||
| severity: critical | ||
|
|
||
| # Catches what the structural rule cannot: a target that is up but | ||
| # rejects the payload. > 0 because the chain is one deep (see | ||
| # proxy.yaml) -- a single failed fallback is the whole safety net. | ||
| - alert: LiteLLMFallbackTargetUnavailable | ||
| expr: |- | ||
| sum by (requested_model, fallback_model) ( | ||
| increase(litellm_deployment_failed_fallbacks_total[30m]) | ||
| ) > 0 | ||
| for: 5m | ||
| annotations: | ||
| summary: >- | ||
| Fallback {{ $labels.fallback_model }} for {{ $labels.requested_model }} | ||
| is failing — the chain is shorter than it looks | ||
| labels: | ||
| severity: warning | ||
|
|
||
| # omniroute briefly dropping to fallback is normal -- its free tier | ||
| # cycles quota every few minutes and recovers itself. Sustained use | ||
| # means it is not recovering and the self-hosted models are carrying | ||
| # traffic they were never sized for. | ||
| - alert: LiteLLMPrimaryDegraded | ||
| expr: |- | ||
| sum by (requested_model, fallback_model) ( | ||
| increase(litellm_deployment_successful_fallbacks_total[30m]) | ||
| ) > 5 | ||
| for: 5m | ||
| annotations: | ||
| summary: >- | ||
| {{ $labels.requested_model }} has leaned on fallback | ||
| {{ $labels.fallback_model }} repeatedly — the primary is not recovering | ||
| labels: | ||
| severity: warning | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,10 +22,16 @@ spec: | |
| app: | ||
| image: | ||
| repository: docker.io/diegosouzapw/omniroute | ||
| # Tried main@digest 2026-08-11 and reverted: main is only dependabot | ||
| # bumps ahead of this tag (provider work lives on the `next` image, | ||
| # which has no public branch to audit) and its CI run was cancelled. | ||
| tag: 3.8.49@sha256:92c768c56e2de32c51a0621ef182835018b00b288c9bb235c5c5e4514658c1a1 | ||
| # On `next` for one upstream fix, not for novelty: 3.8.49 never | ||
| # clears the last-known-good-provider pin when its target starts | ||
| # failing, so every request re-selects the same exhausted model | ||
| # instead of failing over (upstream #10034). No tagged release | ||
| # carries it yet; move to the v3.8.50 tag once it ships. | ||
| # Renovate cannot prompt that move -- `next` is not a version it | ||
| # can diff against, so it will only ever offer digest bumps that | ||
|
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. Info (docs): PR body notes v3.8.50 now requires auth on /v1/models while /v1/chat/completions still accepts the no-auth key; confirm nothing else enumerates models against omniroute before relying on the no-auth path. Automated finding from AI PR review. |
||
| # walk this further along the branch. Digest-pinned so the branch | ||
| # moving does not silently move us. | ||
| tag: next@sha256:2dd8dee4c5247c372af2ebbdfb98d72b4abacca77a07dd41aa6de6aa77e527cc | ||
| env: | ||
| TZ: ${TIMEZONE} | ||
| DATA_DIR: /app/data | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
According to the official Prometheus alerting rules documentation, does an alert expression that remains true require the full configuredforduration before it enters the firing state?💡 Result:
Yes, according to the official Prometheus documentation, an alert expression that remains true does require the full configured for duration before it enters the firing state [1][2]. When an alerting rule includes a for clause, Prometheus monitors the alert expression during each evaluation cycle [1][3]. If the expression results in output for a given label set, the alert is considered active [1]. If the expression remains true for the entirety of the configured for duration, the alert transitions from the pending state to the firing state [1][4][5]. Alerts that are active but have not yet satisfied the full for duration remain in the pending state [1][3]. If an alerting rule does not have a for clause, it will become active and transition to the firing state on the first evaluation where the condition is met [1][6].
Citations:
🏁 Script executed:
Repository: Tanguille/cluster
Length of output: 6288
🏁 Script executed:
Repository: Tanguille/cluster
Length of output: 3860
Remove the unnecessary alert-duration gates.
Prometheus requires the expression to remain true for the full
forduration before firing. Removefor: 10mfromLiteLLMRequestsFailingandfor: 5mfrom both warning alerts. Keep the 10-minute gate onLiteLLMFallbackBackendDownto tolerate single-replica pod rolls.🤖 Prompt for AI Agents