Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

563 lines
20 KiB
Python

"""
Provider Capabilities
=====================
Centralized configuration for LLM provider capabilities.
This replaces scattered hardcoded checks throughout the codebase.
Usage:
from deeptutor.services.llm.capabilities import get_capability, supports_response_format
# Check if a provider supports response_format
if supports_response_format(binding, model):
kwargs["response_format"] = {"type": "json_object"}
# Generic capability check
if get_capability(binding, "streaming", default=True):
# use streaming
"""
# Provider capabilities configuration
# Keys are binding names (lowercase), values are capability dictionaries
PROVIDER_CAPABILITIES: dict[str, dict[str, object]] = {
# OpenAI and OpenAI-compatible providers
"openai": {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": True,
"system_in_messages": True, # System prompt goes in messages array
"newer_models_use_max_completion_tokens": True,
},
# Custom / user-defined OpenAI-compatible endpoints
"custom": {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": True, # Most OpenAI-compat endpoints support function calling
"supports_vision": False, # Per-model; set True via MODEL_OVERRIDES
"vision_url_supported": True,
"system_in_messages": True,
"has_thinking_tags": False, # Per-model; MODEL_OVERRIDES handles qwen/deepseek etc.
},
"azure_openai": {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": True,
"system_in_messages": True,
"newer_models_use_max_completion_tokens": True,
"requires_api_version": True,
},
# Anthropic
"anthropic": {
"supports_response_format": False, # Anthropic uses different format
"supports_streaming": True,
"supports_tools": True,
"supports_vision": True,
"vision_url_supported": False, # Our adapter only emits base64 image source
"system_in_messages": False, # System is a separate parameter
"has_thinking_tags": False,
},
"claude": { # Alias for anthropic
"supports_response_format": False,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": True,
"vision_url_supported": False,
"system_in_messages": False,
"has_thinking_tags": False,
},
"custom_anthropic": {
"supports_response_format": False,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": True,
"vision_url_supported": False,
"system_in_messages": False,
"has_thinking_tags": False,
},
"minimax_anthropic": {
"supports_response_format": False,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": True,
"vision_url_supported": False,
"system_in_messages": False,
"has_thinking_tags": False,
},
# DeepSeek
"deepseek": {
"supports_response_format": False, # DeepSeek doesn't support strict JSON schema yet
"supports_streaming": True,
"supports_tools": True,
"supports_vision": False,
"system_in_messages": True,
"has_thinking_tags": True, # DeepSeek reasoner has thinking tags
},
# SiliconFlow exposes OpenAI-compatible chat completions for hosted models
# such as DeepSeek and Qwen; model-specific overrides below still govern
# response_format, thinking tags, and vision.
"siliconflow": {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": False,
"vision_url_supported": True,
"system_in_messages": True,
"has_thinking_tags": False,
},
# VolcEngine Ark (Doubao) and BytePlus — OpenAI-compatible gateways that
# host natively multimodal models (Doubao-Vision). ``supports_vision`` is
# the Stage-2 fallback hint (see ``multimodal.py``), not a pre-flight gate:
# marking these True means a transient failure never causes images to be
# silently dropped. The Ark API expects inline base64 image data, so
# url-only attachments are resolved to bytes before sending.
"volcengine": {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": True,
"vision_url_supported": False,
"system_in_messages": True,
},
"byteplus": {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": True,
"vision_url_supported": False,
"system_in_messages": True,
},
# OpenRouter (aggregator, generally OpenAI-compatible)
"openrouter": {
"supports_response_format": True, # Depends on underlying model
"supports_streaming": True,
"supports_tools": True,
"supports_vision": True, # Depends on underlying model
"system_in_messages": True,
},
# Groq (fast inference)
"groq": {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": True,
"system_in_messages": True,
},
# Together AI
"together": {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": True,
"system_in_messages": True,
},
"together_ai": { # Alias
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": True,
"system_in_messages": True,
},
# Mistral
"mistral": {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": True,
"system_in_messages": True,
},
# DashScope / Alibaba Cloud (Qwen family)
# Uses OpenAI-compatible API with native function calling support.
"dashscope": {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": False, # Per-model; set True via MODEL_OVERRIDES
"system_in_messages": True,
"has_thinking_tags": True, # Qwen reasoner models emit <think/> tags
},
# Moonshot / Kimi — vision is per-model (see MODEL_OVERRIDES below).
# Per the official docs the image input must be base64-encoded inline; URL
# form is rejected. We therefore force the multimodal layer to resolve any
# url-only attachment to bytes before sending.
"moonshot": {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": False,
"vision_url_supported": False,
"system_in_messages": True,
},
# MiniMax's OpenAI-compatible endpoint supports Chat Completions tools /
# function calling for M-series text models. Response-format support is
# still disabled by the model override below.
"minimax": {
"supports_response_format": False,
"supports_streaming": True,
"supports_tools": True,
"supports_vision": False,
"system_in_messages": True,
},
# Local providers (generally OpenAI-compatible)
"ollama": {
"supports_response_format": True, # Ollama supports JSON mode
"supports_streaming": True,
"supports_tools": False, # Limited tool support
"supports_vision": False, # Depends on model; set True via model overrides
"system_in_messages": True,
},
"lm_studio": {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": False,
"supports_vision": False,
"system_in_messages": True,
},
"vllm": {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": False,
"supports_vision": False,
"system_in_messages": True,
},
"llama_cpp": {
"supports_response_format": True, # llama.cpp server supports JSON grammar
"supports_streaming": True,
"supports_tools": False,
"supports_vision": False,
"system_in_messages": True,
},
}
# Default capabilities for unknown providers (assume OpenAI-compatible)
DEFAULT_CAPABILITIES: dict[str, object] = {
"supports_response_format": True,
"supports_streaming": True,
"supports_tools": False,
"supports_vision": False,
"vision_url_supported": True, # Most OpenAI-compat providers accept image_url URLs
"system_in_messages": True,
"has_thinking_tags": False,
"forced_temperature": None, # None means no forced value, use requested temperature
}
# Model-specific overrides
# Format: {model_pattern: {capability: value}}
# Patterns are matched with case-insensitive startswith
MODEL_OVERRIDES: dict[str, dict[str, object]] = {
"deepseek": {
"supports_response_format": False,
"has_thinking_tags": True,
"supports_vision": False,
},
"deepseek-reasoner": {
"supports_response_format": False,
"has_thinking_tags": True,
"supports_vision": False,
},
# Qwen text models often share the same provider/gateway as Qwen-VL.
# Keep thinking-tag handling broad, but only mark explicit VL/vision model
# names as image-capable so RAG image indexing can fail closed.
"qwen/qwen2.5-vl": {"has_thinking_tags": True, "supports_vision": True},
"qwen/qwen3-vl": {"has_thinking_tags": True, "supports_vision": True},
"qwen/qwen2-vl": {"has_thinking_tags": True, "supports_vision": True},
"qwen/qwen-vl": {"has_thinking_tags": True, "supports_vision": True},
"qwen2.5-vl": {"has_thinking_tags": True, "supports_vision": True},
"qwen3-vl": {"has_thinking_tags": True, "supports_vision": True},
"qwen2-vl": {"has_thinking_tags": True, "supports_vision": True},
"qwen-vl": {"has_thinking_tags": True, "supports_vision": True},
"qwen": {
"has_thinking_tags": True,
"supports_vision": False,
},
"qwq": {
"has_thinking_tags": True,
},
"minimax": {
"supports_response_format": False,
},
# NOTE: supports_response_format and system_in_messages are binding-level
# capabilities, NOT model-level. When using OpenRouter or other OpenAI-compatible
# proxies (binding="openai"), they handle response_format translation and expect
# system prompts in messages. The native Anthropic limitations are already
# handled by PROVIDER_CAPABILITIES["anthropic"] / ["claude"] above.
# Only model-intrinsic capabilities (like has_thinking_tags) belong here.
# Reasoning models - only support temperature=1.0
# See: https://github.com/HKUDS/DeepTutor/issues/141
"gpt-5": {
"forced_temperature": 1.0,
},
"o1": {
"forced_temperature": 1.0,
},
"o3": {
"forced_temperature": 1.0,
},
# Vision-capable model families
"gpt-4o": {"supports_vision": True},
"gpt-4-turbo": {"supports_vision": True},
"gpt-4-vision": {"supports_vision": True},
"claude-3": {"supports_vision": True},
"claude-4": {"supports_vision": True},
"gemini": {"supports_vision": True},
"gemma": {"supports_vision": False, "supports_response_format": False},
"llava": {"supports_vision": True},
"bakllava": {"supports_vision": True},
"moondream": {"supports_vision": True},
"minicpm-v": {"supports_vision": True},
"gpt-3.5": {"supports_vision": False},
# Moonshot / Kimi vision models
# https://platform.kimi.com/docs/guide/use-kimi-vision-model
"moonshot-v1-8k-vision": {"supports_vision": True},
"moonshot-v1-32k-vision": {"supports_vision": True},
"moonshot-v1-128k-vision": {"supports_vision": True},
"kimi-k2.5": {"supports_vision": True},
"kimi-k2.6": {"supports_vision": True},
}
def get_capability(
binding: str,
capability: str,
model: str | None = None,
default: object = None,
) -> object:
"""
Get a capability value for a provider/model combination.
Checks in order:
1. Model-specific overrides (matched by prefix)
2. Provider/binding capabilities
3. Default capabilities for unknown providers
4. Explicit default value
Args:
binding: Provider binding name (e.g., "openai", "anthropic", "deepseek")
capability: Capability name (e.g., "supports_response_format")
model: Optional model name for model-specific overrides
default: Default value if capability is not defined
Returns:
Capability value or default
"""
binding_lower = (binding or "openai").lower()
# 1. Check model-specific overrides first
if model:
model_lower = model.lower()
# Sort by pattern length descending to match most specific first
for pattern, overrides in sorted(MODEL_OVERRIDES.items(), key=lambda x: -len(x[0])):
if model_lower.startswith(pattern):
if capability in overrides:
return overrides[capability]
# 2. Check provider capabilities
provider_caps = PROVIDER_CAPABILITIES.get(binding_lower, {})
if capability in provider_caps:
return provider_caps[capability]
# 3. Check default capabilities for unknown providers
if capability in DEFAULT_CAPABILITIES:
return DEFAULT_CAPABILITIES[capability]
# 4. Return explicit default
return default
# Runtime cache for response_format incompatibilities discovered at request time.
# Keyed by (binding_lower, model_lower). Populated when a provider rejects a
# request with response_format={"type": "json_object"} (commonly LM Studio /
# Ollama serving Gemma/Qwen-style models that only accept "json_schema" or "text").
# Once a pair is recorded here, subsequent calls skip response_format entirely
# instead of paying the cost of a failed request + retry.
_RUNTIME_DISABLED_RESPONSE_FORMAT: set[tuple[str, str]] = set()
def disable_response_format_at_runtime(binding: str | None, model: str | None) -> None:
"""Mark a (binding, model) pair as not supporting ``response_format``.
Subsequent calls to :func:`supports_response_format` for the same pair
will return ``False`` without re-checking the static configuration. This
is useful when a provider unexpectedly rejects ``response_format`` at
runtime (e.g. LM Studio + ``gemma-4-e2b`` returning
``"'response_format.type' must be 'json_schema' or 'text'"``).
"""
if not binding or not model:
return
_RUNTIME_DISABLED_RESPONSE_FORMAT.add((binding.lower(), model.lower()))
def is_response_format_disabled_at_runtime(binding: str | None, model: str | None) -> bool:
"""Return True if (binding, model) was disabled via :func:`disable_response_format_at_runtime`."""
if not binding or not model:
return False
return (binding.lower(), model.lower()) in _RUNTIME_DISABLED_RESPONSE_FORMAT
def supports_response_format(binding: str, model: str | None = None) -> bool:
"""
Check if the provider/model supports response_format parameter.
This is a convenience function for the most common capability check.
A runtime override (set via :func:`disable_response_format_at_runtime`)
always wins over static capability configuration.
Args:
binding: Provider binding name
model: Optional model name for model-specific overrides
Returns:
True if response_format is supported
"""
if is_response_format_disabled_at_runtime(binding, model):
return False
value = get_capability(binding, "supports_response_format", model, default=True)
return bool(value)
def supports_streaming(binding: str, model: str | None = None) -> bool:
"""
Check if the provider/model supports streaming responses.
Args:
binding: Provider binding name
model: Optional model name
Returns:
True if streaming is supported
"""
value = get_capability(binding, "supports_streaming", model, default=True)
return bool(value)
def system_in_messages(binding: str, model: str | None = None) -> bool:
"""
Check if system prompt should be in messages array (OpenAI style)
or as a separate parameter (Anthropic style).
Args:
binding: Provider binding name
model: Optional model name
Returns:
True if system prompt goes in messages array
"""
value = get_capability(binding, "system_in_messages", model, default=True)
return bool(value)
def has_thinking_tags(binding: str, model: str | None = None) -> bool:
"""
Check if the model output may contain thinking tags (<think>...</think>).
Args:
binding: Provider binding name
model: Optional model name
Returns:
True if thinking tags should be filtered
"""
value = get_capability(binding, "has_thinking_tags", model, default=False)
return bool(value)
def supports_tools(binding: str, model: str | None = None) -> bool:
"""
Check if the provider/model supports function calling / tools.
Args:
binding: Provider binding name
model: Optional model name
Returns:
True if tools/function calling is supported
"""
value = get_capability(binding, "supports_tools", model, default=False)
return bool(value)
def supports_vision(binding: str, model: str | None = None) -> bool:
"""
Check if the provider/model supports multimodal (image) input.
Args:
binding: Provider binding name
model: Optional model name for model-specific overrides
Returns:
True if the model can accept image content in messages
"""
value = get_capability(binding, "supports_vision", model, default=False)
return bool(value)
def supports_vision_url(binding: str, model: str | None = None) -> bool:
"""Whether the provider accepts remote URL image references.
Some providers (Moonshot, our Anthropic adapter) only accept inline
base64-encoded image bytes. The multimodal layer consults this flag to
decide whether url-only attachments need to be resolved to bytes before
being forwarded.
"""
value = get_capability(binding, "vision_url_supported", model, default=True)
return bool(value)
def requires_api_version(binding: str, model: str | None = None) -> bool:
"""
Check if the provider requires an API version parameter (e.g., Azure OpenAI).
Args:
binding: Provider binding name
model: Optional model name
Returns:
True if api_version is required
"""
value = get_capability(binding, "requires_api_version", model, default=False)
return bool(value)
def get_effective_temperature(
binding: str,
model: str | None = None,
requested_temp: float = 0.7,
) -> float:
"""
Get the effective temperature value for a model.
Some models (e.g., o1, o3, gpt-5) only support a fixed temperature value (1.0).
This function returns the forced temperature if defined, otherwise the requested value.
Args:
binding: Provider binding name
model: Optional model name for model-specific overrides
requested_temp: The temperature value requested by the caller (default: 0.7)
Returns:
The effective temperature to use for the API call
"""
forced_temp = get_capability(binding, "forced_temperature", model)
if isinstance(forced_temp, (int, float)):
return float(forced_temp)
return requested_temp
__all__ = [
"PROVIDER_CAPABILITIES",
"MODEL_OVERRIDES",
"DEFAULT_CAPABILITIES",
"get_capability",
"supports_response_format",
"supports_streaming",
"system_in_messages",
"has_thinking_tags",
"supports_tools",
"supports_vision",
"requires_api_version",
"get_effective_temperature",
"disable_response_format_at_runtime",
"is_response_format_disabled_at_runtime",
]