From 4c1c42efac85205d1fc8e4cde2e71a4ec2390523 Mon Sep 17 00:00:00 2001 From: yrk <2493404415@qq.com> Date: Tue, 24 Mar 2026 10:43:45 +0800 Subject: [PATCH 1/2] feat: update modelscope bot --- bridge/agent_bridge.py | 2 + channel/web/web_channel.py | 7 + common/const.py | 14 + models/modelscope/modelscope_bot.py | 895 +++++++++++++++++++++++----- 4 files changed, 766 insertions(+), 152 deletions(-) diff --git a/bridge/agent_bridge.py b/bridge/agent_bridge.py index 81caad3c..1795c35f 100644 --- a/bridge/agent_bridge.py +++ b/bridge/agent_bridge.py @@ -115,6 +115,8 @@ class AgentLLMModel(LLMModel): return const.QWEN_DASHSCOPE if model_name in [const.MOONSHOT, "moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"]: return const.MOONSHOT + if conf().get("bot_type") == "modelscope": + return const.MODELSCOPE for prefix, btype in self._MODEL_PREFIX_MAP: if model_name.startswith(prefix): return btype diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index 2979cd9e..750a251a 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -568,6 +568,13 @@ class ConfigHandler: "api_base_default": "https://api.deepseek.com/v1", "models": [const.DEEPSEEK_CHAT, const.DEEPSEEK_REASONER], }), + ("modelscope", { + "label": "ModelScope", + "api_key_field": "modelscope_api_key", + "api_base_key": None, + "api_base_default": None, + "models": [const.Qwen3_5_27B, const.Qwen3_235B_A22B_Instruct_2507], + }), ("linkai", { "label": "LinkAI", "api_key_field": "linkai_api_key", diff --git a/common/const.py b/common/const.py index c6869c38..224c4d5e 100644 --- a/common/const.py +++ b/common/const.py @@ -124,6 +124,10 @@ DOUBAO_SEED_2_PRO = "doubao-seed-2-0-pro-260215" DOUBAO_SEED_2_LITE = "doubao-seed-2-0-lite-260215" DOUBAO_SEED_2_MINI = "doubao-seed-2-0-mini-260215" +# ModelScope(魔搭社区) +Qwen3_235B_A22B_INSTRUCT_2507 = "Qwen/Qwen3-235B-A22B-Instruct-2507" +Qwen3_5_27B = "Qwen/Qwen3.5-27B" + # 其他模型 WEN_XIN = "wenxin" WEN_XIN_4 = "wenxin-4" @@ -141,6 +145,16 @@ MODELSCOPE_MODEL_LIST = ["LLM-Research/c4ai-command-r-plus-08-2024","mistralai/M "Qwen/Qwen2.5-14B-Instruct-1M","Qwen/Qwen2.5-7B-Instruct-1M","Qwen/Qwen2.5-VL-3B-Instruct","Qwen/Qwen2.5-VL-7B-Instruct","Qwen/Qwen2.5-VL-72B-Instruct","deepseek-ai/DeepSeek-R1-Distill-Llama-70B","deepseek-ai/DeepSeek-R1-Distill-Llama-8B","deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B","deepseek-ai/DeepSeek-R1-Distill-Qwen-7B","deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B","deepseek-ai/DeepSeek-R1","deepseek-ai/DeepSeek-V3","Qwen/QwQ-32B"] +MODELSCOPE_MODEL_LIST = [ + "deepseek-ai/DeepSeek-R1-0528", "deepseek-ai/DeepSeek-R1-Distill-Llama-70B", "deepseek-ai/DeepSeek-R1-Distill-Llama-8B", "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B", "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", + "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B", "deepseek-ai/DeepSeek-V3.2", "LLM-Research/c4ai-command-r-plus-08-2024", "LLM-Research/Llama-4-Maverick-17B-128E-Instruct", "meituan-longcat/LongCat-Flash-Lite", "MiniMax/MiniMax-M1-80k", "MiniMax/MiniMax-M2.5", "mistralai/Ministral-8B-Instruct-2410", + "mistralai/Mistral-Large-Instruct-2407", "mistralai/Mistral-Small-Instruct-2409", "moonshotai/Kimi-K2.5", "MusePublic/Qwen-Image-Edit", "opencompass/CompassJudger-1-32B-Instruct", "OpenGVLab/InternVL3_5-241B-A28B", + "Qwen/QVQ-72B-Preview", "Qwen/Qwen-Image-Edit", "Qwen/Qwen3-0.6B", "Qwen/Qwen3-1.7B", "Qwen/Qwen3-14B", "Qwen/Qwen3-235B-A22B", "Qwen/Qwen3-235B-A22B-Instruct-2507", "Qwen/Qwen3-235B-A22B-Thinking-2507", "Qwen/Qwen3-30B-A3B", "Qwen/Qwen3-30B-A3B-Thinking-2507", + "Qwen/Qwen3-32B", "Qwen/Qwen3-4B", "Qwen/Qwen3-8B", "Qwen/Qwen3-Coder-30B-A3B-Instruct", "Qwen/Qwen3-Coder-480B-A35B-Instruct", "Qwen/Qwen3-Next-80B-A3B-Instruct", "Qwen/Qwen3-Next-80B-A3B-Thinking", "Qwen/Qwen3-VL-235B-A22B-Instruct", "Qwen/Qwen3-VL-8B-Instruct", + "Qwen/Qwen3-VL-8B-Thinking", "Qwen/Qwen3.5-122B-A10B", "Qwen/Qwen3.5-27B", "Qwen/Qwen3.5-35B-A3B", "Qwen/Qwen3.5-397B-A17B", "Qwen/QwQ-32B", "Qwen/QwQ-32B-Preview", "Shanghai_AI_Laboratory/Intern-S1", "Shanghai_AI_Laboratory/Intern-S1-mini", + "stepfun-ai/Step-3.5-Flash", "XiaomiMiMo/MiMo-V2-Flash", "ZhipuAI/GLM-4.7-Flash", "ZhipuAI/GLM-5"] + + MODEL_LIST = [ # Claude CLAUDE3, CLAUDE_4_6_SONNET, CLAUDE_4_6_OPUS, CLAUDE_4_OPUS, CLAUDE_4_5_SONNET, CLAUDE_4_SONNET, CLAUDE_3_OPUS, CLAUDE_3_OPUS_0229, diff --git a/models/modelscope/modelscope_bot.py b/models/modelscope/modelscope_bot.py index e6d26fb9..6d55abce 100644 --- a/models/modelscope/modelscope_bot.py +++ b/models/modelscope/modelscope_bot.py @@ -1,8 +1,9 @@ # encoding:utf-8 -import time import json -import openai +import time +import requests + from models.bot import Bot from models.session_manager import SessionManager from bridge.context import ContextType @@ -10,40 +11,40 @@ from bridge.reply import Reply, ReplyType from common.log import logger from config import conf, load_config from .modelscope_session import ModelScopeSession -import requests -# ModelScope对话模型API class ModelScopeBot(Bot): + def __init__(self): super().__init__() - self.sessions = SessionManager(ModelScopeSession, model=conf().get("model") or "Qwen/Qwen2.5-7B-Instruct") - model = conf().get("model") or "Qwen/Qwen2.5-7B-Instruct" + model = conf().get("model") or "Qwen/Qwen3.5-27B" if model == "modelscope": - model = "Qwen/Qwen2.5-7B-Instruct" + model = "Qwen/Qwen3.5-27B" + self.sessions = SessionManager(ModelScopeSession, model=model) self.args = { - "model": model, # 对话模型的名称 - "temperature": conf().get("temperature", 0.3), # 如果设置,值域须为 [0, 1] 我们推荐 0.3,以达到较合适的效果。 - "top_p": conf().get("top_p", 1.0), # 使用默认值 + "model": model, + "temperature": conf().get("temperature", 0.3), + "top_p": conf().get("top_p", 1.0), } + self.api_key = conf().get("modelscope_api_key") + self.base_url = conf().get("modelscope_base_url", "https://api-inference.modelscope.cn/v1") + if self.base_url.endswith("/chat/completions"): + self.base_url = self.base_url.rsplit("/chat/completions", 1)[0] + if self.base_url.endswith("/"): + self.base_url = self.base_url.rstrip("/") + + # Cache context for Agent mode usage + self._last_context = None + + logger.info("[MODELSCOPE] base_url configured as: {}".format(self.base_url)) - @property - def api_key(self): - return conf().get("modelscope_api_key") - - @property - def base_url(self): - return conf().get("modelscope_base_url", "https://api-inference.modelscope.cn/v1/chat/completions") - """ - 需要获取ModelScope支持API-inference的模型名称列表,请到魔搭社区官网模型中心查看 https://modelscope.cn/models?filter=inference_type&page=1。 - 或者使用命令 curl https://api-inference.modelscope.cn/v1/models 对模型列表和ID进行获取。查看commend/const.py文件也可以获取模型列表。 - 获取ModelScope的免费API Key,请到魔搭社区官网用户中心查看获取方式 https://modelscope.cn/docs/model-service/API-Inference/intro。 - """ def reply(self, query, context=None): - # acquire reply content + # Cache context for Agent mode usage + self._last_context = context + if context.type == ContextType.TEXT: - logger.info("[MODELSCOPE_AI] query={}".format(query)) - + logger.info("[MODELSCOPE] query={}".format(query)) + session_id = context["session_id"] reply = None clear_memory_commands = conf().get("clear_memory_commands", ["#清除记忆"]) @@ -56,113 +57,111 @@ class ModelScopeBot(Bot): elif query == "#更新配置": load_config() reply = Reply(ReplyType.INFO, "配置已更新") + if reply: return reply session = self.sessions.session_query(query, session_id) - logger.debug("[MODELSCOPE_AI] session query={}".format(session.messages)) + logger.debug("[MODELSCOPE] session query={}".format(session.messages)) model = context.get("modelscope_model") new_args = self.args.copy() if model: new_args["model"] = model + + model_name = new_args["model"] - if new_args["model"] == "Qwen/QwQ-32B": + # Unified judgment for thinking model + if self._is_thinking_model(model_name): + new_args["enable_thinking"] = True reply_content = self.reply_text_stream(session, args=new_args) else: reply_content = self.reply_text(session, args=new_args) - + logger.debug( - "[MODELSCOPE_AI] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format( + "[MODELSCOPE] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format( session.messages, session_id, reply_content["content"], reply_content["completion_tokens"], ) ) + if reply_content["completion_tokens"] == 0 and len(reply_content["content"]) > 0: - # 只有当 content 为空且 completion_tokens 为 0 时才标记为错误 - if len(reply_content["content"]) == 0: - reply = Reply(ReplyType.ERROR, reply_content["content"]) - else: - reply = Reply(ReplyType.TEXT, reply_content["content"]) + reply = Reply(ReplyType.TEXT, reply_content["content"]) elif reply_content["completion_tokens"] > 0: - self.sessions.session_reply(reply_content["content"], session_id, reply_content["total_tokens"]) + self.sessions.session_reply( + reply_content["content"], + session_id, + reply_content["total_tokens"] + ) reply = Reply(ReplyType.TEXT, reply_content["content"]) else: reply = Reply(ReplyType.ERROR, reply_content["content"]) - logger.debug("[MODELSCOPE_AI] reply {} used 0 tokens.".format(reply_content)) + logger.debug("[MODELSCOPE] reply {} used 0 tokens.".format(reply_content)) + return reply + elif context.type == ContextType.IMAGE_CREATE: ok, retstring = self.create_img(query, 0) - reply = None - if ok: - reply = Reply(ReplyType.IMAGE_URL, retstring) - else: - reply = Reply(ReplyType.ERROR, retstring) - return reply + return Reply(ReplyType.IMAGE_URL, retstring) if ok else Reply(ReplyType.ERROR, retstring) else: - reply = Reply(ReplyType.ERROR, "Bot不支持处理{}类型的消息".format(context.type)) - return reply + return Reply(ReplyType.ERROR, "Bot 不支持处理{}类型的消息".format(context.type)) - def reply_text(self, session: ModelScopeSession, args=None, retry_count=0) -> dict: - """ - call openai's ChatCompletion to get the answer - :param session: a conversation session - :param session_id: session id - :param retry_count: retry count - :return: {} - """ + def reply_text(self, session, args=None, retry_count=0): try: headers = { "Content-Type": "application/json", "Authorization": "Bearer " + self.api_key } - body = args - body["messages"] = session.messages + body = args.copy() if args else {} + body["messages"] = self._convert_messages_for_modelscope(session.messages) + body["stream"] = False + res = requests.post( - self.base_url, + "{}/chat/completions".format(self.base_url), headers=headers, - data=json.dumps(body) + json=body, + timeout=120 ) - + if res.status_code == 200: response = res.json() return { - "total_tokens": response["usage"]["total_tokens"], - "completion_tokens": response["usage"]["completion_tokens"], - "content": response["choices"][0]["message"]["content"] + "total_tokens": response.get("usage", {}).get("total_tokens", 0), + "completion_tokens": response.get("usage", {}).get("completion_tokens", 0), + "content": response["choices"][0]["message"]["content"] if response.get("choices") else "" } else: response = res.json() - if "errors" in response: - error = response.get("errors") - elif "error" in response: - error = response.get("error") - else: - error = "Unknown error" - logger.error(f"[MODELSCOPE_AI] chat failed, status_code={res.status_code}, " - f"msg={error.get('message')}, type={error.get('type')}") - + error = response.get("error", response.get("errors", {})) + logger.error( + "[MODELSCOPE] chat failed, status_code={}, msg={}".format( + res.status_code, + error.get('message') if isinstance(error, dict) else error + ) + ) + result = {"completion_tokens": 0, "content": "提问太快啦,请休息一下再问我吧"} need_retry = False + if res.status_code >= 500: - # server error, need retry - logger.warn(f"[MODELSCOPE_AI] do retry, times={retry_count}") + logger.warn("[MODELSCOPE] do retry, times={}".format(retry_count)) need_retry = retry_count < 2 elif res.status_code == 401: - result["content"] = "授权失败,请检查API Key是否正确" + result["content"] = "授权失败,请检查 API Key 是否正确" elif res.status_code == 429: result["content"] = "请求过于频繁,请稍后再试" need_retry = retry_count < 2 else: need_retry = False - + if need_retry: time.sleep(3) return self.reply_text(session, args, retry_count + 1) else: return result + except Exception as e: logger.exception(e) need_retry = retry_count < 2 @@ -172,111 +171,703 @@ class ModelScopeBot(Bot): else: return result - def reply_text_stream(self, session: ModelScopeSession, args=None, retry_count=0) -> dict: - """ - call ModelScope's ChatCompletion to get the answer with stream response - :param session: a conversation session - :param session_id: session id - :param retry_count: retry count - :return: {} - """ + def reply_text_stream(self, session, args=None): try: headers = { "Content-Type": "application/json", "Authorization": "Bearer " + self.api_key } - body = args - body["messages"] = session.messages - body["stream"] = True # 启用流式响应 + body = args.copy() if args else {} + body["messages"] = self._convert_messages_for_modelscope(session.messages) + body["stream"] = True res = requests.post( - self.base_url, + "{}/chat/completions".format(self.base_url), headers=headers, - data=json.dumps(body), - stream=True + json=body, + stream=True, + timeout=120 ) if res.status_code == 200: content = "" + total_tokens = completion_tokens = 0 + finish_reason = None + for line in res.iter_lines(): - if line: - decoded_line = line.decode('utf-8') - if decoded_line.startswith("data: "): - try: - json_data = json.loads(decoded_line[6:]) - delta_content = json_data.get("choices", [{}])[0].get("delta", {}).get("content", "") - if delta_content: - content += delta_content - except json.JSONDecodeError as e: - pass + if not line: + continue + + decoded_line = line.decode('utf-8') + if not decoded_line.startswith("data: "): + continue + + data_str = decoded_line[6:] + if data_str.strip() == "[DONE]": + break + + try: + json_data = json.loads(data_str) + + if "usage" in json_data: + total_tokens = json_data["usage"].get("total_tokens", 0) + completion_tokens = json_data["usage"].get("completion_tokens", 0) + + delta = json_data.get("choices", [{}])[0].get("delta", {}) + if delta and delta.get("content"): + content += delta["content"] + + choice = json_data.get("choices", [{}])[0] + if choice.get("finish_reason"): + finish_reason = choice["finish_reason"] + + except json.JSONDecodeError: + continue + + if finish_reason is None and content: + finish_reason = "stop" + return { - "total_tokens": 1, # 流式响应通常不返回token使用情况 - "completion_tokens": 1, + "total_tokens": total_tokens, + "completion_tokens": completion_tokens, "content": content } else: - response = res.json() - if "errors" in response: - error = response.get("errors") - elif "error" in response: - error = response.get("error") - else: - error = "Unknown error" - logger.error(f"[MODELSCOPE_AI] chat failed, status_code={res.status_code}, " - f"msg={error.get('message')}, type={error.get('type')}") - - result = {"completion_tokens": 0, "content": "提问太快啦,请休息一下再问我吧"} - need_retry = False - if res.status_code >= 500: - # server error, need retry - logger.warn(f"[MODELSCOPE_AI] do retry, times={retry_count}") - need_retry = retry_count < 2 - elif res.status_code == 401: - result["content"] = "授权失败,请检查API Key是否正确" - elif res.status_code == 429: - result["content"] = "请求过于频繁,请稍后再试" - need_retry = retry_count < 2 - else: - need_retry = False - - if need_retry: - time.sleep(3) - return self.reply_text_stream(session, args, retry_count + 1) - else: - return result + return {"completion_tokens": 0, "content": "请求失败"} + except Exception as e: logger.exception(e) - need_retry = retry_count < 2 - result = {"completion_tokens": 0, "content": "我现在有点累了,等会再来吧"} - if need_retry: - return self.reply_text_stream(session, args, retry_count + 1) - else: - return result - def create_img(self, query, retry_count=0): + return {"completion_tokens": 0, "content": "我现在有点累了,等会再来吧"} + + def create_img(self, query): try: logger.info("[ModelScopeImage] image_query={}".format(query)) - headers = { - "Content-Type": "application/json; charset=utf-8", # 明确指定编码 - "Authorization": f"Bearer {self.api_key}" + + create_headers = { + "Authorization": "Bearer " + self.api_key, + "Content-Type": "application/json; charset=utf-8", + "X-ModelScope-Async-Mode": "true" } + payload = { - "prompt": query, # required - "n": 1, "model": conf().get("text_to_image"), + "prompt": query, + "n": 1, } - url = "https://api-inference.modelscope.cn/v1/images/generations" - # 手动序列化并保留中文(禁用 ASCII 转义) - json_payload = json.dumps(payload, ensure_ascii=False).encode('utf-8') + logger.debug("[ModelScopeImage] model={}".format(payload["model"])) - # 使用 data 参数发送原始字符串(requests 会自动处理编码) - res = requests.post(url, headers=headers, data=json_payload) + res = requests.post( + "{}/images/generations".format(self.base_url), + headers=create_headers, + data=json.dumps(payload, ensure_ascii=False).encode('utf-8'), + timeout=120 + ) + + logger.debug("[ModelScopeImage] create task status={}".format(res.status_code)) + logger.debug("[ModelScopeImage] create task response={}".format(res.text)) + + if res.status_code != 200: + logger.error("[ModelScopeImage] create task failed: {}".format(res.text)) + return False, "创建画图任务失败:{}".format(res.status_code) + + task_data = res.json() + + task_id = task_data.get("task_id") + if not task_id: + logger.error("[ModelScopeImage] No task_id in response: {}".format(task_data)) + return False, "创建画图任务失败:未返回 task_id" + + logger.info("[ModelScopeImage] task_id={}".format(task_id)) + + max_wait_times = 60 + wait_interval = 5 + + for i in range(max_wait_times): + time.sleep(wait_interval) + + poll_headers = { + "Authorization": "Bearer " + self.api_key, + "X-ModelScope-Task-Type": "image_generation" + } + + poll_url = "{}/tasks/{}".format(self.base_url, task_id) + logger.debug("[ModelScopeImage] poll {} URL: {}".format(i+1, poll_url)) + logger.debug("[ModelScopeImage] poll headers: {}".format(poll_headers)) + + task_res = requests.get( + poll_url, + headers=poll_headers, + timeout=30 + ) + + logger.debug("[ModelScopeImage] poll {} status={}".format(i+1, task_res.status_code)) + logger.debug("[ModelScopeImage] poll response={}".format(task_res.text)) + + if task_res.status_code != 200: + logger.error("[ModelScopeImage] poll task error: {}".format(task_res.text)) + continue + + data = task_res.json() + + task_status = data.get("task_status") + logger.debug("[ModelScopeImage] task_status={}".format(task_status)) + + if task_status == "SUCCEED": + output_images = data.get("output_images", []) + if output_images and len(output_images) > 0: + image_url = output_images[0] + logger.info("[ModelScopeImage] image generated successfully: {}".format(image_url)) + return True, image_url + else: + logger.error("[ModelScopeImage] No output_images in success response: {}".format(data)) + return False, "画图成功但未返回图片 URL" + + elif task_status == "FAILED": + error_msg = "未知错误" + if "errors" in data: + error_msg = data["errors"].get("message", "未知错误") + elif "message" in data: + error_msg = data["message"] + logger.error("[ModelScopeImage] task failed: {}".format(data)) + return False, "画图任务失败:{}".format(error_msg) + + elif task_status == "CANCELED": + logger.error("[ModelScopeImage] task canceled: {}".format(data)) + return False, "画图任务已取消" + + logger.debug("[ModelScopeImage] waiting for task to complete...") + + logger.error("[ModelScopeImage] task timeout after {} seconds".format(max_wait_times * wait_interval)) + return False, "画图超时,请稍后再试" - response_data = res.json() - image_url = response_data['images'][0]['url'] - logger.info("[ModelScopeImage] image_url={}".format(image_url)) - return True, image_url - except Exception as e: - logger.error(format(e)) - return False, "画图出现问题,请休息一下再问我吧" \ No newline at end of file + logger.error("[ModelScopeImage] error: {}".format(format(e))) + return False, "画图出现问题,请休息一下再问我吧" + + # ==================== Agent Mode Support ==================== + + def _detect_image_intent(self, message): + """Detect whether the message has drawing intention (keyword detection)""" + if not message: + return False + + message_lower = message.lower() + image_keywords = ["画", "图片", "图像", "生成图", "photo", "image", "draw", "paint", "generate"] + if any(keyword in message_lower for keyword in image_keywords): + logger.info("[MODELSCOPE] Image intent detected by keyword: {}".format(message[:50])) + return True + + return False + + def _is_thinking_model(self, model_name): + """ + Determine whether it is a thinking model. + A thinking model requires: 1) enabling the enable_thinking parameter, and 2) using streaming responses. + """ + if not model_name: + return False + model_name_lower = model_name.lower() + if "thinking" in model_name_lower or "think" in model_name_lower: + return True + if model_name in ["Qwen/QwQ-32B", ]: + return True + return False + + def call_with_tools(self, messages, tools=None, stream=False, **kwargs): + """ + Call ModelScope API with tool call support. + Also check ContextType and keywords; if either matches, trigger drawing. + """ + try: + # Check the IMAGE_CREATE type from the cached context + context = getattr(self, '_last_context', None) + + # If the context type is IMAGE_CREATE, directly call create_img + if context and hasattr(context, 'type') and context.type == ContextType.IMAGE_CREATE: + logger.info("[MODELSCOPE] IMAGE_CREATE context detected, calling create_img directly") + query = getattr(context, 'content', '') + if query: + ok, result = self.create_img(query) + if ok: + logger.info("[MODELSCOPE] Image generated: {}".format(result)) + if stream: + return self._create_image_stream_response(result) + else: + return self._create_image_response(result) + else: + logger.error("[MODELSCOPE] Image generation failed: {}".format(result)) + error_content = "画图失败:{}".format(result) + if stream: + return self._create_error_stream_response(error_content) + else: + return self._create_error_response(error_content) + + # Extract message content + last_message = "" + if messages and len(messages) > 0: + last_msg = messages[-1] + if isinstance(last_msg, dict): + content = last_msg.get("content", "") + if isinstance(content, list): + text_parts = [] + for block in content: + if isinstance(block, dict): + if block.get("type") == "text": + text_parts.append(block.get("text", "")) + last_message = " ".join(text_parts) + else: + last_message = content + elif isinstance(last_msg, str): + last_message = last_msg + + if not isinstance(last_message, str): + last_message = str(last_message) + + logger.debug("[MODELSCOPE] Extracted message: {}".format(last_message[:100])) + + # Keyword detection + has_image_intent = self._detect_image_intent(last_message) + + if has_image_intent: + logger.info("[MODELSCOPE] Image intent detected by keyword, calling create_img directly") + ok, result = self.create_img(last_message) + if ok: + logger.info("[MODELSCOPE] Image generated: {}".format(result)) + if stream: + return self._create_image_stream_response(result) + else: + return self._create_image_response(result) + else: + logger.error("[MODELSCOPE] Image generation failed: {}".format(result)) + error_content = "画图失败:{}".format(result) + if stream: + return self._create_error_stream_response(error_content) + else: + return self._create_error_response(error_content) + + # No drawing intent, proceed with normal tool call flow + session_id = kwargs.get('session_id', 'default_session') + session = self.sessions.session_query("", session_id) + session.messages = messages + + args = self.args.copy() + args.update(kwargs) + + # Unified judgment for thinking model + model_name = args.get("model", self.args.get("model", "")) + if self._is_thinking_model(model_name): + args["enable_thinking"] = True + + if tools: + args["tools"] = self._convert_tools_to_openai_format(tools) + args["tool_choice"] = "auto" + + logger.debug( + "[MODELSCOPE] call_with_tools: model={}, tools={}, stream={}, enable_thinking={}".format( + args.get('model'), + len(tools) if tools else 0, + stream, + args.get('enable_thinking') + ) + ) + + if stream: + return self._handle_stream_response(session, args) + else: + return self._handle_sync_response(session, args) + + except Exception as e: + logger.error("[MODELSCOPE] call_with_tools error: {}".format(e)) + error_msg = "{}".format(e) + def error_generator(): + yield {"error": True, "message": error_msg, "status_code": 500} + return error_generator() + + def _handle_sync_response(self, session, args): + result = self.reply_text(session, args) + + content = result.get("content", "") + tool_calls = result.get("tool_calls") + + if tool_calls: + for tool_call in tool_calls: + tool_name = tool_call.get("function", {}).get("name", "") + if tool_name in ["create_image", "generate_image"]: + try: + tool_args = json.loads(tool_call.get("function", {}).get("arguments", "{}")) + prompt = tool_args.get("prompt", "") + ok, image_url = self.create_img(prompt) + if ok: + result["tool_execution_result"] = {"image_url": image_url, "success": True} + else: + result["tool_execution_result"] = {"error": image_url, "success": False} + except Exception as e: + logger.error("[MODELSCOPE] Sync tool execution error: {}".format(e)) + + return { + "choices": [{ + "message": { + "role": "assistant", + "content": content, + "tool_calls": tool_calls + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 0, + "completion_tokens": result.get("completion_tokens", 0), + "total_tokens": result.get("total_tokens", 0) + }, + "model": args.get("model", self.args.get("model")) + } + + def _handle_stream_response(self, session, args): + try: + headers = { + "Content-Type": "application/json", + "Authorization": "Bearer " + self.api_key + } + + body = args.copy() + body["messages"] = self._convert_messages_for_modelscope(session.messages) + body["stream"] = True + + response = requests.post( + "{}/chat/completions".format(self.base_url), + headers=headers, + json=body, + stream=True, + timeout=120 + ) + + if response.status_code != 200: + yield {"error": True, "message": response.text, "status_code": response.status_code} + return + + current_tool_calls = {} + finish_reason = None + + for line in response.iter_lines(): + if not line: + continue + + line = line.decode("utf-8") + if not line.startswith("data: ") or line[6:].strip() == "[DONE]": + continue + + try: + chunk = json.loads(line[6:]) + + if chunk.get("error"): + yield {"error": True, "message": str(chunk["error"]), "status_code": 500} + return + + choices = chunk.get("choices") + if not choices or len(choices) == 0: + continue + + choice = choices[0] + if not choice: + continue + + delta = choice.get("delta") + if not delta: + continue + + if delta.get("reasoning_content"): + continue + + tool_call_chunks = delta.get("tool_calls") + if tool_call_chunks: + cleaned_chunks = [] + for tool_call_chunk in tool_call_chunks: + if not tool_call_chunk: + continue + + index = tool_call_chunk.get("index", 0) + func_info = tool_call_chunk.get("function") or {} + + if index not in current_tool_calls: + current_tool_calls[index] = { + "id": tool_call_chunk.get("id") or "", + "name": func_info.get("name") or "", + "arguments": "" + } + logger.debug("[MODELSCOPE] tool_call start: {}".format(func_info.get('name'))) + + args_str = func_info.get("arguments") + if args_str: + current_tool_calls[index]["arguments"] += ( + args_str if isinstance(args_str, str) else str(args_str) + ) + + cleaned_chunk = { + "index": index, + "id": tool_call_chunk.get("id") or "call_{}".format(index), + "type": "function", + "function": { + "name": func_info.get("name") or current_tool_calls[index].get("name", ""), + "arguments": func_info.get("arguments") or "" + } + } + cleaned_chunks.append(cleaned_chunk) + + if cleaned_chunks: + yield { + "choices": [{ + "index": 0, + "delta": { + "role": "assistant", + "tool_calls": cleaned_chunks + } + }] + } + continue + + content = delta.get("content") + if content: + logger.debug("[MODELSCOPE] stream content: {}...".format(content[:50])) + + yield_chunk = { + "choices": [{ + "index": 0, + "delta": { + "role": delta.get("role"), + "content": content + } + }] + } + + if choice.get("finish_reason"): + finish_reason = choice["finish_reason"] + yield_chunk["choices"][0]["finish_reason"] = finish_reason + + yield yield_chunk + + except json.JSONDecodeError: + continue + except Exception as e: + logger.error("[MODELSCOPE] chunk process error: {}".format(e)) + continue + + logger.debug( + "[MODELSCOPE] stream completed: has_tool_calls={}, finish_reason={}".format( + len(current_tool_calls) > 0, + finish_reason + ) + ) + + if current_tool_calls: + logger.debug("[MODELSCOPE] tool_calls collected: {}".format(list(current_tool_calls.values()))) + + for idx, tool_call in current_tool_calls.items(): + tool_name = tool_call.get("name", "") + tool_args_str = tool_call.get("arguments", "{}") + + if tool_name in ["create_image", "generate_image"]: + try: + tool_args = json.loads(tool_args_str) if tool_args_str else {} + prompt = tool_args.get("prompt", "") + + logger.info("[MODELSCOPE] Executing image tool directly: {}".format(prompt[:50])) + + ok, result = self.create_img(prompt) + + if ok: + logger.info("[MODELSCOPE] Image generated: {}".format(result)) + yield { + "choices": [{ + "index": 0, + "delta": { + "role": "tool", + "content": json.dumps({"image_url": result, "success": True}) + }, + "tool_call_id": tool_call.get("id", "") + }] + } + else: + logger.error("[MODELSCOPE] Image generation failed: {}".format(result)) + yield { + "choices": [{ + "index": 0, + "delta": { + "role": "tool", + "content": json.dumps({"error": result, "success": False}) + }, + "tool_call_id": tool_call.get("id", "") + }] + } + except Exception as e: + logger.error("[MODELSCOPE] Image tool execution error: {}".format(e)) + yield { + "choices": [{ + "index": 0, + "delta": { + "role": "tool", + "content": json.dumps({"error": str(e), "success": False}) + }, + "tool_call_id": tool_call.get("id", "") + }] + } + + yield { + "choices": [{ + "index": 0, + "delta": {}, + "finish_reason": finish_reason or "stop" + }] + } + + except Exception as e: + logger.error("[MODELSCOPE] stream tool call error: {}".format(e)) + error_msg = "{}".format(e) + def error_generator(): + yield {"error": True, "message": error_msg, "status_code": 500} + return error_generator() + + # ==================== Format Conversion ==================== + + def _convert_messages_for_modelscope(self, messages): + if not messages: + return [] + converted = [] + for msg in messages: + role = msg.get("role") + content = msg.get("content") + if isinstance(content, str): + converted.append(msg) + continue + if isinstance(content, list): + new_content = [] + for block in content: + if not isinstance(block, dict): + new_content.append(block) + continue + block_type = block.get("type") + if block_type == "tool_result": + tool_content = block.get("content", "") + if not isinstance(tool_content, str): + tool_content = json.dumps(tool_content, ensure_ascii=False) + new_content.append({ + "type": "text", + "text": "[工具执行结果]: {}".format(tool_content) + }) + elif block_type == "tool_use": + tool_name = block.get("name", "unknown") + tool_input = block.get("input", {}) + if not isinstance(tool_input, str): + tool_input = json.dumps(tool_input, ensure_ascii=False) + new_content.append({ + "type": "text", + "text": "[工具调用]: {}({})".format(tool_name, tool_input) + }) + else: + new_content.append(block) + converted.append({"role": role, "content": new_content}) + else: + converted.append(msg) + return converted + + def _convert_tools_to_openai_format(self, tools): + if not tools: + return None + converted = [] + for tool in tools: + if "type" in tool and tool["type"] == "function": + converted.append(tool) + else: + converted.append({ + "type": "function", + "function": { + "name": tool.get("name"), + "description": tool.get("description"), + "parameters": tool.get("input_schema", {}) + } + }) + return converted + + def _create_image_response(self, image_url): + return { + "choices": [{ + "message": { + "role": "assistant", + "content": "已为您生成图片:{}".format(image_url), + "tool_calls": None + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0 + }, + "model": self.args.get("model") + } + + def _create_image_stream_response(self, image_url): + content = "已为您生成图片:{}".format(image_url) + yield { + "choices": [{ + "index": 0, + "delta": {"role": "assistant"} + }] + } + chunk_size = 10 + for i in range(0, len(content), chunk_size): + chunk = content[i:i+chunk_size] + yield { + "choices": [{ + "index": 0, + "delta": {"content": chunk} + }] + } + yield { + "choices": [{ + "index": 0, + "delta": {}, + "finish_reason": "stop" + }] + } + + def _create_error_response(self, error_msg): + return { + "choices": [{ + "message": { + "role": "assistant", + "content": error_msg, + "tool_calls": None + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0 + }, + "model": self.args.get("model") + } + + def _create_error_stream_response(self, error_msg): + yield { + "choices": [{ + "index": 0, + "delta": {"role": "assistant"} + }] + } + chunk_size = 10 + for i in range(0, len(error_msg), chunk_size): + chunk = error_msg[i:i+chunk_size] + yield { + "choices": [{ + "index": 0, + "delta": {"content": chunk} + }] + } + yield { + "choices": [{ + "index": 0, + "delta": {}, + "finish_reason": "stop" + }] + } From 294e3802889310def0df5de8a2659bc41396dfb3 Mon Sep 17 00:00:00 2001 From: yrk <2493404415@qq.com> Date: Tue, 24 Mar 2026 11:00:55 +0800 Subject: [PATCH 2/2] update model_list --- channel/web/web_channel.py | 2 +- common/const.py | 25 +++++++++---------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index 750a251a..2502042d 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -573,7 +573,7 @@ class ConfigHandler: "api_key_field": "modelscope_api_key", "api_base_key": None, "api_base_default": None, - "models": [const.Qwen3_5_27B, const.Qwen3_235B_A22B_Instruct_2507], + "models": [const.QWEN3_5_27B, const.QWEN3_235B_A22B_INSTRUCT_2507], }), ("linkai", { "label": "LinkAI", diff --git a/common/const.py b/common/const.py index 224c4d5e..6c422b8e 100644 --- a/common/const.py +++ b/common/const.py @@ -125,8 +125,8 @@ DOUBAO_SEED_2_LITE = "doubao-seed-2-0-lite-260215" DOUBAO_SEED_2_MINI = "doubao-seed-2-0-mini-260215" # ModelScope(魔搭社区) -Qwen3_235B_A22B_INSTRUCT_2507 = "Qwen/Qwen3-235B-A22B-Instruct-2507" -Qwen3_5_27B = "Qwen/Qwen3.5-27B" +QWEN3_235B_A22B_INSTRUCT_2507 = "Qwen/Qwen3-235B-A22B-Instruct-2507" +QWEN3_5_27B = "Qwen/Qwen3.5-27B" # 其他模型 WEN_XIN = "wenxin" @@ -139,20 +139,13 @@ MODELSCOPE = "modelscope" GITEE_AI_MODEL_LIST = ["Yi-34B-Chat", "InternVL2-8B", "deepseek-coder-33B-instruct", "InternVL2.5-26B", "Qwen2-VL-72B", "Qwen2.5-32B-Instruct", "glm-4-9b-chat", "codegeex4-all-9b", "Qwen2.5-Coder-32B-Instruct", "Qwen2.5-72B-Instruct", "Qwen2.5-7B-Instruct", "Qwen2-72B-Instruct", "Qwen2-7B-Instruct", "code-raccoon-v1", "Qwen2.5-14B-Instruct"] -MODELSCOPE_MODEL_LIST = ["LLM-Research/c4ai-command-r-plus-08-2024","mistralai/Mistral-Small-Instruct-2409","mistralai/Ministral-8B-Instruct-2410","mistralai/Mistral-Large-Instruct-2407", - "Qwen/Qwen2.5-Coder-32B-Instruct","Qwen/Qwen2.5-Coder-14B-Instruct","Qwen/Qwen2.5-Coder-7B-Instruct","Qwen/Qwen2.5-72B-Instruct","Qwen/Qwen2.5-32B-Instruct","Qwen/Qwen2.5-14B-Instruct","Qwen/Qwen2.5-7B-Instruct","Qwen/QwQ-32B-Preview", - "LLM-Research/Llama-3.3-70B-Instruct","opencompass/CompassJudger-1-32B-Instruct","Qwen/QVQ-72B-Preview","LLM-Research/Meta-Llama-3.1-405B-Instruct","LLM-Research/Meta-Llama-3.1-8B-Instruct","Qwen/Qwen2-VL-7B-Instruct","LLM-Research/Meta-Llama-3.1-70B-Instruct", - "Qwen/Qwen2.5-14B-Instruct-1M","Qwen/Qwen2.5-7B-Instruct-1M","Qwen/Qwen2.5-VL-3B-Instruct","Qwen/Qwen2.5-VL-7B-Instruct","Qwen/Qwen2.5-VL-72B-Instruct","deepseek-ai/DeepSeek-R1-Distill-Llama-70B","deepseek-ai/DeepSeek-R1-Distill-Llama-8B","deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", - "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B","deepseek-ai/DeepSeek-R1-Distill-Qwen-7B","deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B","deepseek-ai/DeepSeek-R1","deepseek-ai/DeepSeek-V3","Qwen/QwQ-32B"] - -MODELSCOPE_MODEL_LIST = [ - "deepseek-ai/DeepSeek-R1-0528", "deepseek-ai/DeepSeek-R1-Distill-Llama-70B", "deepseek-ai/DeepSeek-R1-Distill-Llama-8B", "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B", "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", - "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B", "deepseek-ai/DeepSeek-V3.2", "LLM-Research/c4ai-command-r-plus-08-2024", "LLM-Research/Llama-4-Maverick-17B-128E-Instruct", "meituan-longcat/LongCat-Flash-Lite", "MiniMax/MiniMax-M1-80k", "MiniMax/MiniMax-M2.5", "mistralai/Ministral-8B-Instruct-2410", - "mistralai/Mistral-Large-Instruct-2407", "mistralai/Mistral-Small-Instruct-2409", "moonshotai/Kimi-K2.5", "MusePublic/Qwen-Image-Edit", "opencompass/CompassJudger-1-32B-Instruct", "OpenGVLab/InternVL3_5-241B-A28B", - "Qwen/QVQ-72B-Preview", "Qwen/Qwen-Image-Edit", "Qwen/Qwen3-0.6B", "Qwen/Qwen3-1.7B", "Qwen/Qwen3-14B", "Qwen/Qwen3-235B-A22B", "Qwen/Qwen3-235B-A22B-Instruct-2507", "Qwen/Qwen3-235B-A22B-Thinking-2507", "Qwen/Qwen3-30B-A3B", "Qwen/Qwen3-30B-A3B-Thinking-2507", - "Qwen/Qwen3-32B", "Qwen/Qwen3-4B", "Qwen/Qwen3-8B", "Qwen/Qwen3-Coder-30B-A3B-Instruct", "Qwen/Qwen3-Coder-480B-A35B-Instruct", "Qwen/Qwen3-Next-80B-A3B-Instruct", "Qwen/Qwen3-Next-80B-A3B-Thinking", "Qwen/Qwen3-VL-235B-A22B-Instruct", "Qwen/Qwen3-VL-8B-Instruct", - "Qwen/Qwen3-VL-8B-Thinking", "Qwen/Qwen3.5-122B-A10B", "Qwen/Qwen3.5-27B", "Qwen/Qwen3.5-35B-A3B", "Qwen/Qwen3.5-397B-A17B", "Qwen/QwQ-32B", "Qwen/QwQ-32B-Preview", "Shanghai_AI_Laboratory/Intern-S1", "Shanghai_AI_Laboratory/Intern-S1-mini", - "stepfun-ai/Step-3.5-Flash", "XiaomiMiMo/MiMo-V2-Flash", "ZhipuAI/GLM-4.7-Flash", "ZhipuAI/GLM-5"] +MODELSCOPE_MODEL_LIST = ["deepseek-ai/DeepSeek-R1-0528", "deepseek-ai/DeepSeek-R1-Distill-Llama-70B", "deepseek-ai/DeepSeek-R1-Distill-Llama-8B", "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B", "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", + "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B", "deepseek-ai/DeepSeek-V3.2", "LLM-Research/c4ai-command-r-plus-08-2024", "LLM-Research/Llama-4-Maverick-17B-128E-Instruct", "meituan-longcat/LongCat-Flash-Lite", "MiniMax/MiniMax-M1-80k", "MiniMax/MiniMax-M2.5", "mistralai/Ministral-8B-Instruct-2410", + "mistralai/Mistral-Large-Instruct-2407", "mistralai/Mistral-Small-Instruct-2409", "moonshotai/Kimi-K2.5", "MusePublic/Qwen-Image-Edit", "opencompass/CompassJudger-1-32B-Instruct", "OpenGVLab/InternVL3_5-241B-A28B", + "Qwen/QVQ-72B-Preview", "Qwen/Qwen-Image-Edit", "Qwen/Qwen3-0.6B", "Qwen/Qwen3-1.7B", "Qwen/Qwen3-14B", "Qwen/Qwen3-235B-A22B", "Qwen/Qwen3-235B-A22B-Instruct-2507", "Qwen/Qwen3-235B-A22B-Thinking-2507", "Qwen/Qwen3-30B-A3B", "Qwen/Qwen3-30B-A3B-Thinking-2507", + "Qwen/Qwen3-32B", "Qwen/Qwen3-4B", "Qwen/Qwen3-8B", "Qwen/Qwen3-Coder-30B-A3B-Instruct", "Qwen/Qwen3-Coder-480B-A35B-Instruct", "Qwen/Qwen3-Next-80B-A3B-Instruct", "Qwen/Qwen3-Next-80B-A3B-Thinking", "Qwen/Qwen3-VL-235B-A22B-Instruct", "Qwen/Qwen3-VL-8B-Instruct", + "Qwen/Qwen3-VL-8B-Thinking", "Qwen/Qwen3.5-122B-A10B", "Qwen/Qwen3.5-27B", "Qwen/Qwen3.5-35B-A3B", "Qwen/Qwen3.5-397B-A17B", "Qwen/QwQ-32B", "Qwen/QwQ-32B-Preview", "Shanghai_AI_Laboratory/Intern-S1", "Shanghai_AI_Laboratory/Intern-S1-mini", + "stepfun-ai/Step-3.5-Flash", "XiaomiMiMo/MiMo-V2-Flash", "ZhipuAI/GLM-4.7-Flash", "ZhipuAI/GLM-5"] MODEL_LIST = [