返回 JoyAI-Echo
converters.py
1 """Convert Chat Completions messages/tools to Responses API format."""
2
3 from __future__ import annotations
4
5 import json
6 from typing import Any
7
8
9 def convert_messages(messages: list[dict[str, Any]]) -> tuple[str, list[dict[str, Any]]]:
10 """Convert Chat Completions messages to Responses API input items.
11
12 Returns ``(system_prompt, input_items)`` where *system_prompt* is extracted
13 from any ``system`` role message and *input_items* is the Responses API
14 ``input`` array.
15 """
16 system_prompt = ""
17 input_items: list[dict[str, Any]] = []
18
19 for idx, msg in enumerate(messages):
20 role = msg.get("role")
21 content = msg.get("content")
22
23 if role == "system":
24 system_prompt = content if isinstance(content, str) else ""
25 continue
26
27 if role == "user":
28 input_items.append(convert_user_message(content))
29 continue
30
31 if role == "assistant":
32 if isinstance(content, str) and content:
33 input_items.append({
34 "type": "message", "role": "assistant",
35 "content": [{"type": "output_text", "text": content}],
36 "status": "completed", "id": f"msg_{idx}",
37 })
38 for tool_call in msg.get("tool_calls", []) or []:
39 fn = tool_call.get("function") or {}
40 call_id, item_id = split_tool_call_id(tool_call.get("id"))
41 input_items.append({
42 "type": "function_call",
43 "id": item_id or f"fc_{idx}",
44 "call_id": call_id or f"call_{idx}",
45 "name": fn.get("name"),
46 "arguments": fn.get("arguments") or "{}",
47 })
48 continue
49
50 if role == "tool":
51 call_id, _ = split_tool_call_id(msg.get("tool_call_id"))
52 output_text = content if isinstance(content, str) else json.dumps(content, ensure_ascii=False)
53 input_items.append({"type": "function_call_output", "call_id": call_id, "output": output_text})
54
55 return system_prompt, input_items
56
57
58 def convert_user_message(content: Any) -> dict[str, Any]:
59 """Convert a user message's content to Responses API format.
60
61 Handles plain strings, ``text`` blocks -> ``input_text``, and
62 ``image_url`` blocks -> ``input_image``.
63 """
64 if isinstance(content, str):
65 return {"role": "user", "content": [{"type": "input_text", "text": content}]}
66 if isinstance(content, list):
67 converted: list[dict[str, Any]] = []
68 for item in content:
69 if not isinstance(item, dict):
70 continue
71 if item.get("type") == "text":
72 converted.append({"type": "input_text", "text": item.get("text", "")})
73 elif item.get("type") == "image_url":
74 url = (item.get("image_url") or {}).get("url")
75 if url:
76 converted.append({"type": "input_image", "image_url": url, "detail": "auto"})
77 if converted:
78 return {"role": "user", "content": converted}
79 return {"role": "user", "content": [{"type": "input_text", "text": ""}]}
80
81
82 def convert_tools(tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
83 """Convert OpenAI function-calling tool schema to Responses API flat format."""
84 converted: list[dict[str, Any]] = []
85 for tool in tools:
86 fn = (tool.get("function") or {}) if tool.get("type") == "function" else tool
87 name = fn.get("name")
88 if not name:
89 continue
90 params = fn.get("parameters") or {}
91 converted.append({
92 "type": "function",
93 "name": name,
94 "description": fn.get("description") or "",
95 "parameters": params if isinstance(params, dict) else {},
96 })
97 return converted
98
99
100 def split_tool_call_id(tool_call_id: Any) -> tuple[str, str | None]:
101 """Split a compound ``call_id|item_id`` string.
102
103 Returns ``(call_id, item_id)`` where *item_id* may be ``None``.
104 """
105 if isinstance(tool_call_id, str) and tool_call_id:
106 if "|" in tool_call_id:
107 call_id, item_id = tool_call_id.split("|", 1)
108 return call_id, item_id or None
109 return tool_call_id, None
110 return "call_0", None
111
111 lines PYTHON