@oneciel-ai/ciel-runtime 0.2.37 → 0.2.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +5 -2
  3. package/ciel_runtime.py +94 -96
  4. package/ciel_runtime_support/anthropic_model_policy.py +20 -3
  5. package/ciel_runtime_support/architecture.py +5 -0
  6. package/ciel_runtime_support/channel_inflight.py +4 -4
  7. package/ciel_runtime_support/channel_injection.py +56 -1
  8. package/ciel_runtime_support/channel_mcp_tools.py +34 -0
  9. package/ciel_runtime_support/channel_message_policy.py +9 -1
  10. package/ciel_runtime_support/channel_message_prompt.py +23 -0
  11. package/ciel_runtime_support/channel_pending_injection.py +86 -32
  12. package/ciel_runtime_support/channel_pending_poll.py +6 -2
  13. package/ciel_runtime_support/channel_runtime_environment.py +0 -11
  14. package/ciel_runtime_support/channel_terminal_context.py +0 -6
  15. package/ciel_runtime_support/channel_terminal_proxy.py +3 -42
  16. package/ciel_runtime_support/channel_transcript_repository.py +16 -1
  17. package/ciel_runtime_support/channel_wake_claim_repository.py +0 -19
  18. package/ciel_runtime_support/channel_wake_context.py +6 -13
  19. package/ciel_runtime_support/channel_wake_delivery_repository.py +50 -0
  20. package/ciel_runtime_support/chat_http_controller.py +88 -5
  21. package/ciel_runtime_support/claude_environment.py +100 -3
  22. package/ciel_runtime_support/claude_launch_assembly.py +4 -0
  23. package/ciel_runtime_support/claude_session_socket.py +181 -0
  24. package/ciel_runtime_support/cli_application_context.py +18 -10
  25. package/ciel_runtime_support/cli_dispatch.py +19 -3
  26. package/ciel_runtime_support/cli_parser.py +2 -0
  27. package/ciel_runtime_support/cli_usage.py +3 -2
  28. package/ciel_runtime_support/codex_backend_context.py +8 -3
  29. package/ciel_runtime_support/codex_completion_gate.py +198 -0
  30. package/ciel_runtime_support/codex_turn_recovery.py +240 -83
  31. package/ciel_runtime_support/compatibility_protocol.py +5 -2
  32. package/ciel_runtime_support/config_migrations.py +79 -0
  33. package/ciel_runtime_support/config_repository.py +7 -0
  34. package/ciel_runtime_support/context_summary_policy.py +24 -4
  35. package/ciel_runtime_support/external_event_menu.py +107 -0
  36. package/ciel_runtime_support/external_event_receiver.py +19 -1
  37. package/ciel_runtime_support/launch_state.py +2 -0
  38. package/ciel_runtime_support/managed_tool_injection.py +81 -0
  39. package/ciel_runtime_support/muse_runtime_context.py +291 -0
  40. package/ciel_runtime_support/ollama_thinking.py +12 -26
  41. package/ciel_runtime_support/prelaunch.py +13 -0
  42. package/ciel_runtime_support/prelaunch_launch_panel.py +2 -1
  43. package/ciel_runtime_support/prompt_compaction.py +39 -0
  44. package/ciel_runtime_support/provider_files_proxy.py +248 -0
  45. package/ciel_runtime_support/provider_model_identity.py +10 -2
  46. package/ciel_runtime_support/provider_option_cli.py +1 -1
  47. package/ciel_runtime_support/provider_request_access.py +54 -1
  48. package/ciel_runtime_support/provider_responses_passthrough.py +152 -15
  49. package/ciel_runtime_support/providers/alibaba.py +34 -1
  50. package/ciel_runtime_support/providers/anthropic.py +18 -1
  51. package/ciel_runtime_support/providers/meta.py +252 -21
  52. package/ciel_runtime_support/pseudo_tool_parser.py +17 -1
  53. package/ciel_runtime_support/remote_bridge.py +4 -0
  54. package/ciel_runtime_support/remote_instructions.py +5 -0
  55. package/ciel_runtime_support/remote_memory.py +1 -1
  56. package/ciel_runtime_support/responses_cache_diagnostics.py +82 -0
  57. package/ciel_runtime_support/responses_custom_tool_bridge.py +282 -0
  58. package/ciel_runtime_support/responses_input_compatibility.py +19 -3
  59. package/ciel_runtime_support/router_http.py +301 -4
  60. package/ciel_runtime_support/router_observability_context.py +26 -0
  61. package/ciel_runtime_support/router_server_context.py +1 -0
  62. package/ciel_runtime_support/runtime_adapters.py +31 -0
  63. package/ciel_runtime_support/runtime_constants.py +23 -1
  64. package/ciel_runtime_support/runtime_input_gateway.py +85 -5
  65. package/ciel_runtime_support/runtime_input_status.py +150 -0
  66. package/ciel_runtime_support/runtime_launch.py +35 -4
  67. package/ciel_runtime_support/runtime_paths.py +1 -0
  68. package/ciel_runtime_support/speech_http_controller.py +3 -2
  69. package/ciel_runtime_support/streaming_anthropic.py +3 -0
  70. package/ciel_runtime_support/tool_call_events.py +98 -0
  71. package/ciel_runtime_support/transcript_delta_delivery.py +140 -1
  72. package/ciel_runtime_support/ui_text.py +1 -0
  73. package/ciel_runtime_support/web_search_result_events.py +130 -0
  74. package/ciel_runtime_support/web_ui.py +1 -0
  75. package/ciel_runtime_support/windows_conpty.py +92 -8
  76. package/ciel_runtime_support/workspace_mcp.py +4 -1
  77. package/ciel_runtime_support/workspace_state.py +1 -0
  78. package/docs/Configuration.md +20 -4
  79. package/docs/MCP-Channels.md +79 -6
  80. package/docs/Managed-Tool-Injection.md +25 -0
  81. package/docs/Module-Map.md +4 -0
  82. package/docs/Muse-Code.md +73 -0
  83. package/docs/Observability.md +39 -0
  84. package/docs/Providers.md +29 -2
  85. package/docs/Remote-Bridge.md +12 -0
  86. package/docs/Router.md +1 -0
  87. package/docs/journal/2026/09/01/diagnostics/anthropic/router/fable51-usage-credits.okf +70 -0
  88. package/docs/journal/2026/09/01/implementation/claude/session-socket/all-input-paths/default-delivery.okf +133 -0
  89. package/docs/journal/2026/09/01/implementation/claude/session-socket/windows-direct-input.okf +112 -0
  90. package/docs/journal/2026/09/02/diagnostics/cache/alibaba/qwen38-post-restart-hit-rate.okf +107 -0
  91. package/docs/journal/2026/09/02/diagnostics/cache/ollama-cloud/kimi-k3-codex-hit-rate.okf +91 -0
  92. package/docs/journal/2026/09/02/diagnostics/codex/tui/statusline-cache-metrics.okf +44 -0
  93. package/docs/journal/2026/09/02/diagnostics/input/transport/fallback-behavior.okf +31 -0
  94. package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-agent-turn-recovery.okf +125 -0
  95. package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-parameters.okf +128 -0
  96. package/docs/journal/2026/09/02/diagnostics/remote/mia/socket-tui-visibility.okf +47 -0
  97. package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-latency.okf +108 -0
  98. package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-vs-gpt56-latency.okf +92 -0
  99. package/docs/journal/2026/09/02/implementation/anthropic/context/one-million-defaults.okf +85 -0
  100. package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.okf +104 -0
  101. package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.png +0 -0
  102. package/docs/journal/2026/09/02/implementation/cache/alibaba/responses-session-cache.okf +97 -0
  103. package/docs/journal/2026/09/02/implementation/providers/alibaba/qwen38/0902-parameter-alignment.okf +97 -0
  104. package/docs/journal/2026/09/02/implementation/providers/meta/multimodal-tools/contributor-protocol-adoption.okf +140 -0
  105. package/docs/journal/2026/09/02/implementation/providers/meta/muse-spark-1.3-support.okf +53 -0
  106. package/docs/journal/2026/09/02/monitoring/cache/alibaba/cielarvis-qwen38-live-hit-rate.okf +858 -0
  107. package/docs/journal/2026/09/02/operations/release/main-merge-local-deploy.okf +36 -0
  108. package/docs/journal/2026/09/02/operations/release/nightly/local-cache-deployment.okf +54 -0
  109. package/docs/journal/2026/09/02/operations/release/nightly/ollama-kimi-recovery-model-audit-deployment.okf +58 -0
  110. package/docs/journal/2026/09/02/operations/release/nightly/qwen38-cache-improvement-deployment.okf +72 -0
  111. package/docs/journal/2026/09/02/research/cache/alibaba/qwen38-hit-rate-improvement.okf +233 -0
  112. package/docs/journal/2026/09/02/research/cache/codex/provider-scope.okf +77 -0
  113. package/docs/journal/2026/09/02/research/providers/ollama-cloud/desktop-model-exhaustive-audit.okf +159 -0
  114. package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-cache-hit.okf +148 -0
  115. package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-required-schema.okf +127 -0
  116. package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-resumed-session-stall.okf +160 -0
  117. package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-substantive-dangling-action.okf +94 -0
  118. package/docs/journal/2026/09/03/diagnostics/remote/kevin/codex-gpt-early-turn-completion.okf +162 -0
  119. package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/default-yolo.okf +40 -0
  120. package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/native-subscription-router.okf +86 -0
  121. package/docs/journal/2026/09/03/operations/deployment/local/session-socket-default-fallback.okf +41 -0
  122. package/docs/journal/2026/09/03/research/deployment/colab/tailscale/credential-storage.okf +41 -0
  123. package/docs/journal/2026/09/03/research/providers/meta/muse-code/subscription-billing-boundary.okf +64 -0
  124. package/docs/journal/2026/09/03/research/web-chat/voice/instruction-format.okf +135 -0
  125. package/docs/journal/2026/09/04/diagnostics/claude/context/early-auto-compaction.okf +75 -0
  126. package/docs/journal/2026/09/04/diagnostics/claude/context/fable-51-status-200k.okf +54 -0
  127. package/docs/journal/2026/09/04/diagnostics/runtime/codex/responses/replayed-item-id-validation.okf +74 -0
  128. package/docs/journal/2026/09/04/diagnostics/workspaces/onecieldmsui/codex/replay-stall.okf +100 -0
  129. package/docs/journal/2026/09/04/implementation/observability/tool-calls/websocket-stream.okf +49 -0
  130. package/docs/journal/2026/09/04/implementation/web-chat/input/raw-injection.okf +42 -0
  131. package/docs/journal/2026/09/05/diagnostics/tty/claude-raw-idle-render.okf +25 -0
  132. package/docs/journal/2026/09/05/implementation/events/search-response-urls.okf +23 -0
  133. package/docs/journal/2026/09/05/implementation/tools/native-injection.okf +19 -0
  134. package/docs/journal/2026/09/05/implementation/windows/conpty/prompt-delivery-lifecycle.okf +43 -0
  135. package/docs/journal/2026/09/05/release/nightly/conpty-native-tools.okf +13 -0
  136. package/docs/journal/2026/09/05/release/nightly/search-result-urls.okf +14 -0
  137. package/docs/journal/2026/09/06/implementation/codex/inherited-web-mcp.okf +22 -0
  138. package/docs/journal/2026/09/07/fixes/codex/native-web-transport.okf +39 -0
  139. package/docs/journal/2026/09/07/integration/main/nightly-merge.okf +13 -0
  140. package/docs/journal/2026/09/07/release/stable/0.2.38.okf +10 -0
  141. package/docs/journal/2026/09/07/release/stable/0.2.39.okf +10 -0
  142. package/package.json +1 -1
@@ -0,0 +1,282 @@
1
+ """Bridge OpenAI Responses custom tools through function-only providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from copy import deepcopy
7
+ from typing import Any, Mapping
8
+
9
+
10
+ def tool_definitions(body: Mapping[str, Any]) -> dict[str, dict[str, Any]]:
11
+ """Return uniquely named top-level function and custom Responses tools."""
12
+
13
+ tools = body.get("tools")
14
+ if not isinstance(tools, list):
15
+ return {}
16
+ found: dict[str, dict[str, Any]] = {}
17
+ duplicates: set[str] = set()
18
+ for tool in tools:
19
+ if (
20
+ not isinstance(tool, Mapping)
21
+ or tool.get("type") not in {"function", "custom"}
22
+ ):
23
+ continue
24
+ name = str(tool.get("name") or "").strip()
25
+ if not name:
26
+ continue
27
+ if name in found:
28
+ duplicates.add(name)
29
+ else:
30
+ found[name] = deepcopy(dict(tool))
31
+ for name in duplicates:
32
+ found.pop(name, None)
33
+ return found
34
+
35
+
36
+ def _raw_custom_input(arguments: Any) -> str:
37
+ if not isinstance(arguments, str) or not arguments:
38
+ return ""
39
+ try:
40
+ value = json.loads(arguments)
41
+ except (TypeError, ValueError):
42
+ return arguments
43
+ if isinstance(value, Mapping) and isinstance(value.get("input"), str):
44
+ return value["input"]
45
+ return arguments
46
+
47
+
48
+ def _coerce_schema_integers(value: Any, schema: Mapping[str, Any]) -> Any:
49
+ # Meta can serialize whole-number tool arguments as JSON floats even when
50
+ # the receiving Codex field is a Rust integer. JSON 1000 and 1000.0 have
51
+ # the same numeric value, so remove only a zero fractional component.
52
+ if isinstance(value, float) and value.is_integer():
53
+ return int(value)
54
+ if isinstance(value, Mapping):
55
+ properties = schema.get("properties")
56
+ if isinstance(properties, Mapping):
57
+ return {
58
+ key: _coerce_schema_integers(item, properties.get(key, {}))
59
+ if isinstance(properties.get(key), Mapping)
60
+ else deepcopy(item)
61
+ for key, item in value.items()
62
+ }
63
+ if isinstance(value, list) and isinstance(schema.get("items"), Mapping):
64
+ return [_coerce_schema_integers(item, schema["items"]) for item in value]
65
+ return deepcopy(value)
66
+
67
+
68
+ def _normalized_function_arguments(arguments: Any, schema: Any) -> str:
69
+ if not isinstance(arguments, str) or not arguments or not isinstance(schema, Mapping):
70
+ return str(arguments or "")
71
+ try:
72
+ value = json.loads(arguments)
73
+ except (TypeError, ValueError):
74
+ return arguments
75
+ normalized = _coerce_schema_integers(value, schema)
76
+ return json.dumps(normalized, ensure_ascii=False, separators=(",", ":"))
77
+
78
+
79
+ def _project_tool_item(
80
+ item: Any, tool_definitions_by_name: Mapping[str, Mapping[str, Any]]
81
+ ) -> Any:
82
+ if not isinstance(item, Mapping):
83
+ return deepcopy(item)
84
+ projected = deepcopy(dict(item))
85
+ name = str(projected.get("name") or "")
86
+ source_tool = tool_definitions_by_name.get(name)
87
+ if projected.get("type") == "function_call" and isinstance(source_tool, Mapping):
88
+ schema = source_tool.get("parameters")
89
+ projected["arguments"] = _normalized_function_arguments(
90
+ projected.get("arguments"), schema
91
+ )
92
+ if (
93
+ projected.get("type") == "function_call"
94
+ and isinstance(source_tool, Mapping)
95
+ and source_tool.get("type") == "custom"
96
+ ):
97
+ projected["type"] = "custom_tool_call"
98
+ projected["input"] = _raw_custom_input(projected.pop("arguments", ""))
99
+ return projected
100
+
101
+
102
+ def project_response_payload(
103
+ payload: Any,
104
+ tools: Mapping[str, Mapping[str, Any]],
105
+ ) -> Any:
106
+ """Restore function-enveloped calls to the custom-tool Responses contract."""
107
+
108
+ if not isinstance(payload, Mapping) or not tools:
109
+ return deepcopy(payload)
110
+ custom_tools = {
111
+ name: tool for name, tool in tools.items() if tool.get("type") == "custom"
112
+ }
113
+ projected = deepcopy(dict(payload))
114
+ output = projected.get("output")
115
+ if isinstance(output, list):
116
+ projected["output"] = [_project_tool_item(item, tools) for item in output]
117
+ response_tools = projected.get("tools")
118
+ if isinstance(response_tools, list):
119
+ restored: list[Any] = []
120
+ for tool in response_tools:
121
+ if (
122
+ isinstance(tool, Mapping)
123
+ and tool.get("type") == "function"
124
+ and str(tool.get("name") or "") in custom_tools
125
+ ):
126
+ restored.append(deepcopy(dict(custom_tools[str(tool["name"])])))
127
+ else:
128
+ restored.append(deepcopy(tool))
129
+ projected["tools"] = restored
130
+ return projected
131
+
132
+
133
+ class ResponsesCustomToolStreamProjector:
134
+ """Incrementally restore custom-tool item/event shapes in an SSE stream."""
135
+
136
+ def __init__(self, tools: Mapping[str, Mapping[str, Any]]) -> None:
137
+ self._tools = {
138
+ str(name): deepcopy(dict(tool)) for name, tool in tools.items()
139
+ }
140
+ self._custom_tools = {
141
+ str(name): deepcopy(dict(tool)) for name, tool in tools.items()
142
+ if tool.get("type") == "custom"
143
+ }
144
+ self._buffer = b""
145
+ self._item_names: dict[str, str] = {}
146
+ self._index_names: dict[int, str] = {}
147
+
148
+ @staticmethod
149
+ def _separator(buffer: bytes) -> tuple[int, int] | None:
150
+ lf = buffer.find(b"\n\n")
151
+ crlf = buffer.find(b"\r\n\r\n")
152
+ candidates = [
153
+ (offset, size)
154
+ for offset, size in ((lf, 2), (crlf, 4))
155
+ if offset >= 0
156
+ ]
157
+ return min(candidates) if candidates else None
158
+
159
+ @staticmethod
160
+ def _encode_event(event: Mapping[str, Any]) -> bytes:
161
+ event_type = str(event.get("type") or "message")
162
+ data = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
163
+ return f"event: {event_type}\ndata: {data}\n\n".encode("utf-8")
164
+
165
+ def _is_projected_event(self, event: Mapping[str, Any]) -> bool:
166
+ item_id = str(event.get("item_id") or "")
167
+ output_index = event.get("output_index")
168
+ return item_id in self._item_names or (
169
+ isinstance(output_index, int) and output_index in self._index_names
170
+ )
171
+
172
+ def _project_event(self, event: Mapping[str, Any]) -> list[dict[str, Any]]:
173
+ event_type = str(event.get("type") or "")
174
+ projected = deepcopy(dict(event))
175
+ names = set(self._tools)
176
+
177
+ if event_type in {"response.output_item.added", "response.output_item.done"}:
178
+ item = projected.get("item")
179
+ if (
180
+ isinstance(item, Mapping)
181
+ and item.get("type") == "function_call"
182
+ and str(item.get("name") or "") in names
183
+ ):
184
+ item_id = str(item.get("id") or "")
185
+ output_index = projected.get("output_index")
186
+ tool_name = str(item.get("name") or "")
187
+ if item_id:
188
+ self._item_names[item_id] = tool_name
189
+ if isinstance(output_index, int):
190
+ self._index_names[output_index] = tool_name
191
+ projected["item"] = _project_tool_item(item, self._tools)
192
+ return [projected]
193
+
194
+ if event_type == "response.function_call_arguments.delta" and self._is_projected_event(projected):
195
+ # The provider is streaming a JSON object envelope. Wait for the
196
+ # completed value so partial JSON escaping is never exposed as raw
197
+ # custom-tool input.
198
+ return []
199
+
200
+ if event_type == "response.function_call_arguments.done" and self._is_projected_event(projected):
201
+ item_id = str(projected.get("item_id") or "")
202
+ output_index = projected.get("output_index")
203
+ tool_name = self._item_names.get(item_id) or (
204
+ self._index_names.get(output_index)
205
+ if isinstance(output_index, int)
206
+ else None
207
+ )
208
+ source_tool = self._tools.get(str(tool_name or ""), {})
209
+ arguments = _normalized_function_arguments(
210
+ projected.get("arguments"), source_tool.get("parameters")
211
+ )
212
+ is_custom = source_tool.get("type") == "custom"
213
+ if is_custom:
214
+ raw_value = _raw_custom_input(arguments)
215
+ projected.pop("arguments", None)
216
+ projected["type"] = "response.custom_tool_call_input.done"
217
+ projected["input"] = raw_value
218
+ delta_type = "response.custom_tool_call_input.delta"
219
+ done_field = "input"
220
+ else:
221
+ raw_value = arguments
222
+ projected["arguments"] = arguments
223
+ delta_type = "response.function_call_arguments.delta"
224
+ done_field = "arguments"
225
+ events: list[dict[str, Any]] = []
226
+ if raw_value:
227
+ delta = {
228
+ key: deepcopy(value)
229
+ for key, value in projected.items()
230
+ if key not in {"type", done_field}
231
+ }
232
+ delta["type"] = delta_type
233
+ delta["delta"] = raw_value
234
+ events.append(delta)
235
+ events.append(projected)
236
+ return events
237
+
238
+ response = projected.get("response")
239
+ if isinstance(response, Mapping):
240
+ projected["response"] = project_response_payload(
241
+ response, self._tools
242
+ )
243
+ return [projected]
244
+
245
+ def _project_frame(self, frame: bytes) -> bytes:
246
+ normalized = frame.replace(b"\r\n", b"\n")
247
+ data_lines = [line[6:] for line in normalized.splitlines() if line.startswith(b"data: ")]
248
+ if not data_lines:
249
+ return frame + b"\n\n"
250
+ raw_data = b"\n".join(data_lines)
251
+ if raw_data == b"[DONE]":
252
+ return frame + b"\n\n"
253
+ try:
254
+ event = json.loads(raw_data)
255
+ except (UnicodeDecodeError, ValueError):
256
+ return frame + b"\n\n"
257
+ if not isinstance(event, Mapping):
258
+ return frame + b"\n\n"
259
+ return b"".join(self._encode_event(item) for item in self._project_event(event))
260
+
261
+ def feed(self, chunk: bytes) -> bytes:
262
+ self._buffer += chunk
263
+ output = bytearray()
264
+ while (separator := self._separator(self._buffer)) is not None:
265
+ offset, size = separator
266
+ frame = self._buffer[:offset]
267
+ self._buffer = self._buffer[offset + size :]
268
+ output.extend(self._project_frame(frame))
269
+ return bytes(output)
270
+
271
+ def finish(self) -> bytes:
272
+ if not self._buffer:
273
+ return b""
274
+ frame, self._buffer = self._buffer, b""
275
+ return self._project_frame(frame)
276
+
277
+
278
+ __all__ = [
279
+ "ResponsesCustomToolStreamProjector",
280
+ "project_response_payload",
281
+ "tool_definitions",
282
+ ]
@@ -22,6 +22,7 @@ OPENAI_RESPONSES_ITEM_ID_PREFIXES = {
22
22
  # (``rs_0b9eabb999...``) and Codex's client-side IDs are UUIDs
23
23
  # (``fco_019fd660-738b-...``) -- so the shape identifies the router's own.
24
24
  _ROUTER_SYNTHESIZED_ITEM_ID = re.compile(r"^[a-z]+_[0-9a-f]{8}_\d+$")
25
+ _VALID_RESPONSES_ITEM_ID = re.compile(r"^[A-Za-z0-9_-]+$")
25
26
  _MISSING_TOOL_OUTPUT = re.compile(
26
27
  r"No tool output found for (?:custom|function) tool call ([A-Za-z0-9_-]+)",
27
28
  re.IGNORECASE,
@@ -60,8 +61,13 @@ def repair_replayed_response_items(body: dict[str, Any]) -> dict[str, Any]:
60
61
  ``rs_`` and rejects the whole turn otherwise.
61
62
 
62
63
  A foreign summary-only reasoning item is not authoritative conversation
63
- content, so it is dropped. If encrypted reasoning content is present, the
64
- item is retained but its invalid local/provider ID is omitted.
64
+ content, so it is dropped. Encrypted reasoning is also dropped when its ID
65
+ was synthesized by this router or is not a legal Responses ID. Those two
66
+ shapes identify projected replay records rather than an opaque item issued
67
+ by the current OpenAI backend; forwarding only their ciphertext makes the
68
+ backend reject each record serially as unverifiable. For a merely
69
+ type-mismatched but otherwise legal upstream ID, retain the encrypted
70
+ content and omit only the ID so the backend can still verify it inline.
65
71
 
66
72
  Some providers also emit tool items with message IDs (``msg_``) while
67
73
  preserving the independent ``call_id`` used to pair calls with outputs.
@@ -110,9 +116,14 @@ def repair_replayed_response_items(body: dict[str, Any]) -> dict[str, Any]:
110
116
  continue
111
117
  item_id = str(item.get("id") or "")
112
118
  expected_prefix = OPENAI_RESPONSES_ITEM_ID_PREFIXES.get(str(item_type or ""))
119
+ invalid_id_characters = bool(item_id) and not _VALID_RESPONSES_ITEM_ID.fullmatch(
120
+ item_id
121
+ )
122
+ router_synthesized_id = is_router_synthesized_item_id(item_id)
113
123
  invalid_item_id = bool(item_id) and (
114
124
  bool(expected_prefix and not item_id.startswith(expected_prefix))
115
- or is_router_synthesized_item_id(item_id)
125
+ or invalid_id_characters
126
+ or router_synthesized_id
116
127
  )
117
128
  if item_type != "reasoning" and invalid_item_id:
118
129
  retained = dict(item)
@@ -128,6 +139,11 @@ def repair_replayed_response_items(body: dict[str, Any]) -> dict[str, Any]:
128
139
  continue
129
140
 
130
141
  changed = True
142
+ if invalid_id_characters or router_synthesized_id:
143
+ # These reasoning payloads crossed a projection boundary. Their
144
+ # hidden ciphertext is not conversation content and cannot be
145
+ # authenticated by the destination backend.
146
+ continue
131
147
  if not str(item.get("encrypted_content") or "").strip():
132
148
  # A summary generated by another provider is neither signed nor
133
149
  # required to preserve the visible/tool conversation history.