"""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)