ZhuLinsen/daily_stock_analysis · error · ValueError

Responses API surface requires a normalized openai/<model> r

Error message

Responses API surface requires a normalized openai/<model> route; got {normalized_model!r}

What it means

Config-time validation in apply_litellm_api_surface (src/config.py): when a channel declares api_surface='responses', the wire model must already be an explicit openai-prefixed route (get_explicit_llm_channel_model_provider(model) == 'openai'). The Responses API surface is bridged by rewriting 'openai/<model>' into 'openai/responses/<model>' for LiteLLM; a provider-less bare model name or another provider (anthropic/..., bedrock/...) cannot be bridged this way, so the config is rejected.

Source

Thrown at src/config.py:491

        return raw_prefix
    if canonical_prefix in providers:
        return canonical_prefix
    return ""


def apply_litellm_api_surface(model: str, api_surface: Optional[str]) -> str:
    """Encode an explicit API surface in a LiteLLM wire model.

    LiteLLM's ``provider/responses/model`` convention keeps the public Router
    alias stable while letting ``completion()`` bridge messages, streaming,
    tools, responses, and usage through the provider's Responses endpoint.
    """
    normalized_model = (model or "").strip()
    if not normalized_model or normalize_llm_channel_api_surface(api_surface) != "responses":
        return normalized_model
    provider = get_explicit_llm_channel_model_provider(normalized_model)
    if provider != "openai":
        raise ValueError(
            "Responses API surface requires a normalized openai/<model> route; "
            f"got {normalized_model!r}"
        )
    provider, remainder = normalized_model.split("/", 1)
    if remainder.startswith("responses/"):
        return normalized_model
    return f"{provider}/responses/{remainder}"


def resolve_llm_channel_protocol(
    protocol: Optional[str],
    *,
    base_url: Optional[str] = None,
    models: Optional[List[str]] = None,
    channel_name: Optional[str] = None,
) -> str:
    """Resolve the effective protocol for a channel."""
    explicit = canonicalize_llm_channel_protocol(protocol)

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Use an explicit openai route: set model to 'openai/gpt-4o' (or 'openai/responses/gpt-4o') on the channel using api_surface=responses.
  2. If the target provider is not OpenAI, remove api_surface=responses and use the provider's native chat/completions surface.
  3. Verify the channel YAML/env after fixing by re-running config parsing before restarting the service.

Example fix

# before
models: ["gpt-4o"]
api_surface: responses

# after
models: ["openai/gpt-4o"]
api_surface: responses
Defensive patterns

Strategy: validation

Validate before calling

from src.config import get_explicit_llm_channel_model_provider

if api_surface == "responses":
    assert get_explicit_llm_channel_model_provider(model) == "openai", (
        f"responses surface needs openai/<model>, got {model!r}"
    )

Type guard

def is_openai_route(model: str) -> bool:
    return get_explicit_llm_channel_model_provider(model) == "openai"

Try / catch

try:
    wire = apply_litellm_api_surface(model, api_surface)
except ValueError as exc:
    raise ConfigError(str(exc)) from exc

Prevention

When it happens

Trigger: Setting api_surface: responses (or API_SURFACE=responses) on a channel whose model is 'gpt-4o' (no provider prefix), 'anthropic/claude-3', or any non-openai provider route. The function raises before any router is built, so this fails at config load/parse time.

Common situations: Copy-pasting a Responses-API example onto a non-OpenAI channel; assuming LiteLLM's responses bridging works for all providers; forgetting the 'openai/' prefix when migrating from OpenAI direct config.

Related errors


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