@oneciel-ai/ciel-runtime 0.2.2 → 0.2.4

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 (136) hide show
  1. package/ciel_runtime.py +2555 -9635
  2. package/ciel_runtime_support/advisor_request_builder.py +8 -21
  3. package/ciel_runtime_support/anthropic_tool_turns.py +13 -8
  4. package/ciel_runtime_support/architecture.py +68 -0
  5. package/ciel_runtime_support/architecture_budget.py +1 -1
  6. package/ciel_runtime_support/channel_connection_context.py +233 -0
  7. package/ciel_runtime_support/channel_delivery_context.py +332 -0
  8. package/ciel_runtime_support/channel_mcp_context.py +313 -0
  9. package/ciel_runtime_support/channel_mcp_discovery.py +47 -0
  10. package/ciel_runtime_support/channel_mcp_transport.py +5 -1
  11. package/ciel_runtime_support/channel_message_context.py +212 -0
  12. package/ciel_runtime_support/channel_message_repository.py +14 -3
  13. package/ciel_runtime_support/channel_pending_injection.py +9 -0
  14. package/ciel_runtime_support/channel_probe_launch_context.py +213 -0
  15. package/ciel_runtime_support/channel_replay_policy.py +38 -0
  16. package/ciel_runtime_support/channel_runtime_environment.py +8 -0
  17. package/ciel_runtime_support/channel_session_context.py +236 -0
  18. package/ciel_runtime_support/channel_terminal_context.py +350 -0
  19. package/ciel_runtime_support/channel_wake_context.py +532 -0
  20. package/ciel_runtime_support/claude_environment.py +60 -0
  21. package/ciel_runtime_support/claude_launch_assembly.py +249 -0
  22. package/ciel_runtime_support/claude_router.py +62 -12
  23. package/ciel_runtime_support/cli_application_context.py +132 -0
  24. package/ciel_runtime_support/cli_assembly.py +50 -0
  25. package/ciel_runtime_support/codex_backend_context.py +363 -0
  26. package/ciel_runtime_support/codex_config.py +13 -1
  27. package/ciel_runtime_support/codex_launch_assembly.py +213 -0
  28. package/ciel_runtime_support/codex_launch_configuration.py +30 -1
  29. package/ciel_runtime_support/codex_mcp_integration.py +90 -8
  30. package/ciel_runtime_support/codex_model_catalog.py +4 -1
  31. package/ciel_runtime_support/codex_reasoning_rejects.py +225 -0
  32. package/ciel_runtime_support/codex_router.py +38 -8
  33. package/ciel_runtime_support/codex_turn_recovery.py +154 -0
  34. package/ciel_runtime_support/config_migrations.py +103 -0
  35. package/ciel_runtime_support/config_repository.py +30 -0
  36. package/ciel_runtime_support/configuration_cli.py +38 -0
  37. package/ciel_runtime_support/context_compaction.py +9 -4
  38. package/ciel_runtime_support/credential_management.py +12 -0
  39. package/ciel_runtime_support/credentials.py +12 -0
  40. package/ciel_runtime_support/github_copilot_oauth.py +2 -2
  41. package/ciel_runtime_support/hosted_formula_tools.py +216 -0
  42. package/ciel_runtime_support/kimi_runtime_context.py +208 -0
  43. package/ciel_runtime_support/llm_preset_context.py +338 -0
  44. package/ciel_runtime_support/managed_mcp_config.py +8 -4
  45. package/ciel_runtime_support/mcp_configuration_context.py +291 -0
  46. package/ciel_runtime_support/mcp_http_proxy.py +14 -8
  47. package/ciel_runtime_support/mcp_probe_transport.py +47 -15
  48. package/ciel_runtime_support/mcp_transport.py +14 -1
  49. package/ciel_runtime_support/native_context_recovery.py +72 -0
  50. package/ciel_runtime_support/ollama_catalog_context.py +213 -0
  51. package/ciel_runtime_support/ollama_stream_collection.py +103 -0
  52. package/ciel_runtime_support/ollama_thinking.py +6 -1
  53. package/ciel_runtime_support/ollama_wire_projection.py +157 -0
  54. package/ciel_runtime_support/openai_forwarding.py +32 -10
  55. package/ciel_runtime_support/openai_responses_router.py +12 -0
  56. package/ciel_runtime_support/package_lifecycle.py +39 -0
  57. package/ciel_runtime_support/prelaunch_assembly.py +37 -0
  58. package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
  59. package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
  60. package/ciel_runtime_support/prompt_compaction.py +144 -0
  61. package/ciel_runtime_support/prompt_injection.py +45 -0
  62. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
  63. package/ciel_runtime_support/protocols/chat_projection.py +85 -5
  64. package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
  65. package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
  66. package/ciel_runtime_support/protocols/ollama_response.py +57 -5
  67. package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
  68. package/ciel_runtime_support/protocols/openai_responses.py +61 -15
  69. package/ciel_runtime_support/provider_adapters.py +26 -0
  70. package/ciel_runtime_support/provider_administration_context.py +207 -0
  71. package/ciel_runtime_support/provider_config_mutations.py +3 -0
  72. package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
  73. package/ciel_runtime_support/provider_model_context.py +107 -0
  74. package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
  75. package/ciel_runtime_support/provider_model_selection.py +10 -3
  76. package/ciel_runtime_support/provider_models.py +45 -2
  77. package/ciel_runtime_support/provider_option_cli.py +19 -0
  78. package/ciel_runtime_support/provider_policy.py +1 -1
  79. package/ciel_runtime_support/provider_readiness_context.py +189 -0
  80. package/ciel_runtime_support/provider_request_builder.py +64 -28
  81. package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
  82. package/ciel_runtime_support/provider_timeout_policy.py +54 -0
  83. package/ciel_runtime_support/provider_tool_policy.py +9 -1
  84. package/ciel_runtime_support/providers/__init__.py +6 -0
  85. package/ciel_runtime_support/providers/alibaba.py +634 -0
  86. package/ciel_runtime_support/providers/catalog.py +24 -16
  87. package/ciel_runtime_support/providers/deepseek.py +73 -0
  88. package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
  89. package/ciel_runtime_support/providers/kimi.py +69 -9
  90. package/ciel_runtime_support/providers/ollama.py +8 -0
  91. package/ciel_runtime_support/providers/ollama_context.py +21 -2
  92. package/ciel_runtime_support/providers/vllm.py +7 -1
  93. package/ciel_runtime_support/response_collection.py +68 -18
  94. package/ciel_runtime_support/response_collection_context.py +391 -0
  95. package/ciel_runtime_support/response_stream_context.py +555 -0
  96. package/ciel_runtime_support/responses_input_compatibility.py +121 -0
  97. package/ciel_runtime_support/responses_usage_observer.py +83 -0
  98. package/ciel_runtime_support/router_client_lifecycle.py +1 -0
  99. package/ciel_runtime_support/router_http.py +245 -3
  100. package/ciel_runtime_support/router_observability_context.py +251 -0
  101. package/ciel_runtime_support/router_process_context.py +200 -0
  102. package/ciel_runtime_support/router_process_lifecycle.py +2 -0
  103. package/ciel_runtime_support/router_request_assembly.py +399 -0
  104. package/ciel_runtime_support/router_request_context.py +215 -0
  105. package/ciel_runtime_support/router_server_context.py +84 -0
  106. package/ciel_runtime_support/runaway_output_guard.py +488 -0
  107. package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
  108. package/ciel_runtime_support/runtime_asset_context.py +297 -0
  109. package/ciel_runtime_support/runtime_constants.py +16 -1
  110. package/ciel_runtime_support/runtime_launch.py +9 -5
  111. package/ciel_runtime_support/runtime_launch_context.py +130 -0
  112. package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
  113. package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
  114. package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
  115. package/ciel_runtime_support/runtime_paths.py +60 -40
  116. package/ciel_runtime_support/runtime_primitives.py +78 -0
  117. package/ciel_runtime_support/speech_http_controller.py +335 -0
  118. package/ciel_runtime_support/sse_stream_collection.py +236 -0
  119. package/ciel_runtime_support/statusline_script.py +57 -8
  120. package/ciel_runtime_support/streaming_anthropic.py +361 -24
  121. package/ciel_runtime_support/tool_schema.py +40 -2
  122. package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
  123. package/ciel_runtime_support/upstream_dump.py +68 -0
  124. package/ciel_runtime_support/upstream_retry_context.py +259 -0
  125. package/ciel_runtime_support/web_ui.py +248 -1
  126. package/ciel_runtime_support/workspace_router_selection.py +86 -0
  127. package/docs/COLAB_SPEECH.md +32 -0
  128. package/docs/Configuration.md +50 -0
  129. package/docs/Test-Suite.md +1 -0
  130. package/package.json +4 -1
  131. package/scripts/colab/__pycache__/bootstrap_moss_tts.cpython-311.pyc +0 -0
  132. package/scripts/colab/__pycache__/bootstrap_qwen_asr.cpython-311.pyc +0 -0
  133. package/scripts/colab/bootstrap_moss_tts.py +106 -0
  134. package/scripts/colab/bootstrap_qwen_asr.py +106 -0
  135. package/scripts/configure_speech_workers.py +37 -0
  136. package/scripts/deploy_colab_speech.ps1 +47 -0
@@ -0,0 +1,555 @@
1
+ """Provider response normalization and streaming bridge bounded context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from http.server import BaseHTTPRequestHandler
7
+ from typing import Any, Callable
8
+
9
+ from . import streaming_anthropic
10
+ from .openai_responses_stream import OpenAIResponsesStreamServices
11
+ from .protocols.ollama_response import (
12
+ OllamaResponseOutput,
13
+ OllamaResponseRecovery,
14
+ OllamaResponseServices,
15
+ OllamaResponseText,
16
+ OllamaResponseTools,
17
+ )
18
+ from .pseudo_tool_parser import PseudoToolParserServices
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class ResponseStreamAlgorithms:
23
+ normalize_tool_arguments: Callable[..., dict[str, Any]]
24
+ infer_tool_name: Callable[[dict[str, Any]], str]
25
+ parse_pseudo_tool_calls: Callable[..., tuple[str, list[dict[str, Any]]]]
26
+ project_ollama_response: Callable[..., dict[str, Any]]
27
+ project_openai_chat_response: Callable[..., dict[str, Any]]
28
+ split_word_buffer: Callable[..., tuple[str, str]]
29
+ write_openai_response: Callable[..., Any]
30
+ write_openai_error: Callable[..., Any]
31
+ protocol_adapter: Callable[..., Any]
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class ResponseStreamTextPorts:
36
+ decode_ollama: Callable[..., Any]
37
+ ollama_thinking_to_block: Callable[..., Any]
38
+ ollama_reasoning_only_notice: Callable[..., str]
39
+ strip_thinking: Callable[..., str]
40
+ parse_xml_tools: Callable[..., Any]
41
+ find_pseudo_xml_start: Callable[..., Any]
42
+ fuzzy_tool_name: Callable[..., Any]
43
+ reasoning_to_thinking: Callable[..., Any]
44
+ anthropic_content_to_text: Callable[..., str]
45
+ positive_int: Callable[..., int | None]
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class ResponseStreamToolPorts:
50
+ resolve_name: Callable[..., str]
51
+ validate_input: Callable[..., Any]
52
+ plan_mode_name: Callable[..., Any]
53
+ cap_notification_wait: Callable[..., dict[str, Any]]
54
+ should_drop: Callable[..., bool]
55
+ should_drop_duplicate: Callable[..., bool]
56
+ append_log: Callable[..., Any]
57
+ remember_tool_use: Callable[..., Any]
58
+ repair_passthrough_input: Callable[..., bool]
59
+ is_mcp_notification_wait: Callable[..., bool]
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class ResponseStreamRecoveryPorts:
64
+ auto_enter_plan: Callable[..., bool]
65
+ auto_exit_plan: Callable[..., bool]
66
+ recover_empty: Callable[..., bool]
67
+ keep_alive: Callable[..., bool]
68
+ auto_continue_choice: Callable[..., bool]
69
+ empty_notice: Callable[..., str]
70
+ latest_tool_results: Callable[..., list[str]]
71
+ synthetic_tool_response: Callable[..., dict[str, Any]]
72
+ synthesize_tasklist: Callable[..., bool]
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class ResponseStreamConversationPorts:
77
+ backfill_exit_plan: Callable[..., Any]
78
+ ultracode_enabled: Callable[..., bool]
79
+ has_tool: Callable[..., bool]
80
+ latest_intent_index: Callable[..., int]
81
+ suggestion_mode: Callable[..., bool]
82
+ recent_tasklist_count: Callable[..., int]
83
+ remember_suppressed_thinking: Callable[..., Any]
84
+
85
+
86
+ @dataclass(frozen=True, slots=True)
87
+ class ResponseStreamIoPorts:
88
+ encode_message: Callable[..., dict[str, Any]]
89
+ estimate_tokens: Callable[..., int]
90
+ log: Callable[..., Any]
91
+ mark_delivery_failed: Callable[..., Any]
92
+ mark_delivery_success: Callable[..., Any]
93
+ client_connection_closed: Callable[..., bool]
94
+ iter_upstream_lines: Callable[..., Any]
95
+ write_activity: Callable[..., Any]
96
+ write_json: Callable[..., Any]
97
+ write_open_stream_stop: Callable[..., Any]
98
+
99
+
100
+ @dataclass(frozen=True, slots=True)
101
+ class ResponseStreamTracePorts:
102
+ dump_response: Callable[..., Any]
103
+ finish_sse: Callable[..., Any]
104
+ make_sse: Callable[..., Any]
105
+ record_sse: Callable[..., Any]
106
+
107
+
108
+ @dataclass(frozen=True, slots=True)
109
+ class ResponseStreamRuntimePorts:
110
+ timestamp_ms: Callable[[], int]
111
+ process_id: Callable[[], int]
112
+
113
+
114
+ @dataclass(frozen=True, slots=True)
115
+ class ResponseStreamTypes:
116
+ thinking_block_types: Any
117
+ visible_tool_filter: Any
118
+ visible_thinking_filter: Any
119
+ client_disconnected_error: Any
120
+ pseudo_tool_start: str
121
+ pseudo_tool_end: str
122
+ word_chunk_max_buffer: int
123
+
124
+
125
+ @dataclass(frozen=True, slots=True)
126
+ class ResponseStreamContext:
127
+ algorithms: ResponseStreamAlgorithms
128
+ text: ResponseStreamTextPorts
129
+ tools: ResponseStreamToolPorts
130
+ recovery: ResponseStreamRecoveryPorts
131
+ conversation: ResponseStreamConversationPorts
132
+ io: ResponseStreamIoPorts
133
+ trace: ResponseStreamTracePorts
134
+ runtime: ResponseStreamRuntimePorts
135
+ types: ResponseStreamTypes
136
+
137
+ def normalize_tool_arguments(
138
+ self, tool_name: str, args: Any
139
+ ) -> dict[str, Any]:
140
+ return self.algorithms.normalize_tool_arguments(tool_name, args)
141
+
142
+ def infer_tool_name(self, args: dict[str, Any]) -> str:
143
+ return self.algorithms.infer_tool_name(args)
144
+
145
+ def parse_pseudo_tool_calls(
146
+ self,
147
+ text: str,
148
+ source_body: dict[str, Any] | None = None,
149
+ ) -> tuple[str, list[dict[str, Any]]]:
150
+ return self.algorithms.parse_pseudo_tool_calls(
151
+ text,
152
+ source_body,
153
+ PseudoToolParserServices(
154
+ parse_xml=self.text.parse_xml_tools,
155
+ fuzzy_tool_name=self.text.fuzzy_tool_name,
156
+ ),
157
+ )
158
+
159
+ def ollama_response_services(self) -> OllamaResponseServices:
160
+ return OllamaResponseServices(
161
+ text=OllamaResponseText(
162
+ decode=self.text.decode_ollama,
163
+ thinking_to_block=self.text.ollama_thinking_to_block,
164
+ reasoning_only_notice=self.text.ollama_reasoning_only_notice,
165
+ strip_thinking=self.text.strip_thinking,
166
+ parse_pseudo_tools=self.parse_pseudo_tool_calls,
167
+ log=self.io.log,
168
+ ),
169
+ tools=OllamaResponseTools(
170
+ resolve_name=self.tools.resolve_name,
171
+ normalize_arguments=self.normalize_tool_arguments,
172
+ validate_input=self.tools.validate_input,
173
+ plan_mode_name=self.tools.plan_mode_name,
174
+ cap_notification_wait=self.tools.cap_notification_wait,
175
+ should_drop=self.tools.should_drop,
176
+ should_drop_duplicate=self.tools.should_drop_duplicate,
177
+ append_log=self.tools.append_log,
178
+ ),
179
+ recovery=OllamaResponseRecovery(
180
+ auto_enter_plan=self.recovery.auto_enter_plan,
181
+ recover_empty_with_tasklist=self.recovery.recover_empty,
182
+ keep_alive_with_tasklist=self.recovery.keep_alive,
183
+ auto_continue_choice=self.recovery.auto_continue_choice,
184
+ empty_notice=self.recovery.empty_notice,
185
+ latest_tool_result_names=self.recovery.latest_tool_results,
186
+ synthetic_tool_response=self.recovery.synthetic_tool_response,
187
+ ),
188
+ output=OllamaResponseOutput(
189
+ encode_message=self.io.encode_message,
190
+ estimate_tokens=self.io.estimate_tokens,
191
+ timestamp_ms=self.runtime.timestamp_ms,
192
+ process_id=self.runtime.process_id,
193
+ ),
194
+ )
195
+
196
+ def ollama_chat_to_anthropic(
197
+ self,
198
+ data: dict[str, Any],
199
+ model: str,
200
+ source_body: dict[str, Any] | None = None,
201
+ ) -> dict[str, Any]:
202
+ return self.algorithms.project_ollama_response(
203
+ data,
204
+ model,
205
+ source_body,
206
+ self.ollama_response_services(),
207
+ )
208
+
209
+ def split_word_buffer(
210
+ self,
211
+ buf: str,
212
+ force: bool = False,
213
+ max_buffer: int | None = None,
214
+ ) -> tuple[str, str]:
215
+ return self.algorithms.split_word_buffer(
216
+ buf,
217
+ force=force,
218
+ max_buffer=(
219
+ self.types.word_chunk_max_buffer
220
+ if max_buffer is None
221
+ else max_buffer
222
+ ),
223
+ )
224
+
225
+ def anthropic_stream_services(self) -> streaming_anthropic.AnthropicStreamServices:
226
+ return streaming_anthropic.AnthropicStreamServices(
227
+ io=streaming_anthropic.AnthropicStreamIO(
228
+ ANTHROPIC_THINKING_BLOCK_TYPES=self.types.thinking_block_types,
229
+ VisibleToolCallArtifactFilter=self.types.visible_tool_filter,
230
+ _find_pseudo_xml_tool_start=self.text.find_pseudo_xml_start,
231
+ _split_word_buffer=self.split_word_buffer,
232
+ mark_pending_channel_delivery_failed=self.io.mark_delivery_failed,
233
+ mark_pending_channel_delivery_success=self.io.mark_delivery_success,
234
+ remember_suppressed_thinking_passback=(
235
+ self.conversation.remember_suppressed_thinking
236
+ ),
237
+ router_client_connection_closed=self.io.client_connection_closed,
238
+ router_log=self.io.log,
239
+ ),
240
+ tool_projection=streaming_anthropic.AnthropicToolProjection(
241
+ _is_mcp_notification_wait_tool=self.tools.is_mcp_notification_wait,
242
+ _remember_channel_injected_tool_use=self.tools.remember_tool_use,
243
+ _validate_and_fix_tool_input=self.tools.validate_input,
244
+ append_tool_call_log=self.tools.append_log,
245
+ cap_mcp_notification_wait_tool_input=self.tools.cap_notification_wait,
246
+ infer_tool_name_from_args=self.infer_tool_name,
247
+ normalize_tool_arguments=self.normalize_tool_arguments,
248
+ parse_pseudo_tool_calls=self.parse_pseudo_tool_calls,
249
+ plan_mode_tool_name_for_emit=self.tools.plan_mode_name,
250
+ resolve_emitted_tool_name=self.tools.resolve_name,
251
+ ),
252
+ tool_policy=streaming_anthropic.AnthropicToolPolicy(
253
+ should_drop_duplicate_side_effect_tool_call=self.tools.should_drop_duplicate,
254
+ should_drop_emitted_tool_call=self.tools.should_drop,
255
+ should_repair_anthropic_passthrough_tool_input=self.tools.repair_passthrough_input,
256
+ ),
257
+ conversation=streaming_anthropic.AnthropicConversationContext(
258
+ backfill_exit_plan_mode_allowed_prompts=self.conversation.backfill_exit_plan,
259
+ body_ultracode_runtime_enabled=self.conversation.ultracode_enabled,
260
+ empty_end_turn_notice_for_body=self.recovery.empty_notice,
261
+ has_tool=self.conversation.has_tool,
262
+ latest_user_intent_message_index=self.conversation.latest_intent_index,
263
+ latest_user_is_claude_code_suggestion_mode=self.conversation.suggestion_mode,
264
+ latest_user_tool_result_names=self.recovery.latest_tool_results,
265
+ recent_synthetic_tasklist_count=self.conversation.recent_tasklist_count,
266
+ ),
267
+ continuation=streaming_anthropic.AnthropicContinuationPolicy(
268
+ should_auto_continue_choice_question_with_tasklist=self.recovery.auto_continue_choice,
269
+ should_auto_exit_plan_mode=self.recovery.auto_exit_plan,
270
+ should_keep_work_alive_with_tasklist=self.recovery.keep_alive,
271
+ should_recover_empty_end_turn_with_tasklist=self.recovery.recover_empty,
272
+ should_synthesize_tasklist_for_provider=self.recovery.synthesize_tasklist,
273
+ ),
274
+ )
275
+
276
+ def rebatch_anthropic_sse_text(
277
+ self,
278
+ handler: BaseHTTPRequestHandler,
279
+ resp: Any,
280
+ model: str = "ciel-runtime-upstream",
281
+ word_chunking: bool = True,
282
+ source_body: dict[str, Any] | None = None,
283
+ preserve_thinking: bool = True,
284
+ normalize_tool_use: bool = False,
285
+ provider: str = "",
286
+ ) -> None:
287
+ return streaming_anthropic.rebatch_anthropic_sse_text(
288
+ handler,
289
+ resp,
290
+ model=model,
291
+ word_chunking=word_chunking,
292
+ source_body=source_body,
293
+ preserve_thinking=preserve_thinking,
294
+ normalize_tool_use=normalize_tool_use,
295
+ provider=provider,
296
+ services=self.anthropic_stream_services(),
297
+ )
298
+
299
+ def ollama_stream_services(self) -> streaming_anthropic.OllamaStreamServices:
300
+ return streaming_anthropic.OllamaStreamServices(
301
+ io=streaming_anthropic.OllamaStreamIO(
302
+ UpstreamClientDisconnected=self.types.client_disconnected_error,
303
+ VisibleThinkingMarkupFilter=self.types.visible_thinking_filter,
304
+ _split_word_buffer=self.split_word_buffer,
305
+ estimate_tokens=self.io.estimate_tokens,
306
+ iter_upstream_lines_until_client_disconnect=self.io.iter_upstream_lines,
307
+ mark_pending_channel_delivery_failed=self.io.mark_delivery_failed,
308
+ mark_pending_channel_delivery_success=self.io.mark_delivery_success,
309
+ router_log=self.io.log,
310
+ write_router_activity=self.io.write_activity,
311
+ ),
312
+ trace=streaming_anthropic.OllamaStreamTrace(
313
+ dump_response_for_trace=self.trace.dump_response,
314
+ finish_outgoing_sse_trace=self.trace.finish_sse,
315
+ make_outgoing_sse_trace=self.trace.make_sse,
316
+ record_outgoing_sse_event=self.trace.record_sse,
317
+ ),
318
+ tool_projection=streaming_anthropic.OllamaToolProjection(
319
+ _remember_channel_injected_tool_use=self.tools.remember_tool_use,
320
+ _validate_and_fix_tool_input=self.tools.validate_input,
321
+ append_tool_call_log=self.tools.append_log,
322
+ cap_mcp_notification_wait_tool_input=self.tools.cap_notification_wait,
323
+ normalize_tool_arguments=self.normalize_tool_arguments,
324
+ plan_mode_tool_name_for_emit=self.tools.plan_mode_name,
325
+ resolve_emitted_tool_name=self.tools.resolve_name,
326
+ should_drop_duplicate_side_effect_tool_call=self.tools.should_drop_duplicate,
327
+ should_drop_emitted_tool_call=self.tools.should_drop,
328
+ ),
329
+ continuation=streaming_anthropic.OllamaContinuationPolicy(
330
+ empty_end_turn_notice_for_body=self.recovery.empty_notice,
331
+ reasoning_only_notice=self.text.ollama_reasoning_only_notice,
332
+ should_auto_continue_choice_question_with_tasklist=self.recovery.auto_continue_choice,
333
+ should_auto_enter_plan_mode=self.recovery.auto_enter_plan,
334
+ should_keep_work_alive_with_tasklist=self.recovery.keep_alive,
335
+ should_recover_empty_end_turn_with_tasklist=self.recovery.recover_empty,
336
+ ),
337
+ )
338
+
339
+ def ollama_stream_to_anthropic_sse(
340
+ self,
341
+ handler: BaseHTTPRequestHandler,
342
+ resp: Any,
343
+ model: str,
344
+ word_chunking: bool = False,
345
+ provider: str = "ollama",
346
+ source_body: dict[str, Any] | None = None,
347
+ idle_timeout: float = 30.0,
348
+ ) -> None:
349
+ return streaming_anthropic.ollama_stream_to_anthropic_sse(
350
+ handler,
351
+ resp,
352
+ model,
353
+ word_chunking=word_chunking,
354
+ provider=provider,
355
+ source_body=source_body,
356
+ idle_timeout=idle_timeout,
357
+ services=self.ollama_stream_services(),
358
+ )
359
+
360
+ def openai_chat_to_anthropic(
361
+ self,
362
+ data: dict[str, Any],
363
+ model: str,
364
+ source_body: dict[str, Any] | None = None,
365
+ ) -> dict[str, Any]:
366
+ return self.algorithms.project_openai_chat_response(
367
+ data,
368
+ model,
369
+ source_body,
370
+ services=self.ollama_response_services(),
371
+ positive_int=self.text.positive_int,
372
+ reasoning_to_block=self.text.reasoning_to_thinking,
373
+ content_to_text=self.text.anthropic_content_to_text,
374
+ )
375
+
376
+ def openai_responses_to_anthropic_messages(
377
+ self, body: dict[str, Any], fallback_model: str
378
+ ) -> dict[str, Any]:
379
+ adapter = self.algorithms.protocol_adapter(
380
+ "openai_responses", fallback_model=fallback_model
381
+ )
382
+ return dict(adapter.normalize_request(body))
383
+
384
+ def anthropic_message_to_openai_response(
385
+ self,
386
+ message: dict[str, Any],
387
+ source_body: dict[str, Any] | None = None,
388
+ ) -> dict[str, Any]:
389
+ adapter = self.algorithms.protocol_adapter(
390
+ "openai_responses", source_body=source_body
391
+ )
392
+ return dict(adapter.normalize_response(message))
393
+
394
+ def openai_responses_stream_services(self) -> OpenAIResponsesStreamServices:
395
+ return OpenAIResponsesStreamServices(
396
+ to_response=self.anthropic_message_to_openai_response,
397
+ write_json=self.io.write_json,
398
+ )
399
+
400
+ def write_openai_responses_response(
401
+ self,
402
+ handler: BaseHTTPRequestHandler,
403
+ message: dict[str, Any],
404
+ source_body: dict[str, Any] | None = None,
405
+ *,
406
+ stream: bool = True,
407
+ ) -> None:
408
+ self.algorithms.write_openai_response(
409
+ handler,
410
+ message,
411
+ source_body,
412
+ stream=stream,
413
+ services=self.openai_responses_stream_services(),
414
+ )
415
+
416
+ def write_openai_responses_error(
417
+ self,
418
+ handler: BaseHTTPRequestHandler,
419
+ message: str,
420
+ *,
421
+ stream: bool = True,
422
+ status: int = 500,
423
+ ) -> None:
424
+ self.algorithms.write_openai_error(
425
+ handler,
426
+ message,
427
+ stream=stream,
428
+ status=status,
429
+ services=self.openai_responses_stream_services(),
430
+ )
431
+
432
+ def openai_chat_stream_services(
433
+ self,
434
+ ) -> streaming_anthropic.OpenAIChatStreamServices:
435
+ return streaming_anthropic.OpenAIChatStreamServices(
436
+ io=streaming_anthropic.OpenAIChatStreamIO(
437
+ PSEUDO_TOOL_END=self.types.pseudo_tool_end,
438
+ PSEUDO_TOOL_START=self.types.pseudo_tool_start,
439
+ _split_word_buffer=self.split_word_buffer,
440
+ positive_int=self.text.positive_int,
441
+ router_log=self.io.log,
442
+ write_anthropic_open_stream_stop=self.io.write_open_stream_stop,
443
+ write_router_activity=self.io.write_activity,
444
+ ),
445
+ tool_projection=streaming_anthropic.OpenAIChatToolProjection(
446
+ _remember_channel_injected_tool_use=self.tools.remember_tool_use,
447
+ _validate_and_fix_tool_input=self.tools.validate_input,
448
+ append_tool_call_log=self.tools.append_log,
449
+ cap_mcp_notification_wait_tool_input=self.tools.cap_notification_wait,
450
+ normalize_tool_arguments=self.normalize_tool_arguments,
451
+ parse_pseudo_tool_calls=self.parse_pseudo_tool_calls,
452
+ plan_mode_tool_name_for_emit=self.tools.plan_mode_name,
453
+ resolve_emitted_tool_name=self.tools.resolve_name,
454
+ should_drop_duplicate_side_effect_tool_call=self.tools.should_drop_duplicate,
455
+ should_drop_emitted_tool_call=self.tools.should_drop,
456
+ ),
457
+ continuation=streaming_anthropic.OpenAIChatContinuationPolicy(
458
+ empty_end_turn_notice_for_body=self.recovery.empty_notice,
459
+ latest_user_tool_result_names=self.recovery.latest_tool_results,
460
+ should_auto_continue_choice_question_with_tasklist=self.recovery.auto_continue_choice,
461
+ should_auto_enter_plan_mode=self.recovery.auto_enter_plan,
462
+ should_keep_work_alive_with_tasklist=self.recovery.keep_alive,
463
+ should_recover_empty_end_turn_with_tasklist=self.recovery.recover_empty,
464
+ ),
465
+ )
466
+
467
+ def stream_openai_chat_to_anthropic_sse(
468
+ self,
469
+ handler: BaseHTTPRequestHandler,
470
+ resp: Any,
471
+ model: str,
472
+ provider: str,
473
+ source_body: dict[str, Any] | None = None,
474
+ start_index: int = 0,
475
+ word_chunking: bool = False,
476
+ input_tokens: int | None = None,
477
+ input_bytes: int | None = None,
478
+ ) -> bool:
479
+ return streaming_anthropic.forward_openai_chat_to_anthropic_sse(
480
+ handler,
481
+ resp,
482
+ model,
483
+ provider,
484
+ source_body=source_body,
485
+ start_index=start_index,
486
+ word_chunking=word_chunking,
487
+ input_tokens=input_tokens,
488
+ input_bytes=input_bytes,
489
+ services=self.openai_chat_stream_services(),
490
+ )
491
+
492
+
493
+ @dataclass(frozen=True, slots=True)
494
+ class ResponseStreamCompatibilityApi:
495
+ context: Callable[[], ResponseStreamContext]
496
+
497
+ def normalize_tool_arguments(self, *args: Any, **kwargs: Any) -> Any:
498
+ return self.context().normalize_tool_arguments(*args, **kwargs)
499
+
500
+ def infer_tool_name(self, *args: Any, **kwargs: Any) -> Any:
501
+ return self.context().infer_tool_name(*args, **kwargs)
502
+
503
+ def parse_pseudo_tool_calls(self, *args: Any, **kwargs: Any) -> Any:
504
+ return self.context().parse_pseudo_tool_calls(*args, **kwargs)
505
+
506
+ def ollama_response_services(self) -> OllamaResponseServices:
507
+ return self.context().ollama_response_services()
508
+
509
+ def ollama_chat_to_anthropic(self, *args: Any, **kwargs: Any) -> Any:
510
+ return self.context().ollama_chat_to_anthropic(*args, **kwargs)
511
+
512
+ def split_word_buffer(self, *args: Any, **kwargs: Any) -> Any:
513
+ return self.context().split_word_buffer(*args, **kwargs)
514
+
515
+ def rebatch_anthropic_sse_text(self, *args: Any, **kwargs: Any) -> Any:
516
+ return self.context().rebatch_anthropic_sse_text(*args, **kwargs)
517
+
518
+ def ollama_stream_to_anthropic_sse(self, *args: Any, **kwargs: Any) -> Any:
519
+ return self.context().ollama_stream_to_anthropic_sse(*args, **kwargs)
520
+
521
+ def openai_chat_to_anthropic(self, *args: Any, **kwargs: Any) -> Any:
522
+ return self.context().openai_chat_to_anthropic(*args, **kwargs)
523
+
524
+ def openai_responses_to_anthropic_messages(self, *args: Any, **kwargs: Any) -> Any:
525
+ return self.context().openai_responses_to_anthropic_messages(*args, **kwargs)
526
+
527
+ def anthropic_message_to_openai_response(self, *args: Any, **kwargs: Any) -> Any:
528
+ return self.context().anthropic_message_to_openai_response(*args, **kwargs)
529
+
530
+ def openai_responses_stream_services(self) -> OpenAIResponsesStreamServices:
531
+ return self.context().openai_responses_stream_services()
532
+
533
+ def write_openai_responses_response(self, *args: Any, **kwargs: Any) -> Any:
534
+ return self.context().write_openai_responses_response(*args, **kwargs)
535
+
536
+ def write_openai_responses_error(self, *args: Any, **kwargs: Any) -> Any:
537
+ return self.context().write_openai_responses_error(*args, **kwargs)
538
+
539
+ def stream_openai_chat_to_anthropic_sse(self, *args: Any, **kwargs: Any) -> Any:
540
+ return self.context().stream_openai_chat_to_anthropic_sse(*args, **kwargs)
541
+
542
+
543
+ __all__ = [
544
+ "ResponseStreamAlgorithms",
545
+ "ResponseStreamCompatibilityApi",
546
+ "ResponseStreamContext",
547
+ "ResponseStreamConversationPorts",
548
+ "ResponseStreamIoPorts",
549
+ "ResponseStreamRecoveryPorts",
550
+ "ResponseStreamRuntimePorts",
551
+ "ResponseStreamTextPorts",
552
+ "ResponseStreamToolPorts",
553
+ "ResponseStreamTracePorts",
554
+ "ResponseStreamTypes",
555
+ ]
@@ -0,0 +1,121 @@
1
+ """Compatibility repairs for replayed OpenAI Responses input items."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+
9
+ OPENAI_RESPONSES_ITEM_ID_PREFIXES = {
10
+ "message": "msg_",
11
+ "reasoning": "rs_",
12
+ "function_call": "fc_",
13
+ "function_call_output": "fco_",
14
+ "custom_tool_call": "ctc_",
15
+ "custom_tool_call_output": "ctco_",
16
+ }
17
+
18
+ # Item IDs the router mints itself when it projects a foreign provider's answer
19
+ # into a Responses payload: a type prefix, eight hex digits taken from the
20
+ # locally generated response ID, and the block's index. No upstream issues IDs
21
+ # in this shape -- OpenAI's own are a prefix plus a long unbroken hex run
22
+ # (``rs_0b9eabb999...``) and Codex's client-side IDs are UUIDs
23
+ # (``fco_019fd660-738b-...``) -- so the shape identifies the router's own.
24
+ _ROUTER_SYNTHESIZED_ITEM_ID = re.compile(r"^[a-z]+_[0-9a-f]{8}_\d+$")
25
+
26
+
27
+ def router_synthesized_item_id(prefix: str, response_id: str, index: int) -> str:
28
+ """Mint the item ID for one projected block of a foreign response."""
29
+
30
+ return f"{prefix}_{response_id[5:13]}_{index}"
31
+
32
+
33
+ def is_router_synthesized_item_id(item_id: Any) -> bool:
34
+ """Report whether an ID was minted locally rather than by an upstream."""
35
+
36
+ return bool(_ROUTER_SYNTHESIZED_ITEM_ID.match(str(item_id or "")))
37
+
38
+
39
+ def repair_replayed_response_items(body: dict[str, Any]) -> dict[str, Any]:
40
+ """Repair provider response records that cannot be replayed by OpenAI.
41
+
42
+ Some Responses-compatible providers emit reasoning items with message IDs
43
+ (``msg_...``). Codex persists those output items and later replays them as
44
+ input. The OpenAI Codex backend requires reasoning IDs to start with
45
+ ``rs_`` and rejects the whole turn otherwise.
46
+
47
+ A foreign summary-only reasoning item is not authoritative conversation
48
+ content, so it is dropped. If encrypted reasoning content is present, the
49
+ item is retained but its invalid local/provider ID is omitted.
50
+
51
+ Some providers also emit tool items with message IDs (``msg_``) while
52
+ preserving the independent ``call_id`` used to pair calls with outputs.
53
+ Validate IDs by Responses item type and remove only a mismatched item ID;
54
+ keep ``call_id``, the tool name, arguments, output, and message content so
55
+ the transcript remains usable.
56
+
57
+ IDs the router minted itself are treated the same way. They carry the right
58
+ prefix, so the type check alone accepts them, but no backend ever stored
59
+ them: OpenAI answers a replayed one with ``Item with id 'rs_..._0' not
60
+ found. Items are not persisted when 'store' is set to false``, which kills
61
+ the turn. Dropping the ID keeps the content and lets the item be replayed
62
+ inline.
63
+ """
64
+
65
+ value = body.get("input")
66
+ if not isinstance(value, list):
67
+ return body
68
+
69
+ changed = False
70
+ repaired: list[Any] = []
71
+ for value_item in value:
72
+ if not isinstance(value_item, dict):
73
+ repaired.append(value_item)
74
+ continue
75
+ item = value_item
76
+ item_type = item.get("type")
77
+ item_id = str(item.get("id") or "")
78
+ expected_prefix = OPENAI_RESPONSES_ITEM_ID_PREFIXES.get(str(item_type or ""))
79
+ invalid_item_id = bool(item_id) and (
80
+ bool(expected_prefix and not item_id.startswith(expected_prefix))
81
+ or is_router_synthesized_item_id(item_id)
82
+ )
83
+ if item_type != "reasoning" and invalid_item_id:
84
+ retained = dict(item)
85
+ retained.pop("id", None)
86
+ repaired.append(retained)
87
+ changed = True
88
+ continue
89
+ if item_type != "reasoning":
90
+ repaired.append(item)
91
+ continue
92
+ if not invalid_item_id:
93
+ repaired.append(item)
94
+ continue
95
+
96
+ changed = True
97
+ if not str(item.get("encrypted_content") or "").strip():
98
+ # A summary generated by another provider is neither signed nor
99
+ # required to preserve the visible/tool conversation history.
100
+ continue
101
+ retained = dict(item)
102
+ retained.pop("id", None)
103
+ repaired.append(retained)
104
+
105
+ if not changed:
106
+ return body
107
+ projected = dict(body)
108
+ projected["input"] = repaired
109
+ return projected
110
+
111
+
112
+ repair_replayed_reasoning_items = repair_replayed_response_items
113
+
114
+
115
+ __all__ = [
116
+ "OPENAI_RESPONSES_ITEM_ID_PREFIXES",
117
+ "is_router_synthesized_item_id",
118
+ "repair_replayed_reasoning_items",
119
+ "repair_replayed_response_items",
120
+ "router_synthesized_item_id",
121
+ ]