_parse_openai_response: int() на нечисловых usage-токенах бросает ValueError/TypeError мимо fallback-гарантии complete #1601

Closed
opened 2026-06-16 17:16:27 +00:00 by bot-backend · 0 comments
Collaborator

Severity: medium · Категория: error-handling/contract-violation · Файл: backend/app/services/llm/provider.py:117-119

provider.py:117-118 — prompt_tokens=int(usage.get("prompt_tokens", 0) or 0) и completion_tokens=int(usage.get("completion_tokens", 0) or 0). При нечисловом непустом значении (str "abc" → "abc" or 0 == "abc" → int("abc") = ValueError) или вложенном dict/list (int({...}) = TypeError) поднимается исключение. _parse_openai_response вызывается из OpenAIProvider.complete (provider.py:206), которая вызывается в try в _call_with_retries (client.py:217). Этот try ловит ТОЛЬКО LLMRateLimitedError и LLMProviderError (client.py:222,245) — ValueError/TypeError проходят насквозь и пробивают наружу из complete(). Это нарушает жёсткий инвариант (docstring client.py:5-8 и init.py п.1): complete НИКОГДА не падает наружу из-за LLM, любая проблема → LLMResult(ok=False, fallback_used=True). Импакт: если внешний провайдер/прокси перед settings.llm_base_url вернёт usage с нечисловым/неожиданным типом, исключение упадёт в вызывающий код (chat / Celery-extraction), рассчитывавший на безопасную деградацию. Краевой, но реальный путь на границе с недоверенными данными. Фикс: обернуть парсинг usage в try/except (ValueError, TypeError) с дефолтом 0, либо использовать безопасный хелпер _coerce_int(value)->int, либо ловить эти исключения в _post/_parse и поднимать LLMProviderError (тогда сработает существующий fallback). Оценка severity: medium с оговоркой — путь краевой (требует «битый» usage от внешнего источника), штатные ответы OpenAI всегда числовые.

Почему баг: Жёсткий архитектурный инвариант проекта (docstring client.py, init.py п.1): complete НИКОГДА не бросает наружу из-за LLM — любая проблема превращается в LLMResult(ok=False, fallback_used=True). Если внешний провайдер (или прокси/CDN перед base_url, который можно подменить через settings.llm_base_url) вернёт usage с нечисловым/неожиданным типом (например {"prompt_tokens": null-as-string, …} или вложенный объект), исключение пробьёт fallback-гарантию и упадёт в вызывающий код (chat-эндпоинт / Celery-extraction), который рассчитывал на безопасную деградацию. Это краевой, но реальный путь на границе с недоверенными внешними данными.

Как чинить: Парсить usage через защищённый helper: обернуть int(...) в try/except (ValueError, TypeError) с дефолтом 0, ЛИБО ловить в _call_with_retries более широкий Exception вокруг provider.complete() и тоже превращать в LLMResult.fallback("provider_error"), сохраняя инвариант «complete не бросает».


deep-audit backend v2 (после PR #1543) · verify+harden, confidence 0.78 · unit B29

**Severity:** medium · **Категория:** error-handling/contract-violation · **Файл:** `backend/app/services/llm/provider.py:117-119` provider.py:117-118 — prompt_tokens=int(usage.get("prompt_tokens", 0) or 0) и completion_tokens=int(usage.get("completion_tokens", 0) or 0). При нечисловом непустом значении (str "abc" → "abc" or 0 == "abc" → int("abc") = ValueError) или вложенном dict/list (int({...}) = TypeError) поднимается исключение. _parse_openai_response вызывается из OpenAIProvider.complete (provider.py:206), которая вызывается в try в _call_with_retries (client.py:217). Этот try ловит ТОЛЬКО LLMRateLimitedError и LLMProviderError (client.py:222,245) — ValueError/TypeError проходят насквозь и пробивают наружу из complete(). Это нарушает жёсткий инвариант (docstring client.py:5-8 и __init__.py п.1): complete НИКОГДА не падает наружу из-за LLM, любая проблема → LLMResult(ok=False, fallback_used=True). Импакт: если внешний провайдер/прокси перед settings.llm_base_url вернёт usage с нечисловым/неожиданным типом, исключение упадёт в вызывающий код (chat / Celery-extraction), рассчитывавший на безопасную деградацию. Краевой, но реальный путь на границе с недоверенными данными. Фикс: обернуть парсинг usage в try/except (ValueError, TypeError) с дефолтом 0, либо использовать безопасный хелпер _coerce_int(value)->int, либо ловить эти исключения в _post/_parse и поднимать LLMProviderError (тогда сработает существующий fallback). Оценка severity: medium с оговоркой — путь краевой (требует «битый» usage от внешнего источника), штатные ответы OpenAI всегда числовые. **Почему баг:** Жёсткий архитектурный инвариант проекта (docstring client.py, __init__.py п.1): complete НИКОГДА не бросает наружу из-за LLM — любая проблема превращается в LLMResult(ok=False, fallback_used=True). Если внешний провайдер (или прокси/CDN перед base_url, который можно подменить через settings.llm_base_url) вернёт usage с нечисловым/неожиданным типом (например {"prompt_tokens": null-as-string, …} или вложенный объект), исключение пробьёт fallback-гарантию и упадёт в вызывающий код (chat-эндпоинт / Celery-extraction), который рассчитывал на безопасную деградацию. Это краевой, но реальный путь на границе с недоверенными внешними данными. **Как чинить:** Парсить usage через защищённый helper: обернуть int(...) в try/except (ValueError, TypeError) с дефолтом 0, ЛИБО ловить в _call_with_retries более широкий Exception вокруг provider.complete() и тоже превращать в LLMResult.fallback("provider_error"), сохраняя инвариант «complete не бросает». --- <sub>deep-audit backend v2 (после PR #1543) · verify+harden, confidence 0.78 · unit B29</sub>
bot-backend added the
week ревью 1
label 2026-06-16 17:16:27 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1601
No description provided.