{"record":{"id":"df6acbf48c2f5f7c","repo":"ZhuLinsen/daily_stock_analysis","slug":"responses-api-surface-requires-a-normalized-openai","errorCode":null,"errorMessage":"Responses API surface requires a normalized openai/<model> route; got {normalized_model!r}","messagePattern":"Responses API surface requires a normalized openai/<model> route; got (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/config.py","lineNumber":491,"sourceCode":"        return raw_prefix\n    if canonical_prefix in providers:\n        return canonical_prefix\n    return \"\"\n\n\ndef apply_litellm_api_surface(model: str, api_surface: Optional[str]) -> str:\n    \"\"\"Encode an explicit API surface in a LiteLLM wire model.\n\n    LiteLLM's ``provider/responses/model`` convention keeps the public Router\n    alias stable while letting ``completion()`` bridge messages, streaming,\n    tools, responses, and usage through the provider's Responses endpoint.\n    \"\"\"\n    normalized_model = (model or \"\").strip()\n    if not normalized_model or normalize_llm_channel_api_surface(api_surface) != \"responses\":\n        return normalized_model\n    provider = get_explicit_llm_channel_model_provider(normalized_model)\n    if provider != \"openai\":\n        raise ValueError(\n            \"Responses API surface requires a normalized openai/<model> route; \"\n            f\"got {normalized_model!r}\"\n        )\n    provider, remainder = normalized_model.split(\"/\", 1)\n    if remainder.startswith(\"responses/\"):\n        return normalized_model\n    return f\"{provider}/responses/{remainder}\"\n\n\ndef resolve_llm_channel_protocol(\n    protocol: Optional[str],\n    *,\n    base_url: Optional[str] = None,\n    models: Optional[List[str]] = None,\n    channel_name: Optional[str] = None,\n) -> str:\n    \"\"\"Resolve the effective protocol for a channel.\"\"\"\n    explicit = canonicalize_llm_channel_protocol(protocol)","sourceCodeStart":473,"sourceCodeEnd":509,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/config.py#L473-L509","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use an explicit openai route: set model to 'openai/gpt-4o' (or 'openai/responses/gpt-4o') on the channel using api_surface=responses.","If the target provider is not OpenAI, remove api_surface=responses and use the provider's native chat/completions surface.","Verify the channel YAML/env after fixing by re-running config parsing before restarting the service."],"exampleFix":"# before\nmodels: [\"gpt-4o\"]\napi_surface: responses\n\n# after\nmodels: [\"openai/gpt-4o\"]\napi_surface: responses","handlingStrategy":"validation","validationCode":"from src.config import get_explicit_llm_channel_model_provider\n\nif api_surface == \"responses\":\n    assert get_explicit_llm_channel_model_provider(model) == \"openai\", (\n        f\"responses surface needs openai/<model>, got {model!r}\"\n    )","typeGuard":"def is_openai_route(model: str) -> bool:\n    return get_explicit_llm_channel_model_provider(model) == \"openai\"","tryCatchPattern":"try:\n    wire = apply_litellm_api_surface(model, api_surface)\nexcept ValueError as exc:\n    raise ConfigError(str(exc)) from exc","preventionTips":["Always prefix OpenAI models with openai/ in channel config","Reserve api_surface=responses for OpenAI channels only","Validate channel YAML in CI with a config-parse smoke test"],"tags":["config","litellm","llm","validation"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}