2024-07-07 01:58:23 +08:00
|
|
|
|
import os
|
2025-09-18 20:34:10 +08:00
|
|
|
|
import traceback
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2026-01-28 11:30:55 +08:00
|
|
|
|
from openai import AsyncOpenAI
|
2025-09-20 22:53:37 +08:00
|
|
|
|
from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_exponential
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2026-03-17 10:16:44 +08:00
|
|
|
|
from yuxi import config
|
2026-04-26 20:59:40 +08:00
|
|
|
|
from yuxi.services.model_cache import is_v2_spec_format
|
2026-03-17 10:16:44 +08:00
|
|
|
|
from yuxi.utils import logger
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2024-07-07 01:58:23 +08:00
|
|
|
|
|
2025-10-14 02:27:15 +08:00
|
|
|
|
def split_model_spec(model_spec, sep="/"):
|
|
|
|
|
|
"""
|
|
|
|
|
|
将 provider/model 形式的字符串拆分为 (provider, model)
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not model_spec or not isinstance(model_spec, str):
|
|
|
|
|
|
return "", ""
|
|
|
|
|
|
if not sep:
|
|
|
|
|
|
return model_spec, ""
|
|
|
|
|
|
try:
|
|
|
|
|
|
provider, model_name = model_spec.split(sep, 1)
|
|
|
|
|
|
return provider, model_name
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return model_spec, ""
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-05-24 11:29:45 +08:00
|
|
|
|
class OpenAIBase:
|
2025-07-02 02:38:36 +08:00
|
|
|
|
def __init__(self, api_key, base_url, model_name, **kwargs):
|
2025-03-10 16:33:40 +08:00
|
|
|
|
self.api_key = api_key
|
|
|
|
|
|
self.base_url = base_url
|
2026-01-28 11:30:55 +08:00
|
|
|
|
self.client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
2024-07-07 01:58:23 +08:00
|
|
|
|
self.model_name = model_name
|
2025-04-09 11:47:08 +08:00
|
|
|
|
self.info = kwargs
|
2024-07-07 01:58:23 +08:00
|
|
|
|
|
2025-09-19 01:10:20 +08:00
|
|
|
|
@retry(
|
|
|
|
|
|
stop=stop_after_attempt(3),
|
|
|
|
|
|
wait=wait_exponential(multiplier=1, min=1, max=10),
|
|
|
|
|
|
retry=retry_if_exception_type((Exception,)),
|
|
|
|
|
|
before_sleep=before_sleep_log(logger, log_level="WARNING"),
|
2025-09-19 01:32:07 +08:00
|
|
|
|
reraise=True,
|
2025-09-19 01:10:20 +08:00
|
|
|
|
)
|
2026-01-28 11:30:55 +08:00
|
|
|
|
async def call(self, message, stream=False):
|
2024-07-07 01:58:23 +08:00
|
|
|
|
if isinstance(message, str):
|
2025-09-01 22:37:03 +08:00
|
|
|
|
messages = [{"role": "user", "content": message}]
|
2024-07-07 01:58:23 +08:00
|
|
|
|
else:
|
|
|
|
|
|
messages = message
|
|
|
|
|
|
|
2025-05-09 14:48:32 +08:00
|
|
|
|
try:
|
2025-09-19 01:10:20 +08:00
|
|
|
|
if stream:
|
|
|
|
|
|
response = self._stream_response(messages)
|
|
|
|
|
|
else:
|
2026-01-28 11:30:55 +08:00
|
|
|
|
response = await self._get_response(messages)
|
2025-05-09 14:48:32 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-09-02 01:08:42 +08:00
|
|
|
|
err = (
|
|
|
|
|
|
f"Error streaming response: {e}, URL: {self.base_url}, "
|
|
|
|
|
|
f"API Key: {self.api_key[:5]}***, Model: {self.model_name}"
|
|
|
|
|
|
)
|
2025-05-09 14:48:32 +08:00
|
|
|
|
logger.error(err)
|
|
|
|
|
|
raise Exception(err)
|
2024-07-07 01:58:23 +08:00
|
|
|
|
|
2025-09-19 01:10:20 +08:00
|
|
|
|
return response
|
|
|
|
|
|
|
2026-01-28 11:30:55 +08:00
|
|
|
|
async def _stream_response(self, messages):
|
|
|
|
|
|
response = await self.client.chat.completions.create(
|
2025-09-19 01:10:20 +08:00
|
|
|
|
model=self.model_name,
|
|
|
|
|
|
messages=messages,
|
|
|
|
|
|
stream=True,
|
|
|
|
|
|
)
|
2026-01-28 11:30:55 +08:00
|
|
|
|
async for chunk in response:
|
2025-09-19 01:10:20 +08:00
|
|
|
|
if len(chunk.choices) > 0:
|
|
|
|
|
|
yield chunk.choices[0].delta
|
|
|
|
|
|
|
2026-01-28 11:30:55 +08:00
|
|
|
|
async def _get_response(self, messages):
|
|
|
|
|
|
response = await self.client.chat.completions.create(
|
2024-07-07 01:58:23 +08:00
|
|
|
|
model=self.model_name,
|
|
|
|
|
|
messages=messages,
|
|
|
|
|
|
stream=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
return response.choices[0].message
|
2025-03-24 19:07:51 +08:00
|
|
|
|
|
2026-01-28 11:30:55 +08:00
|
|
|
|
async def get_models(self):
|
2025-03-10 16:33:40 +08:00
|
|
|
|
try:
|
2026-01-28 11:30:55 +08:00
|
|
|
|
return await self.client.models.list(extra_query={"type": "text"})
|
2025-03-10 16:33:40 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Error getting models: {e}")
|
|
|
|
|
|
return []
|
2024-07-07 01:58:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
2024-09-09 17:07:03 +08:00
|
|
|
|
class OpenModel(OpenAIBase):
|
|
|
|
|
|
def __init__(self, model_name=None):
|
|
|
|
|
|
model_name = model_name or "gpt-4o-mini"
|
|
|
|
|
|
api_key = os.getenv("OPENAI_API_KEY")
|
|
|
|
|
|
base_url = os.getenv("OPENAI_API_BASE")
|
|
|
|
|
|
super().__init__(api_key=api_key, base_url=base_url, model_name=model_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
2024-07-31 20:22:05 +08:00
|
|
|
|
class GeneralResponse:
|
2024-07-07 17:21:07 +08:00
|
|
|
|
def __init__(self, content):
|
|
|
|
|
|
self.content = content
|
2024-07-31 20:22:05 +08:00
|
|
|
|
self.is_full = False
|
2024-07-07 17:21:07 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2026-04-25 23:58:01 +08:00
|
|
|
|
def select_model_v2(spec: str) -> OpenAIBase:
|
|
|
|
|
|
"""根据 v2 spec(provider_id:model_id)选择聊天模型。
|
|
|
|
|
|
|
2026-04-26 21:04:57 +08:00
|
|
|
|
v2 spec 格式使用冒号分隔,如: siliconflow-cn:deepseek-ai/DeepSeek-V4-Flash
|
2026-04-25 23:58:01 +08:00
|
|
|
|
数据来源为数据库中的 model_providers 表,通过全局缓存访问。
|
|
|
|
|
|
"""
|
|
|
|
|
|
from yuxi.services.model_cache import model_cache
|
|
|
|
|
|
|
|
|
|
|
|
info = model_cache.get_model_info(spec)
|
|
|
|
|
|
if not info:
|
|
|
|
|
|
raise ValueError(f"Unknown v2 model spec: {spec}")
|
|
|
|
|
|
|
|
|
|
|
|
if info.model_type != "chat":
|
|
|
|
|
|
raise ValueError(f"Model {spec} is not a chat model (type={info.model_type})")
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"Selecting v2 model: {spec} (provider_type={info.provider_type})")
|
|
|
|
|
|
|
|
|
|
|
|
return OpenAIBase(
|
|
|
|
|
|
api_key=info.api_key,
|
|
|
|
|
|
base_url=info.base_url,
|
|
|
|
|
|
model_name=info.model_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-14 02:27:15 +08:00
|
|
|
|
def select_model(model_provider=None, model_name=None, model_spec=None):
|
2025-09-18 20:34:10 +08:00
|
|
|
|
"""根据模型提供者选择模型"""
|
2026-04-26 20:59:40 +08:00
|
|
|
|
if model_spec and is_v2_spec_format(model_spec):
|
2026-04-25 23:58:01 +08:00
|
|
|
|
from yuxi.services.model_cache import model_cache
|
|
|
|
|
|
|
|
|
|
|
|
if model_cache.is_v2_spec(model_spec):
|
|
|
|
|
|
return select_model_v2(model_spec)
|
|
|
|
|
|
|
2026-04-26 20:59:40 +08:00
|
|
|
|
available = model_cache.get_all_specs("chat")
|
|
|
|
|
|
available_ids = [s.spec for s in available[:10]]
|
|
|
|
|
|
raise ValueError(f"未找到 V2 模型: '{model_spec}'。可用聊天模型 ({len(available)}): {available_ids}")
|
|
|
|
|
|
|
2026-04-26 16:24:54 +08:00
|
|
|
|
logger.warning(
|
|
|
|
|
|
f"旧版本的模型选择逻辑已废弃,建议尽快迁移至新的模型配置;"
|
|
|
|
|
|
f"当前模型选择参数: provider={model_provider}, model_name={model_name}, spec={model_spec}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-10-14 02:27:15 +08:00
|
|
|
|
if model_spec:
|
|
|
|
|
|
spec_provider, spec_model_name = split_model_spec(model_spec)
|
|
|
|
|
|
model_provider = model_provider or spec_provider
|
|
|
|
|
|
model_name = model_name or spec_model_name
|
|
|
|
|
|
|
|
|
|
|
|
if model_provider is None or not model_name:
|
|
|
|
|
|
default_provider, default_model = split_model_spec(getattr(config, "default_model", ""))
|
|
|
|
|
|
model_provider = model_provider or default_provider
|
|
|
|
|
|
model_name = model_name or default_model
|
|
|
|
|
|
|
|
|
|
|
|
assert model_provider, "Model provider not specified"
|
|
|
|
|
|
|
2025-10-22 11:51:32 +08:00
|
|
|
|
model_info = config.model_names.get(model_provider)
|
|
|
|
|
|
if not model_info:
|
|
|
|
|
|
raise ValueError(f"Unknown model provider: {model_provider}")
|
|
|
|
|
|
|
|
|
|
|
|
model_name = model_name or model_info.default
|
2025-09-18 20:34:10 +08:00
|
|
|
|
|
2025-10-14 02:27:15 +08:00
|
|
|
|
if not model_name:
|
|
|
|
|
|
raise ValueError(f"Model name not specified for provider {model_provider}")
|
|
|
|
|
|
|
2025-09-18 20:34:10 +08:00
|
|
|
|
logger.info(f"Selecting model from `{model_provider}` with `{model_name}`")
|
|
|
|
|
|
|
|
|
|
|
|
if model_provider == "openai":
|
|
|
|
|
|
return OpenModel(model_name)
|
|
|
|
|
|
|
|
|
|
|
|
# 其他模型,默认使用OpenAIBase
|
|
|
|
|
|
try:
|
|
|
|
|
|
model = OpenAIBase(
|
2025-11-16 21:24:26 +08:00
|
|
|
|
api_key=os.environ.get(model_info.env, model_info.env),
|
2025-10-22 11:51:32 +08:00
|
|
|
|
base_url=model_info.base_url,
|
2025-09-18 20:34:10 +08:00
|
|
|
|
model_name=model_name,
|
|
|
|
|
|
)
|
|
|
|
|
|
return model
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise ValueError(f"Model provider {model_provider} load failed, {e} \n {traceback.format_exc()}")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 23:58:01 +08:00
|
|
|
|
async def test_chat_model_status_by_spec(spec: str) -> dict:
|
|
|
|
|
|
"""根据 full spec 测试聊天模型状态(自动识别 V1/V2)。
|
2025-09-22 22:02:57 +08:00
|
|
|
|
|
2026-04-25 23:58:01 +08:00
|
|
|
|
V1 spec 格式: provider/model_name(斜杠分隔)
|
|
|
|
|
|
V2 spec 格式: provider_id:model_id(冒号分隔)
|
2025-09-22 22:02:57 +08:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2026-04-25 23:58:01 +08:00
|
|
|
|
logger.debug(f"Testing model status by spec: {spec}")
|
|
|
|
|
|
model = select_model(model_spec=spec)
|
2025-09-22 22:02:57 +08:00
|
|
|
|
|
|
|
|
|
|
test_messages = [{"role": "user", "content": "Say 1"}]
|
2026-01-28 11:30:55 +08:00
|
|
|
|
response = await model.call(test_messages, stream=False)
|
2025-09-22 22:02:57 +08:00
|
|
|
|
|
|
|
|
|
|
if response and response.content:
|
2026-04-25 23:58:01 +08:00
|
|
|
|
return {"spec": spec, "status": "available", "message": "连接正常"}
|
2025-09-22 22:02:57 +08:00
|
|
|
|
else:
|
2026-04-25 23:58:01 +08:00
|
|
|
|
return {"spec": spec, "status": "unavailable", "message": "响应无效"}
|
2025-09-22 22:02:57 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-04-25 23:58:01 +08:00
|
|
|
|
logger.error(f"测试模型状态失败 {spec}: {e}")
|
|
|
|
|
|
return {"spec": spec, "status": "error", "message": str(e)}
|
2025-09-22 22:02:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
2024-07-31 20:22:05 +08:00
|
|
|
|
if __name__ == "__main__":
|
2025-05-24 11:29:45 +08:00
|
|
|
|
pass
|