"""LLM provider presets used by Settings and the runtime AI provider bridge.""" from __future__ import annotations from typing import Any import httpx MODELS_DEV_URL = "https://models.dev/api.json" FALLBACK_LLM_PROVIDER_PRESETS: dict[str, dict[str, Any]] = { "minimax": { "provider": "minimax", "label": "MiniMax", "provider_api": "anthropic-messages", "base_url": "https://api.minimaxi.com/anthropic", "model": "MiniMax-M2.7", "models": ["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", "MiniMax-M2"], "api_key_env": "MINIMAX_API_KEY", "source": "fallback", }, "openai": { "provider": "openai", "label": "OpenAI", "provider_api": "openai-completions", "base_url": "https://api.openai.com/v1", "model": "gpt-5.1", "models": ["gpt-5.1", "gpt-5.1-codex", "gpt-4.1", "gpt-4o"], "api_key_env": "OPENAI_API_KEY", "source": "fallback", }, "anthropic": { "provider": "anthropic", "label": "Anthropic", "provider_api": "anthropic-messages", "base_url": "https://api.anthropic.com/v1", "model": "claude-sonnet-4-6", "models": ["claude-sonnet-4-6", "claude-opus-4-5", "claude-3-5-haiku-20241022"], "api_key_env": "ANTHROPIC_API_KEY", "source": "fallback", }, "deepseek": { "provider": "deepseek", "label": "DeepSeek", "provider_api": "openai-completions", "base_url": "https://api.deepseek.com/v1", "model": "deepseek-chat", "models": ["deepseek-chat", "deepseek-reasoner"], "api_key_env": "DEEPSEEK_API_KEY", "source": "fallback", }, "alibaba": { "provider": "alibaba", "label": "Alibaba Qwen / DashScope", "provider_api": "openai-completions", "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "model": "qwen3-max", "models": ["qwen3-max", "qwen3.5-plus", "qwen-max", "qwen-plus"], "api_key_env": "DASHSCOPE_API_KEY", "source": "fallback", }, "moonshotai": { "provider": "moonshotai", "label": "Moonshot AI / Kimi", "provider_api": "openai-completions", "base_url": "https://api.moonshot.ai/v1", "model": "kimi-k2.5", "models": ["kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"], "api_key_env": "MOONSHOT_API_KEY", "source": "fallback", }, "openrouter": { "provider": "openrouter", "label": "OpenRouter", "provider_api": "openai-completions", "base_url": "https://openrouter.ai/api/v1", "model": "openai/gpt-5.1", "models": ["openai/gpt-5.1", "anthropic/claude-sonnet-4.5", "qwen/qwen3-max"], "api_key_env": "OPENROUTER_API_KEY", "source": "fallback", }, "ollama": { "provider": "ollama", "label": "Ollama Local", "provider_api": "ollama-generate", "base_url": "http://127.0.0.1:11434", "model": "qwen2.5:7b", "models": ["qwen2.5:7b", "llama3.1:8b", "mistral:7b"], "api_key_env": "", "source": "fallback", }, } MODELS_DEV_PROVIDER_KEYS = { "minimax": "minimax", "openai": "openai", "anthropic": "anthropic", "deepseek": "deepseek", "alibaba": "alibaba", "moonshotai": "moonshotai", "openrouter": "openrouter", } def list_fallback_llm_provider_presets() -> list[dict[str, Any]]: return [dict(value) for value in FALLBACK_LLM_PROVIDER_PRESETS.values()] def get_fallback_llm_provider_preset(provider: str) -> dict[str, Any]: key = provider.strip().lower() if key not in FALLBACK_LLM_PROVIDER_PRESETS: raise ValueError(f"Unsupported LLM provider preset: {provider}") return dict(FALLBACK_LLM_PROVIDER_PRESETS[key]) async def refresh_llm_provider_preset(provider: str) -> dict[str, Any]: fallback = get_fallback_llm_provider_preset(provider) models_dev_key = MODELS_DEV_PROVIDER_KEYS.get(fallback["provider"]) if not models_dev_key: return fallback async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client: response = await client.get( MODELS_DEV_URL, headers={"User-Agent": "Planet/1.0"}, ) response.raise_for_status() catalog = response.json() upstream = catalog.get(models_dev_key) if not isinstance(upstream, dict): return fallback upstream_models = upstream.get("models") if isinstance(upstream.get("models"), dict) else {} model_ids = list(upstream_models.keys())[:80] base_url = upstream.get("api") or fallback["base_url"] if fallback["provider"] == "deepseek" and base_url == "https://api.deepseek.com": base_url = "https://api.deepseek.com/v1" refreshed = { **fallback, "label": upstream.get("name") or fallback["label"], "base_url": base_url, "model": model_ids[0] if model_ids else fallback["model"], "models": model_ids or fallback["models"], "api_key_env": (upstream.get("env") or [fallback["api_key_env"]])[0], "source": MODELS_DEV_URL, } return refreshed