@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,374 @@
1
+ """Application controller for interactive configuration CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Callable, Mapping
8
+
9
+
10
+ RuntimeConfig = dict[str, Any]
11
+ ProviderConfig = dict[str, Any]
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class ConfigurationCliConfigPorts:
16
+ load: Callable[[], RuntimeConfig]
17
+ save: Callable[[RuntimeConfig], None]
18
+ current_provider: Callable[
19
+ [RuntimeConfig],
20
+ tuple[str, ProviderConfig],
21
+ ]
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class ConfigurationCliProviderPorts:
26
+ normalize_choice: Callable[[str], str | None]
27
+ normalize_provider: Callable[[str], str]
28
+ panel_rows: Callable[[RuntimeConfig], tuple[list[str], list[str]]]
29
+ menu_label: Callable[[str, ProviderConfig], str]
30
+ set_choice: Callable[[str], list[str]]
31
+ set_provider: Callable[[str], list[str]]
32
+ set_base_url: Callable[[str, str], list[str]]
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class ConfigurationCliModelPorts:
37
+ cached_ids: Callable[[str, ProviderConfig], list[str]]
38
+ alias_for: Callable[[str, str], str]
39
+ read_cache: Callable[[str, ProviderConfig], list[str] | None]
40
+ set_model: Callable[[str], list[str]]
41
+ upstream_ids: Callable[[str, ProviderConfig], list[str]]
42
+ set_advisor: Callable[[str], list[str]]
43
+ advisor_uses_builtin: Callable[[str, ProviderConfig], bool]
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class ConfigurationCliDisplayPorts:
48
+ log_level_names: Mapping[int, str]
49
+ log_level_status: Callable[[], str]
50
+ log_level_name: Callable[[], str]
51
+ set_log_level: Callable[[str], list[str]]
52
+ languages: Mapping[str, str]
53
+ web_tools_config_path: Path
54
+ language_panel_rows: Callable[
55
+ [RuntimeConfig],
56
+ tuple[list[str], list[str]],
57
+ ]
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class ConfigurationCliIO:
62
+ output: Callable[[str], None]
63
+ select: Callable[[str, list[str], int], int | None]
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class ConfigurationCliController:
68
+ config: ConfigurationCliConfigPorts
69
+ provider: ConfigurationCliProviderPorts
70
+ model: ConfigurationCliModelPorts
71
+ display: ConfigurationCliDisplayPorts
72
+ io: ConfigurationCliIO
73
+
74
+ def provider_command(self, name: str | None) -> None:
75
+ config = self.config.load()
76
+ if not name:
77
+ current = str(config["current_provider"])
78
+ providers = config["providers"]
79
+ current_config = (
80
+ providers.get(current, {})
81
+ if isinstance(providers, dict)
82
+ else {}
83
+ )
84
+ rows, _values = self.provider.panel_rows(config)
85
+ self.io.output(
86
+ "Available providers (current: %s)"
87
+ % self.provider.menu_label(current, current_config)
88
+ )
89
+ for index, row in enumerate(rows, 1):
90
+ self.io.output(f" {index:>2}. {row}")
91
+ self.io.output("\nUse: /provider <name>")
92
+ self.io.output(
93
+ "Examples: /provider codex, /provider codex-routed, "
94
+ "/provider ollama"
95
+ )
96
+ self.io.output(
97
+ "Then run /model to choose a model for the selected provider."
98
+ )
99
+ return
100
+ choice = self.provider.normalize_choice(name)
101
+ lines = (
102
+ self.provider.set_choice(choice)
103
+ if choice
104
+ else self.provider.set_provider(
105
+ self.provider.normalize_provider(name)
106
+ )
107
+ )
108
+ self._output_lines(lines)
109
+ self.io.output(
110
+ "Gateway model cache cleared. Run /model to refresh "
111
+ "the model picker."
112
+ )
113
+
114
+ def base_url_command(self, provider: str, url: str) -> None:
115
+ normalized = self.provider.normalize_provider(provider)
116
+ self._output_lines(self.provider.set_base_url(normalized, url))
117
+
118
+ def model_command(self, values: list[str] | None) -> None:
119
+ config = self.config.load()
120
+ provider, provider_config = self.config.current_provider(config)
121
+ if not values:
122
+ self.io.output(
123
+ f"Model menu for {provider} "
124
+ f"(current: {provider_config.get('current_model')})"
125
+ )
126
+ models = self.model.cached_ids(provider, provider_config)
127
+ for index, model_id in enumerate(models[:100], 1):
128
+ mark = (
129
+ "*"
130
+ if model_id == provider_config.get("current_model")
131
+ else " "
132
+ )
133
+ self.io.output(
134
+ f" {mark} {index:>3}. "
135
+ f"{self.model.alias_for(provider, model_id)} "
136
+ f"[{model_id}]"
137
+ )
138
+ if len(models) > 100:
139
+ self.io.output(f" ... {len(models) - 100} more")
140
+ if self.model.read_cache(provider, provider_config) is None:
141
+ self.io.output(
142
+ "\nProvider model list is not cached yet. Use the menu "
143
+ "refresh row or run: ciel-runtimectl models"
144
+ )
145
+ self.io.output(
146
+ "\nSet direct/custom model with: /set-model MODEL_ID"
147
+ )
148
+ self.io.output(
149
+ "Or from terminal: ciel-runtimectl model MODEL_ID"
150
+ )
151
+ return
152
+ value = " ".join(values).strip()
153
+ if value.startswith("add "):
154
+ value = value[4:].strip()
155
+ if not value:
156
+ raise SystemExit("Missing model id")
157
+ self._output_lines(self.model.set_model(value))
158
+ self.io.output(
159
+ "Gateway model cache cleared. Run /model to refresh if needed."
160
+ )
161
+
162
+ def advisor_model_command(self, values: list[str] | None) -> None:
163
+ if not values:
164
+ config = self.config.load()
165
+ provider, provider_config = self.config.current_provider(config)
166
+ if self.model.advisor_uses_builtin(provider, provider_config):
167
+ self.io.output(
168
+ "Anthropic modes use Claude Code's built-in /advisor; "
169
+ "run /advisor in the session to pick its model."
170
+ )
171
+ return
172
+ current = provider_config.get("advisor_model") or "off"
173
+ self.io.output(f"Advisor Model for {provider}: {current}")
174
+ self.io.output(
175
+ "Set with: ciel-runtimectl advisor-model deepseek-v4-pro"
176
+ )
177
+ self.io.output(
178
+ "Disable with: ciel-runtimectl advisor-model off"
179
+ )
180
+ return
181
+ value = " ".join(values).strip()
182
+ if value.lower() in {
183
+ "off",
184
+ "unset",
185
+ "disable",
186
+ "disabled",
187
+ "none",
188
+ "null",
189
+ }:
190
+ value = ""
191
+ self._output_lines(self.model.set_advisor(value))
192
+
193
+ def models_command(self, provider_override: str | None) -> None:
194
+ config = self.config.load()
195
+ provider, provider_config = self.config.current_provider(config)
196
+ if provider_override:
197
+ provider = self.provider.normalize_provider(provider_override)
198
+ provider_config = config["providers"][provider]
199
+ models = self.model.upstream_ids(provider, provider_config)
200
+ self.io.output(f"{provider}: {len(models)} models")
201
+ for model_id in models:
202
+ self.io.output(
203
+ f"{self.model.alias_for(provider, model_id)}\t{model_id}"
204
+ )
205
+
206
+ def log_level_command(self, value: str | None) -> None:
207
+ if not value:
208
+ self.io.output(f"log_level: {self.display.log_level_status()}")
209
+ for numeric in sorted(self.display.log_level_names):
210
+ name = self.display.log_level_names[numeric]
211
+ mark = (
212
+ "*"
213
+ if name == self.display.log_level_name()
214
+ else " "
215
+ )
216
+ self.io.output(f" {mark} {name:<6} {numeric}")
217
+ self.io.output(" DEFAULT reset to environment/default")
218
+ return
219
+ self._output_lines(self.display.set_log_level(str(value)))
220
+
221
+ def language_command(self, value: str | None) -> None:
222
+ config = self.config.load()
223
+ if not value:
224
+ current = str(config.get("language", "en"))
225
+ label = self.display.languages.get(current, current)
226
+ self.io.output(f"language: {current} ({label})")
227
+ for code, language_label in self.display.languages.items():
228
+ mark = "*" if code == current else " "
229
+ self.io.output(f" {mark} {code:<2} {language_label}")
230
+ return
231
+ normalized = self._language_alias(value)
232
+ if normalized not in self.display.languages:
233
+ raise SystemExit(
234
+ f"Unknown language: {value}\n"
235
+ f"Known: {', '.join(self.display.languages)}"
236
+ )
237
+ config["language"] = normalized
238
+ self.config.save(config)
239
+ self.io.output(
240
+ f"Language set to {normalized} "
241
+ f"({self.display.languages[normalized]})."
242
+ )
243
+
244
+ def portable_provider_menu(self) -> int:
245
+ config = self.config.load()
246
+ rows, values = self.provider.panel_rows(config)
247
+ selected = self.io.select(
248
+ "Select ciel-runtime provider",
249
+ rows,
250
+ values.index(
251
+ str(config.get("current_provider", "nvidia-hosted"))
252
+ ),
253
+ )
254
+ if selected is None:
255
+ self.io.output("Cancelled.")
256
+ return 1
257
+ self._output_lines(self.provider.set_provider(values[selected]))
258
+ return 0
259
+
260
+ def portable_language_menu(self) -> int:
261
+ config = self.config.load()
262
+ rows, values = self.display.language_panel_rows(config)
263
+ selected = self.io.select(
264
+ "Select display language",
265
+ rows,
266
+ values.index(str(config.get("language", "en"))),
267
+ )
268
+ if selected is None:
269
+ self.io.output("Cancelled.")
270
+ return 1
271
+ language = values[selected]
272
+ config["language"] = language
273
+ self.config.save(config)
274
+ self.io.output(
275
+ f"Language set to {language} "
276
+ f"({self.display.languages[language]})."
277
+ )
278
+ return 0
279
+
280
+ def web_search_command(self, value: str | None) -> None:
281
+ config = self.config.load()
282
+ web = config.setdefault("web_search", {})
283
+ if value:
284
+ web["auto_for_non_native"] = self._toggle(
285
+ value,
286
+ "Use: ciel-runtime web-search on|off|status",
287
+ )
288
+ self.config.save(config)
289
+ state = "on" if web.get("auto_for_non_native", True) else "off"
290
+ package = web.get("package", "ddg-mcp-search")
291
+ self.io.output(f"web_search: {state}")
292
+ self.io.output(
293
+ f"search_provider: {web.get('provider', 'duckduckgo')}"
294
+ )
295
+ self.io.output(f"search_package: {package}")
296
+ self.io.output(
297
+ f"web_fetch: {'on' if web.get('fetch_enabled', True) else 'off'}"
298
+ )
299
+ self.io.output(
300
+ f"fetch_package: "
301
+ f"{web.get('fetch_package', 'mcp-server-fetch')}"
302
+ )
303
+ self.io.output(f"mcp_config: {self.display.web_tools_config_path}")
304
+
305
+ def web_fetch_command(self, value: str | None) -> None:
306
+ config = self.config.load()
307
+ web = config.setdefault("web_search", {})
308
+ if value:
309
+ normalized = value.lower()
310
+ if normalized == "ignore-robots-on":
311
+ web["fetch_ignore_robots_txt"] = True
312
+ elif normalized == "ignore-robots-off":
313
+ web["fetch_ignore_robots_txt"] = False
314
+ else:
315
+ web["fetch_enabled"] = self._toggle(
316
+ normalized,
317
+ "Use: ciel-runtime web-fetch "
318
+ "on|off|ignore-robots-on|ignore-robots-off",
319
+ )
320
+ self.config.save(config)
321
+ self.io.output(
322
+ f"web_fetch: {'on' if web.get('fetch_enabled', True) else 'off'}"
323
+ )
324
+ self.io.output(
325
+ f"fetch_package: "
326
+ f"{web.get('fetch_package', 'mcp-server-fetch')}"
327
+ )
328
+ self.io.output(
329
+ "ignore_robots_txt: "
330
+ f"{bool(web.get('fetch_ignore_robots_txt', False))}"
331
+ )
332
+ self.io.output(
333
+ f"user_agent: {web.get('fetch_user_agent') or 'default'}"
334
+ )
335
+ self.io.output(f"mcp_config: {self.display.web_tools_config_path}")
336
+
337
+ def _output_lines(self, lines: list[str]) -> None:
338
+ for line in lines:
339
+ self.io.output(line)
340
+
341
+ @staticmethod
342
+ def _toggle(value: str, usage: str) -> bool:
343
+ normalized = value.lower()
344
+ if normalized in {"on", "enable", "enabled", "true", "1"}:
345
+ return True
346
+ if normalized in {"off", "disable", "disabled", "false", "0"}:
347
+ return False
348
+ raise SystemExit(usage)
349
+
350
+ @staticmethod
351
+ def _language_alias(value: str) -> str:
352
+ normalized = value.strip().lower()
353
+ aliases = {
354
+ "english": "en",
355
+ "korean": "ko",
356
+ "한국어": "ko",
357
+ "japanese": "ja",
358
+ "日本語": "ja",
359
+ "chinese": "zh",
360
+ "中文": "zh",
361
+ "zh-cn": "zh",
362
+ "cn": "zh",
363
+ }
364
+ return aliases.get(normalized, normalized)
365
+
366
+
367
+ __all__ = [
368
+ "ConfigurationCliConfigPorts",
369
+ "ConfigurationCliController",
370
+ "ConfigurationCliDisplayPorts",
371
+ "ConfigurationCliIO",
372
+ "ConfigurationCliModelPorts",
373
+ "ConfigurationCliProviderPorts",
374
+ ]
@@ -0,0 +1,280 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class ContextCompactionTransport:
9
+ summary_output_tokens: Callable[[dict[str, Any], int], int]
10
+ request_timeout: Callable[[dict[str, Any]], float]
11
+ endpoint: Callable[[str, dict[str, Any], str], str]
12
+ post_json: Callable[..., Any]
13
+ headers: Callable[[str, dict[str, Any]], dict[str, str]]
14
+ extract_text: Callable[[Any, str], str]
15
+ native_compat_enabled: Callable[[str, dict[str, Any]], bool]
16
+ native_anthropic_base: Callable[[str, dict[str, Any]], str]
17
+ upstream_base: Callable[[str, dict[str, Any]], str]
18
+ join_url: Callable[[str, str], str]
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class ContextCompactionWorkflow:
23
+ parse_bool: Callable[..., bool]
24
+ compaction_available: Callable[[str, dict[str, Any]], bool]
25
+ instruction_index: Callable[[list[dict[str, Any]]], int | None]
26
+ content_to_text: Callable[[Any], str]
27
+ chunk_target_tokens: Callable[[dict[str, Any], int], int]
28
+ split_messages: Callable[..., list[tuple[int, list[dict[str, Any]]]]]
29
+ parallel_sessions: Callable[[dict[str, Any], int], int]
30
+ write_activity: Callable[..., Any]
31
+ estimate_tokens: Callable[[Any], int]
32
+ request_summary: Callable[..., str]
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class ContextCompactionProjection:
37
+ build_chunk_prompt: Callable[..., str]
38
+ build_fallback_summary: Callable[..., str]
39
+ build_reduce_prompt: Callable[..., str]
40
+ log: Callable[[str, str], None]
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class ContextCompactionServices:
45
+ transport: ContextCompactionTransport
46
+ workflow: ContextCompactionWorkflow
47
+ projection: ContextCompactionProjection
48
+ map_system_prompt: str
49
+
50
+
51
+ def request_context_summary(
52
+ provider: str,
53
+ model: str,
54
+ provider_config: dict[str, Any],
55
+ prompt: str,
56
+ services: ContextCompactionServices,
57
+ *,
58
+ wire: str,
59
+ budget_tokens: int,
60
+ ) -> str:
61
+ transport = services.transport
62
+ max_tokens = transport.summary_output_tokens(provider_config, budget_tokens)
63
+ timeout = transport.request_timeout(provider_config)
64
+ if wire == "ollama":
65
+ request: dict[str, Any] = {
66
+ "model": model,
67
+ "messages": [
68
+ {"role": "system", "content": services.map_system_prompt},
69
+ {"role": "user", "content": prompt},
70
+ ],
71
+ "stream": False,
72
+ "think": False,
73
+ "options": {"num_predict": max_tokens},
74
+ }
75
+ if provider_config.get("keep_alive"):
76
+ request["keep_alive"] = str(provider_config["keep_alive"])
77
+ operation = "ollama_chat"
78
+ elif wire == "openai":
79
+ request = {
80
+ "model": model,
81
+ "messages": [
82
+ {"role": "system", "content": services.map_system_prompt},
83
+ {"role": "user", "content": prompt},
84
+ ],
85
+ "stream": False,
86
+ "max_tokens": max_tokens,
87
+ }
88
+ operation = "openai_chat"
89
+ else:
90
+ request = {
91
+ "model": model,
92
+ "system": services.map_system_prompt,
93
+ "messages": [{"role": "user", "content": prompt}],
94
+ "max_tokens": max_tokens,
95
+ "stream": False,
96
+ }
97
+ base = (
98
+ transport.native_anthropic_base(provider, provider_config)
99
+ if transport.native_compat_enabled(provider, provider_config)
100
+ else transport.upstream_base(provider, provider_config)
101
+ )
102
+ url = transport.join_url(base, "/v1/messages")
103
+ return _post_summary(
104
+ provider, model, provider_config, request, url, "anthropic", timeout, services
105
+ )
106
+ url = transport.endpoint(provider, provider_config, operation)
107
+ return _post_summary(
108
+ provider, model, provider_config, request, url, wire, timeout, services
109
+ )
110
+
111
+
112
+ def _post_summary(
113
+ provider: str,
114
+ model: str,
115
+ provider_config: dict[str, Any],
116
+ request: dict[str, Any],
117
+ url: str,
118
+ wire: str,
119
+ timeout: float,
120
+ services: ContextCompactionServices,
121
+ ) -> str:
122
+ transport = services.transport
123
+ data = transport.post_json(
124
+ url,
125
+ request,
126
+ transport.headers(provider, provider_config),
127
+ timeout,
128
+ provider,
129
+ provider_config,
130
+ model,
131
+ # Summarization is a non-idempotent generation request. A retry can
132
+ # consume quota even when the provider completed the first attempt but
133
+ # the response was lost, so never multiply it implicitly.
134
+ retry_rate_limits=False,
135
+ )
136
+ return transport.extract_text(data, wire)
137
+
138
+
139
+ def build_llm_compacted_messages(
140
+ provider: str,
141
+ model: str,
142
+ provider_config: dict[str, Any] | None,
143
+ messages: list[dict[str, Any]],
144
+ budget_tokens: int,
145
+ services: ContextCompactionServices,
146
+ *,
147
+ wire: str,
148
+ ) -> list[dict[str, Any]] | None:
149
+ if not provider_config or not messages:
150
+ return None
151
+ workflow = services.workflow
152
+ projection = services.projection
153
+ # Claude Code already performs the final compact generation. The former
154
+ # default ran one extra generation per segment before forwarding that
155
+ # request (N map calls + one reduce call), which multiplied provider quota.
156
+ # Keep segmented LLM compaction as an explicit compatibility opt-in only.
157
+ if workflow.parse_bool(provider_config.get("context_compact_llm"), default=False) is False:
158
+ return None
159
+ if not workflow.compaction_available(provider, provider_config):
160
+ return None
161
+ instruction_index = workflow.instruction_index(messages)
162
+ if instruction_index is None:
163
+ return None
164
+ compact_instruction = workflow.content_to_text(messages[instruction_index].get("content"))
165
+ history = [
166
+ message
167
+ for index, message in enumerate(messages)
168
+ if index != instruction_index and str(message.get("role") or "") != "system"
169
+ ]
170
+ system_messages = [message for message in messages if str(message.get("role") or "") == "system"]
171
+ if not history:
172
+ return None
173
+ target_tokens = workflow.chunk_target_tokens(provider_config, budget_tokens)
174
+ chunks = workflow.split_messages(history, target_tokens)
175
+ if not chunks:
176
+ return None
177
+ parallel_sessions = workflow.parallel_sessions(provider_config, len(chunks))
178
+ initial_tokens = workflow.estimate_tokens({"messages": messages})
179
+ _write_activity(
180
+ provider,
181
+ model,
182
+ len(chunks),
183
+ parallel_sessions,
184
+ initial_tokens,
185
+ 0,
186
+ budget_tokens,
187
+ "map",
188
+ 0,
189
+ workflow,
190
+ )
191
+ summaries: list[str] = []
192
+ for chunk_number, (start, chunk) in enumerate(chunks, start=1):
193
+ prompt = projection.build_chunk_prompt(chunk, start, chunk_number, len(chunks))
194
+ try:
195
+ summary = workflow.request_summary(
196
+ provider,
197
+ model,
198
+ provider_config,
199
+ prompt,
200
+ wire=wire,
201
+ budget_tokens=budget_tokens,
202
+ )
203
+ except Exception as exc:
204
+ projection.log(
205
+ "WARN",
206
+ f"context_compact_chunk_failed provider={provider} model={model} "
207
+ f"chunk={chunk_number}/{len(chunks)} error={type(exc).__name__}: {exc}",
208
+ )
209
+ summary = projection.build_fallback_summary(
210
+ chunk, target_tokens, start_index=start
211
+ )
212
+ if not summary.strip():
213
+ summary = projection.build_fallback_summary(
214
+ chunk, target_tokens, start_index=start
215
+ )
216
+ summaries.append(summary.strip())
217
+ _write_activity(
218
+ provider,
219
+ model,
220
+ len(chunks),
221
+ parallel_sessions,
222
+ initial_tokens,
223
+ sum(workflow.estimate_tokens(item) for item in summaries),
224
+ budget_tokens,
225
+ "map",
226
+ chunk_number,
227
+ workflow,
228
+ )
229
+ reduce_prompt = projection.build_reduce_prompt(
230
+ summaries,
231
+ compact_instruction,
232
+ budget_tokens=budget_tokens,
233
+ source_message_count=len(history),
234
+ )
235
+ output = [*system_messages, {"role": "user", "content": reduce_prompt}]
236
+ final_tokens = workflow.estimate_tokens({"messages": output})
237
+ projection.log(
238
+ "WARN",
239
+ f"context_compact_map_reduce provider={provider} model={model} chunks={len(chunks)} "
240
+ f"messages {len(messages)}->{len(output)} tokens {initial_tokens}->{final_tokens} "
241
+ f"budget={budget_tokens}",
242
+ )
243
+ workflow.write_activity(
244
+ provider or "provider",
245
+ model,
246
+ chunks=len(chunks),
247
+ parallel_sessions=parallel_sessions,
248
+ tokens=initial_tokens,
249
+ final_tokens=final_tokens,
250
+ budget=budget_tokens,
251
+ phase="reduce",
252
+ completed_chunks=len(chunks),
253
+ retained_messages=len(output),
254
+ )
255
+ return output
256
+
257
+
258
+ def _write_activity(
259
+ provider: str,
260
+ model: str,
261
+ chunks: int,
262
+ parallel_sessions: int,
263
+ tokens: int,
264
+ final_tokens: int,
265
+ budget: int,
266
+ phase: str,
267
+ completed_chunks: int,
268
+ workflow: ContextCompactionWorkflow,
269
+ ) -> None:
270
+ workflow.write_activity(
271
+ provider or "provider",
272
+ model,
273
+ chunks=chunks,
274
+ parallel_sessions=parallel_sessions,
275
+ tokens=tokens,
276
+ final_tokens=final_tokens,
277
+ budget=budget,
278
+ phase=phase,
279
+ completed_chunks=completed_chunks,
280
+ )