@oneciel-ai/ciel-runtime 0.2.37 → 0.2.38
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.
- package/CHANGELOG.md +78 -0
- package/README.md +5 -2
- package/ciel_runtime.py +94 -96
- package/ciel_runtime_support/anthropic_model_policy.py +20 -3
- package/ciel_runtime_support/architecture.py +5 -0
- package/ciel_runtime_support/channel_inflight.py +4 -4
- package/ciel_runtime_support/channel_injection.py +56 -1
- package/ciel_runtime_support/channel_mcp_tools.py +34 -0
- package/ciel_runtime_support/channel_message_policy.py +9 -1
- package/ciel_runtime_support/channel_message_prompt.py +23 -0
- package/ciel_runtime_support/channel_pending_injection.py +86 -32
- package/ciel_runtime_support/channel_pending_poll.py +6 -2
- package/ciel_runtime_support/channel_runtime_environment.py +0 -11
- package/ciel_runtime_support/channel_terminal_context.py +0 -6
- package/ciel_runtime_support/channel_terminal_proxy.py +3 -42
- package/ciel_runtime_support/channel_transcript_repository.py +16 -1
- package/ciel_runtime_support/channel_wake_claim_repository.py +0 -19
- package/ciel_runtime_support/channel_wake_context.py +6 -13
- package/ciel_runtime_support/channel_wake_delivery_repository.py +50 -0
- package/ciel_runtime_support/chat_http_controller.py +88 -5
- package/ciel_runtime_support/claude_environment.py +100 -3
- package/ciel_runtime_support/claude_launch_assembly.py +4 -0
- package/ciel_runtime_support/claude_session_socket.py +181 -0
- package/ciel_runtime_support/cli_application_context.py +18 -10
- package/ciel_runtime_support/cli_dispatch.py +19 -3
- package/ciel_runtime_support/cli_parser.py +2 -0
- package/ciel_runtime_support/cli_usage.py +3 -2
- package/ciel_runtime_support/codex_backend_context.py +8 -3
- package/ciel_runtime_support/codex_completion_gate.py +198 -0
- package/ciel_runtime_support/codex_turn_recovery.py +240 -83
- package/ciel_runtime_support/compatibility_protocol.py +5 -2
- package/ciel_runtime_support/config_migrations.py +79 -0
- package/ciel_runtime_support/config_repository.py +7 -0
- package/ciel_runtime_support/context_summary_policy.py +24 -4
- package/ciel_runtime_support/external_event_menu.py +107 -0
- package/ciel_runtime_support/external_event_receiver.py +19 -1
- package/ciel_runtime_support/launch_state.py +2 -0
- package/ciel_runtime_support/managed_tool_injection.py +33 -0
- package/ciel_runtime_support/muse_runtime_context.py +291 -0
- package/ciel_runtime_support/ollama_thinking.py +12 -26
- package/ciel_runtime_support/prelaunch.py +13 -0
- package/ciel_runtime_support/prelaunch_launch_panel.py +2 -1
- package/ciel_runtime_support/prompt_compaction.py +39 -0
- package/ciel_runtime_support/provider_files_proxy.py +248 -0
- package/ciel_runtime_support/provider_model_identity.py +10 -2
- package/ciel_runtime_support/provider_option_cli.py +1 -1
- package/ciel_runtime_support/provider_request_access.py +54 -1
- package/ciel_runtime_support/provider_responses_passthrough.py +152 -15
- package/ciel_runtime_support/providers/alibaba.py +34 -1
- package/ciel_runtime_support/providers/anthropic.py +18 -1
- package/ciel_runtime_support/providers/meta.py +252 -21
- package/ciel_runtime_support/pseudo_tool_parser.py +17 -1
- package/ciel_runtime_support/remote_bridge.py +4 -0
- package/ciel_runtime_support/remote_instructions.py +5 -0
- package/ciel_runtime_support/remote_memory.py +1 -1
- package/ciel_runtime_support/responses_cache_diagnostics.py +82 -0
- package/ciel_runtime_support/responses_custom_tool_bridge.py +282 -0
- package/ciel_runtime_support/responses_input_compatibility.py +19 -3
- package/ciel_runtime_support/router_http.py +301 -4
- package/ciel_runtime_support/router_observability_context.py +26 -0
- package/ciel_runtime_support/router_server_context.py +1 -0
- package/ciel_runtime_support/runtime_adapters.py +31 -0
- package/ciel_runtime_support/runtime_constants.py +23 -1
- package/ciel_runtime_support/runtime_input_gateway.py +85 -5
- package/ciel_runtime_support/runtime_input_status.py +150 -0
- package/ciel_runtime_support/runtime_launch.py +27 -4
- package/ciel_runtime_support/runtime_paths.py +1 -0
- package/ciel_runtime_support/speech_http_controller.py +3 -2
- package/ciel_runtime_support/streaming_anthropic.py +3 -0
- package/ciel_runtime_support/tool_call_events.py +98 -0
- package/ciel_runtime_support/transcript_delta_delivery.py +140 -1
- package/ciel_runtime_support/ui_text.py +1 -0
- package/ciel_runtime_support/web_search_result_events.py +130 -0
- package/ciel_runtime_support/web_ui.py +1 -0
- package/ciel_runtime_support/windows_conpty.py +92 -8
- package/ciel_runtime_support/workspace_mcp.py +4 -1
- package/ciel_runtime_support/workspace_state.py +1 -0
- package/docs/Configuration.md +20 -4
- package/docs/MCP-Channels.md +79 -6
- package/docs/Managed-Tool-Injection.md +25 -0
- package/docs/Module-Map.md +4 -0
- package/docs/Muse-Code.md +73 -0
- package/docs/Observability.md +39 -0
- package/docs/Providers.md +29 -2
- package/docs/Remote-Bridge.md +12 -0
- package/docs/Router.md +1 -0
- package/docs/journal/2026/09/01/diagnostics/anthropic/router/fable51-usage-credits.okf +70 -0
- package/docs/journal/2026/09/01/implementation/claude/session-socket/all-input-paths/default-delivery.okf +133 -0
- package/docs/journal/2026/09/01/implementation/claude/session-socket/windows-direct-input.okf +112 -0
- package/docs/journal/2026/09/02/diagnostics/cache/alibaba/qwen38-post-restart-hit-rate.okf +107 -0
- package/docs/journal/2026/09/02/diagnostics/cache/ollama-cloud/kimi-k3-codex-hit-rate.okf +91 -0
- package/docs/journal/2026/09/02/diagnostics/codex/tui/statusline-cache-metrics.okf +44 -0
- package/docs/journal/2026/09/02/diagnostics/input/transport/fallback-behavior.okf +31 -0
- package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-agent-turn-recovery.okf +125 -0
- package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-parameters.okf +128 -0
- package/docs/journal/2026/09/02/diagnostics/remote/mia/socket-tui-visibility.okf +47 -0
- package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-latency.okf +108 -0
- package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-vs-gpt56-latency.okf +92 -0
- package/docs/journal/2026/09/02/implementation/anthropic/context/one-million-defaults.okf +85 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.okf +104 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.png +0 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/responses-session-cache.okf +97 -0
- package/docs/journal/2026/09/02/implementation/providers/alibaba/qwen38/0902-parameter-alignment.okf +97 -0
- package/docs/journal/2026/09/02/implementation/providers/meta/multimodal-tools/contributor-protocol-adoption.okf +140 -0
- package/docs/journal/2026/09/02/implementation/providers/meta/muse-spark-1.3-support.okf +53 -0
- package/docs/journal/2026/09/02/monitoring/cache/alibaba/cielarvis-qwen38-live-hit-rate.okf +858 -0
- package/docs/journal/2026/09/02/operations/release/main-merge-local-deploy.okf +36 -0
- package/docs/journal/2026/09/02/operations/release/nightly/local-cache-deployment.okf +54 -0
- package/docs/journal/2026/09/02/operations/release/nightly/ollama-kimi-recovery-model-audit-deployment.okf +58 -0
- package/docs/journal/2026/09/02/operations/release/nightly/qwen38-cache-improvement-deployment.okf +72 -0
- package/docs/journal/2026/09/02/research/cache/alibaba/qwen38-hit-rate-improvement.okf +233 -0
- package/docs/journal/2026/09/02/research/cache/codex/provider-scope.okf +77 -0
- package/docs/journal/2026/09/02/research/providers/ollama-cloud/desktop-model-exhaustive-audit.okf +159 -0
- package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-cache-hit.okf +148 -0
- package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-required-schema.okf +127 -0
- package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-resumed-session-stall.okf +160 -0
- package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-substantive-dangling-action.okf +94 -0
- package/docs/journal/2026/09/03/diagnostics/remote/kevin/codex-gpt-early-turn-completion.okf +162 -0
- package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/default-yolo.okf +40 -0
- package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/native-subscription-router.okf +86 -0
- package/docs/journal/2026/09/03/operations/deployment/local/session-socket-default-fallback.okf +41 -0
- package/docs/journal/2026/09/03/research/deployment/colab/tailscale/credential-storage.okf +41 -0
- package/docs/journal/2026/09/03/research/providers/meta/muse-code/subscription-billing-boundary.okf +64 -0
- package/docs/journal/2026/09/03/research/web-chat/voice/instruction-format.okf +135 -0
- package/docs/journal/2026/09/04/diagnostics/claude/context/early-auto-compaction.okf +75 -0
- package/docs/journal/2026/09/04/diagnostics/claude/context/fable-51-status-200k.okf +54 -0
- package/docs/journal/2026/09/04/diagnostics/runtime/codex/responses/replayed-item-id-validation.okf +74 -0
- package/docs/journal/2026/09/04/diagnostics/workspaces/onecieldmsui/codex/replay-stall.okf +100 -0
- package/docs/journal/2026/09/04/implementation/observability/tool-calls/websocket-stream.okf +49 -0
- package/docs/journal/2026/09/04/implementation/web-chat/input/raw-injection.okf +42 -0
- package/docs/journal/2026/09/05/diagnostics/tty/claude-raw-idle-render.okf +25 -0
- package/docs/journal/2026/09/05/implementation/events/search-response-urls.okf +23 -0
- package/docs/journal/2026/09/05/implementation/tools/native-injection.okf +19 -0
- package/docs/journal/2026/09/05/implementation/windows/conpty/prompt-delivery-lifecycle.okf +43 -0
- package/docs/journal/2026/09/05/release/nightly/conpty-native-tools.okf +13 -0
- package/docs/journal/2026/09/05/release/nightly/search-result-urls.okf +14 -0
- package/docs/journal/2026/09/06/implementation/codex/inherited-web-mcp.okf +22 -0
- package/docs/journal/2026/09/07/integration/main/nightly-merge.okf +13 -0
- package/docs/journal/2026/09/07/release/stable/0.2.38.okf +10 -0
- 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.
|
|
64
|
-
|
|
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
|
|
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.
|