ZhuLinsen/daily_stock_analysis · error · ValueError

LiteLLM vision returned empty response

Error message

LiteLLM vision returned empty response

What it means

Raised after a successful litellm.completion() call when the response has no choices, an empty first message, or empty content. The HTTP call itself succeeded, but nothing text-like came back, so the extractor cannot parse stock codes.

Source

Thrown at src/services/image_stock_extractor.py:342

        call_kwargs["api_key"] = effective_api_key
    if deployment_params.get("api_base"):
        call_kwargs["api_base"] = deployment_params["api_base"]
    if deployment_params.get("extra_headers"):
        call_kwargs["extra_headers"] = dict(deployment_params["extra_headers"])
    # Add api_base and custom headers for OpenAI-compatible providers
    if not deployment_params and not model.startswith("gemini/") and not model.startswith("anthropic/") and not model.startswith("vertex_ai/"):
        if cfg.openai_base_url:
            call_kwargs["api_base"] = cfg.openai_base_url
        if cfg.openai_base_url and "aihubmix.com" in cfg.openai_base_url:
            call_kwargs["extra_headers"] = {"APP-Code": "GPIJ3886"}

    if getattr(litellm, "completion", None) is None:
        import litellm as litellm_module
        litellm = litellm_module
    response = litellm.completion(**call_kwargs)
    if response and response.choices and response.choices[0].message.content:
        return response.choices[0].message.content
    raise ValueError("LiteLLM vision returned empty response")


def extract_stock_codes_from_image(
    image_bytes: bytes,
    mime_type: str,
) -> Tuple[List[Tuple[str, Optional[str], str]], str]:
    """
    从图片中提取股票代码及名称(使用 Vision LLM)。

    优先级:Gemini -> Anthropic -> OpenAI(首个可用)。
    支持多 Key 轮询与重试(最多 3 次,指数退避)。

    Args:
        image_bytes: 原始图片字节
        mime_type: MIME 类型(如 image/jpeg, image/png)

    Returns:
        (items, raw_text) - items 为 [(code, name?, confidence), ...],raw_text 为原始 LLM 响应。

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Retry the call — transient empty responses often succeed on retry (the outer loop already retries 3x, so persistent failure means provider/config issue)
  2. Log response.choices[0].finish_reason and full response to see why content is empty
  3. Verify the wire model actually supports vision input (check deployment_params['model'])
  4. If content_filter, change image or use a different provider/model

Example fix

// not applicable — runtime/provider issue, no caller-side code fix
Defensive patterns

Strategy: retry

Type guard

def has_content(resp) -> bool:
    try:
        return bool(resp and resp.choices and resp.choices[0].message.content)
    except AttributeError:
        return False

Try / catch

for attempt in range(3):
    try:
        return _call_litellm_vision(b64, mime, key)
    except ValueError as e:
        if 'empty response' not in str(e) or attempt == 2:
            raise
        time.sleep(2 ** attempt)

Prevention

When it happens

Trigger: Provider returns 200 with empty content (safety filter, finish_reason=length/content_filter, tool-call-only response, or a misrouted proxy model returning an object without message content).

Common situations: Image flagged by provider content filters; model alias on a proxy (e.g. aihubmix) resolving to a non-vision model; truncated response due to max_tokens; flaky provider returning malformed payloads.

Related errors


AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15). Data as JSON: /api/errors/864c92b89408e6dd. Report an issue: GitHub.