fix: report provider test upstream errors
This commit is contained in:
@@ -348,6 +348,14 @@ class BaseLLMProvider(ABC):
|
||||
模型列表,格式与 get_default_models 一致
|
||||
"""
|
||||
return []
|
||||
|
||||
async def fetch_models_from_api_strict(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
严格拉取模型列表,用于连接测试。
|
||||
|
||||
子类可在这里保留上游异常,让调用方给出明确错误原因。
|
||||
"""
|
||||
return await self.fetch_models_from_api()
|
||||
|
||||
@classmethod
|
||||
def get_default_models(cls) -> List[Dict[str, Any]]:
|
||||
|
||||
@@ -409,6 +409,15 @@ class ClaudeProvider(BaseLLMProvider):
|
||||
|
||||
async def fetch_models_from_api(self) -> List[Dict[str, Any]]:
|
||||
"""通过 Anthropic /v1/models 端点在线拉取模型列表"""
|
||||
base_url = (self.api_base or self.DEFAULT_API_BASE).rstrip('/')
|
||||
try:
|
||||
return await self.fetch_models_from_api_strict()
|
||||
except Exception as e:
|
||||
logger.warning(f'在线拉取 Anthropic 模型列表失败 ({base_url}): {e}')
|
||||
return []
|
||||
|
||||
async def fetch_models_from_api_strict(self) -> List[Dict[str, Any]]:
|
||||
"""通过 Anthropic 端点严格拉取模型列表,失败时保留上游异常"""
|
||||
import httpx
|
||||
|
||||
base_url = (self.api_base or self.DEFAULT_API_BASE).rstrip('/')
|
||||
@@ -417,43 +426,38 @@ class ClaudeProvider(BaseLLMProvider):
|
||||
'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()
|
||||
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}')
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def get_default_models(cls) -> List[Dict[str, Any]]:
|
||||
"""获取默认模型列表"""
|
||||
|
||||
@@ -376,6 +376,19 @@ class OllamaProvider(BaseLLMProvider):
|
||||
|
||||
async def fetch_models_from_api(self) -> List[Dict[str, Any]]:
|
||||
"""通过 /api/tags 端点拉取 Ollama 本地已安装的模型"""
|
||||
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:
|
||||
return await self.fetch_models_from_api_strict()
|
||||
except Exception as e:
|
||||
logger.warning(f'在线拉取 Ollama 模型列表失败 ({base_url}): {e}')
|
||||
return []
|
||||
|
||||
async def fetch_models_from_api_strict(self) -> List[Dict[str, Any]]:
|
||||
"""通过 Ollama /api/tags 严格拉取模型列表,失败时保留上游异常"""
|
||||
import httpx
|
||||
|
||||
base_url = (self.api_base or 'http://localhost:11434').rstrip('/')
|
||||
@@ -383,50 +396,45 @@ class OllamaProvider(BaseLLMProvider):
|
||||
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()
|
||||
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
|
||||
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'
|
||||
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
|
||||
# 从 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,
|
||||
})
|
||||
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 []
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def get_default_models(cls) -> List[Dict[str, Any]]:
|
||||
|
||||
@@ -387,6 +387,15 @@ class OpenAIProvider(BaseLLMProvider):
|
||||
|
||||
适用于所有 OpenAI 兼容的提供商
|
||||
"""
|
||||
base_url = (self.api_base or self.DEFAULT_API_BASE).rstrip('/')
|
||||
try:
|
||||
return await self.fetch_models_from_api_strict()
|
||||
except Exception as e:
|
||||
logger.warning(f'在线拉取模型列表失败 ({base_url}): {e}')
|
||||
return []
|
||||
|
||||
async def fetch_models_from_api_strict(self) -> List[Dict[str, Any]]:
|
||||
"""通过 /models 端点严格拉取模型列表,失败时保留上游异常"""
|
||||
import httpx
|
||||
|
||||
base_url = (self.api_base or self.DEFAULT_API_BASE).rstrip('/')
|
||||
@@ -394,64 +403,59 @@ class OpenAIProvider(BaseLLMProvider):
|
||||
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()
|
||||
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}')
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def get_default_models(cls) -> List[Dict[str, Any]]:
|
||||
"""获取默认模型列表"""
|
||||
|
||||
@@ -378,6 +378,15 @@ class QwenProvider(BaseLLMProvider):
|
||||
|
||||
async def fetch_models_from_api(self) -> List[Dict[str, Any]]:
|
||||
"""通过 DashScope 兼容的 /v1/models 端点在线拉取模型列表"""
|
||||
base_url = (self.api_base or self.DEFAULT_API_BASE).rstrip('/')
|
||||
try:
|
||||
return await self.fetch_models_from_api_strict()
|
||||
except Exception as e:
|
||||
logger.warning(f'在线拉取模型列表失败 ({base_url}): {e}')
|
||||
return []
|
||||
|
||||
async def fetch_models_from_api_strict(self) -> List[Dict[str, Any]]:
|
||||
"""通过 DashScope 兼容端点严格拉取模型列表,失败时保留上游异常"""
|
||||
import httpx
|
||||
|
||||
base_url = (self.api_base or self.DEFAULT_API_BASE).rstrip('/')
|
||||
@@ -385,62 +394,57 @@ class QwenProvider(BaseLLMProvider):
|
||||
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()
|
||||
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}')
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def get_default_models(cls) -> List[Dict[str, Any]]:
|
||||
"""获取默认模型列表"""
|
||||
|
||||
Reference in New Issue
Block a user