Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
@@ -0,0 +1,24 @@
"""
LLM 提供商适配器
"""
from .base import (
BaseLLMProvider,
LLMResponse,
LLMMessage,
LLMConfig,
LLMStreamChunk,
ToolCall,
ToolDefinition,
)
from .registry import ProviderRegistry
__all__ = [
'BaseLLMProvider',
'LLMResponse',
'LLMMessage',
'LLMConfig',
'LLMStreamChunk',
'ToolCall',
'ToolDefinition',
'ProviderRegistry',
]
@@ -0,0 +1,360 @@
"""
LLM 提供商基类
"""
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, AsyncGenerator, Dict, List, Optional
logger = logging.getLogger(__name__)
@dataclass
class LLMMessage:
"""LLM 消息
支持多模态内容格式:
- 纯文本: content 为字符串
- 多模态: content 为列表 [{"type": "text", "text": "..."}, {"type": "image_url", "image_url": {"url": "..."}}]
"""
role: str # system, user, assistant, tool
content: Any # str 或 List[Dict] (多模态内容)
name: Optional[str] = None
# Function Calling 相关
tool_calls: Optional[List[Dict[str, Any]]] = None # assistant 消息中的工具调用
tool_call_id: Optional[str] = None # tool 消息中的工具调用 ID
def to_dict(self) -> Dict[str, Any]:
result = {'role': self.role, 'content': self.content}
if self.name:
result['name'] = self.name
if self.tool_calls:
result['tool_calls'] = self.tool_calls
if self.tool_call_id:
result['tool_call_id'] = self.tool_call_id
return result
@staticmethod
def create_multimodal_content(text: str, attachments: List[Dict] = None) -> Any:
"""
创建多模态内容
Args:
text: 文本内容
attachments: 附件列表 [{"type": "image/file", "url": "...", "mime_type": "...", "base64": "..."}]
Returns:
str 或 List[Dict] 格式的内容
Note:
对于图片类型,优先使用 base64 格式(因为 LLM 无法访问内部 URL)
base64 格式: data:<mime_type>;base64,<base64_content>
"""
if not attachments:
return text
content = []
# 添加文本
if text:
content.append({"type": "text", "text": text})
# 添加附件
for att in attachments:
att_type = att.get('type', 'file')
mime_type = att.get('mime_type', '')
url = att.get('url', '')
base64_content = att.get('base64', '')
if att_type == 'image' or mime_type.startswith('image/'):
# 图片类型
text_content = att.get('text_content', '')
if base64_content:
# 多模态模型:使用 OpenAI 多模态格式
# 使用 data URL 格式
image_url = f"data:{mime_type};base64,{base64_content}"
content.append({
"type": "image_url",
"image_url": {"url": image_url}
})
elif text_content:
# 非多模态模型:使用 OCR 识别的文本内容
file_name = att.get('name', 'unknown')
content.append({
"type": "text",
"text": f"\n{text_content}"
})
elif url:
# 回退到 URL(仅适用于公网可访问的 URL)
content.append({
"type": "image_url",
"image_url": {"url": url}
})
else:
# 其他文件类型 - 提取文本内容或显示文件信息
file_name = att.get('name', 'unknown')
text_content = att.get('text_content', '')
if text_content:
# 有提取的文本内容,将其添加到消息中
content.append({
"type": "text",
"text": f"\n--- 文件: {file_name} ---\n{text_content}\n--- 文件结束 ---"
})
else:
# 无法提取内容,只显示文件信息
file_size = att.get('size', 0)
size_str = f"{file_size / 1024:.1f}KB" if file_size < 1024 * 1024 else f"{file_size / 1024 / 1024:.1f}MB"
content.append({
"type": "text",
"text": f"\n[附件: {file_name} ({size_str}),该文件类型暂不支持内容提取]"
})
return content if len(content) > 1 else (content[0].get('text', '') if content else text)
@dataclass
class ToolCall:
"""工具调用"""
id: str # 工具调用 ID
name: str # 工具/函数名称
arguments: Dict[str, Any] # 参数
def to_dict(self) -> Dict[str, Any]:
import json
return {
'id': self.id,
'type': 'function',
'function': {
'name': self.name,
'arguments': json.dumps(self.arguments, ensure_ascii=False) if isinstance(self.arguments, dict) else self.arguments,
}
}
@dataclass
class LLMResponse:
"""LLM 响应"""
content: str
model: str = ''
prompt_tokens: int = 0
completion_tokens: int = 0
total_tokens: int = 0
finish_reason: str = ''
raw_response: Optional[Dict] = None
# Function Calling 相关
tool_calls: Optional[List[ToolCall]] = None # 工具调用列表
@property
def is_complete(self) -> bool:
return self.finish_reason in ('stop', 'end_turn', 'length')
@property
def has_tool_calls(self) -> bool:
"""是否包含工具调用"""
return bool(self.tool_calls)
@dataclass
class LLMStreamChunk:
"""LLM 流式响应块"""
content: str = ''
is_finished: bool = False
finish_reason: str = ''
prompt_tokens: int = 0
completion_tokens: int = 0
total_tokens: int = 0
# Function Calling 相关
tool_calls: Optional[List[ToolCall]] = None # 工具调用(流式时可能分多次返回)
tool_call_delta: Optional[Dict[str, Any]] = None # 工具调用增量
@dataclass
class ToolDefinition:
"""工具定义(用于 Function Calling"""
name: str # 工具名称
description: str # 工具描述
parameters: Dict[str, Any] # 参数 Schema (JSON Schema 格式)
def to_openai_format(self) -> Dict[str, Any]:
"""转换为 OpenAI 格式"""
return {
'type': 'function',
'function': {
'name': self.name,
'description': self.description,
'parameters': self.parameters,
}
}
def to_claude_format(self) -> Dict[str, Any]:
"""转换为 Claude 格式"""
return {
'name': self.name,
'description': self.description,
'input_schema': self.parameters,
}
@dataclass
class LLMConfig:
"""LLM 配置"""
model: str
temperature: float = 0.7
top_p: float = 1.0
max_tokens: int = 2048
stop: Optional[List[str]] = None
presence_penalty: float = 0.0
frequency_penalty: float = 0.0
extra_params: Dict[str, Any] = field(default_factory=dict)
# Function Calling 相关
tools: Optional[List[ToolDefinition]] = None # 工具定义列表
tool_choice: str = 'auto' # 工具选择策略: auto, none, required, 或具体工具名
class BaseLLMProvider(ABC):
"""
LLM 提供商基类
所有提供商适配器必须继承此类并实现抽象方法
"""
# 提供商类型标识
provider_type: str = ''
# 提供商显示名称
provider_name: str = ''
# 支持的模型类型
supported_model_types: List[str] = ['chat']
def __init__(
self,
api_key: str = '',
api_base: str = '',
**kwargs
):
self.api_key = api_key
self.api_base = api_base
self.extra_config = kwargs
@abstractmethod
def chat(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> LLMResponse:
"""
同步对话
Args:
messages: 消息列表
config: 配置
Returns:
LLMResponse
"""
pass
@abstractmethod
async def chat_async(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> LLMResponse:
"""
异步对话
Args:
messages: 消息列表
config: 配置
Returns:
LLMResponse
"""
pass
@abstractmethod
async def chat_stream(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> AsyncGenerator[LLMStreamChunk, None]:
"""
异步流式对话
Args:
messages: 消息列表
config: 配置
Yields:
LLMStreamChunk
"""
pass
def chat_stream_sync(
self,
messages: List[LLMMessage],
config: LLMConfig,
):
"""
同步流式对话(生成器)
Args:
messages: 消息列表
config: 配置
Yields:
LLMStreamChunk
"""
# 默认实现:调用同步 chat 并返回单个结果
response = self.chat(messages, config)
yield LLMStreamChunk(
content=response.content,
is_finished=True,
finish_reason=response.finish_reason,
prompt_tokens=response.prompt_tokens,
completion_tokens=response.completion_tokens,
total_tokens=response.total_tokens,
)
def validate_config(self) -> bool:
"""
验证配置是否有效
Returns:
是否有效
"""
return bool(self.api_key)
def get_available_models(self) -> List[Dict[str, Any]]:
"""
获取可用模型列表
Returns:
模型列表
"""
return []
async def fetch_models_from_api(self) -> List[Dict[str, Any]]:
"""
从提供商 API 在线拉取最新模型列表
子类可覆盖此方法实现具体的 API 调用逻辑。
默认返回空列表,表示该提供商不支持在线拉取。
Returns:
模型列表,格式与 get_default_models 一致
"""
return []
@classmethod
def get_default_models(cls) -> List[Dict[str, Any]]:
"""
获取默认模型列表(用于初始化)
Returns:
默认模型列表
"""
return []
@@ -0,0 +1,528 @@
"""
Anthropic Claude 提供商适配器
"""
import logging
from typing import AsyncGenerator, Dict, List, Any
from .base import BaseLLMProvider, LLMConfig, LLMMessage, LLMResponse, LLMStreamChunk, ToolCall
from .registry import ProviderRegistry
logger = logging.getLogger(__name__)
@ProviderRegistry.register
class ClaudeProvider(BaseLLMProvider):
"""
Anthropic Claude 提供商适配器
支持 Claude 3 系列模型
"""
provider_type = 'claude'
provider_name = 'Anthropic Claude'
supported_model_types = ['chat']
DEFAULT_API_BASE = 'https://api.anthropic.com'
def __init__(self, api_key: str = '', api_base: str = '', **kwargs):
super().__init__(api_key, api_base, **kwargs)
self.api_base = api_base or self.DEFAULT_API_BASE
self._client = None
self._async_client = None
def _get_client(self):
"""获取同步客户端"""
if self._client is None:
try:
import anthropic
self._client = anthropic.Anthropic(
api_key=self.api_key,
base_url=self.api_base if self.api_base != self.DEFAULT_API_BASE else None,
)
except ImportError:
raise ImportError('请安装 anthropic 库: pip install anthropic')
return self._client
def _get_async_client(self):
"""获取异步客户端"""
if self._async_client is None:
try:
import anthropic
self._async_client = anthropic.AsyncAnthropic(
api_key=self.api_key,
base_url=self.api_base if self.api_base != self.DEFAULT_API_BASE else None,
)
except ImportError:
raise ImportError('请安装 anthropic 库: pip install anthropic')
return self._async_client
def _convert_messages(self, messages: List[LLMMessage]) -> tuple:
"""
转换消息格式
Claude API 需要将 system 消息单独传递
"""
system_prompt = ''
converted_messages = []
for msg in messages:
if msg.role == 'system':
system_prompt = msg.content
elif msg.role == 'tool':
# Claude 的工具结果消息格式
converted_messages.append({
'role': 'user',
'content': [{
'type': 'tool_result',
'tool_use_id': msg.tool_call_id,
'content': msg.content,
}],
})
elif msg.role == 'assistant' and msg.tool_calls:
# 包含工具调用的助手消息
content = []
if msg.content:
content.append({'type': 'text', 'text': msg.content})
for tc in msg.tool_calls:
content.append({
'type': 'tool_use',
'id': tc.get('id', ''),
'name': tc.get('function', {}).get('name', ''),
'input': tc.get('function', {}).get('arguments', {}),
})
converted_messages.append({
'role': 'assistant',
'content': content,
})
else:
converted_messages.append({
'role': msg.role,
'content': msg.content,
})
return system_prompt, converted_messages
def chat(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> LLMResponse:
"""同步对话"""
client = self._get_client()
system_prompt, converted_messages = self._convert_messages(messages)
kwargs = {
'model': config.model,
'messages': converted_messages,
'max_tokens': config.max_tokens,
'temperature': config.temperature,
'top_p': config.top_p,
}
if system_prompt:
kwargs['system'] = system_prompt
if config.stop:
kwargs['stop_sequences'] = config.stop
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_claude_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = {'type': 'any'}
elif config.tool_choice == 'none':
# Claude 不支持 none,不传递 tools 即可
del kwargs['tools']
elif config.tool_choice != 'auto':
# 指定具体工具
kwargs['tool_choice'] = {'type': 'tool', 'name': config.tool_choice}
else:
kwargs['tool_choice'] = {'type': 'auto'}
response = client.messages.create(**kwargs)
# 解析响应内容和工具调用
content = ''
tool_calls = []
for block in response.content:
if block.type == 'text':
content = block.text
elif block.type == 'tool_use':
tool_calls.append(ToolCall(
id=block.id,
name=block.name,
arguments=block.input if isinstance(block.input, dict) else {},
))
return LLMResponse(
content=content,
model=response.model,
prompt_tokens=response.usage.input_tokens,
completion_tokens=response.usage.output_tokens,
total_tokens=response.usage.input_tokens + response.usage.output_tokens,
finish_reason=response.stop_reason or '',
raw_response=response.model_dump(),
tool_calls=tool_calls if tool_calls else None,
)
async def chat_async(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> LLMResponse:
"""异步对话"""
client = self._get_async_client()
system_prompt, converted_messages = self._convert_messages(messages)
kwargs = {
'model': config.model,
'messages': converted_messages,
'max_tokens': config.max_tokens,
'temperature': config.temperature,
'top_p': config.top_p,
}
if system_prompt:
kwargs['system'] = system_prompt
if config.stop:
kwargs['stop_sequences'] = config.stop
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_claude_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = {'type': 'any'}
elif config.tool_choice == 'none':
del kwargs['tools']
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'tool', 'name': config.tool_choice}
else:
kwargs['tool_choice'] = {'type': 'auto'}
response = await client.messages.create(**kwargs)
# 解析响应内容和工具调用
content = ''
tool_calls = []
for block in response.content:
if block.type == 'text':
content = block.text
elif block.type == 'tool_use':
tool_calls.append(ToolCall(
id=block.id,
name=block.name,
arguments=block.input if isinstance(block.input, dict) else {},
))
return LLMResponse(
content=content,
model=response.model,
prompt_tokens=response.usage.input_tokens,
completion_tokens=response.usage.output_tokens,
total_tokens=response.usage.input_tokens + response.usage.output_tokens,
finish_reason=response.stop_reason or '',
raw_response=response.model_dump(),
tool_calls=tool_calls if tool_calls else None,
)
async def chat_stream(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> AsyncGenerator[LLMStreamChunk, None]:
"""流式对话"""
client = self._get_async_client()
system_prompt, converted_messages = self._convert_messages(messages)
kwargs = {
'model': config.model,
'messages': converted_messages,
'max_tokens': config.max_tokens,
'temperature': config.temperature,
'top_p': config.top_p,
}
if system_prompt:
kwargs['system'] = system_prompt
if config.stop:
kwargs['stop_sequences'] = config.stop
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_claude_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = {'type': 'any'}
elif config.tool_choice == 'none':
del kwargs['tools']
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'tool', 'name': config.tool_choice}
else:
kwargs['tool_choice'] = {'type': 'auto'}
# 用于累积工具调用
tool_calls_accumulator = [] # List of {id, name, input_json}
current_tool_call = None
async with client.messages.stream(**kwargs) as stream:
async for event in stream:
if hasattr(event, 'type'):
if event.type == 'content_block_start':
if hasattr(event, 'content_block') and event.content_block.type == 'tool_use':
current_tool_call = {
'id': event.content_block.id,
'name': event.content_block.name,
'input_json': '',
}
elif event.type == 'content_block_delta':
if hasattr(event, 'delta'):
if event.delta.type == 'text_delta':
yield LLMStreamChunk(
content=event.delta.text,
is_finished=False,
)
elif event.delta.type == 'input_json_delta' and current_tool_call:
current_tool_call['input_json'] += event.delta.partial_json
elif event.type == 'content_block_stop':
if current_tool_call:
tool_calls_accumulator.append(current_tool_call)
current_tool_call = None
# 获取最终的 usage 信息
final_message = await stream.get_final_message()
# 解析工具调用
tool_calls = None
if tool_calls_accumulator:
tool_calls = self._parse_accumulated_tool_calls(tool_calls_accumulator)
yield LLMStreamChunk(
content='',
is_finished=True,
finish_reason=final_message.stop_reason or '',
prompt_tokens=final_message.usage.input_tokens,
completion_tokens=final_message.usage.output_tokens,
total_tokens=final_message.usage.input_tokens + final_message.usage.output_tokens,
tool_calls=tool_calls,
)
def _parse_accumulated_tool_calls(self, accumulator: List[Dict]) -> List[ToolCall]:
"""解析累积的工具调用"""
import json
result = []
for tc in accumulator:
try:
arguments = json.loads(tc['input_json']) if tc['input_json'] else {}
except json.JSONDecodeError:
arguments = {'raw': tc['input_json']}
result.append(ToolCall(
id=tc['id'],
name=tc['name'],
arguments=arguments,
))
return result
def chat_stream_sync(
self,
messages: List[LLMMessage],
config: LLMConfig,
):
"""同步流式对话"""
client = self._get_client()
system_prompt, converted_messages = self._convert_messages(messages)
kwargs = {
'model': config.model,
'messages': converted_messages,
'max_tokens': config.max_tokens,
'temperature': config.temperature,
'top_p': config.top_p,
}
if system_prompt:
kwargs['system'] = system_prompt
if config.stop:
kwargs['stop_sequences'] = config.stop
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_claude_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = {'type': 'any'}
elif config.tool_choice == 'none':
del kwargs['tools']
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'tool', 'name': config.tool_choice}
else:
kwargs['tool_choice'] = {'type': 'auto'}
# 用于累积工具调用
tool_calls_accumulator = []
current_tool_call = None
with client.messages.stream(**kwargs) as stream:
for event in stream:
if hasattr(event, 'type'):
if event.type == 'content_block_start':
if hasattr(event, 'content_block') and event.content_block.type == 'tool_use':
current_tool_call = {
'id': event.content_block.id,
'name': event.content_block.name,
'input_json': '',
}
elif event.type == 'content_block_delta':
if hasattr(event, 'delta'):
if event.delta.type == 'text_delta':
yield LLMStreamChunk(
content=event.delta.text,
is_finished=False,
)
elif event.delta.type == 'input_json_delta' and current_tool_call:
current_tool_call['input_json'] += event.delta.partial_json
elif event.type == 'content_block_stop':
if current_tool_call:
tool_calls_accumulator.append(current_tool_call)
current_tool_call = None
# 获取最终的 usage 信息
final_message = stream.get_final_message()
# 解析工具调用
tool_calls = None
if tool_calls_accumulator:
tool_calls = self._parse_accumulated_tool_calls(tool_calls_accumulator)
yield LLMStreamChunk(
content='',
is_finished=True,
finish_reason=final_message.stop_reason or '',
prompt_tokens=final_message.usage.input_tokens,
completion_tokens=final_message.usage.output_tokens,
total_tokens=final_message.usage.input_tokens + final_message.usage.output_tokens,
tool_calls=tool_calls,
)
async def fetch_models_from_api(self) -> List[Dict[str, Any]]:
"""通过 Anthropic /v1/models 端点在线拉取模型列表"""
import httpx
base_url = (self.api_base or self.DEFAULT_API_BASE).rstrip('/')
headers = {
'x-api-key': self.api_key,
'anthropic-version': '2023-06-01',
}
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(f'{base_url}/v1/models', headers=headers)
resp.raise_for_status()
data = resp.json()
model_list = data.get('data', [])
if not model_list:
return []
results = []
for item in model_list:
model_id = item.get('id', '')
display = item.get('display_name', model_id)
if not model_id:
continue
results.append({
'model_name': model_id,
'display_name': display,
'model_type': 'chat',
'max_tokens': 4096,
'context_window': 200000,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0,
'output_price': 0,
})
results.sort(key=lambda m: m['model_name'])
logger.info(f'从 Anthropic ({base_url}) 拉取到 {len(results)} 个模型')
return results
except Exception as e:
logger.warning(f'在线拉取 Anthropic 模型列表失败 ({base_url}): {e}')
return []
@classmethod
def get_default_models(cls) -> List[Dict[str, Any]]:
"""获取默认模型列表"""
return [
# ---- Chat 模型 ----
{
'model_name': 'claude-sonnet-4-20250514',
'display_name': 'Claude Sonnet 4',
'model_type': 'chat',
'max_tokens': 16384,
'context_window': 200000,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0.003,
'output_price': 0.015,
},
{
'model_name': 'claude-3-7-sonnet-20250219',
'display_name': 'Claude 3.7 Sonnet',
'model_type': 'chat',
'max_tokens': 16384,
'context_window': 200000,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0.003,
'output_price': 0.015,
},
{
'model_name': 'claude-3-5-sonnet-20241022',
'display_name': 'Claude 3.5 Sonnet',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 200000,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0.003,
'output_price': 0.015,
},
{
'model_name': 'claude-3-5-haiku-20241022',
'display_name': 'Claude 3.5 Haiku',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 200000,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0.001,
'output_price': 0.005,
},
{
'model_name': 'claude-3-opus-20240229',
'display_name': 'Claude 3 Opus',
'model_type': 'chat',
'max_tokens': 4096,
'context_window': 200000,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0.015,
'output_price': 0.075,
},
{
'model_name': 'claude-3-haiku-20240307',
'display_name': 'Claude 3 Haiku',
'model_type': 'chat',
'max_tokens': 4096,
'context_window': 200000,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0.00025,
'output_price': 0.00125,
},
]
@@ -0,0 +1,56 @@
"""
DeepSeek 提供商适配器
兼容 OpenAI API,继承 OpenAI 适配器
"""
import logging
from typing import List, Dict, Any
from .openai_provider import OpenAIProvider
from .registry import ProviderRegistry
logger = logging.getLogger(__name__)
@ProviderRegistry.register
class DeepSeekProvider(OpenAIProvider):
"""
DeepSeek 提供商适配器
兼容 OpenAI API 格式,支持 DeepSeek-V3、DeepSeek-R1 等模型
"""
provider_type = 'deepseek'
provider_name = 'DeepSeek'
supported_model_types = ['chat', 'completion']
DEFAULT_API_BASE = 'https://api.deepseek.com/v1'
@classmethod
def get_default_models(cls) -> List[Dict[str, Any]]:
"""获取默认模型列表"""
return [
# ---- Chat 模型 ----
{
'model_name': 'deepseek-chat',
'display_name': 'DeepSeek V3',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 65536,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0.0014,
'output_price': 0.0028,
},
{
'model_name': 'deepseek-reasoner',
'display_name': 'DeepSeek R1',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 65536,
'supports_vision': False,
'supports_function_call': False,
'input_price': 0.004,
'output_price': 0.016,
},
]
@@ -0,0 +1,551 @@
"""
Ollama 本地模型提供商适配器
"""
import logging
from typing import AsyncGenerator, Dict, List, Any
from .base import BaseLLMProvider, LLMConfig, LLMMessage, LLMResponse, LLMStreamChunk, ToolCall
from .registry import ProviderRegistry
logger = logging.getLogger(__name__)
@ProviderRegistry.register
class OllamaProvider(BaseLLMProvider):
"""
Ollama 本地模型提供商适配器
支持本地部署的开源模型
"""
provider_type = 'ollama'
provider_name = 'Ollama (本地)'
supported_model_types = ['chat', 'embedding']
DEFAULT_API_BASE = 'http://localhost:11434/v1'
def __init__(self, api_key: str = '', api_base: str = '', **kwargs):
super().__init__(api_key, api_base, **kwargs)
# Ollama 使用 ollama_host 参数
ollama_host = kwargs.get('ollama_host', '')
if ollama_host:
self.api_base = f'{ollama_host.rstrip("/")}/v1'
else:
self.api_base = api_base or self.DEFAULT_API_BASE
self._client = None
self._async_client = None
def _get_client(self):
"""获取同步客户端"""
if self._client is None:
try:
from openai import OpenAI
self._client = OpenAI(
api_key='ollama', # Ollama 不需要 API Key
base_url=self.api_base,
)
except ImportError:
raise ImportError('请安装 openai 库: pip install openai')
return self._client
def _get_async_client(self):
"""获取异步客户端"""
if self._async_client is None:
try:
from openai import AsyncOpenAI
self._async_client = AsyncOpenAI(
api_key='ollama', # Ollama 不需要 API Key
base_url=self.api_base,
)
except ImportError:
raise ImportError('请安装 openai 库: pip install openai')
return self._async_client
def validate_config(self) -> bool:
"""Ollama 不需要 API Key"""
return True
def chat(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> LLMResponse:
"""同步对话"""
client = self._get_client()
# 构建请求参数
kwargs = {
'model': config.model,
'messages': [m.to_dict() for m in messages],
'temperature': config.temperature,
'top_p': config.top_p,
'max_tokens': config.max_tokens,
'stop': config.stop,
}
# 添加 Function Calling 参数(Ollama 部分模型支持)
if config.tools:
kwargs['tools'] = [t.to_openai_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = 'required'
elif config.tool_choice == 'none':
kwargs['tool_choice'] = 'none'
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'function', 'function': {'name': config.tool_choice}}
else:
kwargs['tool_choice'] = 'auto'
response = client.chat.completions.create(**kwargs)
choice = response.choices[0]
usage = response.usage
# 解析工具调用
tool_calls = None
if choice.message.tool_calls:
tool_calls = self._parse_tool_calls(choice.message.tool_calls)
return LLMResponse(
content=choice.message.content or '',
model=response.model,
prompt_tokens=usage.prompt_tokens if usage else 0,
completion_tokens=usage.completion_tokens if usage else 0,
total_tokens=usage.total_tokens if usage else 0,
finish_reason=choice.finish_reason or '',
raw_response=response.model_dump(),
tool_calls=tool_calls,
)
def _parse_tool_calls(self, tool_calls) -> List[ToolCall]:
"""解析工具调用"""
import json
result = []
for tc in tool_calls:
try:
arguments = json.loads(tc.function.arguments) if tc.function.arguments else {}
except json.JSONDecodeError:
arguments = {'raw': tc.function.arguments}
result.append(ToolCall(
id=tc.id or f'call_{len(result)}',
name=tc.function.name,
arguments=arguments,
))
return result
async def chat_async(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> LLMResponse:
"""异步对话"""
client = self._get_async_client()
# 构建请求参数
kwargs = {
'model': config.model,
'messages': [m.to_dict() for m in messages],
'temperature': config.temperature,
'top_p': config.top_p,
'max_tokens': config.max_tokens,
'stop': config.stop,
}
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_openai_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = 'required'
elif config.tool_choice == 'none':
kwargs['tool_choice'] = 'none'
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'function', 'function': {'name': config.tool_choice}}
else:
kwargs['tool_choice'] = 'auto'
response = await client.chat.completions.create(**kwargs)
choice = response.choices[0]
usage = response.usage
# 解析工具调用
tool_calls = None
if choice.message.tool_calls:
tool_calls = self._parse_tool_calls(choice.message.tool_calls)
return LLMResponse(
content=choice.message.content or '',
model=response.model,
prompt_tokens=usage.prompt_tokens if usage else 0,
completion_tokens=usage.completion_tokens if usage else 0,
total_tokens=usage.total_tokens if usage else 0,
finish_reason=choice.finish_reason or '',
raw_response=response.model_dump(),
tool_calls=tool_calls,
)
async def chat_stream(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> AsyncGenerator[LLMStreamChunk, None]:
"""流式对话"""
client = self._get_async_client()
# 构建请求参数
kwargs = {
'model': config.model,
'messages': [m.to_dict() for m in messages],
'temperature': config.temperature,
'top_p': config.top_p,
'max_tokens': config.max_tokens,
'stop': config.stop,
'stream': True,
}
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_openai_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = 'required'
elif config.tool_choice == 'none':
kwargs['tool_choice'] = 'none'
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'function', 'function': {'name': config.tool_choice}}
else:
kwargs['tool_choice'] = 'auto'
stream = await client.chat.completions.create(**kwargs)
# 用于累积工具调用
tool_call_accumulator = {}
async for chunk in stream:
if chunk.choices:
choice = chunk.choices[0]
delta = choice.delta
# 处理工具调用增量
tool_call_delta = None
if hasattr(delta, 'tool_calls') and delta.tool_calls:
for tc_delta in delta.tool_calls:
tc_index = tc_delta.index if hasattr(tc_delta, 'index') else 0
if tc_index not in tool_call_accumulator:
tool_call_accumulator[tc_index] = {
'id': getattr(tc_delta, 'id', '') or f'call_{tc_index}',
'name': '',
'arguments': '',
}
if hasattr(tc_delta, 'id') and tc_delta.id:
tool_call_accumulator[tc_index]['id'] = tc_delta.id
if tc_delta.function:
if tc_delta.function.name:
tool_call_accumulator[tc_index]['name'] = tc_delta.function.name
if tc_delta.function.arguments:
tool_call_accumulator[tc_index]['arguments'] += tc_delta.function.arguments
tool_call_delta = tool_call_accumulator[tc_index]
# 检查是否完成
tool_calls = None
if choice.finish_reason and tool_call_accumulator:
tool_calls = self._parse_accumulated_tool_calls(tool_call_accumulator)
yield LLMStreamChunk(
content=delta.content or '',
is_finished=choice.finish_reason is not None,
finish_reason=choice.finish_reason or '',
tool_calls=tool_calls,
tool_call_delta=tool_call_delta,
)
def _parse_accumulated_tool_calls(self, accumulator: Dict) -> List[ToolCall]:
"""解析累积的工具调用"""
import json
result = []
for idx in sorted(accumulator.keys()):
tc = accumulator[idx]
try:
arguments = json.loads(tc['arguments']) if tc['arguments'] else {}
except json.JSONDecodeError:
arguments = {'raw': tc['arguments']}
result.append(ToolCall(
id=tc['id'],
name=tc['name'],
arguments=arguments,
))
return result
def chat_stream_sync(
self,
messages: List[LLMMessage],
config: LLMConfig,
):
"""同步流式对话"""
client = self._get_client()
# 构建请求参数
kwargs = {
'model': config.model,
'messages': [m.to_dict() for m in messages],
'temperature': config.temperature,
'top_p': config.top_p,
'max_tokens': config.max_tokens,
'stop': config.stop,
'stream': True,
}
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_openai_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = 'required'
elif config.tool_choice == 'none':
kwargs['tool_choice'] = 'none'
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'function', 'function': {'name': config.tool_choice}}
else:
kwargs['tool_choice'] = 'auto'
stream = client.chat.completions.create(**kwargs)
# 用于累积工具调用
tool_call_accumulator = {}
for chunk in stream:
if chunk.choices:
choice = chunk.choices[0]
delta = choice.delta
# 处理工具调用增量
tool_call_delta = None
if hasattr(delta, 'tool_calls') and delta.tool_calls:
for tc_delta in delta.tool_calls:
tc_index = tc_delta.index if hasattr(tc_delta, 'index') else 0
if tc_index not in tool_call_accumulator:
tool_call_accumulator[tc_index] = {
'id': getattr(tc_delta, 'id', '') or f'call_{tc_index}',
'name': '',
'arguments': '',
}
if hasattr(tc_delta, 'id') and tc_delta.id:
tool_call_accumulator[tc_index]['id'] = tc_delta.id
if tc_delta.function:
if tc_delta.function.name:
tool_call_accumulator[tc_index]['name'] = tc_delta.function.name
if tc_delta.function.arguments:
tool_call_accumulator[tc_index]['arguments'] += tc_delta.function.arguments
tool_call_delta = tool_call_accumulator[tc_index]
# 检查是否完成
tool_calls = None
if choice.finish_reason and tool_call_accumulator:
tool_calls = self._parse_accumulated_tool_calls(tool_call_accumulator)
yield LLMStreamChunk(
content=delta.content or '',
is_finished=choice.finish_reason is not None,
finish_reason=choice.finish_reason or '',
tool_calls=tool_calls,
tool_call_delta=tool_call_delta,
)
_EMBEDDING_KEYWORDS = ('embed', 'bge', 'nomic-embed', 'mxbai-embed')
_RERANK_KEYWORDS = ('rerank', 'reranker')
def get_available_models(self) -> List[Dict[str, Any]]:
"""获取本地可用的模型列表"""
try:
import httpx
base_url = self.api_base.replace('/v1', '')
response = httpx.get(f'{base_url}/api/tags', timeout=10)
if response.status_code == 200:
data = response.json()
models = []
for model in data.get('models', []):
models.append({
'model_name': model.get('name', ''),
'display_name': model.get('name', ''),
'size': model.get('size', 0),
'modified_at': model.get('modified_at', ''),
})
return models
except Exception as e:
logger.warning(f'获取 Ollama 模型列表失败: {e}')
return []
async def fetch_models_from_api(self) -> List[Dict[str, Any]]:
"""通过 /api/tags 端点拉取 Ollama 本地已安装的模型"""
import httpx
base_url = (self.api_base or 'http://localhost:11434').rstrip('/')
# Ollama 的 /api/tags 在根路径,去掉 /v1
if base_url.endswith('/v1'):
base_url = base_url[:-3]
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(f'{base_url}/api/tags')
resp.raise_for_status()
data = resp.json()
results = []
for item in data.get('models', []):
name = item.get('name', '')
if not name:
continue
name_lower = name.lower()
if any(kw in name_lower for kw in self._RERANK_KEYWORDS):
model_type = 'rerank'
elif any(kw in name_lower for kw in self._EMBEDDING_KEYWORDS):
model_type = 'embedding'
else:
model_type = 'chat'
# 从 size 推算显示名称
size_gb = round(item.get('size', 0) / (1024 ** 3), 1)
display = f'{name} ({size_gb}GB)' if size_gb > 0 else name
results.append({
'model_name': name,
'display_name': display,
'model_type': model_type,
'max_tokens': 4096,
'context_window': 4096,
'supports_vision': False,
'supports_function_call': model_type == 'chat',
'input_price': 0,
'output_price': 0,
})
type_order = {'chat': 0, 'embedding': 1, 'rerank': 2}
results.sort(key=lambda m: (type_order.get(m['model_type'], 9), m['model_name']))
logger.info(f'从 Ollama ({base_url}) 拉取到 {len(results)} 个本地模型')
return results
except Exception as e:
logger.warning(f'在线拉取 Ollama 模型列表失败 ({base_url}): {e}')
return []
@classmethod
def get_default_models(cls) -> List[Dict[str, Any]]:
"""获取默认模型列表(常用的开源模型)"""
return [
# ---- Chat 模型 ----
{
'model_name': 'qwen3:32b',
'display_name': 'Qwen3 32B',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 131072,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0,
'output_price': 0,
},
{
'model_name': 'qwen3:8b',
'display_name': 'Qwen3 8B',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 131072,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0,
'output_price': 0,
},
{
'model_name': 'llama3.3',
'display_name': 'Llama 3.3 70B',
'model_type': 'chat',
'max_tokens': 4096,
'context_window': 128000,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0,
'output_price': 0,
},
{
'model_name': 'llama3.2',
'display_name': 'Llama 3.2',
'model_type': 'chat',
'max_tokens': 4096,
'context_window': 128000,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0,
'output_price': 0,
},
{
'model_name': 'deepseek-r1',
'display_name': 'DeepSeek R1',
'model_type': 'chat',
'max_tokens': 4096,
'context_window': 64000,
'supports_vision': False,
'supports_function_call': False,
'input_price': 0,
'output_price': 0,
},
{
'model_name': 'mistral',
'display_name': 'Mistral',
'model_type': 'chat',
'max_tokens': 4096,
'context_window': 32000,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0,
'output_price': 0,
},
{
'model_name': 'gemma3',
'display_name': 'Gemma 3',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 128000,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0,
'output_price': 0,
},
# ---- Embedding 模型 ----
{
'model_name': 'nomic-embed-text',
'display_name': 'Nomic Embed Text',
'model_type': 'embedding',
'max_tokens': 8192,
'context_window': 8192,
'input_price': 0,
'output_price': 0,
},
{
'model_name': 'bge-m3',
'display_name': 'BGE-M3',
'model_type': 'embedding',
'max_tokens': 8192,
'context_window': 8192,
'input_price': 0,
'output_price': 0,
},
{
'model_name': 'mxbai-embed-large',
'display_name': 'MxBai Embed Large',
'model_type': 'embedding',
'max_tokens': 512,
'context_window': 512,
'input_price': 0,
'output_price': 0,
},
# ---- Rerank 模型 ----
{
'model_name': 'bge-reranker-v2-m3',
'display_name': 'BGE Reranker V2 M3',
'model_type': 'rerank',
'max_tokens': 8192,
'context_window': 8192,
'input_price': 0,
'output_price': 0,
},
]
@@ -0,0 +1,565 @@
"""
OpenAI 提供商适配器
"""
import logging
from typing import AsyncGenerator, Dict, List, Any
from .base import BaseLLMProvider, LLMConfig, LLMMessage, LLMResponse, LLMStreamChunk, ToolCall
from .registry import ProviderRegistry
logger = logging.getLogger(__name__)
@ProviderRegistry.register
class OpenAIProvider(BaseLLMProvider):
"""
OpenAI 提供商适配器
支持 GPT-3.5, GPT-4 等模型
"""
provider_type = 'openai'
provider_name = 'OpenAI'
supported_model_types = ['chat', 'completion', 'embedding']
DEFAULT_API_BASE = 'https://api.openai.com/v1'
def __init__(self, api_key: str = '', api_base: str = '', **kwargs):
super().__init__(api_key, api_base, **kwargs)
self.api_base = api_base or self.DEFAULT_API_BASE
self._client = None
self._async_client = None
def _get_client(self):
"""获取同步客户端"""
if self._client is None:
try:
from openai import OpenAI
self._client = OpenAI(
api_key=self.api_key,
base_url=self.api_base,
)
except ImportError:
raise ImportError('请安装 openai 库: pip install openai')
return self._client
def _get_async_client(self):
"""获取异步客户端"""
if self._async_client is None:
try:
from openai import AsyncOpenAI
self._async_client = AsyncOpenAI(
api_key=self.api_key,
base_url=self.api_base,
)
except ImportError:
raise ImportError('请安装 openai 库: pip install openai')
return self._async_client
def chat(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> LLMResponse:
"""同步对话"""
client = self._get_client()
# 构建请求参数
kwargs = {
'model': config.model,
'messages': [m.to_dict() for m in messages],
'temperature': config.temperature,
'top_p': config.top_p,
'max_tokens': config.max_tokens,
'stop': config.stop,
'presence_penalty': config.presence_penalty,
'frequency_penalty': config.frequency_penalty,
**config.extra_params,
}
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_openai_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = 'required'
elif config.tool_choice == 'none':
kwargs['tool_choice'] = 'none'
elif config.tool_choice != 'auto':
# 指定具体工具
kwargs['tool_choice'] = {'type': 'function', 'function': {'name': config.tool_choice}}
else:
kwargs['tool_choice'] = 'auto'
response = client.chat.completions.create(**kwargs)
choice = response.choices[0]
usage = response.usage
# 解析工具调用
tool_calls = None
if choice.message.tool_calls:
tool_calls = self._parse_tool_calls(choice.message.tool_calls)
return LLMResponse(
content=choice.message.content or '',
model=response.model,
prompt_tokens=usage.prompt_tokens if usage else 0,
completion_tokens=usage.completion_tokens if usage else 0,
total_tokens=usage.total_tokens if usage else 0,
finish_reason=choice.finish_reason or '',
raw_response=response.model_dump(),
tool_calls=tool_calls,
)
def _parse_tool_calls(self, tool_calls) -> List[ToolCall]:
"""解析工具调用"""
import json
result = []
for tc in tool_calls:
try:
arguments = json.loads(tc.function.arguments) if tc.function.arguments else {}
except json.JSONDecodeError:
arguments = {'raw': tc.function.arguments}
result.append(ToolCall(
id=tc.id,
name=tc.function.name,
arguments=arguments,
))
return result
async def chat_async(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> LLMResponse:
"""异步对话"""
client = self._get_async_client()
# 构建请求参数
kwargs = {
'model': config.model,
'messages': [m.to_dict() for m in messages],
'temperature': config.temperature,
'top_p': config.top_p,
'max_tokens': config.max_tokens,
'stop': config.stop,
'presence_penalty': config.presence_penalty,
'frequency_penalty': config.frequency_penalty,
**config.extra_params,
}
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_openai_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = 'required'
elif config.tool_choice == 'none':
kwargs['tool_choice'] = 'none'
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'function', 'function': {'name': config.tool_choice}}
else:
kwargs['tool_choice'] = 'auto'
response = await client.chat.completions.create(**kwargs)
choice = response.choices[0]
usage = response.usage
# 解析工具调用
tool_calls = None
if choice.message.tool_calls:
tool_calls = self._parse_tool_calls(choice.message.tool_calls)
return LLMResponse(
content=choice.message.content or '',
model=response.model,
prompt_tokens=usage.prompt_tokens if usage else 0,
completion_tokens=usage.completion_tokens if usage else 0,
total_tokens=usage.total_tokens if usage else 0,
finish_reason=choice.finish_reason or '',
raw_response=response.model_dump(),
tool_calls=tool_calls,
)
async def chat_stream(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> AsyncGenerator[LLMStreamChunk, None]:
"""流式对话"""
client = self._get_async_client()
# 构建请求参数
kwargs = {
'model': config.model,
'messages': [m.to_dict() for m in messages],
'temperature': config.temperature,
'top_p': config.top_p,
'max_tokens': config.max_tokens,
'stop': config.stop,
'presence_penalty': config.presence_penalty,
'frequency_penalty': config.frequency_penalty,
'stream': True,
'stream_options': {"include_usage": True},
**config.extra_params,
}
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_openai_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = 'required'
elif config.tool_choice == 'none':
kwargs['tool_choice'] = 'none'
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'function', 'function': {'name': config.tool_choice}}
else:
kwargs['tool_choice'] = 'auto'
stream = await client.chat.completions.create(**kwargs)
# 用于累积工具调用
tool_call_accumulator = {} # id -> {name, arguments}
async for chunk in stream:
if chunk.choices:
choice = chunk.choices[0]
delta = choice.delta
# 处理工具调用增量
tool_call_delta = None
if hasattr(delta, 'tool_calls') and delta.tool_calls:
for tc_delta in delta.tool_calls:
tc_index = tc_delta.index
if tc_index not in tool_call_accumulator:
tool_call_accumulator[tc_index] = {
'id': tc_delta.id or '',
'name': '',
'arguments': '',
}
if tc_delta.id:
tool_call_accumulator[tc_index]['id'] = tc_delta.id
if tc_delta.function:
if tc_delta.function.name:
tool_call_accumulator[tc_index]['name'] = tc_delta.function.name
if tc_delta.function.arguments:
tool_call_accumulator[tc_index]['arguments'] += tc_delta.function.arguments
tool_call_delta = tool_call_accumulator[tc_index]
# 检查是否完成(包含工具调用)
tool_calls = None
if choice.finish_reason == 'tool_calls' or (choice.finish_reason and tool_call_accumulator):
tool_calls = self._parse_accumulated_tool_calls(tool_call_accumulator)
yield LLMStreamChunk(
content=delta.content or '',
is_finished=choice.finish_reason is not None,
finish_reason=choice.finish_reason or '',
tool_calls=tool_calls,
tool_call_delta=tool_call_delta,
)
# 最后一个 chunk 包含 usage 信息
if chunk.usage:
yield LLMStreamChunk(
content='',
is_finished=True,
prompt_tokens=chunk.usage.prompt_tokens,
completion_tokens=chunk.usage.completion_tokens,
total_tokens=chunk.usage.total_tokens,
)
def _parse_accumulated_tool_calls(self, accumulator: Dict) -> List[ToolCall]:
"""解析累积的工具调用"""
import json
result = []
for idx in sorted(accumulator.keys()):
tc = accumulator[idx]
try:
arguments = json.loads(tc['arguments']) if tc['arguments'] else {}
except json.JSONDecodeError:
arguments = {'raw': tc['arguments']}
result.append(ToolCall(
id=tc['id'],
name=tc['name'],
arguments=arguments,
))
return result
def chat_stream_sync(
self,
messages: List[LLMMessage],
config: LLMConfig,
):
"""同步流式对话"""
client = self._get_client()
# 构建请求参数
kwargs = {
'model': config.model,
'messages': [m.to_dict() for m in messages],
'temperature': config.temperature,
'top_p': config.top_p,
'max_tokens': config.max_tokens,
'stop': config.stop,
'presence_penalty': config.presence_penalty,
'frequency_penalty': config.frequency_penalty,
'stream': True,
'stream_options': {"include_usage": True},
**config.extra_params,
}
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_openai_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = 'required'
elif config.tool_choice == 'none':
kwargs['tool_choice'] = 'none'
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'function', 'function': {'name': config.tool_choice}}
else:
kwargs['tool_choice'] = 'auto'
stream = client.chat.completions.create(**kwargs)
# 用于累积工具调用
tool_call_accumulator = {}
for chunk in stream:
if chunk.choices:
choice = chunk.choices[0]
delta = choice.delta
# 处理工具调用增量
tool_call_delta = None
if hasattr(delta, 'tool_calls') and delta.tool_calls:
for tc_delta in delta.tool_calls:
tc_index = tc_delta.index
if tc_index not in tool_call_accumulator:
tool_call_accumulator[tc_index] = {
'id': tc_delta.id or '',
'name': '',
'arguments': '',
}
if tc_delta.id:
tool_call_accumulator[tc_index]['id'] = tc_delta.id
if tc_delta.function:
if tc_delta.function.name:
tool_call_accumulator[tc_index]['name'] = tc_delta.function.name
if tc_delta.function.arguments:
tool_call_accumulator[tc_index]['arguments'] += tc_delta.function.arguments
tool_call_delta = tool_call_accumulator[tc_index]
# 检查是否完成(包含工具调用)
tool_calls = None
if choice.finish_reason == 'tool_calls' or (choice.finish_reason and tool_call_accumulator):
tool_calls = self._parse_accumulated_tool_calls(tool_call_accumulator)
yield LLMStreamChunk(
content=delta.content or '',
is_finished=choice.finish_reason is not None,
finish_reason=choice.finish_reason or '',
tool_calls=tool_calls,
tool_call_delta=tool_call_delta,
)
# 最后一个 chunk 包含 usage 信息
if chunk.usage:
yield LLMStreamChunk(
content='',
is_finished=True,
prompt_tokens=chunk.usage.prompt_tokens,
completion_tokens=chunk.usage.completion_tokens,
total_tokens=chunk.usage.total_tokens,
)
# 模型类型推断关键词
_EMBEDDING_KEYWORDS = ('embed', 'bge', 'nomic-embed', 'mxbai-embed', 'jina-embedding')
_RERANK_KEYWORDS = ('rerank', 'reranker')
_SKIP_KEYWORDS = ('dall-e', 'tts', 'whisper', 'audio', 'moderation', 'davinci', 'babbage')
async def fetch_models_from_api(self) -> List[Dict[str, Any]]:
"""
通过 /v1/models API 在线拉取最新模型列表
适用于所有 OpenAI 兼容的提供商
"""
import httpx
base_url = (self.api_base or self.DEFAULT_API_BASE).rstrip('/')
headers = {}
if self.api_key:
headers['Authorization'] = f'Bearer {self.api_key}'
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(f'{base_url}/models', headers=headers)
resp.raise_for_status()
data = resp.json()
model_list = data.get('data', [])
if not model_list:
return []
results = []
for item in model_list:
model_id = item.get('id', '')
if not model_id:
continue
model_lower = model_id.lower()
# 跳过非文本模型
if any(kw in model_lower for kw in self._SKIP_KEYWORDS):
continue
# 推断模型类型
if any(kw in model_lower for kw in self._RERANK_KEYWORDS):
model_type = 'rerank'
elif any(kw in model_lower for kw in self._EMBEDDING_KEYWORDS):
model_type = 'embedding'
else:
model_type = 'chat'
results.append({
'model_name': model_id,
'display_name': model_id,
'model_type': model_type,
'max_tokens': 4096,
'context_window': 4096,
'supports_vision': False,
'supports_function_call': model_type == 'chat',
'input_price': 0,
'output_price': 0,
})
# 补充默认列表中的 embedding/rerank 模型(API 可能不返回这些类型)
fetched_names = {m['model_name'] for m in results}
for dm in self.__class__.get_default_models():
if dm['model_type'] in ('embedding', 'rerank') and dm['model_name'] not in fetched_names:
results.append(dm)
# 按类型排序:chat → embedding → rerank
type_order = {'chat': 0, 'embedding': 1, 'rerank': 2}
results.sort(key=lambda m: (type_order.get(m['model_type'], 9), m['model_name']))
logger.info(f'{base_url} 拉取到 {len(results)} 个模型')
return results
except Exception as e:
logger.warning(f'在线拉取模型列表失败 ({base_url}): {e}')
return []
@classmethod
def get_default_models(cls) -> List[Dict[str, Any]]:
"""获取默认模型列表"""
return [
# ---- Chat 模型 ----
{
'model_name': 'o4-mini',
'display_name': 'o4-mini',
'model_type': 'chat',
'max_tokens': 16384,
'context_window': 200000,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0.0011,
'output_price': 0.0044,
},
{
'model_name': 'o3-mini',
'display_name': 'o3-mini',
'model_type': 'chat',
'max_tokens': 16384,
'context_window': 200000,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0.0011,
'output_price': 0.0044,
},
{
'model_name': 'gpt-4.1',
'display_name': 'GPT-4.1',
'model_type': 'chat',
'max_tokens': 32768,
'context_window': 1047576,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0.002,
'output_price': 0.008,
},
{
'model_name': 'gpt-4.1-mini',
'display_name': 'GPT-4.1 Mini',
'model_type': 'chat',
'max_tokens': 32768,
'context_window': 1047576,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0.0004,
'output_price': 0.0016,
},
{
'model_name': 'gpt-4.1-nano',
'display_name': 'GPT-4.1 Nano',
'model_type': 'chat',
'max_tokens': 32768,
'context_window': 1047576,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0.0001,
'output_price': 0.0004,
},
{
'model_name': 'gpt-4o',
'display_name': 'GPT-4o',
'model_type': 'chat',
'max_tokens': 16384,
'context_window': 128000,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0.0025,
'output_price': 0.01,
},
{
'model_name': 'gpt-4o-mini',
'display_name': 'GPT-4o Mini',
'model_type': 'chat',
'max_tokens': 16384,
'context_window': 128000,
'supports_vision': True,
'supports_function_call': True,
'input_price': 0.00015,
'output_price': 0.0006,
},
# ---- Embedding 模型 ----
{
'model_name': 'text-embedding-3-small',
'display_name': 'Text Embedding 3 Small',
'model_type': 'embedding',
'max_tokens': 8191,
'context_window': 8191,
'input_price': 0.00002,
'output_price': 0,
},
{
'model_name': 'text-embedding-3-large',
'display_name': 'Text Embedding 3 Large',
'model_type': 'embedding',
'max_tokens': 8191,
'context_window': 8191,
'input_price': 0.00013,
'output_price': 0,
},
{
'model_name': 'text-embedding-ada-002',
'display_name': 'Text Embedding Ada 002',
'model_type': 'embedding',
'max_tokens': 8191,
'context_window': 8191,
'input_price': 0.0001,
'output_price': 0,
},
]
@@ -0,0 +1,555 @@
"""
阿里通义千问提供商适配器
"""
import logging
from typing import AsyncGenerator, Dict, List, Any
from .base import BaseLLMProvider, LLMConfig, LLMMessage, LLMResponse, LLMStreamChunk, ToolCall
from .registry import ProviderRegistry
logger = logging.getLogger(__name__)
@ProviderRegistry.register
class QwenProvider(BaseLLMProvider):
"""
阿里通义千问提供商适配器
使用 OpenAI 兼容接口
"""
provider_type = 'qwen'
provider_name = '通义千问'
supported_model_types = ['chat', 'embedding']
DEFAULT_API_BASE = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
def __init__(self, api_key: str = '', api_base: str = '', **kwargs):
super().__init__(api_key, api_base, **kwargs)
self.api_base = api_base or self.DEFAULT_API_BASE
self._client = None
self._async_client = None
def _get_client(self):
"""获取同步客户端"""
if self._client is None:
try:
from openai import OpenAI
self._client = OpenAI(
api_key=self.api_key,
base_url=self.api_base,
)
except ImportError:
raise ImportError('请安装 openai 库: pip install openai')
return self._client
def _get_async_client(self):
"""获取异步客户端"""
if self._async_client is None:
try:
from openai import AsyncOpenAI
self._async_client = AsyncOpenAI(
api_key=self.api_key,
base_url=self.api_base,
)
except ImportError:
raise ImportError('请安装 openai 库: pip install openai')
return self._async_client
def chat(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> LLMResponse:
"""同步对话"""
client = self._get_client()
# 构建请求参数
kwargs = {
'model': config.model,
'messages': [m.to_dict() for m in messages],
'temperature': config.temperature,
'top_p': config.top_p,
'max_tokens': config.max_tokens,
'stop': config.stop,
**config.extra_params,
}
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_openai_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = 'required'
elif config.tool_choice == 'none':
kwargs['tool_choice'] = 'none'
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'function', 'function': {'name': config.tool_choice}}
else:
kwargs['tool_choice'] = 'auto'
response = client.chat.completions.create(**kwargs)
choice = response.choices[0]
usage = response.usage
# 解析工具调用
tool_calls = None
if choice.message.tool_calls:
tool_calls = self._parse_tool_calls(choice.message.tool_calls)
return LLMResponse(
content=choice.message.content or '',
model=response.model,
prompt_tokens=usage.prompt_tokens if usage else 0,
completion_tokens=usage.completion_tokens if usage else 0,
total_tokens=usage.total_tokens if usage else 0,
finish_reason=choice.finish_reason or '',
raw_response=response.model_dump(),
tool_calls=tool_calls,
)
def _parse_tool_calls(self, tool_calls) -> List[ToolCall]:
"""解析工具调用"""
import json
result = []
for tc in tool_calls:
try:
arguments = json.loads(tc.function.arguments) if tc.function.arguments else {}
except json.JSONDecodeError:
arguments = {'raw': tc.function.arguments}
result.append(ToolCall(
id=tc.id,
name=tc.function.name,
arguments=arguments,
))
return result
async def chat_async(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> LLMResponse:
"""异步对话"""
client = self._get_async_client()
# 构建请求参数
kwargs = {
'model': config.model,
'messages': [m.to_dict() for m in messages],
'temperature': config.temperature,
'top_p': config.top_p,
'max_tokens': config.max_tokens,
'stop': config.stop,
**config.extra_params,
}
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_openai_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = 'required'
elif config.tool_choice == 'none':
kwargs['tool_choice'] = 'none'
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'function', 'function': {'name': config.tool_choice}}
else:
kwargs['tool_choice'] = 'auto'
response = await client.chat.completions.create(**kwargs)
choice = response.choices[0]
usage = response.usage
# 解析工具调用
tool_calls = None
if choice.message.tool_calls:
tool_calls = self._parse_tool_calls(choice.message.tool_calls)
return LLMResponse(
content=choice.message.content or '',
model=response.model,
prompt_tokens=usage.prompt_tokens if usage else 0,
completion_tokens=usage.completion_tokens if usage else 0,
total_tokens=usage.total_tokens if usage else 0,
finish_reason=choice.finish_reason or '',
raw_response=response.model_dump(),
tool_calls=tool_calls,
)
async def chat_stream(
self,
messages: List[LLMMessage],
config: LLMConfig,
) -> AsyncGenerator[LLMStreamChunk, None]:
"""流式对话"""
client = self._get_async_client()
# 构建请求参数
kwargs = {
'model': config.model,
'messages': [m.to_dict() for m in messages],
'temperature': config.temperature,
'top_p': config.top_p,
'max_tokens': config.max_tokens,
'stop': config.stop,
'stream': True,
'stream_options': {"include_usage": True},
**config.extra_params,
}
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_openai_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = 'required'
elif config.tool_choice == 'none':
kwargs['tool_choice'] = 'none'
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'function', 'function': {'name': config.tool_choice}}
else:
kwargs['tool_choice'] = 'auto'
stream = await client.chat.completions.create(**kwargs)
# 用于累积工具调用
tool_call_accumulator = {}
async for chunk in stream:
if chunk.choices:
choice = chunk.choices[0]
delta = choice.delta
# 处理工具调用增量
tool_call_delta = None
if hasattr(delta, 'tool_calls') and delta.tool_calls:
for tc_delta in delta.tool_calls:
tc_index = tc_delta.index
if tc_index not in tool_call_accumulator:
tool_call_accumulator[tc_index] = {
'id': tc_delta.id or '',
'name': '',
'arguments': '',
}
if tc_delta.id:
tool_call_accumulator[tc_index]['id'] = tc_delta.id
if tc_delta.function:
if tc_delta.function.name:
tool_call_accumulator[tc_index]['name'] = tc_delta.function.name
if tc_delta.function.arguments:
tool_call_accumulator[tc_index]['arguments'] += tc_delta.function.arguments
tool_call_delta = tool_call_accumulator[tc_index]
# 检查是否完成
tool_calls = None
if choice.finish_reason == 'tool_calls' or (choice.finish_reason and tool_call_accumulator):
tool_calls = self._parse_accumulated_tool_calls(tool_call_accumulator)
yield LLMStreamChunk(
content=delta.content or '',
is_finished=choice.finish_reason is not None,
finish_reason=choice.finish_reason or '',
tool_calls=tool_calls,
tool_call_delta=tool_call_delta,
)
if chunk.usage:
yield LLMStreamChunk(
content='',
is_finished=True,
prompt_tokens=chunk.usage.prompt_tokens,
completion_tokens=chunk.usage.completion_tokens,
total_tokens=chunk.usage.total_tokens,
)
def _parse_accumulated_tool_calls(self, accumulator: Dict) -> List[ToolCall]:
"""解析累积的工具调用"""
import json
result = []
for idx in sorted(accumulator.keys()):
tc = accumulator[idx]
try:
arguments = json.loads(tc['arguments']) if tc['arguments'] else {}
except json.JSONDecodeError:
arguments = {'raw': tc['arguments']}
result.append(ToolCall(
id=tc['id'],
name=tc['name'],
arguments=arguments,
))
return result
def chat_stream_sync(
self,
messages: List[LLMMessage],
config: LLMConfig,
):
"""同步流式对话"""
client = self._get_client()
# 构建请求参数
kwargs = {
'model': config.model,
'messages': [m.to_dict() for m in messages],
'temperature': config.temperature,
'top_p': config.top_p,
'max_tokens': config.max_tokens,
'stop': config.stop,
'stream': True,
'stream_options': {"include_usage": True},
**config.extra_params,
}
# 添加 Function Calling 参数
if config.tools:
kwargs['tools'] = [t.to_openai_format() for t in config.tools]
if config.tool_choice == 'required':
kwargs['tool_choice'] = 'required'
elif config.tool_choice == 'none':
kwargs['tool_choice'] = 'none'
elif config.tool_choice != 'auto':
kwargs['tool_choice'] = {'type': 'function', 'function': {'name': config.tool_choice}}
else:
kwargs['tool_choice'] = 'auto'
stream = client.chat.completions.create(**kwargs)
# 用于累积工具调用
tool_call_accumulator = {}
for chunk in stream:
if chunk.choices:
choice = chunk.choices[0]
delta = choice.delta
# 处理工具调用增量
tool_call_delta = None
if hasattr(delta, 'tool_calls') and delta.tool_calls:
for tc_delta in delta.tool_calls:
tc_index = tc_delta.index
if tc_index not in tool_call_accumulator:
tool_call_accumulator[tc_index] = {
'id': tc_delta.id or '',
'name': '',
'arguments': '',
}
if tc_delta.id:
tool_call_accumulator[tc_index]['id'] = tc_delta.id
if tc_delta.function:
if tc_delta.function.name:
tool_call_accumulator[tc_index]['name'] = tc_delta.function.name
if tc_delta.function.arguments:
tool_call_accumulator[tc_index]['arguments'] += tc_delta.function.arguments
tool_call_delta = tool_call_accumulator[tc_index]
# 检查是否完成
tool_calls = None
if choice.finish_reason == 'tool_calls' or (choice.finish_reason and tool_call_accumulator):
tool_calls = self._parse_accumulated_tool_calls(tool_call_accumulator)
yield LLMStreamChunk(
content=delta.content or '',
is_finished=choice.finish_reason is not None,
finish_reason=choice.finish_reason or '',
tool_calls=tool_calls,
tool_call_delta=tool_call_delta,
)
if chunk.usage:
yield LLMStreamChunk(
content='',
is_finished=True,
prompt_tokens=chunk.usage.prompt_tokens,
completion_tokens=chunk.usage.completion_tokens,
total_tokens=chunk.usage.total_tokens,
)
_EMBEDDING_KEYWORDS = ('embed', 'bge')
_RERANK_KEYWORDS = ('rerank',)
_SKIP_KEYWORDS = (
'paraformer', 'sambert', 'wordart', 'wanx', 'cosyvoice',
'qwen-vl-ocr', 'tts', 'asr', 'realtime',
'tongyi-xiaomi', 'flux', 'stable-diffusion',
'qwen-image-', 'z-image-', 'gui-plus',
'qwen-mt-', 'livetranslate', 'deep-search',
'flash-character',
)
async def fetch_models_from_api(self) -> List[Dict[str, Any]]:
"""通过 DashScope 兼容的 /v1/models 端点在线拉取模型列表"""
import httpx
base_url = (self.api_base or self.DEFAULT_API_BASE).rstrip('/')
headers = {}
if self.api_key:
headers['Authorization'] = f'Bearer {self.api_key}'
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(f'{base_url}/models', headers=headers)
resp.raise_for_status()
data = resp.json()
model_list = data.get('data', [])
if not model_list:
return []
results = []
for item in model_list:
model_id = item.get('id', '')
if not model_id:
continue
model_lower = model_id.lower()
if any(kw in model_lower for kw in self._SKIP_KEYWORDS):
continue
if any(kw in model_lower for kw in self._RERANK_KEYWORDS):
model_type = 'rerank'
elif any(kw in model_lower for kw in self._EMBEDDING_KEYWORDS):
model_type = 'embedding'
else:
model_type = 'chat'
results.append({
'model_name': model_id,
'display_name': model_id,
'model_type': model_type,
'max_tokens': 4096,
'context_window': 4096,
'supports_vision': False,
'supports_function_call': model_type == 'chat',
'input_price': 0,
'output_price': 0,
})
# DashScope /v1/models 可能只返回 chat 模型,
# 将默认列表中的 embedding/rerank 模型合并进去
fetched_names = {m['model_name'] for m in results}
for dm in self.__class__.get_default_models():
if dm['model_type'] in ('embedding', 'rerank') and dm['model_name'] not in fetched_names:
results.append(dm)
type_order = {'chat': 0, 'embedding': 1, 'rerank': 2}
results.sort(key=lambda m: (type_order.get(m['model_type'], 9), m['model_name']))
logger.info(f'{base_url} 拉取到 {len(results)} 个模型(含补充的 embedding/rerank')
return results
except Exception as e:
logger.warning(f'在线拉取模型列表失败 ({base_url}): {e}')
return []
@classmethod
def get_default_models(cls) -> List[Dict[str, Any]]:
"""获取默认模型列表"""
return [
# ---- Chat 模型 ----
{
'model_name': 'qwen-max',
'display_name': '通义千问 Max',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 32768,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0.02,
'output_price': 0.06,
},
{
'model_name': 'qwen-plus',
'display_name': '通义千问 Plus',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 131072,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0.0008,
'output_price': 0.002,
},
{
'model_name': 'qwen-turbo',
'display_name': '通义千问 Turbo',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 131072,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0.0003,
'output_price': 0.0006,
},
{
'model_name': 'qwen3-235b-a22b',
'display_name': 'Qwen3 235B-A22B',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 131072,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0.004,
'output_price': 0.012,
},
{
'model_name': 'qwen3-32b',
'display_name': 'Qwen3 32B',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 131072,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0.002,
'output_price': 0.006,
},
{
'model_name': 'qwen-vl-max',
'display_name': '通义千问 VL Max',
'model_type': 'chat',
'max_tokens': 2048,
'context_window': 32768,
'supports_vision': True,
'supports_function_call': False,
'input_price': 0.02,
'output_price': 0.06,
},
{
'model_name': 'qwen-vl-plus',
'display_name': '通义千问 VL Plus',
'model_type': 'chat',
'max_tokens': 2048,
'context_window': 32768,
'supports_vision': True,
'supports_function_call': False,
'input_price': 0.008,
'output_price': 0.02,
},
# ---- Embedding 模型 ----
{
'model_name': 'text-embedding-v3',
'display_name': '通义文本向量 V3',
'model_type': 'embedding',
'max_tokens': 8192,
'context_window': 8192,
'input_price': 0.0007,
'output_price': 0,
},
{
'model_name': 'text-embedding-v2',
'display_name': '通义文本向量 V2',
'model_type': 'embedding',
'max_tokens': 2048,
'context_window': 2048,
'input_price': 0.0007,
'output_price': 0,
},
# ---- Rerank 模型 ----
{
'model_name': 'gte-rerank',
'display_name': 'GTE Rerank',
'model_type': 'rerank',
'max_tokens': 4096,
'context_window': 4096,
'input_price': 0.001,
'output_price': 0,
},
]
@@ -0,0 +1,152 @@
"""
提供商注册中心
"""
import logging
from typing import Dict, List, Optional, Type
from .base import BaseLLMProvider
logger = logging.getLogger(__name__)
class ProviderRegistry:
"""
提供商注册中心
管理所有 LLM 提供商适配器的注册和获取
"""
_providers: Dict[str, Type[BaseLLMProvider]] = {}
@classmethod
def register(cls, provider_class: Type[BaseLLMProvider]) -> Type[BaseLLMProvider]:
"""
注册提供商(可作为装饰器使用)
Args:
provider_class: 提供商类
Returns:
提供商类
"""
provider_type = provider_class.provider_type
if not provider_type:
raise ValueError(f'Provider {provider_class.__name__} must have a provider_type')
cls._providers[provider_type] = provider_class
logger.info(f'Registered LLM provider: {provider_type}')
return provider_class
@classmethod
def get(cls, provider_type: str) -> Optional[Type[BaseLLMProvider]]:
"""
获取提供商类
Args:
provider_type: 提供商类型
Returns:
提供商类或 None
"""
return cls._providers.get(provider_type)
@classmethod
def create_instance(
cls,
provider_type: str,
api_key: str = '',
api_base: str = '',
**kwargs
) -> Optional[BaseLLMProvider]:
"""
创建提供商实例
Args:
provider_type: 提供商类型
api_key: API Key
api_base: API 地址
**kwargs: 其他配置
Returns:
提供商实例或 None
"""
provider_class = cls.get(provider_type)
if not provider_class:
logger.warning(f'Unknown provider type: {provider_type}')
return None
return provider_class(api_key=api_key, api_base=api_base, **kwargs)
@classmethod
def get_all_types(cls) -> List[Dict[str, str]]:
"""
获取所有已注册的提供商类型
Returns:
提供商类型列表
"""
return [
{
'type': provider_type,
'name': provider_class.provider_name,
}
for provider_type, provider_class in cls._providers.items()
]
@classmethod
def get_default_models(cls, provider_type: str) -> List[Dict]:
"""
获取提供商的默认模型列表
Args:
provider_type: 提供商类型
Returns:
默认模型列表
"""
provider_class = cls.get(provider_type)
if not provider_class:
return []
return provider_class.get_default_models()
@classmethod
async def fetch_models_from_api(
cls,
provider_type: str,
api_key: str = '',
api_base: str = '',
**kwargs
) -> List[Dict]:
"""
通过 API 在线拉取提供商的最新模型列表
Args:
provider_type: 提供商类型
api_key: API Key
api_base: API 地址
Returns:
模型列表,失败时返回空列表
"""
instance = cls.create_instance(provider_type, api_key=api_key, api_base=api_base, **kwargs)
if not instance:
return []
return await instance.fetch_models_from_api()
# 自动加载所有提供商
def _load_providers():
"""加载所有提供商适配器"""
from . import openai_provider
from . import claude_provider
from . import qwen_provider
from . import ollama_provider
from . import deepseek_provider
from . import siliconflow_provider
# 延迟加载
try:
_load_providers()
except ImportError as e:
logger.warning(f'Failed to load some providers: {e}')
@@ -0,0 +1,147 @@
"""
硅基流动 (SiliconFlow) 提供商适配器
兼容 OpenAI API,继承 OpenAI 适配器
"""
import logging
from typing import List, Dict, Any
from .openai_provider import OpenAIProvider
from .registry import ProviderRegistry
logger = logging.getLogger(__name__)
@ProviderRegistry.register
class SiliconFlowProvider(OpenAIProvider):
"""
硅基流动 (SiliconFlow) 提供商适配器
兼容 OpenAI API 格式,支持多种开源模型、Embedding 和 Rerank
"""
provider_type = 'siliconflow'
provider_name = 'SiliconFlow'
supported_model_types = ['chat', 'embedding', 'rerank']
DEFAULT_API_BASE = 'https://api.siliconflow.cn/v1'
@classmethod
def get_default_models(cls) -> List[Dict[str, Any]]:
"""获取默认模型列表"""
return [
# ---- Chat 模型 ----
{
'model_name': 'Qwen/Qwen3-235B-A22B',
'display_name': 'Qwen3 235B-A22B',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 131072,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0.004,
'output_price': 0.012,
},
{
'model_name': 'Qwen/Qwen3-32B',
'display_name': 'Qwen3 32B',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 131072,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0.002,
'output_price': 0.006,
},
{
'model_name': 'Qwen/Qwen3-8B',
'display_name': 'Qwen3 8B',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 131072,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0,
'output_price': 0,
},
{
'model_name': 'deepseek-ai/DeepSeek-V3',
'display_name': 'DeepSeek V3',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 65536,
'supports_vision': False,
'supports_function_call': True,
'input_price': 0.002,
'output_price': 0.008,
},
{
'model_name': 'deepseek-ai/DeepSeek-R1',
'display_name': 'DeepSeek R1',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 65536,
'supports_vision': False,
'supports_function_call': False,
'input_price': 0.004,
'output_price': 0.016,
},
{
'model_name': 'Pro/deepseek-ai/DeepSeek-R1',
'display_name': 'DeepSeek R1 (Pro)',
'model_type': 'chat',
'max_tokens': 8192,
'context_window': 65536,
'supports_vision': False,
'supports_function_call': False,
'input_price': 0.008,
'output_price': 0.032,
},
# ---- Embedding 模型 ----
{
'model_name': 'BAAI/bge-m3',
'display_name': 'BGE-M3',
'model_type': 'embedding',
'max_tokens': 8192,
'context_window': 8192,
'input_price': 0,
'output_price': 0,
},
{
'model_name': 'BAAI/bge-large-zh-v1.5',
'display_name': 'BGE Large ZH v1.5',
'model_type': 'embedding',
'max_tokens': 512,
'context_window': 512,
'input_price': 0,
'output_price': 0,
},
{
'model_name': 'jinaai/jina-embeddings-v3',
'display_name': 'Jina Embeddings V3',
'model_type': 'embedding',
'max_tokens': 8192,
'context_window': 8192,
'input_price': 0,
'output_price': 0,
},
# ---- Rerank 模型 ----
{
'model_name': 'BAAI/bge-reranker-v2-m3',
'display_name': 'BGE Reranker V2 M3',
'model_type': 'rerank',
'max_tokens': 8192,
'context_window': 8192,
'input_price': 0,
'output_price': 0,
},
{
'model_name': 'jinaai/jina-reranker-v2-base-multilingual',
'display_name': 'Jina Reranker V2 Multilingual',
'model_type': 'rerank',
'max_tokens': 8192,
'context_window': 8192,
'input_price': 0,
'output_price': 0,
},
]