"""
Provider Registry — single source of truth for LLM provider metadata.

Adding a new provider:
  1. Add a ProviderSpec to PROVIDERS below.
  2. Add a field to ProvidersConfig in config/schema.py.
  Done. Env vars, config matching, status display all derive from here.

Order matters — it controls match priority and fallback. Gateways first.
Every entry writes out all fields so you can copy-paste as a template.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from pydantic.alias_generators import to_snake


@dataclass(frozen=True)
class ProviderModelSpec:
    """Curated model metadata used for fixed catalogs or online fallback."""

    id: str
    label: str = ""
    description: str = ""
    owned_by: str = ""
    context_window: int | None = None
    reasoning_efforts: tuple[str, ...] = ()
    supports_backend_search: bool = False


@dataclass(frozen=True)
class ProviderSpec:
    """One LLM provider's metadata. See PROVIDERS below for real examples.

    Placeholders in env_extras values:
      {api_key}  — the user's API key
      {api_base} — api_base from config, or this spec's default_api_base
    """

    # identity
    name: str  # config field name, e.g. "dashscope"
    keywords: tuple[str, ...]  # model-name keywords for matching (lowercase)
    env_key: str  # env var for API key, e.g. "DASHSCOPE_API_KEY"
    display_name: str = ""  # shown in `nanobot status`
    model_catalog: str = "auto"  # WebUI model-list source, including builtin/hybrid
    builtin_models: tuple[ProviderModelSpec, ...] = ()
    settings_alias_for: str = ""  # compatibility alias grouped under this provider in Settings

    # which provider implementation to use
    # "openai_compat" | "anthropic" | "azure_openai" | "openai_codex" | "xai_grok"
    # | "github_copilot" | "bedrock"
    backend: str = "openai_compat"

    # extra env vars / request headers supplied by the provider integration.
    env_extras: tuple[tuple[str, str], ...] = ()
    default_extra_headers: tuple[tuple[str, str], ...] = ()

    # gateway / local detection
    is_gateway: bool = False  # routes any model (OpenRouter, AiHubMix)
    is_local: bool = False  # local deployment (vLLM, Ollama)
    detect_by_key_prefix: str = ""  # match api_key prefix, e.g. "sk-or-"
    detect_by_base_keyword: str = ""  # match substring in api_base URL
    default_api_base: str = ""  # OpenAI-compatible base URL for this provider

    # gateway behavior
    strip_model_prefix: bool = False  # strip "provider/" before sending to gateway
    strip_model_prefixes: tuple[str, ...] = ()  # strip only when the first model segment matches
    supports_max_completion_tokens: bool = False

    # per-model param overrides, e.g. (("kimi-k2.5", {"temperature": 1.0}),)
    model_overrides: tuple[tuple[str, dict[str, Any]], ...] = ()

    # OAuth-based providers (e.g., OpenAI Codex) don't use API keys
    is_oauth: bool = False

    # Direct providers skip API-key validation (user supplies everything)
    is_direct: bool = False

    # Provider is listed for shared credentials but cannot serve chat completions.
    is_transcription_only: bool = False

    # Provider supports cache_control on content blocks (e.g. Anthropic prompt caching)
    supports_prompt_caching: bool = False

    # How to inject the thinking on/off toggle into extra_body.
    # ""              — no extra_body needed (default)
    # "thinking_type" — {"thinking": {"type": "enabled"/"disabled"}}
    #                   (DeepSeek, VolcEngine, BytePlus)
    # "enable_thinking" — {"enable_thinking": true/false}  (DashScope)
    # "reasoning_split" — {"reasoning_split": true/false}  (MiniMax)
    thinking_style: str = ""

    # Gateway-native reasoning control to pair with model-level thinking styles.
    # "reasoning_effort" — {"reasoning": {"effort": <none|minimal|...>}}
    #                      (OpenRouter)
    gateway_reasoning_style: str = ""

    # When True, treat the "reasoning" response field as formal content
    # when "content" is empty.  Only set this for providers (e.g. StepFun)
    # whose API returns the actual answer in "reasoning" instead of "content".
    reasoning_as_content: bool = False

    # Map user-supplied reasoning_effort (OpenAI vocab: minimal/low/medium/high)
    # to the value this provider accepts on the wire. Set when the provider's
    # accepted set differs from OpenAI's. An empty mapped value omits the kwarg.
    # Mistral: only "high"/"none" — low/minimal map to "none", medium maps to "high".
    reasoning_effort_remap: tuple[tuple[str, str], ...] = ()

    # Models whose API rejects the reasoning_effort kwarg because reasoning is
    # implicit (Magistral always reasons; sending the kwarg returns HTTP 400).
    # Substring match against the wire model name (lowercased).
    implicit_reasoning_models: tuple[str, ...] = ()

    # Models that expose the OpenAI Responses wire format.  This is model-level
    # because providers may add Responses support incrementally.
    responses_models: tuple[str, ...] = ()

    # Provider-hosted Responses tools sent unless extraBody.tools explicitly
    # supplies the hosted-tool selection. Values are raw Responses tool types.
    responses_default_tools: tuple[str, ...] = ()

    # When the model returns content as a list of {"type":"thinking",...} +
    # {"type":"text",...} blocks, extract the thinking text into
    # reasoning_content. Mistral's Magistral / reasoning-enabled responses use
    # this shape.
    extract_thinking_blocks: bool = False

    # Strip ``reasoning_content`` from assistant history messages before
    # sending. Mistral validates its request schema strictly and 400s on
    # any extra fields; other providers (DeepSeek) require this key on the
    # wire to keep thinking-mode history intact.
    strip_history_reasoning_content: bool = False

    @property
    def label(self) -> str:
        return self.display_name or self.name.title()


# ---------------------------------------------------------------------------
# PROVIDERS — the registry. Order = priority. Copy any entry as template.
# ---------------------------------------------------------------------------

PROVIDERS: tuple[ProviderSpec, ...] = (
    # === Custom (direct OpenAI-compatible endpoint) ========================
    ProviderSpec(
        name="custom",
        keywords=(),
        env_key="",
        display_name="Custom",
        backend="openai_compat",
        is_direct=True,
    ),

    # === Azure OpenAI (direct API calls with API version 2024-10-21) =====
    ProviderSpec(
        name="azure_openai",
        keywords=("azure", "azure-openai"),
        env_key="",
        display_name="Azure OpenAI",
        backend="azure_openai",
        is_direct=True,
    ),
    # === AWS Bedrock (native Converse API via bedrock-runtime) =============
    ProviderSpec(
        name="bedrock",
        keywords=(
            "bedrock",
            "anthropic.claude",
            "amazon.nova",
            "meta.",
            "mistral.",
            "cohere.",
            "qwen.",
            "deepseek.",
            "openai.gpt-oss",
            "ai21.",
            "moonshot.",
            "writer.",
            "zai.",
        ),
        env_key="AWS_BEARER_TOKEN_BEDROCK",
        display_name="AWS Bedrock",
        backend="bedrock",
        is_direct=True,
    ),
    # === Gateways (detected by api_key / api_base, not model name) =========
    # Gateways can route any model, so they win in fallback.
    # OpenRouter: global gateway, keys start with "sk-or-"
    ProviderSpec(
        name="openrouter",
        keywords=("openrouter",),
        env_key="OPENROUTER_API_KEY",
        display_name="OpenRouter",
        backend="openai_compat",
        is_gateway=True,
        detect_by_key_prefix="sk-or-",
        detect_by_base_keyword="openrouter",
        default_api_base="https://openrouter.ai/api/v1",
        supports_prompt_caching=True,
        gateway_reasoning_style="reasoning_effort",
    ),
    # OrcaRouter: global gateway, keys start with "sk-orca-"
    ProviderSpec(
        name="orcarouter",
        keywords=("orcarouter",),
        env_key="ORCAROUTER_API_KEY",
        display_name="OrcaRouter",
        backend="openai_compat",
        is_gateway=True,
        detect_by_key_prefix="sk-orca-",
        detect_by_base_keyword="orcarouter",
        default_api_base="https://api.orcarouter.ai/v1",
    ),
    # Eden AI: OpenAI-compatible gateway. Models use the "provider/model"
    # naming scheme (e.g. "anthropic/claude-sonnet-4-5"); the full id is sent upstream.
    ProviderSpec(
        name="edenai",
        keywords=("edenai",),
        env_key="EDENAI_API_KEY",
        display_name="Eden AI",
        backend="openai_compat",
        is_gateway=True,
        detect_by_base_keyword="edenai",
        default_api_base="https://api.edenai.run/v3",
    ),
    # OpenCode Zen: OpenAI-compatible chat-completions gateway for coding models.
    # models.dev/OpenCode use provider id "opencode" and model ids like
    # "opencode/<model>"; send the bare model upstream.
    ProviderSpec(
        name="opencode",
        keywords=("opencode/", "opencode", "opencode-zen", "opencode_zen"),
        env_key="OPENCODE_API_KEY",
        display_name="OpenCode Zen",
        backend="openai_compat",
        is_gateway=True,
        detect_by_base_keyword="opencode.ai/zen",
        default_api_base="https://opencode.ai/zen/v1",
        strip_model_prefixes=("opencode", "opencode_zen", "opencode-zen"),
    ),
    # Compatibility alias for configs that already used providers.opencodeZen.
    ProviderSpec(
        name="opencode_zen",
        keywords=("opencode/", "opencode_zen", "opencode-zen"),
        env_key="OPENCODE_API_KEY",
        display_name="OpenCode Zen",
        settings_alias_for="opencode",
        backend="openai_compat",
        is_gateway=True,
        detect_by_base_keyword="opencode.ai/zen",
        default_api_base="https://opencode.ai/zen/v1",
        strip_model_prefixes=("opencode", "opencode_zen", "opencode-zen"),
    ),
    # OpenCode Go: OpenAI-compatible chat-completions gateway for low-cost models.
    # OpenCode's own config uses "opencode-go/<model>"; send the bare model upstream.
    ProviderSpec(
        name="opencode_go",
        keywords=("opencode-go", "opencode_go"),
        env_key="OPENCODE_API_KEY",
        display_name="OpenCode Go",
        backend="openai_compat",
        is_gateway=True,
        detect_by_base_keyword="opencode.ai/zen/go",
        default_api_base="https://opencode.ai/zen/go/v1",
        strip_model_prefixes=("opencode-go", "opencode_go"),
    ),
    # Hugging Face Inference Providers: OpenAI-compatible router for chat models.
    ProviderSpec(
        name="huggingface",
        keywords=("huggingface", "hugging-face"),
        env_key="HF_TOKEN",
        display_name="Hugging Face",
        backend="openai_compat",
        is_gateway=True,
        detect_by_key_prefix="hf_",
        detect_by_base_keyword="huggingface",
        default_api_base="https://router.huggingface.co/v1",
    ),
    # Skywork API platform (APIFree): OpenAI-compatible MaaS gateway.
    ProviderSpec(
        name="skywork",
        keywords=("skywork", "skyclaw", "apifree"),
        env_key="SKYWORK_API_KEY",
        display_name="Skywork",
        model_catalog="official",
        backend="openai_compat",
        env_extras=(("APIFREE_API_KEY", "{api_key}"),),
        is_gateway=True,
        detect_by_base_keyword="apifree.ai",
        default_api_base="https://api.apifree.ai/agent/v1",
    ),
    # AiHubMix: global gateway, OpenAI-compatible interface.
    # strip_model_prefix=True: doesn't understand "anthropic/claude-3",
    # strips to bare "claude-3".
    ProviderSpec(
        name="aihubmix",
        keywords=("aihubmix",),
        env_key="OPENAI_API_KEY",
        display_name="AiHubMix",
        backend="openai_compat",
        is_gateway=True,
        detect_by_base_keyword="aihubmix",
        default_api_base="https://aihubmix.com/v1",
        strip_model_prefix=True,
    ),
    # SiliconFlow (硅基流动): OpenAI-compatible gateway, model names keep org prefix
    ProviderSpec(
        name="siliconflow",
        keywords=("siliconflow",),
        env_key="OPENAI_API_KEY",
        display_name="SiliconFlow",
        backend="openai_compat",
        is_gateway=True,
        detect_by_base_keyword="siliconflow",
        default_api_base="https://api.siliconflow.cn/v1",
    ),

    # Novita AI: OpenAI-compatible gateway for hosted model APIs.
    ProviderSpec(
        name="novita",
        keywords=("novita",),
        env_key="NOVITA_API_KEY",
        display_name="Novita AI",
        backend="openai_compat",
        is_gateway=True,
        detect_by_base_keyword="novita",
        default_api_base="https://api.novita.ai/openai",
    ),

    # VolcEngine (火山引擎): OpenAI-compatible gateway, pay-per-use models
    ProviderSpec(
        name="volcengine",
        keywords=("volcengine", "volces", "ark"),
        env_key="OPENAI_API_KEY",
        display_name="VolcEngine",
        backend="openai_compat",
        is_gateway=True,
        detect_by_base_keyword="volces",
        default_api_base="https://ark.cn-beijing.volces.com/api/v3",
        thinking_style="thinking_type",
        supports_max_completion_tokens=True,
    ),

    # VolcEngine Coding Plan (火山引擎 Coding Plan): same key as volcengine
    ProviderSpec(
        name="volcengine_coding_plan",
        keywords=("volcengine-plan",),
        env_key="OPENAI_API_KEY",
        display_name="VolcEngine Coding Plan",
        backend="openai_compat",
        is_gateway=True,
        default_api_base="https://ark.cn-beijing.volces.com/api/coding/v3",
        strip_model_prefix=True,
        thinking_style="thinking_type",
        supports_max_completion_tokens=True,
    ),

    # BytePlus: VolcEngine international, pay-per-use models
    ProviderSpec(
        name="byteplus",
        keywords=("byteplus",),
        env_key="OPENAI_API_KEY",
        display_name="BytePlus",
        backend="openai_compat",
        is_gateway=True,
        detect_by_base_keyword="bytepluses",
        default_api_base="https://ark.ap-southeast.bytepluses.com/api/v3",
        strip_model_prefix=True,
        thinking_style="thinking_type",
    ),

    # BytePlus Coding Plan: same key as byteplus
    ProviderSpec(
        name="byteplus_coding_plan",
        keywords=("byteplus-plan",),
        env_key="OPENAI_API_KEY",
        display_name="BytePlus Coding Plan",
        backend="openai_compat",
        is_gateway=True,
        default_api_base="https://ark.ap-southeast.bytepluses.com/api/coding/v3",
        strip_model_prefix=True,
        thinking_style="thinking_type",
    ),


    # === Standard providers (matched by model-name keywords) ===============
    # Anthropic: native Anthropic SDK
    ProviderSpec(
        name="anthropic",
        keywords=("anthropic", "claude"),
        env_key="ANTHROPIC_API_KEY",
        display_name="Anthropic",
        backend="anthropic",
        supports_prompt_caching=True,
    ),
    # OpenAI: SDK default base URL (no override needed)
    ProviderSpec(
        name="openai",
        keywords=("openai", "gpt"),
        env_key="OPENAI_API_KEY",
        display_name="OpenAI",
        backend="openai_compat",
        supports_max_completion_tokens=True,
    ),
    # OpenAI Codex: OAuth-based, dedicated provider
    ProviderSpec(
        name="openai_codex",
        keywords=("openai-codex",),
        env_key="",
        display_name="OpenAI Codex",
        model_catalog="hybrid",
        builtin_models=(
            ProviderModelSpec(
                id="openai-codex/gpt-5.6-sol",
                label="GPT-5.6-Sol",
                description="Latest frontier agentic coding model.",
                context_window=272_000,
                reasoning_efforts=("low", "medium", "high", "xhigh", "max", "ultra"),
            ),
            ProviderModelSpec(
                id="openai-codex/gpt-5.6-terra",
                label="GPT-5.6-Terra",
                description="Balanced agentic coding model for everyday work.",
                context_window=272_000,
                reasoning_efforts=("low", "medium", "high", "xhigh", "max", "ultra"),
            ),
            ProviderModelSpec(
                id="openai-codex/gpt-5.6-luna",
                label="GPT-5.6-Luna",
                description="Fast and affordable agentic coding model.",
                context_window=272_000,
                reasoning_efforts=("low", "medium", "high", "xhigh", "max"),
            ),
            ProviderModelSpec(
                id="openai-codex/gpt-5.5",
                label="GPT-5.5",
                description="Frontier model for complex coding, research, and real-world work.",
                context_window=272_000,
                reasoning_efforts=("low", "medium", "high", "xhigh"),
            ),
            ProviderModelSpec(
                id="openai-codex/gpt-5.4",
                label="GPT-5.4",
                description="Strong model for everyday coding.",
                context_window=272_000,
                reasoning_efforts=("low", "medium", "high", "xhigh"),
            ),
            ProviderModelSpec(
                id="openai-codex/gpt-5.4-mini",
                label="GPT-5.4-Mini",
                description="Small, fast, and cost-efficient model for simpler coding tasks.",
                context_window=272_000,
                reasoning_efforts=("low", "medium", "high", "xhigh"),
            ),
            ProviderModelSpec(
                id="openai-codex/gpt-5.3-codex-spark",
                label="GPT-5.3-Codex-Spark",
                description="Ultra-fast coding model.",
                context_window=128_000,
                reasoning_efforts=("low", "medium", "high", "xhigh"),
            ),
        ),
        backend="openai_codex",
        detect_by_base_keyword="codex",
        default_api_base="https://chatgpt.com/backend-api",
        is_oauth=True,
    ),
    # xAI subscription: OAuth-based, with capability-gated server-hosted X Search.
    ProviderSpec(
        name="xai_grok",
        keywords=("xai-grok", "xai_grok"),
        env_key="",
        display_name="xAI Grok",
        model_catalog="hybrid",
        builtin_models=(
            ProviderModelSpec(
                id="xai-grok/grok-4.6",
                label="Grok 4.6",
                description="Grok via xAI subscription; X Search is enabled when supported.",
                context_window=500_000,
            ),
            ProviderModelSpec(
                id="xai-grok/grok-4.5",
                label="Grok 4.5",
                description="Grok via xAI subscription; X Search is enabled when supported.",
                context_window=500_000,
            ),
        ),
        backend="xai_grok",
        default_api_base="https://cli-chat-proxy.grok.com/v1",
        is_oauth=True,
    ),
    # GitHub Copilot: OAuth-based
    ProviderSpec(
        name="github_copilot",
        keywords=("github_copilot", "copilot"),
        env_key="",
        display_name="Github Copilot",
        model_catalog="hybrid",
        builtin_models=(
            ProviderModelSpec(
                id="github-copilot/gpt-5.4-mini",
                label="GPT-5.4 Mini",
                description="GitHub Copilot Responses model.",
            ),
            ProviderModelSpec(
                id="github-copilot/gpt-4.1",
                label="GPT-4.1",
                description="GitHub Copilot chat model.",
            ),
        ),
        backend="github_copilot",
        default_api_base="https://api.githubcopilot.com",
        strip_model_prefix=True,
        is_oauth=True,
        supports_max_completion_tokens=True,
    ),
    # DeepSeek: OpenAI-compatible at api.deepseek.com
    ProviderSpec(
        name="deepseek",
        keywords=("deepseek",),
        env_key="DEEPSEEK_API_KEY",
        display_name="DeepSeek",
        backend="openai_compat",
        default_api_base="https://api.deepseek.com",
        thinking_style="thinking_type",
        responses_models=(
            "deepseek-v4-flash",
            "deepseek-v4-pro",
            "deepseek-v4-flash-vision-exp",
        ),
        responses_default_tools=("web_search",),
    ),
    # Gemini: Google's OpenAI-compatible endpoint
    ProviderSpec(
        name="gemini",
        keywords=("gemini", "gemma"),
        env_key="GEMINI_API_KEY",
        display_name="Gemini",
        backend="openai_compat",
        default_api_base="https://generativelanguage.googleapis.com/v1beta/openai/",
    ),
    # Zhipu (智谱): OpenAI-compatible at open.bigmodel.cn
    ProviderSpec(
        name="zhipu",
        keywords=("zhipu", "glm", "zai"),
        env_key="ZAI_API_KEY",
        display_name="Zhipu AI",
        backend="openai_compat",
        env_extras=(("ZHIPUAI_API_KEY", "{api_key}"),),
        default_api_base="https://open.bigmodel.cn/api/paas/v4",
    ),
    # DashScope (通义): Qwen models, OpenAI-compatible endpoint
    ProviderSpec(
        name="dashscope",
        keywords=("qwen", "dashscope"),
        env_key="DASHSCOPE_API_KEY",
        display_name="DashScope",
        backend="openai_compat",
        default_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
        thinking_style="enable_thinking",
    ),
    # ModelScope (魔搭社区): OpenAI-compatible API
    ProviderSpec(
        name="modelscope",
        keywords=("modelscope",),
        env_key="MODELSCOPE_API_KEY",
        display_name="ModelScope",
        backend="openai_compat",
        is_gateway=True,
        detect_by_base_keyword="modelscope",
        default_api_base="https://api-inference.modelscope.cn/v1",
        strip_model_prefixes=("modelscope",),
        thinking_style="enable_thinking",
    ),
    # Moonshot (月之暗面): Kimi K2.5/K2.6 choose temperature from thinking mode;
    # the OpenAI-compatible provider omits it. K2.7 models require 1.0.
    ProviderSpec(
        name="moonshot",
        keywords=("moonshot", "kimi"),
        env_key="MOONSHOT_API_KEY",
        display_name="Moonshot",
        backend="openai_compat",
        default_api_base="https://api.moonshot.ai/v1",
        model_overrides=(
            ("kimi-k2.7", {"temperature": 1.0}),
            ("kimi-k2.7-code", {"temperature": 1.0}),
            ("kimi-k2.7-code-highspeed", {"temperature": 1.0}),
        ),
    ),
    # Kimi Coding Plan — Anthropic Messages API at api.kimi.com/coding
    # sk-kimi-* keys; requires User-Agent: claude-code/0.1.0 header.
    ProviderSpec(
        name="kimi_coding",
        keywords=("kimi-coding", "kimi_coding", "kimi-for-coding"),
        env_key="KIMI_CODING_API_KEY",
        display_name="Kimi Coding",
        backend="anthropic",
        default_api_base="https://api.kimi.com/coding/v1",
        default_extra_headers=(("User-Agent", "claude-code/0.1.0"),),
    ),
    # MiniMax: OpenAI-compatible API
    ProviderSpec(
        name="minimax",
        keywords=("minimax",),
        env_key="MINIMAX_API_KEY",
        display_name="MiniMax",
        backend="openai_compat",
        default_api_base="https://api.minimax.io/v1",
        thinking_style="reasoning_split",
    ),
    # MiniMax Anthropic-compatible endpoint: supports thinking mode
    ProviderSpec(
        name="minimax_anthropic",
        keywords=("minimax_anthropic",),
        env_key="MINIMAX_API_KEY",
        display_name="MiniMax (Anthropic)",
        backend="anthropic",
        default_api_base="https://api.minimax.io/anthropic",
    ),
    # Mistral AI: OpenAI-compatible API.
    # Reasoning quirks:
    #   * mistral-medium-3-5 / mistral-vibe-cli-* accept reasoning_effort but
    #     only "high" or "none" — low/medium/minimal must be remapped.
    #   * Magistral-* models reason implicitly and reject the kwarg entirely.
    #   * Reasoning responses return content as a list of thinking + text
    #     blocks; thinking text gets extracted into reasoning_content.
    ProviderSpec(
        name="mistral",
        keywords=("mistral", "magistral", "ministral", "codestral", "devstral"),
        env_key="MISTRAL_API_KEY",
        display_name="Mistral",
        backend="openai_compat",
        default_api_base="https://api.mistral.ai/v1",
        reasoning_effort_remap=(
            ("minimal", "none"),
            ("low", "none"),
            ("medium", "high"),
            ("high", "high"),
            ("none", "none"),
        ),
        implicit_reasoning_models=("magistral",),
        extract_thinking_blocks=True,
        strip_history_reasoning_content=True,
    ),
    # Step Fun (阶跃星辰): OpenAI-compatible API
    ProviderSpec(
        name="stepfun",
        keywords=("stepfun", "step"),
        env_key="STEPFUN_API_KEY",
        display_name="Step Fun",
        backend="openai_compat",
        default_api_base="https://api.stepfun.com/v1",
        reasoning_as_content=True,
    ),
    # Xiaomi MIMO (小米): OpenAI-compatible API
    # Hosted API (api.xiaomimimo.com) accepts {"thinking": {"type": "enabled"|"disabled"}}
    # to toggle reasoning, matching the existing thinking_type style.
    ProviderSpec(
        name="xiaomi_mimo",
        keywords=("xiaomi_mimo", "mimo"),
        env_key="XIAOMIMIMO_API_KEY",
        display_name="Xiaomi MIMO",
        backend="openai_compat",
        default_api_base="https://api.xiaomimimo.com/v1",
        thinking_style="thinking_type",
    ),
    # LongCat: OpenAI-compatible API
    ProviderSpec(
        name="longcat",
        keywords=("longcat",),
        env_key="LONGCAT_API_KEY",
        display_name="LongCat",
        backend="openai_compat",
        default_api_base="https://api.longcat.chat/openai/v1",
    ),
    # Ant Ling: OpenAI-compatible API for Ling/Ring model families.
    ProviderSpec(
        name="ant_ling",
        keywords=("ant_ling", "ant-ling", "ling-", "ring-"),
        env_key="ANT_LING_API_KEY",
        display_name="Ant Ling",
        backend="openai_compat",
        detect_by_base_keyword="ant-ling.com",
        default_api_base="https://api.ant-ling.com/v1",
    ),
    # === Local deployment (matched by config key, NOT by api_base) =========
    # vLLM / any OpenAI-compatible local server
    ProviderSpec(
        name="vllm",
        keywords=("vllm",),
        env_key="HOSTED_VLLM_API_KEY",
        display_name="vLLM",
        backend="openai_compat",
        is_local=True,
    ),
    # Ollama (local, OpenAI-compatible)
    ProviderSpec(
        name="ollama",
        keywords=("ollama", "nemotron"),
        env_key="OLLAMA_API_KEY",
        display_name="Ollama",
        backend="openai_compat",
        is_local=True,
        detect_by_base_keyword="11434",
        default_api_base="http://localhost:11434/v1",
    ),
    # LM Studio (local, OpenAI-compatible)
    ProviderSpec(
        name="lm_studio",
        keywords=("lm-studio", "lmstudio", "lm_studio"),
        env_key="LM_STUDIO_API_KEY",
        display_name="LM Studio",
        backend="openai_compat",
        is_local=True,
        detect_by_base_keyword="1234",
        default_api_base="http://localhost:1234/v1",
    ),
    # Atomic Chat (local, OpenAI-compatible) — https://atomic.chat/
    ProviderSpec(
        name="atomic_chat",
        keywords=("atomic-chat", "atomic_chat", "atomicchat"),
        env_key="ATOMIC_CHAT_API_KEY",
        display_name="Atomic Chat",
        backend="openai_compat",
        is_local=True,
        detect_by_base_keyword="1337",
        default_api_base="http://localhost:1337/v1",
    ),
    # === OpenVINO Model Server (direct, local, OpenAI-compatible at /v3) ===
    ProviderSpec(
        name="ovms",
        keywords=("openvino", "ovms"),
        env_key="",
        display_name="OpenVINO Model Server",
        backend="openai_compat",
        is_direct=True,
        is_local=True,
        default_api_base="http://localhost:8000/v3",
    ),
    # === NVIDIA NIM (NVIDIA Inference Microservices) =======================
    # Keys start with "nvapi-", base URL at integrate.api.nvidia.com
    ProviderSpec(
        name="nvidia",
        keywords=("nvidia", "nemotron", "nvapi"),
        env_key="NVIDIA_NIM_API_KEY",
        display_name="NVIDIA NIM",
        backend="openai_compat",
        is_gateway=False,
        detect_by_key_prefix="nvapi-",
        detect_by_base_keyword="nvidia.com",
        default_api_base="https://integrate.api.nvidia.com/v1",
    ),
    # === Auxiliary (not a primary LLM provider) ============================
    # Groq: mainly used for Whisper voice transcription, also usable for LLM
    ProviderSpec(
        name="groq",
        keywords=("groq",),
        env_key="GROQ_API_KEY",
        display_name="Groq",
        backend="openai_compat",
        default_api_base="https://api.groq.com/openai/v1",
    ),
    # AssemblyAI: voice transcription only. It appears in provider settings so
    # users can manage credentials, but WebUI excludes it from chat model pickers.
    ProviderSpec(
        name="assemblyai",
        keywords=("assemblyai",),
        env_key="ASSEMBLYAI_API_KEY",
        display_name="AssemblyAI",
        backend="openai_compat",
        default_api_base="https://api.assemblyai.com/v2",
        is_transcription_only=True,
    ),
    # Qianfan (百度千帆): OpenAI-compatible API
    ProviderSpec(
        name="qianfan",
        keywords=("qianfan", "ernie"),
        env_key="QIANFAN_API_KEY",
        display_name="Qianfan",
        backend="openai_compat",
        default_api_base="https://qianfan.baidubce.com/v2"
    ),
)


# ---------------------------------------------------------------------------
# Lookup helpers
# ---------------------------------------------------------------------------


def find_by_name(name: str) -> ProviderSpec | None:
    """Find a provider spec by config field name, e.g. "dashscope"."""
    normalized = to_snake(name.replace("-", "_"))
    for spec in PROVIDERS:
        if spec.name == normalized:
            return spec
    return None


def create_dynamic_spec(
    name: str,
    *,
    display_name: str = "",
    thinking_style: str = "",
) -> ProviderSpec:
    """Create a dynamic ProviderSpec for custom user-defined providers."""
    normalized = to_snake(name.replace("-", "_"))
    strip_prefixes = tuple(dict.fromkeys((name, normalized)))
    return ProviderSpec(
        name=normalized,
        keywords=(),
        env_key="",
        display_name=display_name or name.replace("-", " ").replace("_", " ").title(),
        backend="openai_compat",
        is_direct=True,
        strip_model_prefixes=strip_prefixes,
        thinking_style=thinking_style,
    )
