{"record":{"id":"c98cdb384020305f","repo":"ZhuLinsen/daily_stock_analysis","slug":"llm-route-aliases-cannot-mix-api-surfaces-sorted","errorCode":null,"errorMessage":"LLM route aliases cannot mix API surfaces: {sorted(surface_conflicts)}","messagePattern":"LLM route aliases cannot mix API surfaces: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/config.py","lineNumber":2570,"sourceCode":"                        str(channel.get(\"base_url\") or \"\"),\n                    )\n                    for model in channel.get(\"models\") or []\n                }.intersection(conflicting_models)\n            ]\n\n        return channels, issues, blocks_legacy_fallback, blocked_hermes_routes\n\n    @classmethod\n    def _channels_to_model_list(cls, channels: List[Dict[str, Any]]) -> List[Dict[str, Any]]:\n        \"\"\"Convert parsed LLM channels to LiteLLM Router model_list format.\n\n        Mapping follows:\n        - LiteLLM providers: https://docs.litellm.ai/docs/providers\n        - LiteLLM model_list 语义: https://docs.litellm.ai/docs/proxy/configs#the-model_list-key\n        \"\"\"\n        surface_conflicts = find_llm_channel_surface_conflicts(channels)\n        if surface_conflicts:\n            raise ValueError(\n                \"LLM route aliases cannot mix API surfaces: \"\n                + \", \".join(sorted(surface_conflicts))\n            )\n        model_list: List[Dict[str, Any]] = []\n        for ch in channels:\n            hermes_refs = {\n                str(ref.get(\"route_model\") or \"\"): ref\n                for ref in (ch.get(\"model_refs\") or [])\n                if isinstance(ref, dict)\n            }\n            for model_name in ch['models']:\n                for api_key in ch['api_keys']:\n                    model_ref = hermes_refs.get(str(model_name))\n                    wire_model = str((model_ref or {}).get(\"wire_model\") or model_name)\n                    api_surface = normalize_llm_channel_api_surface(ch.get(\"api_surface\"))\n                    wire_model = apply_litellm_api_surface(wire_model, api_surface)\n                    litellm_params: Dict[str, Any] = {\n                        'model': wire_model,","sourceCodeStart":2552,"sourceCodeEnd":2588,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/config.py#L2552-L2588","documentation":"Config-time validation in _channels_to_model_list (src/config.py): find_llm_channel_surface_conflicts detects that the same public route alias (the normalized model name LiteLLM Router routes on) is declared by more than one enabled channel with different api_surface values (e.g. 'responses' vs 'chat'). LiteLLM's Router keys deployments by model_name, so one alias with two surfaces is ambiguous — requests could hit either wire format nondeterministically. The conflicting alias names are embedded in the message.","triggerScenarios":"Two enabled channels both listing model 'openai/gpt-4o' but one with api_surface: responses and the other without (or 'chat'); or a channel re-declaring an alias after protocol/base_url normalization with a different surface. Raised while building the Router model_list, i.e. at startup/config load.","commonSituations":"Adding a new Responses-API channel while keeping the old chat-completions channel for 'migration'; YAML merge duplicating a channel block with edited surface; copy-paste between staging and production configs.","solutions":["Give each surface a distinct route alias (e.g. 'openai/gpt-4o' for chat and 'openai/gpt-4o-responses' for responses) so no alias maps to two surfaces.","Or remove/disable (enabled: false) the old-surface channel declaring the same alias.","Search all channel configs for the aliases listed in the error message to find both declarations."],"exampleFix":"# before (two channels share alias 'openai/gpt-4o')\nchannel A: models=[\"openai/gpt-4o\"], api_surface: responses\nchannel B: models=[\"openai/gpt-4o\"]\n\n# after\nchannel A: models=[\"openai/gpt-4o-resp\"], api_surface: responses\nchannel B: models=[\"openai/gpt-4o\"]","handlingStrategy":"validation","validationCode":"from src.config import find_llm_channel_surface_conflicts\n\nconflicts = find_llm_channel_surface_conflicts(channels)\nif conflicts:\n    raise SystemExit(f\"alias/surface conflicts: {sorted(conflicts)}\")","typeGuard":null,"tryCatchPattern":"try:\n    model_list = Config._channels_to_model_list(channels)\nexcept ValueError as exc:\n    if \"mix API surfaces\" in str(exc):\n        # rename or disable the conflicting alias\n    raise","preventionTips":["One route alias = one api_surface; introduce a new alias when migrating surfaces","Run find_llm_channel_surface_conflicts as a preflight check on channel configs","Disable (enabled: false) old channels instead of leaving duplicates"],"tags":["config","litellm","llm","routing","validation"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}