feat: 新增qwen3.5系列模型

This commit is contained in:
肖应宇 2026-03-11 11:32:45 +08:00
parent d81fb4d0a0
commit 3c53e89b43
5 changed files with 413 additions and 92 deletions

View File

@ -14,12 +14,40 @@ from core import get_logger
logger = get_logger()
# 支持深度思考的模型
THINKING_MODELS = {"qwen3-max", "qwen3.5-plus"}
# 需要使用多模态接口的模型qwen3.5 系列)
MULTIMODAL_API_MODELS = {"qwen3.5-plus", "qwen3.5-flash"}
# 百炼模型配置
DASHSCOPE_MODELS = [
ModelInfo(
id="qwen-max",
name="通义千问 Max",
description="最强大的模型",
id="qwen3-max",
name="Qwen3-Max",
description="千问系列效果最好的模型,适合复杂、多步骤的任务。",
max_tokens=8192,
provider="Aliyun",
supports_thinking=True,
supports_web_search=False,
supports_vision=False,
supports_files=False,
),
ModelInfo(
id="qwen3.5-plus",
name="Qwen3.5-Plus",
description="能力均衡推理效果、成本和速度介于千问Max和千问Flash之间适合中等复杂任务。",
max_tokens=8192,
provider="Aliyun",
supports_thinking=True,
supports_web_search=False,
supports_vision=True,
supports_files=False,
),
ModelInfo(
id="qwen3.5-flash",
name="Qwen3.5-Flash",
description="千问系列速度最快、成本极低的模型适合简单任务。千问Flash采用灵活的阶梯定价相比千问Turbo计费更合理。",
max_tokens=8192,
provider="Aliyun",
supports_thinking=False,
@ -27,28 +55,6 @@ DASHSCOPE_MODELS = [
supports_vision=False,
supports_files=False,
),
ModelInfo(
id="qwen-plus",
name="通义千问 Plus",
description="能力均衡",
max_tokens=8192,
provider="Aliyun",
supports_thinking=True,
supports_web_search=False,
supports_vision=False,
supports_files=False,
),
ModelInfo(
id="qwen-turbo",
name="通义千问 Turbo",
description="速度更快、成本更低",
max_tokens=8192,
provider="Aliyun",
supports_thinking=True,
supports_web_search=False,
supports_vision=False,
supports_files=False,
),
ModelInfo(
id="qwen-vl-max",
name="通义万相 VL-Max",
@ -60,17 +66,6 @@ DASHSCOPE_MODELS = [
supports_vision=True,
supports_files=False,
),
ModelInfo(
id="qwen-vl-plus",
name="通义万相 VL-Plus",
description="支持视觉理解的多模态模型",
max_tokens=8192,
provider="Aliyun",
supports_thinking=False,
supports_web_search=False,
supports_vision=True,
supports_files=False,
),
]
@ -89,6 +84,14 @@ class DashScopeAdapter(BaseAdapter):
"""获取 API Key"""
return os.getenv("ALIYUN_API_KEY") or os.getenv("DASHSCOPE_API_KEY", "")
def _needs_multimodal_api(self, model: str) -> bool:
"""检查模型是否需要使用多模态 API"""
return model.lower() in MULTIMODAL_API_MODELS
def _supports_thinking(self, model: str) -> bool:
"""检查模型是否支持深度思考"""
return model.lower() in THINKING_MODELS
def list_models(self) -> List[ModelInfo]:
return DASHSCOPE_MODELS
@ -104,6 +107,7 @@ class DashScopeAdapter(BaseAdapter):
logger.info(f" - temperature: {request.temperature}")
logger.info(f" - max_tokens: {request.max_tokens}")
logger.info(f" - files: {request.files}")
logger.info(f" - deep_thinking: {request.deep_thinking}")
logger.info(
f" - messages: {json.dumps(request.messages, ensure_ascii=False, indent=2)}"
)
@ -112,7 +116,11 @@ class DashScopeAdapter(BaseAdapter):
has_multimodal = self._has_multimodal_content(request)
logger.info(f" - has_multimodal: {has_multimodal}")
if has_multimodal:
# 检查是否需要使用多模态接口qwen3.5 系列)
needs_multimodal_api = self._needs_multimodal_api(request.model)
logger.info(f" - needs_multimodal_api: {needs_multimodal_api}")
if has_multimodal or needs_multimodal_api:
return await self._multimodal_chat(request)
else:
return await self._text_chat(request)
@ -136,6 +144,9 @@ class DashScopeAdapter(BaseAdapter):
# 转换消息格式
messages = self._build_text_messages(request)
logger.info(f"[DashScope] 文本聊天 - 转换后的消息:")
logger.info(f" - messages_count: {len(messages)}")
logger.info(f" - messages: {json.dumps(messages, ensure_ascii=False, indent=2)}")
if request.stream:
return self._stream_text_chat(messages, request)
@ -163,26 +174,97 @@ class DashScopeAdapter(BaseAdapter):
"""流式文本聊天"""
logger.info(f"[DashScope] 开始流式文本响应...")
# 检查是否启用深度思考
thinking_enabled = request.deep_thinking and self._supports_thinking(request.model)
logger.info(f"[DashScope] 深度思考: {thinking_enabled} (request={request.deep_thinking}, supports={self._supports_thinking(request.model)})")
def generator():
from utils.helpers import generate_unique_id, get_current_timestamp
from dashscope import Generation
full_content = ""
full_reasoning = ""
chunk_count = 0
responses = Generation.call(
model=request.model,
messages=messages,
stream=True,
temperature=request.temperature,
max_tokens=request.max_tokens,
result_format="message",
)
error_occurred = False
# 构建 API 调用参数
api_params = {
"model": request.model,
"messages": messages,
"stream": True,
"temperature": request.temperature,
"max_tokens": request.max_tokens,
"result_format": "message",
}
# 添加深度思考参数
if thinking_enabled:
api_params["enable_thinking"] = True
# 打印 API 调用参数
logger.info(f"[DashScope] API 调用参数:")
logger.info(f" - model: {api_params['model']}")
logger.info(f" - stream: {api_params['stream']}")
logger.info(f" - temperature: {api_params['temperature']}")
logger.info(f" - max_tokens: {api_params['max_tokens']}")
logger.info(f" - result_format: {api_params['result_format']}")
if thinking_enabled:
logger.info(f" - enable_thinking: True")
try:
responses = Generation.call(**api_params)
except Exception as e:
error_occurred = True
logger.error(f"[DashScope] API 调用异常: {str(e)}")
import traceback
logger.error(traceback.format_exc())
# 返回错误响应
error_data = {
"id": f"chatcmpl-{generate_unique_id()}",
"object": "chat.completion.chunk",
"created": get_current_timestamp(),
"model": request.model,
"choices": [{
"index": 0,
"delta": {"content": f"API 调用失败: {str(e)}"},
"finish_reason": "stop",
}],
}
yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
yield "data: [DONE]\n\n"
return
for resp in responses:
if resp.status_code == 200:
chunk_count += 1
content = resp.output.choices[0].message.content
choice = resp.output.choices[0]
# 处理深度思考内容reasoning_content
reasoning_content = getattr(choice.message, "reasoning_content", None)
if reasoning_content:
# 计算增量
if len(reasoning_content) > len(full_reasoning):
delta_reasoning = reasoning_content[len(full_reasoning):]
full_reasoning = reasoning_content
data = {
"id": f"chatcmpl-{generate_unique_id()}",
"object": "chat.completion.chunk",
"created": get_current_timestamp(),
"model": request.model,
"choices": [
{
"index": 0,
"delta": {"reasoning_content": delta_reasoning},
"finish_reason": None,
}
],
}
yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
continue
# 处理普通内容
content = choice.message.content
if content and len(content) > len(full_content):
# DashScope 流式响应返回完整内容,计算增量
delta = content[len(full_content) :]
@ -201,6 +283,9 @@ class DashScopeAdapter(BaseAdapter):
],
}
yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
else:
# 记录非200响应
logger.warning(f"[DashScope] 非200响应: status_code={resp.status_code}, code={resp.code}, message={resp.message}")
finish = {
"id": f"chatcmpl-{generate_unique_id()}",
@ -216,6 +301,8 @@ class DashScopeAdapter(BaseAdapter):
logger.info(f"[DashScope] 流式文本响应完成:")
logger.info(f" - chunks: {chunk_count}")
logger.info(f" - content_length: {len(full_content)} 字符")
if full_reasoning:
logger.info(f" - reasoning_length: {len(full_reasoning)} 字符")
logger.info(
f" - content_preview: {full_content[:200]}..."
if len(full_content) > 200
@ -230,17 +317,57 @@ class DashScopeAdapter(BaseAdapter):
from dashscope import Generation
resp = Generation.call(
model=request.model,
messages=messages,
stream=False,
temperature=request.temperature,
max_tokens=request.max_tokens,
result_format="message",
)
# 检查是否启用深度思考
thinking_enabled = request.deep_thinking and self._supports_thinking(request.model)
logger.info(f"[DashScope] 深度思考: {thinking_enabled} (request={request.deep_thinking}, supports={self._supports_thinking(request.model)})")
# 构建 API 调用参数
api_params = {
"model": request.model,
"messages": messages,
"stream": False,
"temperature": request.temperature,
"max_tokens": request.max_tokens,
"result_format": "message",
}
# 添加深度思考参数
if thinking_enabled:
api_params["enable_thinking"] = True
# 打印 API 调用参数
logger.info(f"[DashScope] API 调用参数:")
logger.info(f" - model: {api_params['model']}")
logger.info(f" - stream: {api_params['stream']}")
logger.info(f" - temperature: {api_params['temperature']}")
logger.info(f" - max_tokens: {api_params['max_tokens']}")
logger.info(f" - result_format: {api_params['result_format']}")
if thinking_enabled:
logger.info(f" - enable_thinking: True")
try:
resp = Generation.call(**api_params)
except Exception as e:
logger.error(f"[DashScope] API 调用异常: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return JSONResponse(
status_code=500,
content={"error": f"DashScope API 调用异常: {str(e)}"},
)
if resp.status_code == 200:
content = resp.output.choices[0].message.content
message = resp.output.choices[0].message
content = message.content or ""
# 构建响应消息
response_message = {"role": "assistant", "content": content}
# 处理深度思考内容
reasoning_content = getattr(message, "reasoning_content", None)
if reasoning_content:
response_message["reasoning_content"] = reasoning_content
response = {
"id": f"chatcmpl-{generate_unique_id()}",
"object": "chat.completion",
@ -249,7 +376,7 @@ class DashScopeAdapter(BaseAdapter):
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": content},
"message": response_message,
"finish_reason": "stop",
}
],
@ -263,8 +390,11 @@ class DashScopeAdapter(BaseAdapter):
}
# 打印响应结果
logger.info(f"[DashScope] 响应结果:")
logger.info(f"[DashScope] 响应成功:")
logger.info(f" - status_code: {resp.status_code}")
logger.info(f" - content_length: {len(content)} 字符")
if reasoning_content:
logger.info(f" - reasoning_length: {len(reasoning_content)} 字符")
logger.info(
f" - content_preview: {content[:200]}..."
if len(content) > 200
@ -275,7 +405,10 @@ class DashScopeAdapter(BaseAdapter):
return JSONResponse(content=response)
logger.error(f"[DashScope] 请求失败: {resp.code} - {resp.message}")
logger.error(f"[DashScope] 请求失败:")
logger.error(f" - status_code: {resp.status_code}")
logger.error(f" - code: {resp.code}")
logger.error(f" - message: {resp.message}")
return JSONResponse(
status_code=500,
content={"error": f"DashScope Error: {resp.code} - {resp.message}"},
@ -288,13 +421,20 @@ class DashScopeAdapter(BaseAdapter):
dashscope.api_key = self._get_api_key()
logger.info(f"[DashScope] 开始多模态聊天...")
# 转换消息格式
messages = self._build_multimodal_messages(request)
logger.info(f"[DashScope] 多模态消息转换完成:")
logger.info(f" - messages_count: {len(messages)}")
logger.info(f" - messages: {json.dumps(messages, ensure_ascii=False, indent=2)}")
# 选择多模态模型
model = request.model
if "qwen-" in model and "vl" not in model:
original_model = model
model = model.replace("qwen-", "qwen-vl-")
logger.info(f"[DashScope] 模型自动切换: {original_model} -> {model}")
if request.stream:
return self._stream_multimodal_chat(messages, model, request)
@ -338,6 +478,8 @@ class DashScopeAdapter(BaseAdapter):
else:
img_url = ""
logger.info(f"[DashScope] 原始图片URL: {img_url}")
# 转换 http URL 为 file:// 格式(如果是本地文件)
if img_url.startswith(("http://", "https://")):
from urllib.parse import urlparse
@ -350,42 +492,133 @@ class DashScopeAdapter(BaseAdapter):
img_url = f"file://{'/'.join(path_parts[uploads_idx:])}"
except ValueError:
pass
elif not img_url.startswith("file://"):
elif not img_url.startswith("file://") and not img_url.startswith(("http://", "https://")):
img_url = f"file://{img_url}"
logger.info(f"[DashScope] 转换后图片URL: {img_url}")
return img_url
def _stream_multimodal_chat(
self, messages: List[Dict], model: str, request: ChatCompletionRequest
):
"""流式多模态聊天"""
logger.info(f"[DashScope] 开始流式多模态响应...")
logger.info(f" - model: {model}")
logger.info(f" - max_tokens: {request.max_tokens}")
logger.info(f" - temperature: {request.temperature}")
# 检查是否启用深度思考
thinking_enabled = request.deep_thinking and self._supports_thinking(model)
logger.info(f"[DashScope] 深度思考: {thinking_enabled} (request={request.deep_thinking}, supports={self._supports_thinking(model)})")
def generator():
from utils.helpers import generate_unique_id, get_current_timestamp
from dashscope import MultiModalConversation
responses = MultiModalConversation.call(
model=model,
messages=messages,
stream=True,
max_tokens=request.max_tokens,
temperature=request.temperature,
)
full_content = ""
full_reasoning = ""
chunk_count = 0
error_occurred = False
# 打印 API 调用参数
api_params = {
"model": model,
"messages": messages,
"stream": True,
"max_tokens": request.max_tokens,
"temperature": request.temperature,
}
# 添加深度思考参数
if thinking_enabled:
api_params["enable_thinking"] = True
logger.info(f"[DashScope] 多模态 API 调用参数:")
logger.info(f" - model: {api_params['model']}")
logger.info(f" - stream: {api_params['stream']}")
logger.info(f" - max_tokens: {api_params['max_tokens']}")
logger.info(f" - temperature: {api_params['temperature']}")
if thinking_enabled:
logger.info(f" - enable_thinking: True")
logger.info(f" - messages: {json.dumps(messages, ensure_ascii=False, indent=2)}")
try:
responses = MultiModalConversation.call(**api_params)
except Exception as e:
error_occurred = True
logger.error(f"[DashScope] 多模态 API 调用异常: {str(e)}")
import traceback
logger.error(traceback.format_exc())
error_data = {
"id": f"chatcmpl-{generate_unique_id()}",
"object": "chat.completion.chunk",
"created": get_current_timestamp(),
"model": model,
"choices": [{
"index": 0,
"delta": {"content": f"API 调用失败: {str(e)}"},
"finish_reason": "stop",
}],
}
yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
yield "data: [DONE]\n\n"
return
for resp in responses:
chunk_count += 1
logger.info(f"[DashScope] === chunk {chunk_count} ===")
if resp.status_code == 200:
try:
content_items = resp.output.choices[0]["message"]["content"]
# 打印原始响应结构
logger.info(f" - resp.status_code: {resp.status_code}")
logger.info(f" - resp.output: {resp.output}")
choice = resp.output.choices[0]
message = choice["message"]
# 处理深度思考内容reasoning_content
# 多模态 API 返回的 reasoning_content 也是独立的片段
reasoning_content = message.get("reasoning_content", "")
if reasoning_content:
delta_reasoning = reasoning_content
full_reasoning += reasoning_content
logger.info(f" - reasoning_delta: {delta_reasoning}")
data = {
"id": f"chatcmpl-{generate_unique_id()}",
"object": "chat.completion.chunk",
"created": get_current_timestamp(),
"model": model,
"choices": [
{
"index": 0,
"delta": {"reasoning_content": delta_reasoning},
"finish_reason": None,
}
],
}
yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
continue
# 处理普通内容
content_items = message.get("content", [])
text = ""
for item in content_items:
if isinstance(item, dict) and "text" in item:
text += item["text"]
if len(text) > len(full_content):
delta = text[len(full_content) :]
full_content = text
# 打印每个 chunk 的内容
logger.info(f" - text_len: {len(text)}, full_len: {len(full_content)}")
logger.info(f" - text: {text}")
# 多模态 API 返回的 content 是独立的片段(不是累积的),直接作为 delta
if text:
delta = text
full_content += text
logger.info(f" - delta: {delta}")
data = {
"id": f"chatcmpl-{generate_unique_id()}",
@ -401,8 +634,10 @@ class DashScopeAdapter(BaseAdapter):
],
}
yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
except (KeyError, IndexError, TypeError):
pass
except (KeyError, IndexError, TypeError) as e:
logger.warning(f"[DashScope] 解析多模态响应异常: {str(e)}")
else:
logger.warning(f"[DashScope] 非200响应: status_code={resp.status_code}, code={resp.code}, message={resp.message}")
finish = {
"id": f"chatcmpl-{generate_unique_id()}",
@ -414,6 +649,18 @@ class DashScopeAdapter(BaseAdapter):
yield f"data: {json.dumps(finish, ensure_ascii=False)}\n\n"
yield "data: [DONE]\n\n"
# 打印流式响应结果
logger.info(f"[DashScope] 流式多模态响应完成:")
logger.info(f" - chunks: {chunk_count}")
logger.info(f" - content_length: {len(full_content)} 字符")
if full_reasoning:
logger.info(f" - reasoning_length: {len(full_reasoning)} 字符")
logger.info(
f" - content_preview: {full_content[:200]}..."
if len(full_content) > 200
else f" - content: {full_content}"
)
return StreamingResponse(generator(), media_type="text/event-stream")
def _sync_multimodal_chat(
@ -424,22 +671,64 @@ class DashScopeAdapter(BaseAdapter):
from dashscope import MultiModalConversation
resp = MultiModalConversation.call(
model=model,
messages=messages,
stream=False,
max_tokens=request.max_tokens,
temperature=request.temperature,
)
# 检查是否启用深度思考
thinking_enabled = request.deep_thinking and self._supports_thinking(model)
logger.info(f"[DashScope] 深度思考: {thinking_enabled} (request={request.deep_thinking}, supports={self._supports_thinking(model)})")
logger.info(f"[DashScope] 开始非流式多模态响应...")
logger.info(f" - model: {model}")
logger.info(f" - max_tokens: {request.max_tokens}")
logger.info(f" - temperature: {request.temperature}")
# 打印 API 调用参数
api_params = {
"model": model,
"messages": messages,
"stream": False,
"max_tokens": request.max_tokens,
"temperature": request.temperature,
}
# 添加深度思考参数
if thinking_enabled:
api_params["enable_thinking"] = True
logger.info(f"[DashScope] 多模态 API 调用参数:")
logger.info(f" - model: {api_params['model']}")
logger.info(f" - stream: {api_params['stream']}")
logger.info(f" - max_tokens: {api_params['max_tokens']}")
logger.info(f" - temperature: {api_params['temperature']}")
if thinking_enabled:
logger.info(f" - enable_thinking: True")
try:
resp = MultiModalConversation.call(**api_params)
except Exception as e:
logger.error(f"[DashScope] 多模态 API 调用异常: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return JSONResponse(
status_code=500,
content={"error": f"DashScope API 调用异常: {str(e)}"},
)
if resp.status_code == 200:
try:
content_items = resp.output.choices[0]["message"]["content"]
message = resp.output.choices[0]["message"]
content_items = message.get("content", [])
text = ""
for item in content_items:
if isinstance(item, dict) and "text" in item:
text += item["text"]
# 构建响应消息
response_message = {"role": "assistant", "content": text}
# 处理深度思考内容
reasoning_content = message.get("reasoning_content")
if reasoning_content:
response_message["reasoning_content"] = reasoning_content
response = {
"id": f"chatcmpl-{generate_unique_id()}",
"object": "chat.completion",
@ -448,18 +737,38 @@ class DashScopeAdapter(BaseAdapter):
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": text},
"message": response_message,
"finish_reason": "stop",
}
],
}
# 打印响应结果
logger.info(f"[DashScope] 多模态响应成功:")
logger.info(f" - status_code: {resp.status_code}")
logger.info(f" - content_length: {len(text)} 字符")
if reasoning_content:
logger.info(f" - reasoning_length: {len(reasoning_content)} 字符")
logger.info(
f" - content_preview: {text[:200]}..."
if len(text) > 200
else f" - content: {text}"
)
return JSONResponse(content=response)
except (KeyError, IndexError, TypeError) as e:
logger.error(f"[DashScope] 解析多模态响应异常: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return JSONResponse(
status_code=500,
content={"error": f"Parse error: {str(e)}"},
)
logger.error(f"[DashScope] 多模态请求失败:")
logger.error(f" - status_code: {resp.status_code}")
logger.error(f" - code: {resp.code}")
logger.error(f" - message: {resp.message}")
return JSONResponse(
status_code=500,
content={"error": f"DashScope Error: {resp.code} - {resp.message}"},

View File

@ -17,6 +17,17 @@ logger = get_logger()
# GLM 模型配置
GLM_MODELS = [
ModelInfo(
id="glm-5",
name="GLM-5",
description="Coding与长程Agent能力SOTA",
max_tokens=128000,
provider="ZhipuAI",
supports_thinking=True,
supports_web_search=False,
supports_vision=True,
supports_files=True,
),
ModelInfo(
id="glm-4.6v",
name="GLM-4.6V(推荐)",

View File

@ -11,15 +11,15 @@ from .base import BaseAdapter
# 模型前缀到平台名称的映射
MODEL_PREFIX_MAP = {
# 智谱 GLM
"glm-": "glm",
"glm": "glm",
# 阿里云百炼Qwen 系列)
"qwen-": "dashscope",
"qwen": "dashscope",
# OpenAI
"gpt-": "openai",
"o1-": "openai",
"o3-": "openai",
"gpt": "openai",
"o1": "openai",
"o3": "openai",
# Deepseek
"deepseek-": "deepseek",
"deepseek": "deepseek",
}
# 已注册的适配器实例

View File

@ -8,9 +8,9 @@
</div>
<div class="logo-glow"></div>
</div>
<h1 class="title">Kexue AI 智能助手</h1>
<h1 class="title">大学教育助手</h1>
<p class="subtitle">
大学生用GPT把自己学废了? Study模式拒绝直接给答案引导学生思考
等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入等待输入
</p>
</div>

View File

@ -251,7 +251,8 @@ function autoResize() {
textarea.style.height = "auto";
const maxHeight = isExpanded.value ? 400 : 160;
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;
// 1px
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)+1}px`;
}
//
@ -631,7 +632,7 @@ onMounted(() => {
textarea {
width: 100%;
min-height: 24px;
min-height: 25px;
max-height: 160px;
padding: 8px 0;
border: none;