Merge branch 'feat/openai-compatible'
This commit is contained in:
commit
547ba742b7
|
|
@ -16,6 +16,7 @@ uploads
|
||||||
.venv
|
.venv
|
||||||
__pycache__
|
__pycache__
|
||||||
.claude
|
.claude
|
||||||
|
*.db
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|
@ -27,3 +28,10 @@ __pycache__
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Skills
|
||||||
|
.skills
|
||||||
|
.agent
|
||||||
|
.agents
|
||||||
|
.trae
|
||||||
|
skills-lock.json
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,150 +0,0 @@
|
||||||
"""
|
|
||||||
GLM-4.6V 平台路由处理器(zai-sdk)
|
|
||||||
所有智谱 GLM 相关逻辑均集中在此文件,main.py 无感知任何平台细节。
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from fastapi.responses import JSONResponse, StreamingResponse
|
|
||||||
|
|
||||||
from utils.helpers import generate_unique_id, get_current_timestamp
|
|
||||||
from utils.logger import log_info
|
|
||||||
|
|
||||||
|
|
||||||
def init():
|
|
||||||
"""
|
|
||||||
初始化 GLM 后端:验证 API Key 是否配置。
|
|
||||||
由 main.py 在启动时调用(若 LLM_BACKEND=glm)。
|
|
||||||
"""
|
|
||||||
api_key = os.getenv("ZHIPU_API_KEY") or os.getenv("GLM_API_KEY")
|
|
||||||
if not api_key:
|
|
||||||
raise ValueError(
|
|
||||||
"GLM 模式需要设置环境变量 ZHIPU_API_KEY(在 https://open.bigmodel.cn 申请)"
|
|
||||||
)
|
|
||||||
log_info(f"[GLM] 初始化完成,ZHIPU_API_KEY 已配置")
|
|
||||||
|
|
||||||
|
|
||||||
async def chat_handler(body: dict):
|
|
||||||
"""
|
|
||||||
GLM 聊天处理器(对外接口与百炼 chat_endpoint_handler 完全兼容)。
|
|
||||||
流式/非流式自动适配,支持图像、文档附件、联网搜索、深度思考。
|
|
||||||
"""
|
|
||||||
from utils.glm_adapter import glm_chat_sync, glm_stream_generator
|
|
||||||
|
|
||||||
if not isinstance(body, dict):
|
|
||||||
raise HTTPException(status_code=400, detail="请求体必须是 JSON 对象")
|
|
||||||
|
|
||||||
messages = body.get("messages", [])
|
|
||||||
model = body.get("model", "glm-4.6v")
|
|
||||||
stream = body.get("stream", True)
|
|
||||||
temperature = body.get("temperature", 0.7)
|
|
||||||
max_tokens = body.get("max_tokens", body.get("maxTokens", 2000))
|
|
||||||
# 区分搜索模式:深度搜索 > 简单搜索 > 不搜索
|
|
||||||
if body.get("deepSearch", False):
|
|
||||||
web_search = "deep"
|
|
||||||
elif body.get("webSearch", False):
|
|
||||||
web_search = "simple"
|
|
||||||
else:
|
|
||||||
web_search = False
|
|
||||||
deep_think = body.get("deepThinking", False)
|
|
||||||
files = body.get("files", [])
|
|
||||||
|
|
||||||
# 兼容前端简化格式(非 messages 结构)
|
|
||||||
if not messages:
|
|
||||||
msg_text = body.get("message", "")
|
|
||||||
sys_prompt = body.get("systemPrompt", "你是一个智能助手。")
|
|
||||||
user_content = (
|
|
||||||
msg_text
|
|
||||||
if isinstance(msg_text, list)
|
|
||||||
else [{"type": "text", "text": msg_text}]
|
|
||||||
)
|
|
||||||
messages = [
|
|
||||||
{"role": "system", "content": sys_prompt},
|
|
||||||
{"role": "user", "content": user_content},
|
|
||||||
]
|
|
||||||
|
|
||||||
log_info(
|
|
||||||
f"[GLM] model={model} stream={stream} web_search={web_search} "
|
|
||||||
f"thinking={deep_think} files={len(files)} msgs={len(messages)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if stream:
|
|
||||||
return StreamingResponse(
|
|
||||||
glm_stream_generator(
|
|
||||||
messages=messages,
|
|
||||||
model=model,
|
|
||||||
temperature=temperature,
|
|
||||||
max_tokens=max_tokens,
|
|
||||||
files=files or None,
|
|
||||||
web_search=web_search,
|
|
||||||
deep_thinking=deep_think,
|
|
||||||
),
|
|
||||||
media_type="text/event-stream",
|
|
||||||
)
|
|
||||||
|
|
||||||
result = glm_chat_sync(
|
|
||||||
messages=messages,
|
|
||||||
model=model,
|
|
||||||
temperature=temperature,
|
|
||||||
max_tokens=max_tokens,
|
|
||||||
files=files or None,
|
|
||||||
web_search=web_search,
|
|
||||||
deep_thinking=deep_think,
|
|
||||||
)
|
|
||||||
resp = {
|
|
||||||
"id": f"chatcmpl-{generate_unique_id()}",
|
|
||||||
"object": "chat.completion",
|
|
||||||
"created": get_current_timestamp(),
|
|
||||||
"model": result["model"],
|
|
||||||
"choices": [
|
|
||||||
{
|
|
||||||
"index": 0,
|
|
||||||
"message": {"role": "assistant", "content": result["content"]},
|
|
||||||
"finish_reason": "stop",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
if result.get("usage"):
|
|
||||||
resp["usage"] = result["usage"]
|
|
||||||
return JSONResponse(content=resp)
|
|
||||||
|
|
||||||
|
|
||||||
def models_handler():
|
|
||||||
"""返回 GLM 可用模型列表"""
|
|
||||||
return {
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": "glm-4.6v",
|
|
||||||
"name": "GLM-4.6V(推荐)",
|
|
||||||
"description": "最新旗舰模型,支持文本/图像/文档/深度思考",
|
|
||||||
"maxTokens": 128000,
|
|
||||||
"provider": "ZhipuAI",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "glm-4-flash",
|
|
||||||
"name": "GLM-4 Flash",
|
|
||||||
"description": "高性价比文本模型(0.2元/千token)",
|
|
||||||
"maxTokens": 128000,
|
|
||||||
"provider": "ZhipuAI",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "glm-4v-plus-0111",
|
|
||||||
"name": "GLM-4V Plus",
|
|
||||||
"description": "图像 + PDF/DOCX 原生多模态",
|
|
||||||
"maxTokens": 128000,
|
|
||||||
"provider": "ZhipuAI",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "glm-z1-flash",
|
|
||||||
"name": "GLM-Z1 Flash",
|
|
||||||
"description": "深度思考推理模型",
|
|
||||||
"maxTokens": 128000,
|
|
||||||
"provider": "ZhipuAI",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"object": "list",
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
"""
|
||||||
|
会话管理路由 - 对话 CRUD 和文件上传
|
||||||
|
|
||||||
|
与平台无关的通用功能,不涉及任何 LLM 平台细节。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from fastapi import File, HTTPException, UploadFile
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.append(str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
from database import get_db
|
||||||
|
from utils.helpers import generate_unique_id
|
||||||
|
from utils.logger import log_error, log_exception, log_info
|
||||||
|
|
||||||
|
# 配置上传目录
|
||||||
|
upload_dir = Path(__file__).parent.parent / "uploads"
|
||||||
|
upload_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 会话管理 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
async def get_conversations_handler():
|
||||||
|
"""获取所有对话处理器"""
|
||||||
|
db = get_db()
|
||||||
|
return db.list_conversations()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_conversation_handler(conversation_id: str):
|
||||||
|
"""获取特定对话处理器"""
|
||||||
|
db = get_db()
|
||||||
|
conversation = db.get_conversation(conversation_id)
|
||||||
|
if not conversation:
|
||||||
|
raise HTTPException(status_code=404, detail="对话不存在")
|
||||||
|
return conversation
|
||||||
|
|
||||||
|
|
||||||
|
async def save_conversation_handler(data: dict):
|
||||||
|
"""保存或更新对话处理器"""
|
||||||
|
try:
|
||||||
|
db = get_db()
|
||||||
|
conversation_id = data.get("id")
|
||||||
|
|
||||||
|
# 检查是否已存在
|
||||||
|
existing = db.get_conversation(conversation_id) if conversation_id else None
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# 更新现有会话
|
||||||
|
return db.update_conversation(conversation_id, data)
|
||||||
|
else:
|
||||||
|
# 创建新会话
|
||||||
|
return db.create_conversation(data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log_error(f"Error saving conversation: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_conversation_handler(conversation_id: str):
|
||||||
|
"""删除对话处理器"""
|
||||||
|
db = get_db()
|
||||||
|
success = db.delete_conversation(conversation_id)
|
||||||
|
if success:
|
||||||
|
return {"success": True, "message": "删除成功"}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="对话不存在")
|
||||||
|
|
||||||
|
|
||||||
|
# ── 文件上传 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
async def upload_file_handler(file: UploadFile = File(...)):
|
||||||
|
"""文件上传处理器"""
|
||||||
|
try:
|
||||||
|
# 允许的 MIME 类型(宽松策略)
|
||||||
|
allowed_types = {
|
||||||
|
# 图片
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp",
|
||||||
|
"image/bmp",
|
||||||
|
"image/svg+xml",
|
||||||
|
# 文本类
|
||||||
|
"text/plain",
|
||||||
|
"text/csv",
|
||||||
|
"text/markdown",
|
||||||
|
"text/html",
|
||||||
|
"text/xml",
|
||||||
|
"application/json",
|
||||||
|
"application/xml",
|
||||||
|
# PDF
|
||||||
|
"application/pdf",
|
||||||
|
# Office 文档
|
||||||
|
"application/msword",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"application/vnd.ms-excel",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"application/vnd.ms-powerpoint",
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 允许的扩展名(兜底:MIME 类型可能被浏览器误判)
|
||||||
|
allowed_extensions = {
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".png",
|
||||||
|
".gif",
|
||||||
|
".webp",
|
||||||
|
".bmp",
|
||||||
|
".txt",
|
||||||
|
".md",
|
||||||
|
".csv",
|
||||||
|
".json",
|
||||||
|
".xml",
|
||||||
|
".yaml",
|
||||||
|
".yml",
|
||||||
|
".log",
|
||||||
|
".pdf",
|
||||||
|
".doc",
|
||||||
|
".docx",
|
||||||
|
".xls",
|
||||||
|
".xlsx",
|
||||||
|
".ppt",
|
||||||
|
".pptx",
|
||||||
|
".py",
|
||||||
|
".js",
|
||||||
|
".ts",
|
||||||
|
".html",
|
||||||
|
".css",
|
||||||
|
}
|
||||||
|
|
||||||
|
file_extension = Path(file.filename).suffix.lower()
|
||||||
|
|
||||||
|
if (
|
||||||
|
file.content_type not in allowed_types
|
||||||
|
and file_extension not in allowed_extensions
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"不支持的文件类型: {file.content_type}({file_extension})",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 生成唯一文件名
|
||||||
|
unique_filename = f"{int(datetime.utcnow().timestamp())}_{generate_unique_id()}{file_extension}"
|
||||||
|
file_path = upload_dir / unique_filename
|
||||||
|
|
||||||
|
# 保存文件到本地(临时缓存)
|
||||||
|
content = await file.read()
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
# 文件关闭后再上传到 OSS
|
||||||
|
from utils.oss_uploader import upload_file as oss_upload
|
||||||
|
|
||||||
|
oss_result = oss_upload(str(file_path))
|
||||||
|
file_url = oss_result["url"]
|
||||||
|
|
||||||
|
# 返回文件信息
|
||||||
|
result = {
|
||||||
|
"url": file_url,
|
||||||
|
"name": file.filename,
|
||||||
|
"size": len(content),
|
||||||
|
"mimeType": file.content_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info(f"File uploaded: {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log_error(f"Upload error: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"上传失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def serve_upload_handler(filename: str):
|
||||||
|
"""提供上传文件访问处理器"""
|
||||||
|
file_path = upload_dir / filename
|
||||||
|
if not file_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="文件不存在")
|
||||||
|
|
||||||
|
return FileResponse(str(file_path))
|
||||||
|
|
||||||
|
|
||||||
|
# ── 停止生成 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
async def stop_generation_handler(message_id: str = None):
|
||||||
|
"""停止生成处理器"""
|
||||||
|
message = (
|
||||||
|
f"已发出停止指令,消息ID: {message_id}" if message_id else "已发出停止指令"
|
||||||
|
)
|
||||||
|
return {"success": True, "message": message}
|
||||||
|
|
@ -45,28 +45,22 @@ from utils.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger()
|
logger = get_logger()
|
||||||
|
|
||||||
|
# ── 初始化数据库 ───────────────────────────────────────────────────────
|
||||||
|
from database import init_db
|
||||||
|
|
||||||
|
init_db()
|
||||||
|
|
||||||
# ── 加载环境变量 ──────────────────────────────────────────────────────
|
# ── 加载环境变量 ──────────────────────────────────────────────────────
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
LLM_BACKEND = os.getenv("LLM_BACKEND", "dashscope").lower().strip()
|
# ── 会话管理路由处理器 ────────────────────────────────────────────────
|
||||||
if LLM_BACKEND not in {"dashscope", "glm"}:
|
from api.conversation_routes import (delete_conversation_handler,
|
||||||
logger.warning(f"未知的 LLM_BACKEND='{LLM_BACKEND}',回退到 dashscope")
|
|
||||||
LLM_BACKEND = "dashscope"
|
|
||||||
|
|
||||||
# ── 动态加载平台模块 ──────────────────────────────────────────────────
|
|
||||||
if LLM_BACKEND == "glm":
|
|
||||||
import api.chat_routes_glm as _platform
|
|
||||||
else:
|
|
||||||
import api.chat_routes as _platform
|
|
||||||
|
|
||||||
_platform.init() # 各平台自行完成初始化(API Key 校验等)
|
|
||||||
|
|
||||||
# 通用路由处理器(文件上传、会话管理等,与平台无关,统一用百炼路由中的实现)
|
|
||||||
from api.chat_routes import (delete_conversation_handler,
|
|
||||||
get_conversation_handler,
|
get_conversation_handler,
|
||||||
get_conversations_handler,
|
get_conversations_handler,
|
||||||
save_conversation_handler, serve_upload_handler,
|
save_conversation_handler,
|
||||||
stop_generation_handler, upload_file_handler)
|
serve_upload_handler,
|
||||||
|
stop_generation_handler,
|
||||||
|
upload_file_handler)
|
||||||
|
|
||||||
# ── OpenAI 兼容网关初始化 ───────────────────────────────────────────────
|
# ── OpenAI 兼容网关初始化 ───────────────────────────────────────────────
|
||||||
from api.openai_gateway import init_adapters, router as openai_router
|
from api.openai_gateway import init_adapters, router as openai_router
|
||||||
|
|
@ -110,12 +104,11 @@ async def health_check():
|
||||||
return {
|
return {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"default_backend": LLM_BACKEND,
|
|
||||||
"available_providers": get_available_providers(),
|
"available_providers": get_available_providers(),
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"openai_compatible": "/v1/chat/completions",
|
"openai_compatible": "/v1/chat/completions",
|
||||||
"legacy": "/api/chat-ui/chat",
|
|
||||||
"models": "/v1/models",
|
"models": "/v1/models",
|
||||||
|
"conversations": "/api/chat-ui/conversations",
|
||||||
},
|
},
|
||||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
}
|
}
|
||||||
|
|
@ -234,7 +227,6 @@ if __name__ == "__main__":
|
||||||
print(f" 模型列表 : http://localhost:{port}/v1/models")
|
print(f" 模型列表 : http://localhost:{port}/v1/models")
|
||||||
print("-" * 60)
|
print("-" * 60)
|
||||||
print(f" 可用平台 : {', '.join(available) or '无(请配置 API Key)'}")
|
print(f" 可用平台 : {', '.join(available) or '无(请配置 API Key)'}")
|
||||||
print(f" 默认平台 : {LLM_BACKEND} (向后兼容模式)")
|
|
||||||
print("-" * 60)
|
print("-" * 60)
|
||||||
print(" 使用方法:")
|
print(" 使用方法:")
|
||||||
print(" curl -X POST http://localhost:8000/v1/chat/completions \\")
|
print(" curl -X POST http://localhost:8000/v1/chat/completions \\")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue