# 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): ```json { "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): ```python 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: ```python # retoor """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 `muse` CLI: `/tmp/mitm_probe/request.txt` (internal evidence, 11 requests) ## 8. Changelog | Date | Change | |---|---| | 2026-10-11 | Consolidated research + verbatim transpiler source into one document. |