47 KiB
Muse Responses API: protocol documentation and transpiler
Status snapshot: 2026-10-11. Research complete and archived under
research/muse-responses-openai-transpiler/. Transpiler module complete,
stdlib-only, 21/21 unit tests green; devplacepy gateway integration in
progress (module tests green, gateway wiring under test).
1. What this document is
One place for everything learned about the API that Meta's muse CLI
speaks, plus the complete bidirectional transpiler between that wire
format and classic OpenAI Chat Completions. Part 1 is the research
(what the protocol is, with evidence). Part 2 is the transpiler
(contract, usage, full source). Nothing here is theorized: request-side
claims come from a captured MITM log of the real CLI, schema claims
come from the official OpenAI docs fetched the same day, and every
mapping in the code is covered by a test.
2. The wire protocol
2.1 Endpoints
The CLI talks to https://api.meta.ai/v1 (from its own
~/.config/muse/auth.json, providers.meta.api_base_url, overridable
per-run with --base-url):
| Method | Path | Purpose |
|---|---|---|
POST |
/responses |
Model turn (OpenAI Responses API shape) |
GET |
/muse-code/models |
Model listing (Meta-specific path) |
The capture (/tmp/mitm_probe/request.txt, 795KB, 11 requests) shows
one models GET plus ten /responses POSTs in two alternating shapes:
the main turn (full 19-function tool namespace, reasoning.effort
max) and a reminder turn (single submit_reminder_decision tool,
effort low).
2.2 Headers
Every request carries Accept: text/event-stream (streaming SSE),
Content-Type: application/json, Authorization: Bearer ...
(redacted in the capture, never logged), a client UA
(muse-build/...), x-client-id: tbh:exec, x-tbh-session-id, and
x-meta-ai-gateway-session-id. The x-tbh-* / x-meta-ai-gateway-*
trio is Meta-specific; the rest is stock OpenAI practice.
2.3 Request schema
All ten POSTs share these top-level keys: model, instructions,
input, tools, stream, store, max_output_tokens, reasoning,
include, prompt_cache_key. Observed values: stream: true,
store: false, max_output_tokens: 128000,
reasoning: {effort, summary: "auto"},
include: ["reasoning.encrypted_content"],
prompt_cache_key: "tbh:skill-reminder:...", model
muse-spark-1.3-contributor. The client is stateless (store: false)
and replays encrypted reasoning items instead of server-side
conversation state.
Illustrative Meta-flavored request (trimmed, synthetic content):
{
"model": "muse-spark-1.3-contributor",
"instructions": "<25-44KB system string>",
"input": [
{"type": "message", "role": "developer", "content": "<31KB reminder>"},
{"type": "message", "role": "user", "content": "hi"}
],
"tools": [{"type": "namespace", "name": "muse", "tools": ["...19 builtins..."]}],
"stream": true,
"store": false,
"max_output_tokens": 128000,
"reasoning": {"effort": "max", "summary": "auto"},
"include": ["reasoning.encrypted_content"],
"prompt_cache_key": "tbh:skill-reminder:..."
}
2.4 The tools namespace
tools holds exactly one entry: {type: "namespace", name: "muse"}
with 19 nested harness functions (workflow, read_file, search,
write_file, edit_file, bash, web_fetch, web_search,
monitor, cron_*, subagent_*, read_skill, add_memory,
edit_memory, read_memory, request_user_input, snooze_reminder,
update_goal, ...). The reminder shape swaps this for a single
submit_reminder_decision function. namespace is not in the public
reference: it is a Meta vendor extension, executed natively by the
harness in its own sandbox. tai-style tool loops do not use it at all;
they constrain the answer to {content, tool_calls} JSON via
--output-schema and execute calls themselves.
2.5 Meta extensions vs stock OpenAI
Only four things are Meta-specific: the namespace tool type, the
x-tbh-* / x-meta-ai-gateway-* headers, the /muse-code/models
path, and the prompt_cache_key values. Everything else matches the
public Responses reference field for field, which is why a generic
transpiler works.
2.6 Response object and SSE (unobserved, per reference)
The probe stub answered HTTP 500 to all 11 requests, so no Meta
response bytes were ever seen. The transpiler therefore implements the
documented standard shapes, which the Meta client demonstrably shares
on the request side: a Response object with output items of type
message (output_text / refusal parts) and function_call
(call_id, name, string arguments), plus usage {input_tokens, output_tokens, total_tokens}; streamed as typed SSE
(response.created, response.output_text.delta, response.output_item.added,
response.function_call_arguments.delta, response.completed,
response.failed / error). A recording forward proxy would close
this gap.
2.7 CLI translation
The CLI converts SSE into newline-delimited JSON on stdout
(run.output.delta text events, run.terminal.* terminal events),
which is what muse_run parses. Timeouts, retries, and the
--no-session-log / --session-id session modes all live above this
layer and are unaffected by the protocol shape.
2.8 System messages
System guidance works through three channels, and Meta's client uses
two of them at once: top-level instructions (preferred, 25KB+
observed), developer-role input messages (31KB observed), and
system-role input messages (spec-allowed, equivalent for older
clients). The transpiler folds leading Chat system/developer messages
into instructions, keeps mid-transcript ones as role-kept Items, and
reverses instructions into a leading Chat system message.
3. Transpiler contract
Pure functions returning (converted, warnings). Core conversions are
exact both ways; everything outside the core warns on drop. Unknown
top-level fields warn too, so a gateway dialect field can never vanish
silently.
3.1 Exact mappings
| Chat Completions | Responses |
|---|---|
messages[] |
input[] Items |
| system/developer (leading) | instructions |
assistant tool_calls |
function_call (id to call_id) |
tool role |
function_call_output |
| externally-tagged function tools | internally-tagged function tools |
response_format |
text.format |
max_tokens |
max_output_tokens |
reasoning_effort |
reasoning.effort |
| delta chunks | typed SSE events |
The strict default flips explicitly: Chat omits it (non-strict),
Responses omits it (attempts strict), so Chat-to-Responses pins
strict: false.
3.2 Fixed loss table
Chat to Responses drops: n (single generation only), stop,
seed, logprobs, sampling penalties, logit_bias, prediction,
modalities, audio, web_search_options, audio/file content parts,
custom tools, message name/audio. Responses to Chat drops:
namespace/built-in tools, reasoning and other native Items,
previous_response_id / conversation, truncation, include,
extra choices, non-text call-output parts.
Thinking dialects collapse into reasoning: explicit
reasoning_effort / reasoning.effort wins; thinking: {type},
think, enable_thinking map disabled to effort: "none" and leave
enabled as upstream default.
3.3 Streaming and errors
ResponsesToChatStream maps text and function-argument deltas
exactly, emits role: assistant once, yields the terminal chunk with
the real finish reason and usage from response.completed, and turns
response.failed / error into Chat stream error payloads (also
stashed on failed). ChatToResponsesStream is best effort: exact
deltas, synthesized lifecycle, assembled response.completed.
Response failures normalize to Chat-style {error: ...} objects via
responses_error_object. Synthesized ids (chatcmpl_transpiled,
resp_transpiled, msg_transpiled_0) are documented fakes.
4. Usage
Configure a devplacepy provider row with base URL
https://api.meta.ai/v1/responses, protocol Responses API, and a
route whose target model is muse-spark-1.3-contributor; clients keep
sending normal POST /v1/chat/completions. Verified example (zero
warnings both ways):
from responses import chat_to_responses, responses_to_chat
chat = {
"model": "gpt-6-astra",
"messages": [
{"role": "system", "content": "Be terse."},
{"role": "user", "content": "Weather in Berlin?"},
{"role": "assistant", "content": None, "tool_calls": [
{"id": "call_1", "type": "function",
"function": {"name": "get_weather",
"arguments": '{"location": "Berlin"}'}}]},
{"role": "tool", "tool_call_id": "call_1", "content": "12C, rain"},
],
"tools": [{"type": "function", "function": {
"name": "get_weather", "description": "City weather",
"parameters": {"type": "object",
"properties": {"location": {"type": "string"}}}}}],
"tool_choice": "auto",
}
resp, w1 = chat_to_responses(chat) # instructions + 3 Items, flat tool
back, w2 = responses_to_chat(resp) # [system, user, assistant, tool]
assert (w1, w2) == ([], [])
5. Complete transpiler source
Stdlib only (typing). Verbatim copy of the tested module:
# retoor <retoor@molodetz.nl>
"""Chat Completions <-> Responses protocol transpiler.
Lets the gateway keep its normal OpenAI surface (`POST /v1/chat/completions`
in, ChatCompletion JSON or Chat chunks out) while forwarding to a
Responses-protocol upstream such as Meta's `.../v1/responses`. The provider
row selects the protocol; `handle_chat` calls into this module at the send
boundary and on the way back.
Every lossy mapping is reported in a warnings list, never dropped silently.
Core conversions (text, images, function tools/calls, structured outputs,
scalars, common stream events) are exact both ways. Known drops: Chat `n`,
`stop`, seeds/penalties, audio/file parts, custom tools; Responses
`namespace`/built-in tools, reasoning items, `previous_response_id` /
conversation state, `truncation`, `include`, extra choices.
Grounded in the official OpenAI migration guide and both API references
(retrieved 2026-10-11) plus the Meta wire capture; Meta response bytes were
never observed, so the Response-object/SSE shapes are the OpenAI standard
ones the Meta client demonstrably shares on the request side.
"""
from __future__ import annotations
from typing import Any, Optional
def _text_of_content(content: Any) -> str:
if isinstance(content, str):
return content
if isinstance(content, list):
return "".join(
str(part.get("text") or part.get("refusal") or "")
for part in content
if isinstance(part, dict)
)
return ""
def _chat_parts_to_input_parts(parts: Any, warn: list, where: str) -> list:
out = []
for part in parts or []:
if not isinstance(part, dict):
continue
kind = part.get("type")
if kind == "text":
out.append({"type": "input_text", "text": part.get("text") or ""})
elif kind == "image_url":
image = part.get("image_url") or {}
item = {"type": "input_image", "image_url": image.get("url") or ""}
if image.get("detail"):
item["detail"] = image["detail"]
out.append(item)
elif kind == "refusal":
warn.append(
"%s: refusal part has no Responses input shape; kept as text"
% (where,)
)
out.append({"type": "input_text", "text": part.get("refusal") or ""})
else:
warn.append(
"%s: dropped content part type %r (no verified Responses mapping)"
% (where, kind)
)
return out
def _chat_messages_to_input(messages: Any) -> tuple[str, list, list]:
"""Split Chat messages into Responses `instructions` + `input` items."""
warn: list = []
instructions: list = []
items: list = []
leading = True
for message in messages or []:
if not isinstance(message, dict):
continue
role = message.get("role") or "user"
if role in ("system", "developer") and leading:
text = _text_of_content(message.get("content")).strip()
if text:
instructions.append(text)
continue
leading = False
if role == "tool":
items.append(
{
"type": "function_call_output",
"call_id": message.get("tool_call_id") or "",
"output": _text_of_content(message.get("content")),
}
)
continue
content = message.get("content")
if isinstance(content, list):
content = _chat_parts_to_input_parts(
content, warn, "message[%s]" % (role,)
)
calls = (
list(message.get("tool_calls") or []) if role == "assistant" else []
)
legacy = (
(message.get("function_call") or {}) if role == "assistant" else {}
)
empty = content is None or content == "" or content == []
if not (role == "assistant" and empty and (calls or legacy.get("name"))):
items.append(
{
"type": "message",
"role": role,
"content": content if content is not None else "",
}
)
if role == "assistant":
for call in calls:
func = (
(call.get("function") or {})
if isinstance(call, dict)
else {}
)
items.append(
{
"type": "function_call",
"call_id": (
(call.get("id") if isinstance(call, dict) else "")
or ""
),
"name": func.get("name") or "",
"arguments": func.get("arguments") or "",
}
)
if legacy.get("name"):
warn.append(
"deprecated assistant.function_call converted without a "
"stable id"
)
items.append(
{
"type": "function_call",
"call_id": "call_legacy",
"name": legacy.get("name") or "",
"arguments": legacy.get("arguments") or "",
}
)
for dropped in ("name", "audio"):
if message.get(dropped) is not None:
warn.append(
"message[%s]: dropped %r (no Responses input field)"
% (role, dropped)
)
return "\n\n".join(instructions), items, warn
def _chat_tool_to_responses(tool: Any, warn: list) -> Optional[dict]:
if not isinstance(tool, dict):
return None
if tool.get("type") == "function":
func = tool.get("function") or {}
out = {
"type": "function",
"name": func.get("name") or "",
"parameters": func.get("parameters")
or {"type": "object", "properties": {}},
}
if func.get("description") is not None:
out["description"] = func["description"]
# Chat is non-strict by default; Responses attempts strict mode when
# the key is omitted, so pin the Chat default explicitly.
out["strict"] = bool(func.get("strict", False))
return out
warn.append(
"dropped tool type %r (only function tools transpile)"
% (tool.get("type"),)
)
return None
def _chat_tool_choice_to_responses(choice: Any, warn: list) -> Any:
if choice in (None, "none", "auto", "required"):
return choice
if isinstance(choice, dict):
if choice.get("type") == "function":
return {
"type": "function",
"name": (choice.get("function") or {}).get("name") or "",
}
if choice.get("type") == "allowed_tools":
tools = []
for entry in choice.get("tools") or []:
if isinstance(entry, dict) and entry.get("type") == "function":
tools.append(
{
"type": "function",
"name": (entry.get("function") or {}).get("name")
or "",
}
)
else:
warn.append("dropped allowed_tools entry %r" % (entry,))
return {
"type": "allowed_tools",
"mode": choice.get("mode") or "auto",
"tools": tools,
}
warn.append("dropped tool_choice %r (no Responses equivalent)" % (choice,))
return None
def _chat_response_format_to_text(fmt: Any, warn: list) -> Optional[dict]:
if not isinstance(fmt, dict):
return None
kind = fmt.get("type") or "text"
if kind == "text":
return {"format": {"type": "text"}}
if kind == "json_object":
return {"format": {"type": "json_object"}}
if kind == "json_schema":
schema = fmt.get("json_schema") or {}
out = {
"format": {
"type": "json_schema",
"name": schema.get("name") or "result",
"schema": schema.get("schema") or {},
}
}
if schema.get("description") is not None:
out["format"]["description"] = schema["description"]
if schema.get("strict") is not None:
out["format"]["strict"] = schema["strict"]
return out
warn.append("dropped response_format type %r" % (kind,))
return None
# Chat scalar fields with a direct Responses counterpart.
_CHAT_SCALARS = (
("model", "model"),
("temperature", "temperature"),
("top_p", "top_p"),
("stream", "stream"),
("store", "store"),
("metadata", "metadata"),
("user", "user"),
("service_tier", "service_tier"),
("parallel_tool_calls", "parallel_tool_calls"),
("prompt_cache_key", "prompt_cache_key"),
("max_tokens", "max_output_tokens"),
("max_completion_tokens", "max_output_tokens"),
)
# Chat fields with no Responses equivalent (verified against the reference).
_CHAT_DROPPED = (
"n",
"stop",
"seed",
"logprobs",
"frequency_penalty",
"presence_penalty",
"logit_bias",
"prediction",
"modalities",
"audio",
"web_search_options",
"prompt_cache_options",
"prompt_cache_retention",
"safety_identifier",
"stream_options",
"verbosity",
)
# Top-level Chat fields the transpiler understands (mapped, converted, or
# explicitly dropped above). Anything else is warned about, never silently
# ignored, so a gateway dialect field can never vanish here unnoticed.
_CHAT_KNOWN = (
frozenset(src for src, _ in _CHAT_SCALARS)
| frozenset(_CHAT_DROPPED)
| frozenset(
{
"messages",
"tools",
"tool_choice",
"response_format",
"reasoning_effort",
"reasoning",
"thinking",
"think",
"enable_thinking",
"top_logprobs",
}
)
)
_THINKING_FALSE = {False, 0, "0", "false", "no", "off", "none", "disabled"}
_THINKING_TRUE = {True, 1, "1", "true", "yes", "on", "enabled"}
def _thinking_flag(value: Any) -> Optional[bool]:
if isinstance(value, str):
value = value.strip().lower()
if value in _THINKING_FALSE:
return False
if value in _THINKING_TRUE:
return True
return None
def _chat_thinking_to_reasoning(chat: dict, out: dict, warn: list) -> None:
# Explicit effort wins; a Responses-shaped reasoning dict passes its
# effort through; chat-dialect thinking flags collapse to effort "none"
# when disabled and to "upstream default" (unset) when enabled.
if chat.get("reasoning_effort") is not None:
out["reasoning"] = {"effort": chat["reasoning_effort"]}
return
reasoning = chat.get("reasoning")
if isinstance(reasoning, dict):
if reasoning.get("effort") is not None:
out["reasoning"] = {"effort": reasoning["effort"]}
extra = sorted(k for k in reasoning if k != "effort")
if extra:
warn.append(
"dropped reasoning keys %r (only effort transpiles)"
% (extra,)
)
return
enabled = _thinking_flag(reasoning.get("enabled"))
if enabled is False:
out["reasoning"] = {"effort": "none"}
return
if enabled is True:
return
thinking = chat.get("thinking")
if isinstance(thinking, dict):
enabled = _thinking_flag(thinking.get("type"))
if enabled is False:
out["reasoning"] = {"effort": "none"}
return
if enabled is True:
return
for key in ("think", "enable_thinking"):
enabled = _thinking_flag(chat.get(key))
if enabled is False:
out["reasoning"] = {"effort": "none"}
return
if enabled is True:
return
def chat_to_responses(chat: dict) -> tuple[dict, list]:
"""Convert a `POST /v1/chat/completions` body to `POST /responses`."""
warn: list = []
out: dict = {}
for src, dst in _CHAT_SCALARS:
if chat.get(src) is not None and dst not in out:
out[dst] = chat[src]
if chat.get("top_logprobs") is not None:
out["top_logprobs"] = chat["top_logprobs"]
_chat_thinking_to_reasoning(chat, out, warn)
instructions, items, converted_warnings = _chat_messages_to_input(
chat.get("messages")
)
warn += converted_warnings
if instructions:
out["instructions"] = instructions
out["input"] = items
tools = []
for tool in chat.get("tools") or []:
converted = _chat_tool_to_responses(tool, warn)
if converted is not None:
tools.append(converted)
if tools:
out["tools"] = tools
if chat.get("tool_choice") is not None:
choice = _chat_tool_choice_to_responses(chat["tool_choice"], warn)
if choice is not None:
out["tool_choice"] = choice
if chat.get("response_format") is not None:
text = _chat_response_format_to_text(chat["response_format"], warn)
if text is not None:
out["text"] = text
for key in _CHAT_DROPPED:
if chat.get(key) is not None:
warn.append("dropped chat field %r (no Responses equivalent)" % (key,))
for key in sorted(chat):
if key not in _CHAT_KNOWN:
warn.append("ignored unknown chat field %r" % (key,))
return out, warn
def _input_parts_to_chat_parts(parts: Any, warn: list, where: str) -> list:
out = []
for part in parts or []:
if not isinstance(part, dict):
continue
kind = part.get("type")
if kind in ("input_text", "output_text", "text"):
out.append({"type": "text", "text": part.get("text") or ""})
elif kind == "input_image":
image = {"url": part.get("image_url") or ""}
if part.get("detail"):
image["detail"] = part["detail"]
out.append({"type": "image_url", "image_url": image})
elif kind == "refusal":
out.append({"type": "refusal", "refusal": part.get("refusal") or ""})
else:
warn.append(
"%s: dropped input part type %r (no Chat mapping)"
% (where, kind)
)
return out
def _call_output_to_text(output: Any, warn: list) -> str:
if isinstance(output, str):
return output
if isinstance(output, list):
texts = []
for part in output:
if not isinstance(part, dict):
continue
if part.get("type") == "input_text":
texts.append(part.get("text") or "")
else:
warn.append(
"dropped function_call_output part type %r "
"(tool messages carry text only)" % (part.get("type"),)
)
return "".join(texts)
return ""
def _responses_input_to_chat_messages(
input_items: Any, warn: list
) -> list:
messages: list = []
pending_calls: list = []
for item in input_items or []:
if isinstance(item, str):
messages.append({"role": "user", "content": item})
continue
if not isinstance(item, dict):
continue
kind = item.get("type") or "message"
if kind == "message":
content = item.get("content")
if isinstance(content, list):
content = _input_parts_to_chat_parts(
content, warn, "input.message"
)
messages.append(
{"role": item.get("role") or "user", "content": content}
)
continue
if kind == "function_call":
pending_calls.append(
{
"id": item.get("call_id") or item.get("id") or "",
"type": "function",
"function": {
"name": item.get("name") or "",
"arguments": item.get("arguments") or "",
},
}
)
continue
if kind == "function_call_output":
if pending_calls:
messages.append(
{
"role": "assistant",
"content": None,
"tool_calls": pending_calls,
}
)
pending_calls = []
messages.append(
{
"role": "tool",
"tool_call_id": item.get("call_id") or "",
"content": _call_output_to_text(item.get("output"), warn),
}
)
continue
warn.append(
"dropped input item type %r "
"(reasoning/native-tool items do not replay in Chat)" % (kind,)
)
if pending_calls:
messages.append(
{"role": "assistant", "content": None, "tool_calls": pending_calls}
)
return messages
def _responses_tool_to_chat(tool: Any, warn: list) -> Optional[dict]:
if not isinstance(tool, dict):
return None
if tool.get("type") == "function":
func: dict = {"name": tool.get("name") or ""}
if tool.get("description") is not None:
func["description"] = tool["description"]
if tool.get("parameters") is not None:
func["parameters"] = tool["parameters"]
if tool.get("strict") is not None:
func["strict"] = tool["strict"]
return {"type": "function", "function": func}
warn.append(
"dropped tool type %r (namespaces and builtins have no Chat shape)"
% (tool.get("type"),)
)
return None
def _responses_tool_choice_to_chat(choice: Any, warn: list) -> Any:
if choice in (None, "none", "auto", "required"):
return choice
if isinstance(choice, dict):
if choice.get("type") == "function":
return {
"type": "function",
"function": {"name": choice.get("name") or ""},
}
if choice.get("type") == "allowed_tools":
tools = []
for entry in choice.get("tools") or []:
if isinstance(entry, dict) and entry.get("type") == "function":
tools.append(
{
"type": "function",
"function": {"name": entry.get("name") or ""},
}
)
else:
warn.append("dropped allowed_tools entry %r" % (entry,))
return {
"type": "allowed_tools",
"mode": choice.get("mode") or "auto",
"tools": tools,
}
warn.append("dropped tool_choice %r (no Chat equivalent)" % (choice,))
return None
def _responses_text_to_response_format(text: Any, warn: list) -> Optional[dict]:
fmt = (text or {}).get("format") if isinstance(text, dict) else None
if not isinstance(fmt, dict):
return None
kind = fmt.get("type") or "text"
if kind == "text":
return None
if kind == "json_object":
return {"type": "json_object"}
if kind == "json_schema":
schema: dict = {
"name": fmt.get("name") or "result",
"schema": fmt.get("schema") or {},
}
if fmt.get("description") is not None:
schema["description"] = fmt["description"]
if fmt.get("strict") is not None:
schema["strict"] = fmt["strict"]
return {"type": "json_schema", "json_schema": schema}
warn.append("dropped text.format type %r" % (kind,))
return None
_RESPONSES_SCALARS = (
("model", "model"),
("temperature", "temperature"),
("top_p", "top_p"),
("stream", "stream"),
("store", "store"),
("metadata", "metadata"),
("user", "user"),
("service_tier", "service_tier"),
("parallel_tool_calls", "parallel_tool_calls"),
("prompt_cache_key", "prompt_cache_key"),
("top_logprobs", "top_logprobs"),
("max_output_tokens", "max_tokens"),
)
_RESPONSES_DROPPED = (
"previous_response_id",
"conversation",
"truncation",
"include",
"background",
"max_tool_calls",
"prompt",
"prompt_cache_options",
"prompt_cache_retention",
"safety_identifier",
"stream_options",
"context_management",
"moderation",
"access_programs",
)
_RESPONSES_KNOWN = (
frozenset(src for src, _ in _RESPONSES_SCALARS)
| frozenset(_RESPONSES_DROPPED)
| frozenset(
{
"input",
"instructions",
"tools",
"tool_choice",
"text",
"reasoning",
}
)
)
def responses_to_chat(resp: dict) -> tuple[dict, list]:
"""Convert a `POST /responses` body to `POST /v1/chat/completions`."""
warn: list = []
out: dict = {}
for src, dst in _RESPONSES_SCALARS:
if resp.get(src) is not None:
out[dst] = resp[src]
reasoning = resp.get("reasoning") or {}
if isinstance(reasoning, dict) and reasoning.get("effort") is not None:
out["reasoning_effort"] = reasoning["effort"]
raw_input = resp.get("input")
items = (
[{"type": "message", "role": "user", "content": raw_input}]
if isinstance(raw_input, str)
else raw_input
)
messages = _responses_input_to_chat_messages(items, warn)
instructions = resp.get("instructions")
if isinstance(instructions, str) and instructions.strip():
messages.insert(0, {"role": "system", "content": instructions})
elif isinstance(instructions, list):
messages[0:0] = _responses_input_to_chat_messages(instructions, warn)
out["messages"] = messages
tools = []
for tool in resp.get("tools") or []:
converted = _responses_tool_to_chat(tool, warn)
if converted is not None:
tools.append(converted)
if tools:
out["tools"] = tools
if resp.get("tool_choice") is not None:
choice = _responses_tool_choice_to_chat(resp["tool_choice"], warn)
if choice is not None:
out["tool_choice"] = choice
if resp.get("text") is not None:
fmt = _responses_text_to_response_format(resp["text"], warn)
if fmt is not None:
out["response_format"] = fmt
for key in _RESPONSES_DROPPED:
if resp.get(key) is not None:
warn.append(
"dropped responses field %r (no Chat equivalent)" % (key,)
)
for key in sorted(resp):
if key not in _RESPONSES_KNOWN:
warn.append("ignored unknown responses field %r" % (key,))
return out, warn
def responses_object_to_chat_completion(resp: dict) -> tuple[dict, list]:
"""Convert a Responses object to a ChatCompletion object (one choice)."""
warn: list = []
text: list = []
refusal: list = []
calls: list = []
for item in resp.get("output") or []:
if not isinstance(item, dict):
continue
if item.get("type") == "message":
for part in item.get("content") or []:
if not isinstance(part, dict):
continue
if part.get("type") == "output_text":
text.append(part.get("text") or "")
elif part.get("type") == "refusal":
refusal.append(part.get("refusal") or "")
else:
warn.append(
"dropped output part type %r" % (part.get("type"),)
)
elif item.get("type") == "function_call":
calls.append(
{
"id": item.get("call_id") or item.get("id") or "",
"type": "function",
"function": {
"name": item.get("name") or "",
"arguments": item.get("arguments") or "",
},
}
)
else:
warn.append("dropped output item type %r" % (item.get("type"),))
message: dict = {"role": "assistant", "content": "".join(text) or None}
if refusal:
message["refusal"] = "".join(refusal)
if calls:
message["tool_calls"] = calls
incomplete = (resp.get("incomplete_details") or {}).get("reason")
if calls:
finish = "tool_calls"
elif incomplete == "max_output_tokens":
finish = "length"
elif incomplete == "content_filter":
finish = "content_filter"
else:
finish = "stop"
usage = resp.get("usage") or {}
return {
"id": resp.get("id") or "",
"object": "chat.completion",
"created": resp.get("created_at") or 0,
"model": resp.get("model") or "",
"choices": [{"index": 0, "message": message, "finish_reason": finish}],
"usage": {
"prompt_tokens": usage.get("input_tokens") or 0,
"completion_tokens": usage.get("output_tokens") or 0,
"total_tokens": usage.get("total_tokens") or 0,
},
}, warn
def chat_completion_to_responses_object(comp: dict) -> tuple[dict, list]:
"""Convert a ChatCompletion object to a Responses object (choice 0)."""
warn: list = []
choices = comp.get("choices") or []
if len(choices) > 1:
warn.append(
"kept choice 0 of %d (Responses returns one generation)"
% (len(choices),)
)
message = (choices[0] or {}).get("message") if choices else {}
output: list = []
if (message.get("content") or None) is not None or not message.get(
"tool_calls"
):
output.append(
{
"id": "msg_transpiled_0",
"type": "message",
"status": "completed",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": message.get("content") or "",
"annotations": [],
}
],
}
)
if message.get("refusal"):
output.append(
{
"id": "msg_transpiled_refusal",
"type": "message",
"status": "completed",
"role": "assistant",
"content": [
{"type": "refusal", "refusal": message["refusal"]}
],
}
)
for call in message.get("tool_calls") or []:
func = (
(call.get("function") or {}) if isinstance(call, dict) else {}
)
output.append(
{
"type": "function_call",
"call_id": (
(call.get("id") if isinstance(call, dict) else "") or ""
),
"name": func.get("name") or "",
"arguments": func.get("arguments") or "",
}
)
usage = comp.get("usage") or {}
return {
"id": comp.get("id") or "",
"object": "response",
"created_at": comp.get("created") or 0,
"status": "completed",
"model": comp.get("model") or "",
"output": output,
"usage": {
"input_tokens": usage.get("prompt_tokens") or 0,
"output_tokens": usage.get("completion_tokens") or 0,
"total_tokens": usage.get("total_tokens") or 0,
},
}, warn
def responses_failure_message(data: Any) -> Optional[str]:
"""Extract a failure message from a 200 Responses body, if any."""
if not isinstance(data, dict):
return None
error = data.get("error")
if isinstance(error, dict):
message = error.get("message") or error.get("code") or "upstream error"
return str(message)[:300]
if data.get("object") == "response" and data.get("status") in (
"failed",
"incomplete",
"cancelled",
):
details = data.get("incomplete_details") or {}
reason = details.get("reason") or data.get("status")
return "response %s: %s" % (data.get("status"), reason)
return None
def responses_error_object(data: Any) -> Optional[dict]:
"""Normalize a Responses error body to a Chat-style error object."""
if not isinstance(data, dict):
return None
error = data.get("error")
if isinstance(error, dict):
out = {"message": str(error.get("message") or "upstream error")}
if error.get("type") is not None:
out["type"] = error["type"]
if error.get("code") is not None:
out["code"] = error["code"]
return {"error": out}
message = responses_failure_message(data)
if message:
return {"error": {"message": message, "type": "upstream_error"}}
return None
class ResponsesToChatStream:
"""Translate Responses SSE events into ChatCompletion chunks.
Text deltas and function-call argument deltas map exactly; lifecycle
events are consumed; `response.completed` yields the terminal chunk with
the real finish reason and usage. Error events yield `{"error": ...}`
payloads (the Chat stream error shape) and are also stashed on `failed`.
"""
def __init__(self, model: str = ""):
self.model = model
self.calls: dict = {}
self.order: list = []
self.failed: Any = None
self.role_sent = False
self.text_sent = False
def feed(self, event: Any) -> list:
if not isinstance(event, dict):
return []
kind = event.get("type") or ""
if kind == "response.output_text.delta":
delta: dict = {"content": event.get("delta") or ""}
if not self.role_sent:
delta = {"role": "assistant", **delta}
self.role_sent = True
self.text_sent = True
return [self._chunk(delta)]
if kind == "response.output_item.added":
item = event.get("item") or {}
if item.get("type") == "function_call" and item.get("call_id"):
index = self._index(item["call_id"])
return [
self._chunk(
{
"tool_calls": [
{
"index": index,
"id": item["call_id"],
"type": "function",
"function": {
"name": item.get("name") or "",
"arguments": "",
},
}
]
}
)
]
return []
if kind == "response.function_call_arguments.delta":
call_id = event.get("item_id") or ""
return [
self._chunk(
{
"tool_calls": [
{
"index": self._index(call_id),
"function": {
"arguments": event.get("delta") or ""
},
}
]
}
)
]
if kind == "response.completed":
response = event.get("response") or {}
converted, _ = responses_object_to_chat_completion(response)
chunks = []
message = (converted["choices"][0] or {}).get("message") or {}
if message.get("refusal") and not self.text_sent:
chunks.append(self._chunk({"refusal": message["refusal"]}))
chunk = self._chunk({})
chunk["choices"][0]["finish_reason"] = (
converted["choices"][0] or {}
).get("finish_reason")
chunk["usage"] = converted["usage"]
chunks.append(chunk)
return chunks
if kind in ("response.failed", "response.incomplete", "error"):
self.failed = event
payload = responses_error_object(
event.get("response") or event.get("error") or event
) or {"error": {"message": kind, "type": "upstream_error"}}
return [payload]
return []
def _index(self, call_id: str) -> int:
if call_id not in self.calls:
self.calls[call_id] = len(self.order)
self.order.append(call_id)
return self.calls[call_id]
def _chunk(self, delta: dict) -> dict:
return {
"id": "chatcmpl_transpiled",
"object": "chat.completion.chunk",
"created": 0,
"model": self.model,
"choices": [{"index": 0, "delta": delta, "finish_reason": None}],
}
class ChatToResponsesStream:
"""Translate ChatCompletion chunks into Responses SSE events.
Best effort: text and tool-call deltas map exactly, but Chat chunks
carry no lifecycle events, so the synthesized stream skips
`response.created` / `output_item.added` for text and emits
`response.completed` with an assembled object. Streaming call ids are
tracked by tool-call index, per the Chat streaming contract (the id
arrives on the first chunk for each index).
"""
def __init__(self):
self.text: list = []
self.calls: dict = {}
self.order: list = []
self.warnings: list = []
def feed(self, chunk: Any) -> list:
events = []
choices = (chunk or {}).get("choices") or []
delta = (choices[0] or {}).get("delta") if choices else {}
for call in (delta or {}).get("tool_calls") or []:
if not isinstance(call, dict):
continue
index = call.get("index")
func = call.get("function") or {}
call_id = call.get("id")
if call_id and call_id not in self.calls:
self.calls[call_id] = True
self.order.append(call_id)
events.append(
{
"type": "response.output_item.added",
"output_index": len(self.order) - 1,
"item": {
"type": "function_call",
"call_id": call_id,
"name": func.get("name") or "",
"arguments": "",
},
}
)
if not call_id and isinstance(index, int) and index < len(
self.order
):
call_id = self.order[index]
if func.get("arguments"):
if not call_id:
self.warnings.append(
"tool-call arguments arrived before any call id"
)
events.append(
{
"type": "response.function_call_arguments.delta",
"item_id": call_id or "",
"output_index": 0,
"delta": func["arguments"],
}
)
piece = (delta or {}).get("content")
if piece:
self.text.append(piece)
events.append(
{
"type": "response.output_text.delta",
"item_id": "msg_transpiled_0",
"output_index": 0,
"content_index": 0,
"delta": piece,
}
)
finish = (choices[0] or {}).get("finish_reason") if choices else None
if finish:
output = [
{
"id": "msg_transpiled_0",
"type": "message",
"status": "completed",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "".join(self.text),
"annotations": [],
}
],
}
]
events.append(
{
"type": "response.completed",
"response": {
"id": "resp_transpiled",
"object": "response",
"status": "completed",
"model": (chunk or {}).get("model") or "",
"output": output,
},
}
)
return events
6. Verification status and known gaps
Module tests: 21/21 green
(tests/unit/services/openai_gateway/responses.py in devplacepy),
covering both request directions, both object converters, both
stream translators, error/failure shapes, thinking-dialect collapse,
and every documented drop. A real captured Meta request round-trips
through responses_to_chat and back with only the expected
namespace and include warnings. The weather example in section 4
was executed with zero warnings both ways.
Known gaps, stated plainly:
- Meta response and SSE bytes were never observed; that side rests on the documented standard shapes. A recording forward proxy closes it.
- Audio and file content parts warn and drop; their exact cross-API shapes were not verified.
- Synthesized stream/object ids are documented fakes, not upstream ids.
- The devplacepy gateway wiring (provider protocol setting, send-time transpile, Responses stream relay) was mid-integration at snapshot time with gateway unit failures under diagnosis; only the module itself is green.
7. Sources
Retrieved 2026-10-11. Full dossiers live in
research/muse-responses-openai-transpiler/.
- OpenAI, Migrate to the Responses API: https://developers.openai.com/api/docs/guides/migrate-to-responses
- OpenAI, Responses API reference: https://developers.openai.com/api/reference/resources/responses
- OpenAI, Chat Completions API reference: https://developers.openai.com/api/reference/resources/chat
- Local MITM capture of the real
museCLI:/tmp/mitm_probe/request.txt(internal evidence, 11 requests)
8. Changelog
| Date | Change |
|---|---|
| 2026-10-11 | Consolidated research + verbatim transpiler source into one document. |