Виникла на днях цікава ситуація з LiteLLM, коли з одного боку метрики показують, що було багато помилок “від провайдера”, а з іншого – в трейсах і алертах помилка була тільки один раз.
Довелось трохи покопатись і розібратись з деякими нюансами того, як LiteLLM взагалі рахує метрики і що вона дійсно пише в метрики і трейси.
Плюс вийшов непоганий гайд для себе самого на майбутнє по тому, як такі проблеми дебажити.
Зміст
The Issue
Отже, що маємо:
- о ~22:00 за UTC маємо перший спайк алертів за метрикою
litellm_deployment_failure_responses_totalзexception_class="ValueError" - о ~04:00 ранку – другий спайк з тою самою проблемою
Обидва спрацювали на один і той самий API Key конкретного сервісу, і в Grafana це виглядало так:
При цьому алерт спрацював тільки один раз близько 22:00, теж з exception_class="ValueError".
В результаті виявилось, що було дві проблеми з різними помилками – хоч і пов’язані.
І, що заплутало більше всього – це те, що в трейсах і метриках обидві проблеми відображались саме з exception_class="ValueError".
Поїхали розбиратись – треба знайти причину і, головне, зрозуміти чому ж клієнт отримав тільки один failed response, хоча на графіках помилок багато?
Debugging
Перше, що прийшло в голову: раз маємо помилки – то повинні мати і трейси з status_code:="2", логічно? Бо згадаємо OTel специфікацію otel/status_code:
otel.status_code |
string | Name of the code, either “OK” or “ERROR”. MUST NOT be set if the status code is UNSET. |
В цифрах це:
- 0:
UNSET - 1:
OK - 2:
ERROR
Єдине, що тут варто уточнити один момент: зазвичай інструментарії OTel не задають значення “OK”, і якщо запит завершився без помилок – то він буде саме з 0/UNSET, див. Set Status.
Шукаємо всі трейси по цьому ключу з кодом “2”, запит для VictoriaMetrics:
{"resource_attr:service.name"="litellm"} "span_attr:litellm.metadata.user_api_key_alias":="svc-mainframe-prod" status_code:="2"
| fields _time, trace_id, span_id, name, status_message,
"event:event_attr:exception.message:0",
"span_attr:error.type",
"span_attr:error.message",
"span_attr:http.status_code"
І… маємо тільки одну помилку о 22:12:01:
При цьому :
- час події: 22:11:02
exception.message: “Not allowed to access model due to tags configuration. Passed model=gpt-5.6-luna and tags=None“exception.type: “ValueError“
І ту саму одну єдину помилку бачимо в самій LiteLLM в 01:11:06 – але тут Kyiv, UTC+3, тобто це 22:11:06 по UTC:
А 4 секунди різниці (22:11:02 в VictoriaTraces та 22:11:06 в LiteLLM) – бо в VictoriaTraces вказаний час початку запиту (з моменту отримання HTTP-запиту), а в LiteLLM > Logs подія записується по завершенню.
Тобто – трейси показують, що помилка була один раз – але метрика litellm_deployment_failure_responses_total відображає два спайки помилок в різний час.
Запит в VictoriaMetrics – те саме, що бачили в Grafana, тільки тут increase() замість rate():
sum(
increase(
litellm_deployment_failure_responses_total{
exception_class="ValueError",
api_key_alias="svc-mainframe-prod"
}[5m]
)
) by (
api_key_alias,
exception_class
)
Почались помилки о 22:01:00 з найбільшим (на той момент) спайком о 22:16:00.
Отже, що маємо:
- багато помилок
ValueErrorв метриціlitellm_deployment_failure_responses_totalв періоди 22:00 – 23:30 і другий раз між 04:00 – 04:27 - але тільки одна помилка з повідомленням “Not allowed to access model due to tags configuration” в трейсах
Власне… WTF?
“Оманлива” метрика litellm_deployment_failure_responses
Перше, на чому я застряг – це сама метрика “litellm_deployment_failure_responses“.
В офіційній документації вона описана як “Total number of failed LLM API calls for a specific LLM deployment“, і коли розбирався з метриками (див. LiteLLM: метрики, traces та інтеграція з VictoriaMetrics Stack), то я це сприйняв як “Помилки, які виникли при запиті до або від провайдеру/моделі“.
Але! Якщо зробити той самий запит з litellm_deployment_failure_responses_total до VictoriaMetrics, тільки вже з групуванням по лейблам requested_model, litellm_model_name та api_provider і api_base – то побачимо цікаву картину:
sum(
increase(
litellm_deployment_failure_responses_total{
exception_class="ValueError",
api_key_alias="svc-mainframe-prod",
requested_model="gpt-5.6-luna"
}[5m]
)
) by (
requested_model,
litellm_model_name,
api_provider,
api_base,
exception_class
)
Результат:
В лейблі requested_model значення є – але всі інші пусті.
А значить коли ми отримали реквест від клієнта – то не змогли знайти Deployment/model, якому цей запит передати і, відповідно, в рамках цього routing attempt запит до провайдеру не потрапив.
При цьому зрештою запит клієнта все ж може бути оброблений через fallback routing – але про це трохи далі.
Про tag based routing та fallbacks писав в LiteLLM: OpenRouter та налаштування Fallbacks – і тут проблема пов’язана якраз з routing конфігом tag_regex, описаним там.
Зараз цікаво розібратись з іншим – значення метрики litellm_deployment_failure_responses_total інкрементиться навіть тоді, коли deployment ще не обраний і запиту до провайдера взагалі не було і, значить, litellm_deployment_failure_responses_total – це не просто “помилки, які виникли при запиті до або від провайдеру/моделі” і API провайдера – але і про роботу самої LiteLLM.
Аби зрозуміти на що ж саме спрацьовує litellm_deployment_failure_responses_total – глянемо в код LiteLLM.
LiteLLM та код метрики litellm_deployment_failure_responses_total
litellm_deployment_failure_responses – це counter, створюється стандартною Prometheus-бібліотекою prometheus-client.
Ось, де відбувається її інкремент в коді LiteLLM – файл prometheus.py:
...
def set_llm_deployment_failure_metrics(self, request_kwargs: dict):
...
exception: Final = request_kwargs.get("exception", None)
...
if exception is not None:
PrometheusLogger._inc_labeled_counter(
self,
self.litellm_deployment_failure_responses,
"litellm_deployment_failure_responses",
enum_values,
label_context=_deployment_label_ctx,
)
...
А ValueError ми отримуємо із tag_based_routing.py:
async def get_deployments_for_tag(
...
if len(new_healthy_deployments) == 0 and len(default_deployments) == 0:
raise ValueError(
f"{RouterErrors.no_deployments_with_tag_routing.value}."
f" Passed model={model} and tags={request_tags}"
)
...
Де “RouterErrors.no_deployments_with_tag_routing.value” якраз має те саме повідомлення – router.py:
class RouterErrors(enum.Enum):
...
no_deployments_with_tag_routing = "Not allowed to access model due to tags configuration"
...
Тобто значення метрики litellm_deployment_failure_responses_total інкрементиться не тільки при помилках від провайдера – але і при помилках у внутрішніх механізмах самого LiteLLM, в цьому випадку – при помилках роутингу/вибору моделей.
І якщо ми глянемо логи:
То весь цей час це була одна і та ж сама проблема з тегами – а не з запитами до провайдера.
Але… При цьому у нас реальних помилок повернених клієнту була тільки одна, яку ми бачимо в трейсах, бо в усіх інших випадках спрацьовував fallback – і клієнт врешті-решт отримував свій респонс:
І ці fallbacks ми бачимо по litellm_deployment_successful_fallbacks_total:
Але тоді…
Звідки взявся той єдиний трейс зі status_code="2"?
OpenAI та “Invalid prompt: your prompt was flagged as potentially violating our usage policy”
Якщо глянемо на метрику litellm_deployment_failed_fallbacks_total – то якраз о 22:12 побачимо невдалий fallback:
І саме він потрапив в трейси з ValueError – але цього разу з іншої причини, яка знайшлась в логах з повідомленням від OpenAI “Invalid prompt: your prompt was flagged as potentially violating our usage policy“:
- ~22:11 UTC ми отримали від клієнта запит
- LiteLLM перевірив tag based routing, виявив, що нема моделей, на які можна відправити цей запит – і повернув
ValueError- значення
litellm_deployment_failure_responses_total{exception_class="ValueError"}збільшилось на 1
- значення
- не знайшовши деплойментів, на які можна відправити запит – LiteLLM перейшов до fallback опцій і все ж відправив цей запит до OpenAI
- але OpenAI відхилив його з помилкою “Invalid prompt: your prompt was flagged as potentially violating our usage policy” (
ContentPolicyViolationErrorвexception_mapping_utils.py) - fallback сфейлився – бо інших опцій не було
- в результаті ми повернули помилку клієнту, яку бачимо в метриках
litellm_deployment_failed_fallbacks_totalтаlitellm_proxy_failed_requests_metric_total
LiteLLM та exception_class “ContentPolicyViolationError” vs “ValueError”
Але якщо ми отримали ContentPolicyViolationError – то чому в exception_class метрик litellm_deployment_failed_fallbacks_total та litellm_proxy_failed_requests_metric_total все одно відображалась ValueError?
Знов йдемо в код LiteLLM: на початку async_function_with_fallbacks_common_utils() LiteLLM зберігає початковий tag-routing ValueError в original_exception:
...
async def async_function_with_fallbacks_common_utils(
...
original_exception = e
...
А ContentPolicyViolationError, отриманий під час fallback, потрапляє в окремий new_exception і записується в лог – тоді як Router повертає original_exception зі значенням ValueError, яке було задане при помилках роутингу:
...
except Exception as new_exception:
...
raise original_exception
...
В результаті ми всюди бачимо ValueError – що, в принципі, логічно, бо саме це стало першопричиною проблеми – але фактично помилка клієнту була викликана саме ContentPolicyViolationError – яку ми бачимо тільки в логах.
І якби tag routing відпрацював без помилок – то ми б мали саме ContentPolicyViolationError як exception_class.
Замість висновків
Отже, що маємо в результаті:
- метрика
litellm_deployment_failure_responses_total– це не лише помилки API провайдера, і ця метрика може інкрементитись через внутрішні помилки LiteLLM ще до вибору deployment, наприклад на етапі routing exception_class="ValueError"повідомляє лише клас помилки, але не її конкретну причину, аValueErrorможе виникати в різних частинах коду, тому кожен такий випадок варто досліджувати окремо – принаймні на початку використання LiteLLM- фінальний trace не завжди містить усю історію обробки запиту – в нашому випадку він показував початковий
ValueError, тоді як причина провалу fallback –ContentPolicyViolationErrorвід OpenAI залишилася лише в логах - варто створити recording rules для основних помилок та окремі log-based alerts для відомих повідомлень про помилки, бо самих метрик і трейсів може бути недостатньо, щоб швидко знайти справжню причину проблеми
Bonus: приклад VictoriaLogs RecordingRule для “ContentPolicyViolationError”
Про RecordingRules в VictoriaLogs писав в VictoriaLogs: створення Recording Rules з VMAlert.
Додаємо нове правило – створюємо метрику vmlogs:litellm:logs:content_policy_violation:rate з лейблами model та provider:
- record: vmlogs:litellm:logs:content_policy_violation:rate
expr: |
{namespace="ops-litellm-ns"} app:="litellm" "ageneric_api_call_with_fallbacks(model=" "ContentPolicyViolationError"
| extract_regexp "ageneric_api_call_with_fallbacks\\(model=(?P<model>[^)]+)\\)"
| extract_regexp "ContentPolicyViolationError: (?P<provider>[A-Za-z0-9_-]+)Exception"
| stats by (namespace, app, model, provider) rate() errors_per_second
Перевіряємо результат в самій VictoriaLogs:
- alert: LiteLLM Content Policy Violation
expr: |
sum by (namespace, app, model, provider) (
vmlogs:litellm:logs:content_policy_violation:rate
) > 0
for: 1s
labels:
component: devops
environment: ops
severity: warning
ilert_routingkey: devops-ops-warning
annotations:
summary: LiteLLM Content Policy Violation
description: |-
An LLM provider rejected a LiteLLM request due to its content policy.
*Namespace*: `{{ "{{" }} $labels.namespace }}`
*Application*: `{{ "{{" }} $labels.app }}`
*Model*: `{{ "{{" }} $labels.model }}`
*Provider*: `{{ "{{" }} $labels.provider }}`
*Rate*: `{{ "{{" }} $value | humanize }}`/s
<https://{{ $.Values.monitoring.root_url }}/d/adrmshg/litellm-system-overview |:grafana: LiteLLM System overview>
Готово.
![]()








