@oneciel-ai/ciel-runtime 0.1.1 → 0.2.0

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 (287) hide show
  1. package/README.md +132 -0
  2. package/ciel-runtime-menu.py +56 -6
  3. package/ciel_runtime.py +9379 -35909
  4. package/ciel_runtime_support/advisor_client.py +193 -0
  5. package/ciel_runtime_support/advisor_policy.py +320 -0
  6. package/ciel_runtime_support/advisor_refinement.py +160 -0
  7. package/ciel_runtime_support/advisor_request_builder.py +261 -0
  8. package/ciel_runtime_support/agy_installer.py +169 -0
  9. package/ciel_runtime_support/agy_mcp_restore.py +182 -0
  10. package/ciel_runtime_support/anthropic_model_policy.py +186 -0
  11. package/ciel_runtime_support/anthropic_response_writer.py +255 -0
  12. package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
  13. package/ciel_runtime_support/api_key_cooldown.py +159 -0
  14. package/ciel_runtime_support/architecture.py +488 -1
  15. package/ciel_runtime_support/architecture_budget.py +42 -0
  16. package/ciel_runtime_support/channel_backlog.py +90 -0
  17. package/ciel_runtime_support/channel_cli.py +119 -0
  18. package/ciel_runtime_support/channel_compact_injection.py +82 -0
  19. package/ciel_runtime_support/channel_compact_poll.py +67 -0
  20. package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
  21. package/ciel_runtime_support/channel_config_service.py +281 -0
  22. package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
  23. package/ciel_runtime_support/channel_connection_registry.py +128 -0
  24. package/ciel_runtime_support/channel_connection_worker.py +284 -0
  25. package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
  26. package/ciel_runtime_support/channel_cursor_repository.py +89 -0
  27. package/ciel_runtime_support/channel_cursor_service.py +178 -0
  28. package/ciel_runtime_support/channel_event_identity.py +212 -0
  29. package/ciel_runtime_support/channel_event_projection.py +315 -0
  30. package/ciel_runtime_support/channel_inflight.py +127 -0
  31. package/ciel_runtime_support/channel_injection.py +115 -0
  32. package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
  33. package/ciel_runtime_support/channel_launch_policy.py +180 -0
  34. package/ciel_runtime_support/channel_llm_context.py +156 -0
  35. package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
  36. package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
  37. package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
  38. package/ciel_runtime_support/channel_mcp_tools.py +240 -0
  39. package/ciel_runtime_support/channel_mcp_transport.py +394 -0
  40. package/ciel_runtime_support/channel_message_dedupe.py +65 -0
  41. package/ciel_runtime_support/channel_message_policy.py +256 -0
  42. package/ciel_runtime_support/channel_message_prompt.py +305 -0
  43. package/ciel_runtime_support/channel_message_repository.py +234 -0
  44. package/ciel_runtime_support/channel_notification_projection.py +217 -0
  45. package/ciel_runtime_support/channel_panel.py +162 -0
  46. package/ciel_runtime_support/channel_pending_injection.py +209 -0
  47. package/ciel_runtime_support/channel_pending_poll.py +109 -0
  48. package/ciel_runtime_support/channel_probe_cache.py +433 -0
  49. package/ciel_runtime_support/channel_probe_report.py +101 -0
  50. package/ciel_runtime_support/channel_runtime_environment.py +181 -0
  51. package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
  52. package/ciel_runtime_support/channel_session_repository.py +90 -0
  53. package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
  54. package/ciel_runtime_support/channel_terminal_input.py +277 -0
  55. package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
  56. package/ciel_runtime_support/channel_tool_context.py +166 -0
  57. package/ciel_runtime_support/channel_transcript.py +414 -0
  58. package/ciel_runtime_support/channel_transcript_repository.py +96 -0
  59. package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
  60. package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
  61. package/ciel_runtime_support/chat_files.py +138 -0
  62. package/ciel_runtime_support/chat_http_controller.py +235 -0
  63. package/ciel_runtime_support/claude_environment.py +375 -0
  64. package/ciel_runtime_support/claude_router.py +247 -193
  65. package/ciel_runtime_support/cli_dispatch.py +792 -0
  66. package/ciel_runtime_support/cli_parser.py +165 -0
  67. package/ciel_runtime_support/cli_usage.py +100 -0
  68. package/ciel_runtime_support/codex_app_server.py +20 -5
  69. package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
  70. package/ciel_runtime_support/codex_cli.py +42 -6
  71. package/ciel_runtime_support/codex_config.py +323 -0
  72. package/ciel_runtime_support/codex_launch_configuration.py +240 -0
  73. package/ciel_runtime_support/codex_launch_policy.py +66 -0
  74. package/ciel_runtime_support/codex_mcp_integration.py +195 -0
  75. package/ciel_runtime_support/codex_mcp_restore.py +304 -0
  76. package/ciel_runtime_support/codex_model_catalog.py +133 -0
  77. package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
  78. package/ciel_runtime_support/codex_router.py +147 -1
  79. package/ciel_runtime_support/codex_session_repository.py +115 -0
  80. package/ciel_runtime_support/codex_session_selection.py +114 -0
  81. package/ciel_runtime_support/command_asset_installer.py +103 -0
  82. package/ciel_runtime_support/compatibility_probe.py +295 -0
  83. package/ciel_runtime_support/compatibility_protocol.py +251 -0
  84. package/ciel_runtime_support/compatibility_runtime.py +166 -0
  85. package/ciel_runtime_support/compatibility_test.py +370 -0
  86. package/ciel_runtime_support/config_migrations.py +307 -0
  87. package/ciel_runtime_support/config_repository.py +175 -0
  88. package/ciel_runtime_support/config_value_codec.py +64 -0
  89. package/ciel_runtime_support/configuration_cli.py +374 -0
  90. package/ciel_runtime_support/context_compaction.py +280 -0
  91. package/ciel_runtime_support/context_setup.py +208 -0
  92. package/ciel_runtime_support/context_summary_policy.py +392 -0
  93. package/ciel_runtime_support/credential_cli.py +104 -0
  94. package/ciel_runtime_support/credential_management.py +261 -0
  95. package/ciel_runtime_support/credentials.py +269 -0
  96. package/ciel_runtime_support/executable_discovery.py +141 -0
  97. package/ciel_runtime_support/github_copilot_oauth.py +335 -0
  98. package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
  99. package/ciel_runtime_support/header_forwarding.py +73 -0
  100. package/ciel_runtime_support/headless_config.py +221 -0
  101. package/ciel_runtime_support/http_response.py +129 -0
  102. package/ciel_runtime_support/install_diagnostics.py +149 -0
  103. package/ciel_runtime_support/kimi_identity.py +123 -0
  104. package/ciel_runtime_support/launch_diagnostics.py +204 -0
  105. package/ciel_runtime_support/launch_state.py +127 -0
  106. package/ciel_runtime_support/live_api_key_controller.py +58 -0
  107. package/ciel_runtime_support/llm_config_http.py +148 -0
  108. package/ciel_runtime_support/llm_option_config.py +259 -0
  109. package/ciel_runtime_support/llm_presentation_data.py +447 -0
  110. package/ciel_runtime_support/llm_presets.py +773 -0
  111. package/ciel_runtime_support/lm_studio_runtime.py +401 -0
  112. package/ciel_runtime_support/managed_mcp_config.py +144 -0
  113. package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
  114. package/ciel_runtime_support/managed_service_cleanup.py +89 -0
  115. package/ciel_runtime_support/mcp_config_reader.py +230 -0
  116. package/ciel_runtime_support/mcp_http_proxy.py +607 -0
  117. package/ciel_runtime_support/mcp_inventory.py +59 -0
  118. package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
  119. package/ciel_runtime_support/mcp_probe_codec.py +136 -0
  120. package/ciel_runtime_support/mcp_probe_transport.py +328 -0
  121. package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
  122. package/ciel_runtime_support/mcp_proxy_config.py +107 -0
  123. package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
  124. package/ciel_runtime_support/mcp_proxy_process.py +560 -0
  125. package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
  126. package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
  127. package/ciel_runtime_support/mcp_transport.py +146 -0
  128. package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
  129. package/ciel_runtime_support/model_catalog_projection.py +61 -0
  130. package/ciel_runtime_support/model_context_hints.py +109 -0
  131. package/ciel_runtime_support/model_panel.py +147 -0
  132. package/ciel_runtime_support/model_registry_repository.py +231 -0
  133. package/ciel_runtime_support/npm_runtime.py +191 -0
  134. package/ciel_runtime_support/ollama_catalog.py +462 -0
  135. package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
  136. package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
  137. package/ciel_runtime_support/ollama_context_sync.py +87 -0
  138. package/ciel_runtime_support/ollama_forwarding.py +449 -0
  139. package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
  140. package/ciel_runtime_support/openai_chat_router.py +64 -0
  141. package/ciel_runtime_support/openai_forwarding.py +194 -0
  142. package/ciel_runtime_support/openai_responses_router.py +291 -0
  143. package/ciel_runtime_support/openai_responses_stream.py +135 -0
  144. package/ciel_runtime_support/output_budget.py +89 -0
  145. package/ciel_runtime_support/package_lifecycle.py +217 -0
  146. package/ciel_runtime_support/plan_artifact_controller.py +104 -0
  147. package/ciel_runtime_support/prelaunch.py +959 -0
  148. package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
  149. package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
  150. package/ciel_runtime_support/prelaunch_terminal.py +764 -0
  151. package/ciel_runtime_support/process_control.py +708 -0
  152. package/ciel_runtime_support/prompt_compaction.py +322 -0
  153. package/ciel_runtime_support/prompt_injection.py +176 -0
  154. package/ciel_runtime_support/protocols/__init__.py +24 -0
  155. package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
  156. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
  157. package/ciel_runtime_support/protocols/chat_projection.py +315 -0
  158. package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
  159. package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
  160. package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
  161. package/ciel_runtime_support/protocols/ollama_response.py +231 -0
  162. package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
  163. package/ciel_runtime_support/protocols/openai_responses.py +271 -0
  164. package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
  165. package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
  166. package/ciel_runtime_support/provider_adapters.py +160 -0
  167. package/ciel_runtime_support/provider_catalog_sources.py +316 -0
  168. package/ciel_runtime_support/provider_choice.py +201 -0
  169. package/ciel_runtime_support/provider_compatibility.py +165 -0
  170. package/ciel_runtime_support/provider_config_mutations.py +361 -0
  171. package/ciel_runtime_support/provider_configuration_service.py +162 -0
  172. package/ciel_runtime_support/provider_context.py +308 -0
  173. package/ciel_runtime_support/provider_contract_projection.py +73 -0
  174. package/ciel_runtime_support/provider_descriptor.py +82 -0
  175. package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
  176. package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
  177. package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
  178. package/ciel_runtime_support/provider_limits.py +457 -0
  179. package/ciel_runtime_support/provider_model_identity.py +139 -0
  180. package/ciel_runtime_support/provider_model_selection.py +431 -0
  181. package/ciel_runtime_support/provider_model_specs.py +142 -0
  182. package/ciel_runtime_support/provider_models.py +263 -0
  183. package/ciel_runtime_support/provider_network.py +176 -0
  184. package/ciel_runtime_support/provider_option_cli.py +238 -0
  185. package/ciel_runtime_support/provider_option_panel.py +275 -0
  186. package/ciel_runtime_support/provider_option_status.py +192 -0
  187. package/ciel_runtime_support/provider_policy.py +101 -0
  188. package/ciel_runtime_support/provider_query_policy.py +67 -0
  189. package/ciel_runtime_support/provider_readiness.py +112 -0
  190. package/ciel_runtime_support/provider_request_access.py +131 -0
  191. package/ciel_runtime_support/provider_request_builder.py +250 -0
  192. package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
  193. package/ciel_runtime_support/provider_runtime_info.py +113 -0
  194. package/ciel_runtime_support/provider_runtime_modes.py +150 -0
  195. package/ciel_runtime_support/provider_sampling_policy.py +46 -0
  196. package/ciel_runtime_support/provider_status.py +145 -0
  197. package/ciel_runtime_support/provider_timeout_policy.py +184 -0
  198. package/ciel_runtime_support/provider_tool_policy.py +145 -0
  199. package/ciel_runtime_support/providers/__init__.py +65 -0
  200. package/ciel_runtime_support/providers/anthropic.py +160 -0
  201. package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
  202. package/ciel_runtime_support/providers/base.py +232 -0
  203. package/ciel_runtime_support/providers/catalog.py +326 -0
  204. package/ciel_runtime_support/providers/cloud.py +194 -0
  205. package/ciel_runtime_support/providers/constants.py +54 -0
  206. package/ciel_runtime_support/providers/deepseek.py +115 -0
  207. package/ciel_runtime_support/providers/fireworks.py +158 -0
  208. package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
  209. package/ciel_runtime_support/providers/kimi.py +297 -0
  210. package/ciel_runtime_support/providers/lm_studio.py +76 -0
  211. package/ciel_runtime_support/providers/meta.py +257 -0
  212. package/ciel_runtime_support/providers/native.py +194 -0
  213. package/ciel_runtime_support/providers/nim.py +69 -0
  214. package/ciel_runtime_support/providers/nvidia.py +158 -0
  215. package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
  216. package/ciel_runtime_support/providers/ollama.py +181 -0
  217. package/ciel_runtime_support/providers/ollama_context.py +195 -0
  218. package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
  219. package/ciel_runtime_support/providers/opencode.py +220 -0
  220. package/ciel_runtime_support/providers/opencode_go.py +37 -0
  221. package/ciel_runtime_support/providers/openrouter.py +57 -0
  222. package/ciel_runtime_support/providers/vllm.py +65 -0
  223. package/ciel_runtime_support/providers/zai.py +119 -0
  224. package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
  225. package/ciel_runtime_support/rate_limit_policy.py +117 -0
  226. package/ciel_runtime_support/rate_limit_repository.py +154 -0
  227. package/ciel_runtime_support/registry.py +46 -0
  228. package/ciel_runtime_support/request_shortcuts.py +253 -0
  229. package/ciel_runtime_support/request_trace.py +323 -0
  230. package/ciel_runtime_support/response_collection.py +209 -0
  231. package/ciel_runtime_support/router_access.py +238 -0
  232. package/ciel_runtime_support/router_client_lifecycle.py +366 -0
  233. package/ciel_runtime_support/router_health_policy.py +101 -0
  234. package/ciel_runtime_support/router_http.py +513 -0
  235. package/ciel_runtime_support/router_process_lifecycle.py +401 -0
  236. package/ciel_runtime_support/router_rate_limit_service.py +285 -0
  237. package/ciel_runtime_support/router_server_runtime.py +103 -0
  238. package/ciel_runtime_support/router_shortcuts.py +201 -0
  239. package/ciel_runtime_support/routing_fallback.py +73 -0
  240. package/ciel_runtime_support/runtime_activity_repository.py +143 -0
  241. package/ciel_runtime_support/runtime_adapters.py +104 -0
  242. package/ciel_runtime_support/runtime_command_factory.py +73 -0
  243. package/ciel_runtime_support/runtime_compatibility.py +50 -0
  244. package/ciel_runtime_support/runtime_constants.py +178 -0
  245. package/ciel_runtime_support/runtime_launch.py +1602 -0
  246. package/ciel_runtime_support/runtime_llm_options.py +312 -0
  247. package/ciel_runtime_support/runtime_logging.py +161 -0
  248. package/ciel_runtime_support/runtime_paths.py +157 -0
  249. package/ciel_runtime_support/runtime_restart.py +84 -0
  250. package/ciel_runtime_support/runtime_upgrade.py +149 -0
  251. package/ciel_runtime_support/secure_json_repository.py +55 -0
  252. package/ciel_runtime_support/session_import.py +356 -0
  253. package/ciel_runtime_support/settings_repository.py +8 -0
  254. package/ciel_runtime_support/slash_command_assets.py +211 -0
  255. package/ciel_runtime_support/sse_stream.py +57 -0
  256. package/ciel_runtime_support/sse_trace.py +225 -0
  257. package/ciel_runtime_support/statusline_script.py +593 -0
  258. package/ciel_runtime_support/statusline_settings.py +53 -0
  259. package/ciel_runtime_support/stream_chunk_policy.py +18 -0
  260. package/ciel_runtime_support/streaming_anthropic.py +1955 -0
  261. package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
  262. package/ciel_runtime_support/terminal_platform_io.py +127 -0
  263. package/ciel_runtime_support/timeout_profile.py +196 -0
  264. package/ciel_runtime_support/tool_dialects.py +85 -0
  265. package/ciel_runtime_support/tool_exposure_policy.py +63 -0
  266. package/ciel_runtime_support/tool_guard_hooks.py +218 -0
  267. package/ciel_runtime_support/tool_request_projection.py +96 -0
  268. package/ciel_runtime_support/tool_schema.py +483 -0
  269. package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
  270. package/ciel_runtime_support/ui_text.py +266 -0
  271. package/ciel_runtime_support/upstream_error_policy.py +104 -0
  272. package/ciel_runtime_support/upstream_retry.py +419 -0
  273. package/ciel_runtime_support/upstream_stream_io.py +106 -0
  274. package/ciel_runtime_support/usage_events.py +96 -0
  275. package/ciel_runtime_support/visible_stream_filters.py +130 -0
  276. package/ciel_runtime_support/web_endpoints.py +447 -0
  277. package/ciel_runtime_support/web_ui.py +915 -0
  278. package/ciel_runtime_support/web_ui_controller.py +189 -0
  279. package/ciel_runtime_support/windows_console_input.py +137 -0
  280. package/ciel_runtime_support/windows_console_mode.py +112 -0
  281. package/docs/Architecture.md +54 -0
  282. package/docs/Configuration.md +17 -0
  283. package/docs/Module-Map.md +1093 -26
  284. package/docs/Providers.md +46 -1
  285. package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
  286. package/npm-bin/run-ciel-runtime.js +22 -2
  287. package/package.json +9 -2
@@ -0,0 +1,111 @@
1
+ """Pure Anthropic-to-Ollama chat wire projections."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from ciel_runtime_support.protocols.anthropic_content import content_to_text
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class DecodedOllamaChatResponse:
13
+ """Provider-neutral values decoded from one Ollama `/api/chat` response."""
14
+
15
+ text: str
16
+ tool_calls: tuple[dict[str, Any], ...]
17
+ done_reason: str
18
+ input_tokens: int
19
+ output_tokens: int
20
+
21
+
22
+ def decode_ollama_chat_response(data: dict[str, Any]) -> DecodedOllamaChatResponse:
23
+ """Decode the Ollama envelope without applying runtime/tool policies."""
24
+
25
+ message = data.get("message") if isinstance(data.get("message"), dict) else {}
26
+ calls = message.get("tool_calls") if isinstance(message, dict) else None
27
+ return DecodedOllamaChatResponse(
28
+ text=str(message.get("content") or ""),
29
+ tool_calls=tuple(call for call in (calls or []) if isinstance(call, dict)),
30
+ done_reason=str(data.get("done_reason") or ""),
31
+ input_tokens=max(0, int(data.get("prompt_eval_count") or 0)),
32
+ output_tokens=max(0, int(data.get("eval_count") or 0)),
33
+ )
34
+
35
+
36
+ def encode_anthropic_message(
37
+ *,
38
+ message_id: str,
39
+ model: str,
40
+ content: list[dict[str, Any]],
41
+ done_reason: str,
42
+ input_tokens: int,
43
+ output_tokens: int,
44
+ ) -> dict[str, Any]:
45
+ """Encode finalized content blocks as an Anthropic Messages response."""
46
+
47
+ has_tool_use = any(block.get("type") == "tool_use" for block in content)
48
+ stop_reason = "tool_use" if has_tool_use else "end_turn"
49
+ if done_reason == "length":
50
+ stop_reason = "max_tokens"
51
+ return {
52
+ "id": message_id,
53
+ "type": "message",
54
+ "role": "assistant",
55
+ "model": model,
56
+ "content": content or [{"type": "text", "text": ""}],
57
+ "stop_reason": stop_reason,
58
+ "stop_sequence": None,
59
+ "usage": {
60
+ "input_tokens": input_tokens,
61
+ "output_tokens": output_tokens,
62
+ },
63
+ }
64
+
65
+
66
+ def anthropic_system_to_ollama_messages(system: Any) -> list[dict[str, Any]]:
67
+ if not system:
68
+ return []
69
+ text = content_to_text(system)
70
+ return [{"role": "system", "content": text}] if text else []
71
+
72
+
73
+ def ollama_claude_code_reminder() -> dict[str, str]:
74
+ return {
75
+ "role": "system",
76
+ "content": (
77
+ "Claude Code execution reminder: when the user asks to create, edit, or run code, "
78
+ "use the available tools such as Write, Edit, Read, and Bash. Do not stop after saying "
79
+ "you will run something. Do not present a code block as a substitute for creating the file "
80
+ "unless the user explicitly asks for code only. Exception: while Claude Code is in Plan Mode, "
81
+ "do not implement normal project files; explore as needed, write or update the plan file, "
82
+ "then call ExitPlanMode when the plan is ready for approval."
83
+ ),
84
+ }
85
+
86
+
87
+ def anthropic_tools_to_ollama(tools: Any) -> list[dict[str, Any]]:
88
+ if not isinstance(tools, list):
89
+ return []
90
+ return [
91
+ {
92
+ "type": "function",
93
+ "function": {
94
+ "name": tool["name"],
95
+ "description": tool.get("description", ""),
96
+ "parameters": tool.get("input_schema") or {"type": "object", "properties": {}},
97
+ },
98
+ }
99
+ for tool in tools
100
+ if isinstance(tool, dict) and tool.get("name")
101
+ ]
102
+
103
+
104
+ __all__ = [
105
+ "DecodedOllamaChatResponse",
106
+ "anthropic_system_to_ollama_messages",
107
+ "anthropic_tools_to_ollama",
108
+ "decode_ollama_chat_response",
109
+ "encode_anthropic_message",
110
+ "ollama_claude_code_reminder",
111
+ ]
@@ -0,0 +1,231 @@
1
+ """Ollama response projection into Anthropic Messages content."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class OllamaResponseText:
11
+ decode: Callable[[dict[str, Any]], Any]
12
+ strip_thinking: Callable[[str], str]
13
+ parse_pseudo_tools: Callable[..., tuple[str, list[dict[str, Any]]]]
14
+ log: Callable[[str, str], Any]
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class OllamaResponseTools:
19
+ resolve_name: Callable[..., str]
20
+ normalize_arguments: Callable[..., dict[str, Any]]
21
+ validate_input: Callable[..., dict[str, Any]]
22
+ plan_mode_name: Callable[..., tuple[str | None, dict[str, Any]]]
23
+ cap_notification_wait: Callable[..., dict[str, Any]]
24
+ should_drop: Callable[..., bool]
25
+ append_log: Callable[..., Any]
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class OllamaResponseRecovery:
30
+ auto_enter_plan: Callable[..., bool]
31
+ recover_empty_with_tasklist: Callable[..., bool]
32
+ keep_alive_with_tasklist: Callable[..., bool]
33
+ auto_continue_choice: Callable[..., bool]
34
+ empty_notice: Callable[[dict[str, Any]], str]
35
+ latest_tool_result_names: Callable[[dict[str, Any]], list[str]]
36
+ synthetic_tool_response: Callable[..., dict[str, Any]]
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class OllamaResponseOutput:
41
+ encode_message: Callable[..., dict[str, Any]]
42
+ estimate_tokens: Callable[[dict[str, Any]], int]
43
+ timestamp_ms: Callable[[], int]
44
+ process_id: Callable[[], int]
45
+
46
+
47
+ @dataclass(frozen=True, slots=True)
48
+ class OllamaResponseServices:
49
+ text: OllamaResponseText
50
+ tools: OllamaResponseTools
51
+ recovery: OllamaResponseRecovery
52
+ output: OllamaResponseOutput
53
+
54
+
55
+ def project_ollama_response(
56
+ data: dict[str, Any],
57
+ model: str,
58
+ source_body: dict[str, Any] | None,
59
+ services: OllamaResponseServices,
60
+ ) -> dict[str, Any]:
61
+ decoded = services.text.decode(data)
62
+ content: list[dict[str, Any]] = []
63
+ raw_text = decoded.text
64
+ text = services.text.strip_thinking(raw_text)
65
+ if text != raw_text:
66
+ services.text.log(
67
+ "WARN",
68
+ f"suppressed visible Ollama thinking markup model={model} removed_chars={len(raw_text) - len(text)}",
69
+ )
70
+ text, pseudo_tool_calls = services.text.parse_pseudo_tools(text, source_body)
71
+ if text:
72
+ content.append({"type": "text", "text": text})
73
+ tool_id_prefix = f"toolu_ollama_{services.output.timestamp_ms()}_{services.output.process_id()}"
74
+ for index, call in enumerate(list(decoded.tool_calls) + pseudo_tool_calls):
75
+ tool_block = _project_tool_call(call, index, tool_id_prefix, model, source_body, services)
76
+ if tool_block is not None:
77
+ content.append(tool_block)
78
+
79
+ emitted = [block for block in content if block.get("type") == "tool_use"]
80
+ recovered = _recover_response(model, source_body, text, emitted, content, tool_id_prefix, services)
81
+ if recovered is not None:
82
+ return recovered
83
+ if source_body is not None and not text.strip() and not emitted:
84
+ text = services.recovery.empty_notice(source_body)
85
+ names = ",".join(services.recovery.latest_tool_result_names(source_body)) or "-"
86
+ services.text.log("WARN", f"ollama_empty_end_turn_notice model={model} latest_tool_results={names}")
87
+ content.append({"type": "text", "text": text})
88
+
89
+ input_tokens = decoded.input_tokens
90
+ if input_tokens <= 0 and isinstance(source_body, dict):
91
+ input_tokens = services.output.estimate_tokens(source_body)
92
+ output_tokens = decoded.output_tokens or max(1, len(text) // 4)
93
+ return services.output.encode_message(
94
+ message_id=f"msg_ollama_{services.output.timestamp_ms()}",
95
+ model=model,
96
+ content=content,
97
+ done_reason=decoded.done_reason,
98
+ input_tokens=input_tokens,
99
+ output_tokens=output_tokens,
100
+ )
101
+
102
+
103
+ def project_openai_chat_response(
104
+ data: dict[str, Any],
105
+ model: str,
106
+ source_body: dict[str, Any] | None,
107
+ *,
108
+ services: OllamaResponseServices,
109
+ positive_int: Callable[[Any], int | None],
110
+ reasoning_to_block: Callable[[Any], dict[str, Any] | None],
111
+ content_to_text: Callable[[Any], str],
112
+ ) -> dict[str, Any]:
113
+ """Project an OpenAI-compatible chat response through the shared codec."""
114
+ choices = data.get("choices")
115
+ choice = (
116
+ choices[0]
117
+ if isinstance(choices, list)
118
+ and choices
119
+ and isinstance(choices[0], dict)
120
+ else {}
121
+ )
122
+ raw_message = choice.get("message")
123
+ message = raw_message if isinstance(raw_message, dict) else {}
124
+ usage_value = data.get("usage")
125
+ usage = usage_value if isinstance(usage_value, dict) else {}
126
+ wrapped = {
127
+ "message": {
128
+ "content": message.get("content") or "",
129
+ "tool_calls": message.get("tool_calls") or [],
130
+ },
131
+ "done_reason": (
132
+ "length"
133
+ if choice.get("finish_reason") == "length"
134
+ else "stop"
135
+ ),
136
+ "prompt_eval_count": positive_int(usage.get("prompt_tokens"))
137
+ or (
138
+ services.output.estimate_tokens(source_body)
139
+ if isinstance(source_body, dict)
140
+ else 0
141
+ ),
142
+ "eval_count": positive_int(usage.get("completion_tokens")) or 0,
143
+ }
144
+ output = project_ollama_response(
145
+ wrapped,
146
+ model,
147
+ source_body,
148
+ services,
149
+ )
150
+ thinking_block = reasoning_to_block(message.get("reasoning_content"))
151
+ if thinking_block is None:
152
+ return output
153
+ content = output.get("content")
154
+ if not isinstance(content, list):
155
+ content = [{"type": "text", "text": content_to_text(content)}]
156
+ output = dict(output)
157
+ output["content"] = [thinking_block, *content]
158
+ return output
159
+
160
+
161
+ def _project_tool_call(
162
+ call: dict[str, Any],
163
+ index: int,
164
+ id_prefix: str,
165
+ model: str,
166
+ source_body: dict[str, Any] | None,
167
+ services: OllamaResponseServices,
168
+ ) -> dict[str, Any] | None:
169
+ function = call.get("function") if isinstance(call, dict) else {}
170
+ if not isinstance(function, dict) or not function.get("name"):
171
+ return None
172
+ raw_name = str(function["name"])
173
+ name = services.tools.resolve_name(raw_name, source_body)
174
+ raw_arguments = function.get("arguments")
175
+ normalized = services.tools.normalize_arguments(name, raw_arguments)
176
+ tool_input = services.tools.validate_input(name, normalized)
177
+ if source_body is not None:
178
+ name, tool_input = services.tools.plan_mode_name(source_body, name, tool_input)
179
+ if name is None:
180
+ return None
181
+ tool_input = services.tools.cap_notification_wait(name, tool_input)
182
+ if services.tools.should_drop(name, tool_input, raw_name, source_body):
183
+ return None
184
+ services.tools.append_log(
185
+ "ollama_nonstream_tool_call",
186
+ {
187
+ "model": model,
188
+ "raw_name": raw_name,
189
+ "matched_name": name,
190
+ "raw_arguments": raw_arguments,
191
+ "normalized_arguments": normalized,
192
+ "emitted_input": tool_input,
193
+ },
194
+ )
195
+ return {"type": "tool_use", "id": f"{id_prefix}_{index}", "name": name, "input": tool_input}
196
+
197
+
198
+ def _recover_response(
199
+ model: str,
200
+ source_body: dict[str, Any] | None,
201
+ text: str,
202
+ emitted: list[dict[str, Any]],
203
+ content: list[dict[str, Any]],
204
+ id_prefix: str,
205
+ services: OllamaResponseServices,
206
+ ) -> dict[str, Any] | None:
207
+ if source_body is None:
208
+ return None
209
+ recovery = services.recovery
210
+ if recovery.auto_enter_plan(source_body, text, emitted):
211
+ services.text.log("WARN", "auto-synthesized EnterPlanMode from short/empty upstream response")
212
+ return recovery.synthetic_tool_response(model, "EnterPlanMode")
213
+ if recovery.recover_empty_with_tasklist(source_body, text, emitted):
214
+ services.text.log("WARN", "auto-synthesized TaskList from empty upstream end_turn")
215
+ return recovery.synthetic_tool_response(model, "TaskList")
216
+ if recovery.keep_alive_with_tasklist(source_body, text, emitted):
217
+ services.text.log("WARN", "auto-synthesized TaskList to keep work moving after tool result")
218
+ block = {"type": "tool_use", "id": f"{id_prefix}_keepalive", "name": "TaskList", "input": {}}
219
+ content.append(block)
220
+ emitted.append(block)
221
+ if recovery.auto_continue_choice(source_body, text, emitted):
222
+ services.text.log("WARN", "auto-synthesized TaskList after clarification question")
223
+ block = {
224
+ "type": "tool_use",
225
+ "id": f"toolu_ollama_choice_{services.output.timestamp_ms()}",
226
+ "name": "TaskList",
227
+ "input": {},
228
+ }
229
+ content.append(block)
230
+ emitted.append(block)
231
+ return None
@@ -0,0 +1,75 @@
1
+ """OpenAI reasoning/tool-choice projection and provider policy orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from dataclasses import dataclass
7
+ from typing import Any, Callable
8
+
9
+ from ..architecture import ProviderAdapter, ProviderConfig
10
+
11
+
12
+ def anthropic_tool_choice_to_openai(tool_choice: Any) -> Any:
13
+ if not isinstance(tool_choice, dict):
14
+ return tool_choice
15
+ choice_type = tool_choice.get("type")
16
+ if choice_type == "tool" and tool_choice.get("name"):
17
+ return {"type": "function", "function": {"name": str(tool_choice["name"])}}
18
+ if choice_type == "any":
19
+ return "required"
20
+ if choice_type == "auto":
21
+ return "auto"
22
+ return tool_choice
23
+
24
+
25
+ def openai_reasoning_to_anthropic_thinking_block(
26
+ reasoning_content: Any,
27
+ ) -> dict[str, Any] | None:
28
+ reasoning = str(reasoning_content or "")
29
+ if not reasoning:
30
+ return None
31
+ digest = hashlib.sha256(reasoning.encode("utf-8", errors="replace")).hexdigest()[:24]
32
+ return {
33
+ "type": "thinking",
34
+ "thinking": reasoning,
35
+ "signature": f"ciel-runtime-openai-reasoning-{digest}",
36
+ }
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class OpenAiReasoningPolicy:
41
+ """Delegate provider-specific reasoning support to its registered adapter."""
42
+
43
+ adapter_for: Callable[[str, dict[str, Any]], ProviderAdapter]
44
+ config_for: Callable[[str, dict[str, Any]], ProviderConfig]
45
+
46
+ def passback_enabled(
47
+ self, provider: str, model: str | None, config: dict[str, Any]
48
+ ) -> bool:
49
+ adapter = self.adapter_for(provider, config)
50
+ return adapter.openai_reasoning_passback_enabled(
51
+ self.config_for(provider, config), model
52
+ )
53
+
54
+ def passback_enabled_for_body(
55
+ self, provider: str, config: dict[str, Any], body: dict[str, Any]
56
+ ) -> bool:
57
+ return self.passback_enabled(provider, str(body.get("model") or ""), config)
58
+
59
+ def should_omit_tool_choice(
60
+ self,
61
+ provider: str,
62
+ model: str,
63
+ body: dict[str, Any],
64
+ config: dict[str, Any],
65
+ ) -> bool:
66
+ if body.get("tool_choice") is None:
67
+ return False
68
+ return self.passback_enabled(provider, model, config)
69
+
70
+
71
+ __all__ = [
72
+ "OpenAiReasoningPolicy",
73
+ "anthropic_tool_choice_to_openai",
74
+ "openai_reasoning_to_anthropic_thinking_block",
75
+ ]
@@ -0,0 +1,271 @@
1
+ """Pure OpenAI Responses <-> Anthropic Messages conversions.
2
+
3
+ This module intentionally has no router, configuration, or network dependencies.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import time
10
+ import uuid
11
+ from typing import Any
12
+
13
+ from ..architecture import MessageProtocolAdapter
14
+
15
+
16
+ def _positive_int(value: Any) -> int | None:
17
+ try:
18
+ parsed = int(value)
19
+ except (TypeError, ValueError):
20
+ return None
21
+ return parsed if parsed > 0 else None
22
+
23
+
24
+ def _json_object(value: Any) -> dict[str, Any]:
25
+ if isinstance(value, dict):
26
+ return value
27
+ if isinstance(value, str):
28
+ try:
29
+ parsed = json.loads(value)
30
+ return parsed if isinstance(parsed, dict) else {}
31
+ except (TypeError, ValueError, json.JSONDecodeError):
32
+ return {}
33
+ return {}
34
+
35
+
36
+ def _content_text(content: Any) -> str:
37
+ if isinstance(content, str):
38
+ return content
39
+ if isinstance(content, dict):
40
+ content_type = str(content.get("type") or "")
41
+ if content_type in ("input_text", "output_text", "text"):
42
+ return str(content.get("text") or "")
43
+ if content_type == "refusal":
44
+ return str(content.get("refusal") or "")
45
+ return str(content.get("text") or content.get("output") or "")
46
+ if isinstance(content, list):
47
+ parts = [_content_text(item) for item in content]
48
+ return "\n".join(part for part in parts if part)
49
+ return ""
50
+
51
+
52
+ def _content_blocks(content: Any) -> list[dict[str, Any]]:
53
+ text = _content_text(content)
54
+ return [{"type": "text", "text": text}] if text else []
55
+
56
+
57
+ def _tools_to_anthropic(tools: Any) -> list[dict[str, Any]]:
58
+ out: list[dict[str, Any]] = []
59
+ if not isinstance(tools, list):
60
+ return out
61
+ for tool in tools:
62
+ if not isinstance(tool, dict):
63
+ continue
64
+ name = tool.get("name")
65
+ description = tool.get("description", "")
66
+ parameters = tool.get("parameters")
67
+ if not name and isinstance(tool.get("function"), dict):
68
+ function = tool["function"]
69
+ name = function.get("name")
70
+ description = function.get("description", description)
71
+ parameters = function.get("parameters", parameters)
72
+ if tool.get("type") not in (None, "function") and not name:
73
+ continue
74
+ if not name:
75
+ continue
76
+ out.append(
77
+ {
78
+ "name": str(name),
79
+ "description": str(description or ""),
80
+ "input_schema": parameters if isinstance(parameters, dict) else {"type": "object", "properties": {}},
81
+ }
82
+ )
83
+ return out
84
+
85
+
86
+ def _tool_choice_to_anthropic(tool_choice: Any) -> Any:
87
+ if tool_choice is None:
88
+ return None
89
+ if isinstance(tool_choice, str):
90
+ lowered = tool_choice.strip().lower()
91
+ if lowered == "required":
92
+ return {"type": "any"}
93
+ if lowered in ("auto", "none"):
94
+ return {"type": "auto"} if lowered == "auto" else None
95
+ return tool_choice
96
+ if not isinstance(tool_choice, dict):
97
+ return tool_choice
98
+ if tool_choice.get("type") == "function":
99
+ name = tool_choice.get("name")
100
+ if not name and isinstance(tool_choice.get("function"), dict):
101
+ name = tool_choice["function"].get("name")
102
+ if name:
103
+ return {"type": "tool", "name": str(name)}
104
+ return tool_choice
105
+
106
+
107
+ def openai_responses_to_anthropic_messages(body: dict[str, Any], fallback_model: str) -> dict[str, Any]:
108
+ system_parts: list[str] = []
109
+ instructions = str(body.get("instructions") or "").strip()
110
+ if instructions:
111
+ system_parts.append(instructions)
112
+ messages: list[dict[str, Any]] = []
113
+ raw_input = body.get("input", [])
114
+ if isinstance(raw_input, str):
115
+ raw_input = [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": raw_input}]}]
116
+ if isinstance(raw_input, dict):
117
+ raw_input = [raw_input]
118
+ if not isinstance(raw_input, list):
119
+ raw_input = []
120
+ for item in raw_input:
121
+ if not isinstance(item, dict):
122
+ continue
123
+ item_type = str(item.get("type") or "message")
124
+ if item_type == "function_call":
125
+ call_id = str(item.get("call_id") or item.get("id") or f"call_{len(messages) + 1}")
126
+ messages.append(
127
+ {
128
+ "role": "assistant",
129
+ "content": [
130
+ {
131
+ "type": "tool_use",
132
+ "id": call_id,
133
+ "name": str(item.get("name") or "tool"),
134
+ "input": _json_object(item.get("arguments")),
135
+ }
136
+ ],
137
+ }
138
+ )
139
+ continue
140
+ if item_type == "function_call_output":
141
+ call_id = str(item.get("call_id") or item.get("id") or "call_tool")
142
+ messages.append(
143
+ {
144
+ "role": "user",
145
+ "content": [
146
+ {
147
+ "type": "tool_result",
148
+ "tool_use_id": call_id,
149
+ "content": _content_text(item.get("output")),
150
+ }
151
+ ],
152
+ }
153
+ )
154
+ continue
155
+ role = str(item.get("role") or "user").strip().lower()
156
+ blocks = _content_blocks(item.get("content", item.get("text", "")))
157
+ if not blocks:
158
+ continue
159
+ if role in ("system", "developer"):
160
+ system_parts.append(_content_text(blocks))
161
+ continue
162
+ if role not in ("user", "assistant"):
163
+ role = "user"
164
+ messages.append({"role": role, "content": blocks})
165
+ if not messages:
166
+ messages.append({"role": "user", "content": [{"type": "text", "text": ""}]})
167
+ out: dict[str, Any] = {
168
+ "model": str(body.get("model") or fallback_model or "model"),
169
+ "messages": messages,
170
+ "stream": bool(body.get("stream", True)),
171
+ }
172
+ tools = _tools_to_anthropic(body.get("tools"))
173
+ if tools:
174
+ out["tools"] = tools
175
+ tool_choice = _tool_choice_to_anthropic(body.get("tool_choice"))
176
+ if tool_choice is not None:
177
+ out["tool_choice"] = tool_choice
178
+ max_tokens = _positive_int(body.get("max_output_tokens")) or _positive_int(body.get("max_tokens"))
179
+ if max_tokens:
180
+ out["max_tokens"] = max_tokens
181
+ reasoning = body.get("reasoning")
182
+ if isinstance(reasoning, dict) and reasoning.get("effort") is not None:
183
+ out["thinking"] = {
184
+ "type": "enabled",
185
+ "effort": str(reasoning["effort"]),
186
+ }
187
+ if system_parts:
188
+ out["system"] = [{"type": "text", "text": part} for part in system_parts if part]
189
+ return out
190
+
191
+
192
+ def _usage_from_anthropic(message: dict[str, Any]) -> dict[str, int]:
193
+ usage = message.get("usage") if isinstance(message.get("usage"), dict) else {}
194
+ input_tokens = _positive_int(usage.get("input_tokens")) or 0
195
+ output_tokens = _positive_int(usage.get("output_tokens")) or 0
196
+ return {
197
+ "input_tokens": input_tokens,
198
+ "output_tokens": output_tokens,
199
+ "total_tokens": input_tokens + output_tokens,
200
+ }
201
+
202
+
203
+ def anthropic_message_to_openai_response(
204
+ message: dict[str, Any], source_body: dict[str, Any] | None = None
205
+ ) -> dict[str, Any]:
206
+ response_id = f"resp_{uuid.uuid4().hex}"
207
+ created_at = int(time.time())
208
+ model = str(message.get("model") or (source_body or {}).get("model") or "")
209
+ output: list[dict[str, Any]] = []
210
+ for index, block in enumerate(message.get("content") or []):
211
+ if not isinstance(block, dict):
212
+ continue
213
+ block_type = block.get("type")
214
+ if block_type == "text":
215
+ text = str(block.get("text") or "")
216
+ output.append(
217
+ {
218
+ "id": f"msg_{response_id[5:13]}_{index}",
219
+ "type": "message",
220
+ "status": "completed",
221
+ "role": "assistant",
222
+ "content": [{"type": "output_text", "text": text, "annotations": []}],
223
+ }
224
+ )
225
+ elif block_type == "tool_use":
226
+ call_id = str(block.get("id") or f"call_{index + 1}")
227
+ output.append(
228
+ {
229
+ "id": f"fc_{response_id[5:13]}_{index}",
230
+ "type": "function_call",
231
+ "status": "completed",
232
+ "call_id": call_id,
233
+ "name": str(block.get("name") or "tool"),
234
+ "arguments": json.dumps(block.get("input") or {}, ensure_ascii=False),
235
+ }
236
+ )
237
+ return {
238
+ "id": response_id,
239
+ "object": "response",
240
+ "created_at": created_at,
241
+ "status": "completed",
242
+ "model": model,
243
+ "output": output,
244
+ "parallel_tool_calls": bool((source_body or {}).get("parallel_tool_calls", True)),
245
+ "tool_choice": (source_body or {}).get("tool_choice", "auto"),
246
+ "tools": (source_body or {}).get("tools", []),
247
+ "usage": _usage_from_anthropic(message),
248
+ }
249
+
250
+
251
+ class OpenAIResponsesProtocolAdapter(MessageProtocolAdapter):
252
+ """Concrete adapter between OpenAI Responses and Anthropic Messages."""
253
+
254
+ name = "openai_responses"
255
+
256
+ def __init__(self, *, fallback_model: str = "model", source_body: dict[str, Any] | None = None) -> None:
257
+ self._fallback_model = fallback_model
258
+ self._source_body = source_body
259
+
260
+ def normalize_request(self, request: Any) -> dict[str, Any]:
261
+ return openai_responses_to_anthropic_messages(dict(request), self._fallback_model)
262
+
263
+ def normalize_response(self, response: Any) -> dict[str, Any]:
264
+ return anthropic_message_to_openai_response(dict(response), self._source_body)
265
+
266
+
267
+ __all__ = [
268
+ "OpenAIResponsesProtocolAdapter",
269
+ "anthropic_message_to_openai_response",
270
+ "openai_responses_to_anthropic_messages",
271
+ ]