# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 """ Static registry of supported external LLM providers. All providers expose OpenAI-compatible /v1/chat/completions endpoints with Bearer token auth and SSE streaming. """ import re from typing import Any PROVIDER_REGISTRY: dict[str, dict[str, Any]] = { "openai": { "display_name": "OpenAI", "base_url": "https://api.openai.com/v1", "default_models": [ "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "o3", ], "supports_streaming": True, "supports_vision": True, "supports_tool_calling": True, "auth_header": "Authorization", "auth_prefix": "Bearer ", # Scope the picker to the current generation. /v1/models returns many # historical snapshots, fine-tunes, and non-chat models we don't want. "model_id_allowlist": re.compile(r"^(gpt-5\.[345]|gpt-4\.5|o3)(?:[-.]|$)"), # Hide dated snapshots and the retired plain gpt-5.3 id. "model_id_denylist": re.compile(r"^(gpt-5\.3)$|-\d{4}-\d{2}-\d{2}$"), }, "anthropic": { "display_name": "Anthropic", "base_url": "https://api.anthropic.com/v1", "default_models": [ "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5", ], # Hide YYYYMMDD-suffixed snapshot ids (e.g. claude-3-5-sonnet-20241022). "model_id_denylist": re.compile(r"-\d{8}$"), "supports_streaming": True, "supports_vision": True, "supports_tool_calling": False, "auth_header": "x-api-key", "auth_prefix": "", "extra_headers": { "anthropic-version": "2023-06-01", }, "openai_compatible": False, "notes": "Native Anthropic Messages API. Uses x-api-key header and /v1/messages endpoint with SSE translation.", }, "gemini": { "display_name": "Google Gemini", # Native Gemini REST endpoint -- does NOT speak OpenAI Chat Completions; # translated in `_stream_gemini` (external_provider.py). # https://ai.google.dev/gemini-api/docs "base_url": "https://generativelanguage.googleapis.com/v1beta", # Curated lineup (ListModels returns many historical/experimental ids). # Excluded on purpose: # - `gemini-2.0-flash*` (retired 2026-06-01; 404 on use) # - `gemini-3-pro-preview` (shut down 2026-03-09; auto-redirects to # `gemini-3.1-pro-preview`, so we surface 3.1 directly). "default_models": [ "gemini-3.1-pro-preview", "gemini-3.5-flash", "gemini-3.1-flash-lite", "gemini-3-flash-preview", "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-3-pro-image-preview", "gemini-3.1-flash-image-preview", "gemini-2.5-flash-image", ], "supports_streaming": True, "supports_vision": True, "supports_tool_calling": True, # Native API takes the bare key on `x-goog-api-key`. "auth_header": "x-goog-api-key", "auth_prefix": "", "openai_compatible": False, "notes": ( "Native Gemini API. Translation lives in _stream_gemini. " "API key from https://aistudio.google.com/apikey. " "See https://ai.google.dev/gemini-api/docs for endpoint shapes." ), # gemini-3-pro-preview was shut down 2026-03-09 and auto-aliased to # gemini-3.1-pro-preview; drop it so users see one canonical card. "model_id_deny_exact": ("gemini-3-pro-preview",), # Chat-capable 3.5 / 3.1 / 3 / 2.5 families plus rolling *-latest # aliases. Image-tier ids flow through the Nano Banana # `responseModalities` path in `_stream_gemini`. Retired 2.0 ids # excluded (they 404 on use). "model_id_allowlist": re.compile( r"^(" r"gemini-3\.5-(?:flash|pro)(?:-preview)?|" r"gemini-3\.1-(?:flash|pro|flash-lite)(?:-preview)?(?:-customtools)?|" r"gemini-3\.1-flash-image-preview|" r"gemini-3-(?:flash|pro)(?:-preview)?|" r"gemini-3-pro-image-preview|" r"nano-banana-pro-preview|" r"gemini-2\.5-pro|gemini-2\.5-flash|gemini-2\.5-flash-lite|" r"gemini-2\.5-flash-image|" r"gemini-pro-latest|gemini-flash-latest|gemini-flash-lite-latest" r")$" ), }, "deepseek": { "display_name": "DeepSeek", "base_url": "https://api.deepseek.com/v1", "default_models": [ "deepseek-chat", "deepseek-reasoner", ], "supports_streaming": True, "supports_vision": False, "supports_tool_calling": True, "auth_header": "Authorization", "auth_prefix": "Bearer ", "notes": "OpenAI-compatible API. deepseek-chat = V3, deepseek-reasoner = R1 thinking mode.", }, "mistral": { "display_name": "Mistral AI", "base_url": "https://api.mistral.ai/v1", "default_models": [ "codestral-latest", "devstral-latest", "devstral-medium-latest", "magistral-medium-latest", "ministral-14b-latest", "ministral-3b-latest", "ministral-8b-latest", "mistral-large-latest", "mistral-medium-latest", "mistral-small-latest", "mistral-tiny-latest", "mistral-vibe-cli-latest", ], "supports_streaming": True, "supports_vision": True, "supports_tool_calling": True, "auth_header": "Authorization", "auth_prefix": "Bearer ", "model_id_allowlist": re.compile( r"^(codestral-latest|devstral-latest|devstral-medium-latest|" r"magistral-medium-latest|ministral-(?:14b|3b|8b)-latest|" r"mistral-(?:large|medium|small|tiny)-latest|" r"mistral-vibe-cli-latest)$" ), }, "kimi": { "display_name": "Kimi", "base_url": "https://api.moonshot.ai/v1", # Surface only the two SoTA multimodal models (kimi-k2.6/k2.5); # moonshot-v1-* and dated k2 previews are filtered by the allowlist. # Docs: https://platform.kimi.ai/docs/models # Listing/overview: https://platform.kimi.ai/docs/api/list-models # https://platform.kimi.ai/docs/api/overview "default_models": [ "kimi-k2.6", "kimi-k2.5", ], "supports_streaming": True, "supports_vision": True, "supports_tool_calling": True, "auth_header": "Authorization", "auth_prefix": "Bearer ", "notes": "Moonshot API key. China: use base URL https://api.moonshot.cn/v1", "model_id_allowlist": re.compile(r"^kimi-k2\.[56]$"), # Reasoning-class: the API rejects custom temperature/top_p ("only 1 # is allowed"). Strip both so the server uses its required defaults. "body_omit": ("temperature", "top_p"), }, "qwen": { "display_name": "Qwen", "base_url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "default_models": [ "qwen-plus", "qwen-turbo", "qwen-max", "qwen2.5-72b-instruct", ], "supports_streaming": True, "supports_vision": True, "supports_tool_calling": True, "auth_header": "Authorization", "auth_prefix": "Bearer ", "notes": "DashScope API key. China mainland: override base URL to https://dashscope.aliyuncs.com/compatible-mode/v1", }, "huggingface": { "display_name": "Hugging Face", "base_url": "https://router.huggingface.co/v1", # Seed the picker before the live /v1/models call resolves; the remote # listing (see model_list_mode) is the source of truth. "default_models": [ "openai/gpt-oss-120b", "deepseek-ai/DeepSeek-V3", "meta-llama/Llama-3.3-70B-Instruct", "Qwen/Qwen2.5-72B-Instruct", ], "supports_streaming": True, "supports_vision": True, "supports_tool_calling": True, "auth_header": "Authorization", "auth_prefix": "Bearer ", "notes": ( "HF token from huggingface.co/settings/tokens. Uses the " "OpenAI-compatible router at /v1/chat/completions; /v1/models " "returns the cross-provider chat catalog. See " "https://huggingface.co/docs/inference-providers/index." ), # Remote so users see live availability; loadModels() merges defaults # so they stay visible if the remote call fails. "model_list_mode": "remote", # Scope to trusted first-party org repos (the response is otherwise # hundreds of community fine-tunes, mirrors, fp8 variants). "model_id_allowlist": re.compile( r"^(openai|deepseek-ai|google|meta-llama|Qwen|moonshotai|mistralai|zai-org)/" ), # Cap the post-filter list to first N matches (no server-side sort); # default_models keeps flagship ids near the top. "model_id_limit": 15, }, "vllm": { "display_name": "vLLM", # User-supplied via provider_base_url; the route falls back to the # payload's base_url when the registry entry has none. "base_url": "", "default_models": [], "supports_streaming": True, "supports_vision": True, "supports_tool_calling": True, "auth_header": "Authorization", "auth_prefix": "Bearer ", # Force /v1/chat/completions -- vLLM's /v1/responses rebuilds messages # through the chat template, 400ing on strict-alternation templates # (Gemma 3). The chat-completions path takes messages verbatim. "notes": "Self-hosted vLLM server. Always routed to /v1/chat/completions.", # Surfaced via the frontend's CUSTOM_PROVIDER_PRESETS, not the dropdown. "hidden": True, }, "custom": { "display_name": "Custom", # User-supplied via provider_base_url. "base_url": "", "default_models": [], "supports_streaming": True, "supports_vision": True, "supports_tool_calling": True, "auth_header": "Authorization", "auth_prefix": "Bearer ", "notes": ( "User-supplied OpenAI-compatible server. Routed to " "/v1/chat/completions; /models is optional." ), # Surfaced by the frontend's generic Custom option, not the dropdown. "hidden": True, }, "ollama": { "display_name": "Ollama", "base_url": "http://localhost:11434/v1", "default_models": [], "supports_streaming": True, "supports_vision": True, "supports_tool_calling": True, "auth_header": "Authorization", "auth_prefix": "Bearer ", "notes": ( "Local Ollama server. OpenAI-compatible /v1/chat/completions; " "no API key. Surfaced via CUSTOM_PROVIDER_PRESETS in the frontend." ), "hidden": True, }, "llama_cpp": { "display_name": "llama.cpp", "base_url": "http://localhost:8080/v1", "default_models": [], "supports_streaming": True, "supports_vision": True, "supports_tool_calling": True, "auth_header": "Authorization", "auth_prefix": "Bearer ", "notes": ( "Local llama.cpp server (llama-server). OpenAI-compatible " "/v1/chat/completions. Surfaced via CUSTOM_PROVIDER_PRESETS." ), "hidden": True, }, "openrouter": { "display_name": "OpenRouter", "base_url": "https://openrouter.ai/api/v1", # Curated picker list (locked, not live /models). "default_models": [ "openrouter/free", "openai/gpt-4o", "anthropic/claude-sonnet-4-5", "google/gemini-2.5-flash", "mistralai/mistral-large-2411", "deepseek/deepseek-r1", "mistralai/mistral-small-3.1-24b-instruct", "perceptron/perceptron-mk1", "inclusionai/ring-2.6-1t:free", "google/gemini-3.1-flash-lite", "baidu/cobuddy:free", "openai/gpt-chat-latest", "x-ai/grok-4.3", "ibm-granite/granite-4.1-8b", "openrouter/owl-alpha", "poolside/laguna-xs.2:free", "~google/gemini-pro-latest", "~moonshotai/kimi-latest", ], "supports_streaming": True, "supports_vision": True, "supports_tool_calling": True, "auth_header": "Authorization", "auth_prefix": "Bearer ", "extra_headers": { "HTTP-Referer": "https://unsloth.ai", "X-Title": "Unsloth Studio", }, "notes": "Unified gateway to 300+ models across all major providers. HTTP-Referer and X-Title headers sent for attribution.", "model_list_mode": "curated", }, } def get_provider_info(provider_type: str) -> dict[str, Any] | None: """Return the registry entry for a provider type, or None if unknown.""" return PROVIDER_REGISTRY.get(provider_type) def get_base_url(provider_type: str) -> str | None: """Return the default base URL for a provider type.""" info = PROVIDER_REGISTRY.get(provider_type) return info["base_url"] if info else None def list_available_providers() -> list[dict[str, Any]]: """Return all registered providers (for the /registry endpoint). Hidden entries are filtered out: they exist only for backend lookups and are surfaced via ``CUSTOM_PROVIDER_PRESETS`` instead of the dropdown. """ result = [] for provider_type, info in PROVIDER_REGISTRY.items(): if info.get("hidden"): continue result.append( { "provider_type": provider_type, "display_name": info["display_name"], "base_url": info["base_url"], "default_models": info["default_models"], "supports_streaming": info["supports_streaming"], "supports_vision": info.get("supports_vision", False), "supports_tool_calling": info.get("supports_tool_calling", False), "model_list_mode": info.get("model_list_mode", "remote"), } ) return result