diff --git a/zhenxun/builtin_plugins/llm_manager/__init__.py b/zhenxun/builtin_plugins/llm_manager/__init__.py index de0e0caf..d48893fc 100644 --- a/zhenxun/builtin_plugins/llm_manager/__init__.py +++ b/zhenxun/builtin_plugins/llm_manager/__init__.py @@ -1,3 +1,5 @@ +from collections import defaultdict + from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import ( @@ -58,7 +60,12 @@ __plugin_meta__ = PluginMetadata( llm_cmd = on_alconna( Alconna( "llm", - Subcommand("list", alias=["ls"], help_text="查看模型列表"), + Subcommand( + "list", + Option("--text", action=store_true, help_text="以纯文本格式输出模型列表"), + alias=["ls"], + help_text="查看模型列表", + ), Subcommand("info", Args["model_name", str], help_text="查看模型详情"), Subcommand("default", Args["model_name?", str], help_text="查看或设置默认模型"), Subcommand( @@ -80,13 +87,36 @@ llm_cmd = on_alconna( @llm_cmd.assign("list") -async def handle_list(arp: Arparma, show_all: Query[bool] = Query("all")): +async def handle_list( + arp: Arparma, + show_all: Query[bool] = Query("all"), + text_mode: Query[bool] = Query("list.text.value", False), +): """处理 'llm list' 命令""" logger.info("获取LLM模型列表", command="LLM Manage", session=arp.header_result) models = await DataSource.get_model_list(show_all=show_all.result) - image = await Presenters.format_model_list_as_image(models, show_all.result) - await llm_cmd.finish(MessageUtils.build_message(image)) + if text_mode.result: + if not models: + await llm_cmd.finish("当前没有配置任何LLM模型。") + + grouped_models = defaultdict(list) + for model in models: + grouped_models[model["provider_name"]].append(model) + + response_parts = ["可用的LLM模型列表:"] + for provider, model_list in grouped_models.items(): + response_parts.append(f"\n{provider}:") + for model in model_list: + response_parts.append( + f" {model['provider_name']}/{model['model_name']}" + ) + + response_text = "\n".join(response_parts) + await llm_cmd.finish(response_text) + else: + image = await Presenters.format_model_list_as_image(models, show_all.result) + await llm_cmd.finish(MessageUtils.build_message(image)) @llm_cmd.assign("info") @@ -114,7 +144,7 @@ async def handle_default(arp: Arparma, model_name: Match[str]): command="LLM Manage", session=arp.header_result, ) - success, message = await DataSource.set_default_model(model_name.result) + _success, message = await DataSource.set_default_model(model_name.result) await llm_cmd.finish(message) else: logger.info("查看默认模型", command="LLM Manage", session=arp.header_result) @@ -132,7 +162,7 @@ async def handle_test(arp: Arparma, model_name: Match[str]): ) await llm_cmd.send(f"正在测试模型 '{model_name.result}',请稍候...") - success, message = await DataSource.test_model_connectivity(model_name.result) + _success, message = await DataSource.test_model_connectivity(model_name.result) await llm_cmd.finish(message) @@ -167,5 +197,5 @@ async def handle_reset_key( ) logger.info(log_msg, command="LLM Manage", session=arp.header_result) - success, message = await DataSource.reset_key(provider_name.result, key_to_reset) + _success, message = await DataSource.reset_key(provider_name.result, key_to_reset) await llm_cmd.finish(message) diff --git a/zhenxun/services/llm/config/providers.py b/zhenxun/services/llm/config/providers.py index 61f6189c..f3895481 100644 --- a/zhenxun/services/llm/config/providers.py +++ b/zhenxun/services/llm/config/providers.py @@ -192,10 +192,20 @@ def get_default_providers() -> list[dict[str, Any]]: "api_base": "https://generativelanguage.googleapis.com", "api_type": "gemini", "models": [ - {"model_name": "gemini-2.0-flash"}, {"model_name": "gemini-2.5-flash"}, {"model_name": "gemini-2.5-pro"}, - {"model_name": "gemini-2.5-flash-lite-preview-06-17"}, + {"model_name": "gemini-2.5-flash-lite"}, + ], + }, + { + "name": "OpenRouter", + "api_key": "YOUR_OPENROUTER_API_KEY", + "api_base": "https://openrouter.ai/api", + "api_type": "openrouter", + "models": [ + {"model_name": "google/gemini-2.5-pro"}, + {"model_name": "google/gemini-2.5-flash"}, + {"model_name": "x-ai/grok-4"}, ], }, ] diff --git a/zhenxun/services/llm/types/capabilities.py b/zhenxun/services/llm/types/capabilities.py index 51a7d8d1..2e083708 100644 --- a/zhenxun/services/llm/types/capabilities.py +++ b/zhenxun/services/llm/types/capabilities.py @@ -9,6 +9,8 @@ import fnmatch from pydantic import BaseModel, Field +from zhenxun.services.log import logger + class ModelModality(str, Enum): TEXT = "text" @@ -50,6 +52,46 @@ GEMINI_IMAGE_GEN_CAPABILITIES = ModelCapabilities( supports_tool_calling=True, ) +GPT_ADVANCED_TEXT_IMAGE_CAPABILITIES = ModelCapabilities( + input_modalities={ModelModality.TEXT, ModelModality.IMAGE}, + output_modalities={ModelModality.TEXT}, + supports_tool_calling=True, +) + +GPT_MULTIMODAL_IO_CAPABILITIES = ModelCapabilities( + input_modalities={ModelModality.TEXT, ModelModality.AUDIO, ModelModality.IMAGE}, + output_modalities={ModelModality.TEXT, ModelModality.AUDIO}, + supports_tool_calling=True, +) + +GPT_IMAGE_GENERATION_CAPABILITIES = ModelCapabilities( + input_modalities={ModelModality.TEXT, ModelModality.IMAGE}, + output_modalities={ModelModality.IMAGE}, + supports_tool_calling=True, +) + +GPT_VIDEO_GENERATION_CAPABILITIES = ModelCapabilities( + input_modalities={ModelModality.TEXT, ModelModality.IMAGE, ModelModality.VIDEO}, + output_modalities={ModelModality.VIDEO}, + supports_tool_calling=True, +) + +DEFAULT_PERMISSIVE_CAPABILITIES = ModelCapabilities( + input_modalities={ + ModelModality.TEXT, + ModelModality.IMAGE, + ModelModality.AUDIO, + ModelModality.VIDEO, + }, + output_modalities={ + ModelModality.TEXT, + ModelModality.IMAGE, + ModelModality.AUDIO, + ModelModality.VIDEO, + }, + supports_tool_calling=True, +) + DOUBAO_ADVANCED_MULTIMODAL_CAPABILITIES = ModelCapabilities( input_modalities={ModelModality.TEXT, ModelModality.IMAGE, ModelModality.VIDEO}, @@ -91,11 +133,8 @@ MODEL_CAPABILITIES_REGISTRY: dict[str, ModelCapabilities] = { is_embedding_model=True, ), "*gemini-*-image-preview*": GEMINI_IMAGE_GEN_CAPABILITIES, - "gemini-2.5-pro*": GEMINI_CAPABILITIES, - "gemini-1.5-pro*": GEMINI_CAPABILITIES, - "gemini-2.5-flash*": GEMINI_CAPABILITIES, - "gemini-2.0-flash*": GEMINI_CAPABILITIES, - "gemini-1.5-flash*": GEMINI_CAPABILITIES, + "gemini-*-pro*": GEMINI_CAPABILITIES, + "gemini-*-flash*": GEMINI_CAPABILITIES, "GLM-4V-Flash": ModelCapabilities( input_modalities={ModelModality.TEXT, ModelModality.IMAGE}, output_modalities={ModelModality.TEXT}, @@ -112,6 +151,13 @@ MODEL_CAPABILITIES_REGISTRY: dict[str, ModelCapabilities] = { "doubao-1-5-thinking-vision-pro": DOUBAO_ADVANCED_MULTIMODAL_CAPABILITIES, "deepseek-chat": STANDARD_TEXT_TOOL_CAPABILITIES, "deepseek-reasoner": STANDARD_TEXT_TOOL_CAPABILITIES, + "gpt-5*": GPT_ADVANCED_TEXT_IMAGE_CAPABILITIES, + "gpt-4.1*": GPT_ADVANCED_TEXT_IMAGE_CAPABILITIES, + "gpt-4o*": GPT_MULTIMODAL_IO_CAPABILITIES, + "o3*": GPT_ADVANCED_TEXT_IMAGE_CAPABILITIES, + "o4-mini*": GPT_ADVANCED_TEXT_IMAGE_CAPABILITIES, + "gpt image*": GPT_IMAGE_GENERATION_CAPABILITIES, + "sora*": GPT_VIDEO_GENERATION_CAPABILITIES, } @@ -126,11 +172,25 @@ def get_model_capabilities(model_name: str) -> ModelCapabilities: canonical_name = c_name break - if canonical_name in MODEL_CAPABILITIES_REGISTRY: - return MODEL_CAPABILITIES_REGISTRY[canonical_name] + parts = canonical_name.split("/") + names_to_check = ["/".join(parts[i:]) for i in range(len(parts))] - for pattern, capabilities in MODEL_CAPABILITIES_REGISTRY.items(): - if "*" in pattern and fnmatch.fnmatch(model_name, pattern): - return capabilities + logger.trace(f"为 '{model_name}' 生成的检查列表: {names_to_check}") - return ModelCapabilities() + for name in names_to_check: + if name in MODEL_CAPABILITIES_REGISTRY: + logger.debug(f"模型 '{model_name}' 通过精确匹配 '{name}' 找到能力定义。") + return MODEL_CAPABILITIES_REGISTRY[name] + + for pattern, capabilities in MODEL_CAPABILITIES_REGISTRY.items(): + if "*" in pattern and fnmatch.fnmatch(name, pattern): + logger.debug( + f"模型 '{model_name}' 通过通配符匹配 '{name}'(pattern: '{pattern}')" + f"找到能力定义。" + ) + return capabilities + + logger.warning( + f"模型 '{model_name}' 的能力定义未在注册表中找到,将使用默认的'全功能'回退配置" + ) + return DEFAULT_PERMISSIVE_CAPABILITIES