"""Convert between OpenAI chat format and AI SDK v6 format.""" from __future__ import annotations import json from nanoid import generate as nanoid from config import MODEL_MAP _ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789" def _gen_id(prefix: str = "msg", size: int = 12) -> str: return f"{prefix}_{nanoid(_ALPHABET, size)}" def _resolve_model(model: str) -> str: """Map short model name to assistant-ui API identifier.""" if model in MODEL_MAP: return MODEL_MAP[model] if "/" in model: return model return f"openai/{model}" def _guess_media_type(url: str) -> str: """Infer media type from a data-URI or file extension.""" if url.startswith("data:"): # data:image/png;base64,... header = url.split(",", 1)[0] if ";" in header: return header[5:].split(";")[0] # strip "data:" prefix return header[5:] lower = url.lower() for ext, mt in ( (".png", "image/png"), (".jpg", "image/jpeg"), (".jpeg", "image/jpeg"), (".gif", "image/gif"), (".webp", "image/webp"), (".svg", "image/svg+xml"), ): if ext in lower: return mt return "image/png" def _convert_tools(tools: list[dict] | None) -> dict: """Convert OpenAI tools list to AI SDK frontend tools format. OpenAI format: [{"type": "function", "function": {"name": "...", "description": "...", "parameters": {...}}}] AI SDK format: {"tool_name": {"description": "...", "parameters": {...}}} """ if not tools: return {} result = {} for tool in tools: if tool.get("type") != "function": continue func = tool.get("function", {}) name = func.get("name", "") if not name: continue entry: dict = {"parameters": func.get("parameters", {"type": "object"})} if func.get("description"): entry["description"] = func["description"] result[name] = entry return result def openai_to_ai_sdk( messages: list[dict], model: str, tools: list[dict] | None = None, ) -> dict: """Convert an OpenAI chat-completions request to AI SDK v6 payload.""" sdk_messages: list[dict] = [] for msg in messages: role = msg.get("role", "") content = msg.get("content", "") if role == "system": # Inject as AI SDK system message in the messages array. # convertToModelMessages() handles role:"system" and forwards # it as a model-level system message — bypasses the server's # missing top-level "system" param in streamText(). text = content if isinstance(content, str) else "" if text: sdk_messages.append({ "role": "system", "parts": [{"type": "text", "text": text}], "metadata": {"custom": {}}, "id": _gen_id("sys"), }) continue if role == "user": if isinstance(content, list): parts = [] for part in content: if isinstance(part, str): parts.append({"type": "text", "text": part}) elif isinstance(part, dict): ptype = part.get("type", "") if ptype == "text": parts.append({"type": "text", "text": part["text"]}) elif ptype == "image_url": # OpenAI vision format → AI SDK file part img = part.get("image_url", {}) url = img.get("url", "") if isinstance(img, dict) else str(img) media_type = _guess_media_type(url) parts.append({ "type": "file", "mediaType": media_type, "url": url, }) else: parts = [{"type": "text", "text": str(content)}] sdk_messages.append({ "role": "user", "parts": parts, "metadata": {"custom": {}}, "id": _gen_id("msg"), }) elif role == "assistant": parts = [] # Text content if isinstance(content, str) and content: parts.append({"type": "text", "text": content}) elif isinstance(content, list): for part in content: if isinstance(part, dict) and part.get("type") == "text": parts.append({"type": "text", "text": part["text"]}) # Tool calls → tool-invocation parts # Initially set state to "input-available" (args ready, no result yet) for tc in msg.get("tool_calls") or []: func = tc.get("function", {}) try: args = json.loads(func.get("arguments", "{}")) except (json.JSONDecodeError, TypeError): args = {} parts.append({ "type": "tool-invocation", "toolCallId": tc.get("id", _gen_id("call")), "toolName": func.get("name", ""), "input": args, "state": "input-available", }) sdk_messages.append({ "role": "assistant", "parts": parts, "metadata": {"custom": {}}, "id": _gen_id("msg"), }) elif role == "tool": # Tool result — attach output to the matching tool-invocation # in the preceding assistant message, using AI SDK v6 field names. tool_call_id = msg.get("tool_call_id", "") # Parse result: try JSON object first, fall back to string if isinstance(content, str): try: result_obj = json.loads(content) except (json.JSONDecodeError, TypeError): result_obj = content else: result_obj = content for prev in reversed(sdk_messages): if prev["role"] != "assistant": continue for part in prev["parts"]: if ( part.get("type") == "tool-invocation" and part.get("toolCallId") == tool_call_id ): part["state"] = "output-available" part["output"] = result_obj break break return { "system": "", "config": {"modelName": _resolve_model(model)}, "tools": _convert_tools(tools), "id": _gen_id("thread"), "messages": sdk_messages, "trigger": "submit-message", "metadata": {}, }