新增设备身份管理、认证限流、并发通道、Webhook路由、RBAC权限控制、SSE/轮询降级等全套网关通道功能,包含: 1. 设备身份生成与签名验证 2. 设备令牌认证与速率限制 3. 内存+数据库双重设备注册表 4. 并发通道限流管理 5. Webhook安全处理与路由 6. RBAC权限校验系统 7. OpenAI API兼容适配层 8. Tailscale认证支持 9. HTTP轮询降级机制
162 lines
5.0 KiB
Python
162 lines
5.0 KiB
Python
"""OpenAI API 兼容层 — 请求格式转换与 SS 流式响应"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
from collections.abc import AsyncIterator
|
|
|
|
OPENAI_CHAT_COMPLETIONS_PATH = "/v1/chat/completions"
|
|
OPENAI_MODELS_PATH = "/v1/models"
|
|
|
|
|
|
class OpenAIMessageConverter:
|
|
@staticmethod
|
|
def extract_system_prompt(messages: list[dict]) -> str | None:
|
|
for msg in messages:
|
|
if msg.get("role") == "system":
|
|
return msg.get("content", "").strip() or None
|
|
return None
|
|
|
|
@staticmethod
|
|
def extract_user_query(messages: list[dict]) -> tuple[str, str | None]:
|
|
user_texts: list[str] = []
|
|
image_base64: str | None = None
|
|
|
|
for msg in messages:
|
|
if msg.get("role") != "user":
|
|
continue
|
|
content = msg.get("content", "")
|
|
if isinstance(content, str):
|
|
user_texts.append(content.strip())
|
|
elif isinstance(content, list):
|
|
for part in content:
|
|
if isinstance(part, dict):
|
|
if part.get("type") == "text":
|
|
user_texts.append(part.get("text", "").strip())
|
|
elif part.get("type") == "image_url":
|
|
url = part.get("image_url", {}).get("url", "")
|
|
if url.startswith("data:"):
|
|
image_base64 = url.split(",", 1)[1] if "," in url else url
|
|
|
|
return "\n".join(t for t in user_texts if t), image_base64
|
|
|
|
@staticmethod
|
|
def extract_conversation_history(messages: list[dict]) -> list[dict]:
|
|
history: list[dict] = []
|
|
for msg in messages:
|
|
role = msg.get("role", "")
|
|
content = msg.get("content", "")
|
|
if role in ("user", "assistant") and content:
|
|
if isinstance(content, str) and content.strip():
|
|
history.append({"role": role, "content": content.strip()})
|
|
return history
|
|
|
|
@staticmethod
|
|
def extract_model_name(request_data: dict) -> str:
|
|
return request_data.get("model", "default")
|
|
|
|
@staticmethod
|
|
def extract_stream_flag(request_data: dict) -> bool:
|
|
return bool(request_data.get("stream", False))
|
|
|
|
@staticmethod
|
|
def extract_max_tokens(request_data: dict) -> int | None:
|
|
return request_data.get("max_tokens")
|
|
|
|
@staticmethod
|
|
def extract_temperature(request_data: dict) -> float | None:
|
|
return request_data.get("temperature")
|
|
|
|
|
|
def _format_sse_chunk(
|
|
content: str | None = None,
|
|
status: str = "streaming",
|
|
*,
|
|
finish_reason: str | None = None,
|
|
model: str = "default",
|
|
index: int = 0,
|
|
) -> bytes:
|
|
delta: dict = {}
|
|
if content is not None:
|
|
delta["content"] = content
|
|
if status is not None:
|
|
payload: dict = {
|
|
"id": f"chatcmpl-{int(time.time() * 1000)}",
|
|
"object": "chat.completion.chunk",
|
|
"created": int(time.time()),
|
|
"model": model,
|
|
"choices": [
|
|
{
|
|
"index": index,
|
|
"delta": delta,
|
|
"finish_reason": finish_reason,
|
|
}
|
|
],
|
|
}
|
|
return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n".encode()
|
|
|
|
return b"data: [DONE]\n\n"
|
|
|
|
|
|
def _format_openai_non_stream_response(
|
|
content: str,
|
|
*,
|
|
model: str = "default",
|
|
finish_reason: str = "stop",
|
|
) -> dict:
|
|
return {
|
|
"id": f"chatcmpl-{int(time.time() * 1000)}",
|
|
"object": "chat.completion",
|
|
"created": int(time.time()),
|
|
"model": model,
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"message": {
|
|
"role": "assistant",
|
|
"content": content,
|
|
},
|
|
"finish_reason": finish_reason,
|
|
}
|
|
],
|
|
"usage": {
|
|
"prompt_tokens": 0,
|
|
"completion_tokens": 0,
|
|
"total_tokens": 0,
|
|
},
|
|
}
|
|
|
|
|
|
async def format_sse_stream(
|
|
raw_stream: AsyncIterator[bytes],
|
|
*,
|
|
model: str = "default",
|
|
) -> AsyncIterator[bytes]:
|
|
accumulated: list[str] = []
|
|
async for raw in raw_stream:
|
|
try:
|
|
data = json.loads(raw.decode("utf-8"))
|
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
continue
|
|
|
|
status = data.get("status", "")
|
|
content = data.get("response", "")
|
|
|
|
if status == "streaming" and content:
|
|
accumulated.append(content)
|
|
yield _format_sse_chunk(content=content, status="streaming", model=model)
|
|
elif status == "finished":
|
|
pass
|
|
elif status == "error":
|
|
error_msg = data.get("error_message", content or "未知错误")
|
|
yield _format_sse_chunk(
|
|
content=error_msg,
|
|
status="error",
|
|
finish_reason="error",
|
|
model=model,
|
|
)
|
|
return
|
|
|
|
yield _format_sse_chunk(content="", status=None, finish_reason="stop", model=model)
|