@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,307 @@
1
+ """Configuration schema migrations independent from persistence and runtime I/O."""
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 ConfigMigrationPolicy:
11
+ default_request_timeout_ms: int
12
+ kimi_k3_model: str
13
+ opencode_provider_names: tuple[str, ...]
14
+ is_qwen36_plus_model_id: Callable[..., Any]
15
+ normalize_channel_delivery: Callable[..., Any]
16
+ normalize_model_id: Callable[..., Any]
17
+ nvidia_hosted_context_default: Callable[..., Any]
18
+ positive_int: Callable[..., Any]
19
+ strip_claude_context_suffix: Callable[..., Any]
20
+
21
+
22
+ def apply_config_migrations(cfg: dict[str, Any], *, policy: ConfigMigrationPolicy) -> None:
23
+ DEFAULT_REQUEST_TIMEOUT_MS = policy.default_request_timeout_ms
24
+ KIMI_K3_MODEL = policy.kimi_k3_model
25
+ OPENCODE_PROVIDER_NAMES = policy.opencode_provider_names
26
+ is_qwen36_plus_model_id = policy.is_qwen36_plus_model_id
27
+ normalize_channel_delivery = policy.normalize_channel_delivery
28
+ normalize_model_id = policy.normalize_model_id
29
+ nvidia_hosted_context_default = policy.nvidia_hosted_context_default
30
+ positive_int = policy.positive_int
31
+ strip_claude_context_suffix = policy.strip_claude_context_suffix
32
+ migrations = cfg.setdefault("migrations", {})
33
+ if not isinstance(migrations, dict):
34
+ migrations = {}
35
+ cfg["migrations"] = migrations
36
+
37
+ marker = "ollama_cloud_glm52_thinking_context_20260711"
38
+ if not migrations.get(marker):
39
+ pcfg = cfg.get("providers", {}).get("ollama-cloud", {})
40
+ if isinstance(pcfg, dict):
41
+ model = strip_claude_context_suffix(str(pcfg.get("current_model") or "")).lower()
42
+ if model in {"glm-5.2", "glm-5.2:cloud"}:
43
+ pcfg["think"] = True
44
+ if positive_int(pcfg.get("num_ctx_max")) in {0, 131072, 1048576}:
45
+ pcfg["num_ctx_max"] = 999424
46
+ migrations[marker] = True
47
+
48
+ marker = "ollama_cloud_provider_context_default_20260724"
49
+ if not migrations.get(marker):
50
+ pcfg = cfg.get("providers", {}).get("ollama-cloud", {})
51
+ if isinstance(pcfg, dict):
52
+ model = strip_claude_context_suffix(
53
+ str(pcfg.get("current_model") or "")
54
+ ).lower()
55
+ if model in {"glm-5.2", "glm-5.2:cloud"}:
56
+ detected_limit = 1_000_000
57
+ legacy_auto_512k = (
58
+ str(pcfg.get("llm_preset") or "") == "long-context-512k"
59
+ and str(pcfg.get("num_ctx") or "auto").strip().lower()
60
+ in {"", "auto", "dynamic"}
61
+ and positive_int(pcfg.get("num_ctx_max")) == 524288
62
+ )
63
+ if legacy_auto_512k:
64
+ pcfg.pop("llm_preset", None)
65
+ pcfg["num_ctx_max"] = detected_limit
66
+ elif not pcfg.get("llm_preset") and positive_int(
67
+ pcfg.get("num_ctx_max")
68
+ ) in {131072, 999424, 1048576}:
69
+ pcfg["num_ctx_max"] = detected_limit
70
+ pcfg["model_context_max"] = detected_limit
71
+ pcfg["model_context_model"] = "glm-5.2"
72
+ pcfg["think"] = True
73
+ migrations[marker] = True
74
+
75
+ marker = "nvidia_hosted_router_default_20260509"
76
+ if not migrations.get(marker):
77
+ pcfg = cfg.get("providers", {}).get("nvidia-hosted", {})
78
+ if isinstance(pcfg, dict) and bool(pcfg.get("native_compat", False)):
79
+ pcfg["native_compat"] = False
80
+ migrations[marker] = True
81
+
82
+ marker = "lm_studio_native_default_20260523"
83
+ if not migrations.get(marker):
84
+ pcfg = cfg.get("providers", {}).get("lm-studio", {})
85
+ if isinstance(pcfg, dict):
86
+ pcfg["native_compat"] = True
87
+ migrations[marker] = True
88
+
89
+ marker = "default_timeout_5m_20260513"
90
+ if not migrations.get(marker):
91
+ # Historical marker kept for configs that have already recorded it.
92
+ # Do not rewrite 10/30 minute values here anymore: those can now be
93
+ # deliberate long-running timeout profiles.
94
+ migrations[marker] = True
95
+
96
+ marker = "default_timeout_2m_20260514"
97
+ if not migrations.get(marker):
98
+ # Historical marker only. The 2-minute default was too short for
99
+ # 50k+ token hosted requests, so newer migrations no longer apply it.
100
+ migrations[marker] = True
101
+
102
+ marker = "default_timeout_restore_5m_20260515"
103
+ if not migrations.get(marker):
104
+ for pcfg in (cfg.get("providers") or {}).values():
105
+ if not isinstance(pcfg, dict):
106
+ continue
107
+ if positive_int(pcfg.get("request_timeout_ms")) == 120000 and not pcfg.get("llm_preset"):
108
+ pcfg["request_timeout_ms"] = DEFAULT_REQUEST_TIMEOUT_MS
109
+ if "stream_idle_timeout_ms" not in pcfg:
110
+ pcfg["stream_idle_timeout_ms"] = DEFAULT_REQUEST_TIMEOUT_MS
111
+ migrations[marker] = True
112
+
113
+ marker = "nvidia_context_window_32k_20260513"
114
+ if not migrations.get(marker):
115
+ pcfg = cfg.get("providers", {}).get("nvidia-hosted", {})
116
+ if isinstance(pcfg, dict) and not positive_int(pcfg.get("context_window")):
117
+ pcfg["context_window"] = nvidia_hosted_context_default(str(pcfg.get("current_model") or ""))
118
+ migrations[marker] = True
119
+
120
+ marker = "nvidia_context_window_unforce_32k_20260513"
121
+ if not migrations.get(marker):
122
+ pcfg = cfg.get("providers", {}).get("nvidia-hosted", {})
123
+ if isinstance(pcfg, dict) and positive_int(pcfg.get("context_window")) == 32768:
124
+ pcfg["context_window"] = nvidia_hosted_context_default(str(pcfg.get("current_model") or ""))
125
+ migrations[marker] = True
126
+
127
+ marker = "stream_enabled_default_true_20260513"
128
+ if not migrations.get(marker):
129
+ for pcfg in (cfg.get("providers") or {}).values():
130
+ if isinstance(pcfg, dict) and "stream_enabled" not in pcfg:
131
+ pcfg["stream_enabled"] = True
132
+ migrations[marker] = True
133
+
134
+ marker = "default_channel_delivery_native_20260520"
135
+ if not migrations.get(marker):
136
+ ccfg = cfg.setdefault("claude_code", {})
137
+ if not isinstance(ccfg, dict):
138
+ ccfg = {}
139
+ cfg["claude_code"] = ccfg
140
+ if normalize_channel_delivery(ccfg.get("channel_delivery")) == "stdin":
141
+ ccfg["channel_delivery"] = "native"
142
+ migrations[marker] = True
143
+
144
+ marker = "default_channel_delivery_llm_20260523"
145
+ if not migrations.get(marker):
146
+ ccfg = cfg.setdefault("claude_code", {})
147
+ if not isinstance(ccfg, dict):
148
+ ccfg = {}
149
+ cfg["claude_code"] = ccfg
150
+ if normalize_channel_delivery(ccfg.get("channel_delivery")) == "native":
151
+ ccfg["channel_delivery"] = "llm"
152
+ migrations[marker] = True
153
+
154
+ marker = "rate_limit_defaults_off_20260526"
155
+ if not migrations.get(marker):
156
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
157
+ for provider_name in ("ollama", "ollama-cloud", "nvidia-hosted", "self-hosted-nim"):
158
+ pcfg = providers.get(provider_name)
159
+ if not isinstance(pcfg, dict):
160
+ continue
161
+ old_default_rpm = str(pcfg.get("rate_limit_rpm", "")).strip() == "40"
162
+ old_default_status = bool(pcfg.get("rate_limit_status", True))
163
+ if old_default_rpm and old_default_status:
164
+ pcfg["rate_limit_rpm"] = 0
165
+ pcfg["rate_limit_status"] = False
166
+ pcfg = providers.get("lm-studio")
167
+ if (
168
+ isinstance(pcfg, dict)
169
+ and str(pcfg.get("rate_limit_rpm", "")).strip() == "0"
170
+ and bool(pcfg.get("rate_limit_status", True))
171
+ ):
172
+ pcfg["rate_limit_status"] = False
173
+ migrations[marker] = True
174
+
175
+ marker = "opencode_go_qwen36_plus_context_1m_20260530"
176
+ if not migrations.get(marker):
177
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
178
+ pcfg = providers.get("opencode-go")
179
+ if (
180
+ isinstance(pcfg, dict)
181
+ and is_qwen36_plus_model_id(str(pcfg.get("current_model") or ""))
182
+ and positive_int(pcfg.get("context_window")) == 262144
183
+ ):
184
+ pcfg["context_window"] = 1048576
185
+ migrations[marker] = True
186
+
187
+ marker = "opencode_zen_qwen36_plus_free_model_20260614"
188
+ if not migrations.get(marker):
189
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
190
+ pcfg = providers.get("opencode")
191
+ if isinstance(pcfg, dict):
192
+ custom = pcfg.get("custom_models")
193
+ if not isinstance(custom, list):
194
+ custom = []
195
+ pcfg["custom_models"] = custom
196
+ normalized_custom = {normalize_model_id("opencode", str(mid)) for mid in custom if str(mid).strip()}
197
+ if "qwen3.6-plus-free" not in normalized_custom:
198
+ custom.append("qwen3.6-plus-free")
199
+ migrations[marker] = True
200
+
201
+ marker = "opencode_qwen36_plus_parameters_20260614"
202
+ if not migrations.get(marker):
203
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
204
+ for provider_name in OPENCODE_PROVIDER_NAMES:
205
+ pcfg = providers.get(provider_name)
206
+ if not isinstance(pcfg, dict):
207
+ continue
208
+ if not is_qwen36_plus_model_id(str(pcfg.get("current_model") or "")):
209
+ continue
210
+ if (positive_int(pcfg.get("context_window")) or 0) < 1048576:
211
+ pcfg["context_window"] = 1048576
212
+ if (positive_int(pcfg.get("context_reserve_tokens")) or 0) < 16384:
213
+ pcfg["context_reserve_tokens"] = 16384
214
+ if (positive_int(pcfg.get("max_output_tokens")) or 0) < 8192:
215
+ pcfg["max_output_tokens"] = 8192
216
+ migrations[marker] = True
217
+
218
+ marker = "anthropic_drop_preset_output_tokens_20260610"
219
+ if not migrations.get(marker):
220
+ # Older anthropic presets force-wrote max_output_tokens (2048/4096/6144/8192),
221
+ # which pinned CLAUDE_CODE_MAX_OUTPUT_TOKENS and overrode Claude Code's native
222
+ # per-model default in routed mode. Those values are no longer written. Drop a
223
+ # stale preset-origin value so existing routed configs recover the native cap.
224
+ # Trade-off (matches the rate_limit precedent): a user who deliberately set one
225
+ # of these exact round numbers via the CLI is cleared once; re-setting persists.
226
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
227
+ pcfg = providers.get("anthropic")
228
+ if isinstance(pcfg, dict) and positive_int(pcfg.get("max_output_tokens")) in (2048, 4096, 6144, 8192):
229
+ pcfg.pop("max_output_tokens", None)
230
+ migrations[marker] = True
231
+
232
+ marker = "opencode_default_ipv6_preferred_20260611"
233
+ if not migrations.get(marker):
234
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
235
+ for provider_name in OPENCODE_PROVIDER_NAMES:
236
+ pcfg = providers.get(provider_name)
237
+ if isinstance(pcfg, dict) and not pcfg.get("ip_family"):
238
+ pcfg["ip_family"] = "ipv6-preferred"
239
+ migrations[marker] = True
240
+
241
+ marker = "kimi_tool_choice_auto_only_20260625"
242
+ if not migrations.get(marker):
243
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
244
+ pcfg = providers.get("kimi")
245
+ if isinstance(pcfg, dict):
246
+ pcfg["normalize_anthropic_tool_use"] = True
247
+ pcfg["supports_tool_choice"] = False
248
+ migrations[marker] = True
249
+
250
+ marker = "kimi_forward_tool_choice_20260628"
251
+ if not migrations.get(marker):
252
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
253
+ pcfg = providers.get("kimi")
254
+ if isinstance(pcfg, dict):
255
+ pcfg["normalize_anthropic_tool_use"] = True
256
+ pcfg["supports_tool_choice"] = True
257
+ migrations[marker] = True
258
+
259
+ marker = "kimi_k3_model_20260716"
260
+ if not migrations.get(marker):
261
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
262
+ pcfg = providers.get("kimi")
263
+ if isinstance(pcfg, dict):
264
+ custom = pcfg.get("custom_models")
265
+ if not isinstance(custom, list):
266
+ custom = []
267
+ pcfg["custom_models"] = custom
268
+ normalized_custom = {normalize_model_id("kimi", str(mid)) for mid in custom if str(mid).strip()}
269
+ if KIMI_K3_MODEL not in normalized_custom:
270
+ custom.append(KIMI_K3_MODEL)
271
+ migrations[marker] = True
272
+
273
+ marker = "kimi_k3_official_profile_20260722"
274
+ if not migrations.get(marker):
275
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
276
+ pcfg = providers.get("kimi")
277
+ if isinstance(pcfg, dict):
278
+ custom = pcfg.get("custom_models")
279
+ if not isinstance(custom, list):
280
+ custom = []
281
+ pcfg["custom_models"] = custom
282
+ normalized_custom = {normalize_model_id("kimi", str(mid)) for mid in custom if str(mid).strip()}
283
+ for model_id in (f"{KIMI_K3_MODEL}[1m]", "kimi-for-coding-highspeed"):
284
+ if normalize_model_id("kimi", model_id) not in normalized_custom:
285
+ custom.append(model_id)
286
+ current = normalize_model_id("kimi", str(pcfg.get("current_model") or ""))
287
+ if current in {KIMI_K3_MODEL, f"{KIMI_K3_MODEL}[1m]"}:
288
+ pcfg["context_window"] = 1048576
289
+ pcfg["max_model_len"] = 1048576
290
+ pcfg["effort_level"] = "high"
291
+ pcfg["model_profile"] = "kimi-k3-1m"
292
+ migrations[marker] = True
293
+
294
+ marker = "kimi_k3_default_1m_20260723"
295
+ if not migrations.get(marker):
296
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
297
+ pcfg = providers.get("kimi")
298
+ if isinstance(pcfg, dict):
299
+ current = normalize_model_id("kimi", str(pcfg.get("current_model") or ""))
300
+ if current in {KIMI_K3_MODEL, f"{KIMI_K3_MODEL}[1m]"}:
301
+ pcfg["context_window"] = 1048576
302
+ pcfg["max_model_len"] = 1048576
303
+ pcfg["effort_level"] = "high"
304
+ pcfg["model_profile"] = "kimi-k3-1m"
305
+ migrations[marker] = True
306
+
307
+ __all__ = ["ConfigMigrationPolicy", "apply_config_migrations"]
@@ -0,0 +1,175 @@
1
+ """Configuration persistence port with atomic JSON file storage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import json
7
+ import os
8
+ import time
9
+ from collections.abc import Callable
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ def build_default_config(provider_defaults: dict[str, Any]) -> dict[str, Any]:
15
+ return {
16
+ "current_provider": "nvidia-hosted",
17
+ "last_launch_action": "",
18
+ "language": "en",
19
+ "migrations": {},
20
+ "router_debug_external_access": False,
21
+ "router_debug_external_access_confirmed": False,
22
+ "router_debug_message_preview_chars": 0,
23
+ "web_backend": {
24
+ "enabled": False,
25
+ "host": "127.0.0.1",
26
+ "port": 0,
27
+ "tailscale_https": False,
28
+ },
29
+ "claude_code": {
30
+ "compat_prompt_for_non_anthropic": True,
31
+ "channels": [],
32
+ "development_channels": False,
33
+ "channel_delivery": "llm",
34
+ },
35
+ "cleanup": {"managed_services_on_launch": True},
36
+ "web_search": {
37
+ "auto_for_non_native": True,
38
+ "provider": "duckduckgo",
39
+ "package": "ddg-mcp-search",
40
+ "fetch_enabled": True,
41
+ "fetch_package": "mcp-server-fetch",
42
+ "fetch_ignore_robots_txt": False,
43
+ "fetch_user_agent": "",
44
+ },
45
+ "providers": provider_defaults,
46
+ }
47
+
48
+
49
+ def deep_merge(defaults: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
50
+ merged = json.loads(json.dumps(defaults))
51
+ for key, value in overrides.items():
52
+ if isinstance(value, dict) and isinstance(merged.get(key), dict):
53
+ merged[key] = deep_merge(merged[key], value)
54
+ else:
55
+ merged[key] = value
56
+ return merged
57
+
58
+
59
+ def normalize_loaded_config(
60
+ config: dict[str, Any],
61
+ normalize_model_id: Callable[[str, str], str],
62
+ ) -> None:
63
+ providers = config["providers"]
64
+ cloud = providers.get("ollama-cloud", {})
65
+ local_key = providers.get("ollama", {}).get("api_key", "")
66
+ if not cloud.get("api_key") and local_key and local_key not in {"ollama", "dummy", "not-used"}:
67
+ cloud["api_key"] = local_key
68
+ for provider_name, provider_config in providers.items():
69
+ if not isinstance(provider_config, dict):
70
+ continue
71
+ if provider_config.get("current_model"):
72
+ provider_config["current_model"] = normalize_model_id(
73
+ provider_name, str(provider_config["current_model"])
74
+ )
75
+ custom_models = provider_config.get("custom_models")
76
+ if isinstance(custom_models, list):
77
+ provider_config["custom_models"] = [
78
+ normalize_model_id(provider_name, str(model_id))
79
+ for model_id in custom_models
80
+ if str(model_id).strip()
81
+ ]
82
+
83
+
84
+ class JsonConfigRepository:
85
+ """Own configuration caching, persistence, migration, and normalization."""
86
+
87
+ def __init__(
88
+ self,
89
+ *,
90
+ path: Path,
91
+ defaults: dict[str, Any],
92
+ merge: Callable[[dict[str, Any], dict[str, Any]], dict[str, Any]],
93
+ migrate: Callable[[dict[str, Any]], None],
94
+ normalize: Callable[[dict[str, Any]], None],
95
+ ) -> None:
96
+ self._path = path
97
+ self._defaults = defaults
98
+ self._merge = merge
99
+ self._migrate = migrate
100
+ self._normalize = normalize
101
+ self._cache: dict[str, Any] | None = None
102
+ self._cache_mtime = 0.0
103
+
104
+ @property
105
+ def path(self) -> Path:
106
+ return self._path
107
+
108
+ def load(self) -> dict[str, Any]:
109
+ try:
110
+ mtime = self._path.stat().st_mtime
111
+ except OSError:
112
+ mtime = 0.0
113
+ if self._cache is not None and mtime == self._cache_mtime:
114
+ return copy.deepcopy(self._cache)
115
+ try:
116
+ data = json.loads(self._path.read_text(encoding="utf-8")) if self._path.exists() else {}
117
+ except (OSError, ValueError, TypeError):
118
+ data = {}
119
+ config = self._merge(self._defaults, data if isinstance(data, dict) else {})
120
+ self._migrate(config)
121
+ self._normalize(config)
122
+ self._cache = config
123
+ self._cache_mtime = mtime
124
+ return copy.deepcopy(config)
125
+
126
+ def save(self, config: dict[str, Any]) -> None:
127
+ self._path.parent.mkdir(parents=True, exist_ok=True)
128
+ temporary = self._path.with_name(f"{self._path.name}.{os.getpid()}.{time.time_ns()}.tmp")
129
+ temporary.write_text(json.dumps(config, indent=2, sort_keys=True) + "\n", encoding="utf-8")
130
+ os.chmod(temporary, 0o600)
131
+ temporary.replace(self._path)
132
+ self._cache = copy.deepcopy(config)
133
+ try:
134
+ self._cache_mtime = self._path.stat().st_mtime
135
+ except OSError:
136
+ self._cache_mtime = 0.0
137
+
138
+ def invalidate(self) -> None:
139
+ self._cache = None
140
+ self._cache_mtime = 0.0
141
+
142
+
143
+ class ConfigRepositoryProvider:
144
+ """Path-aware repository factory that owns the mutable cache instance."""
145
+
146
+ def __init__(self) -> None:
147
+ self._repository: JsonConfigRepository | None = None
148
+
149
+ def get(
150
+ self,
151
+ *,
152
+ path: Path,
153
+ defaults: dict[str, Any],
154
+ merge: Callable[[dict[str, Any], dict[str, Any]], dict[str, Any]],
155
+ migrate: Callable[[dict[str, Any]], None],
156
+ normalize: Callable[[dict[str, Any]], None],
157
+ ) -> JsonConfigRepository:
158
+ if self._repository is None or self._repository.path != path:
159
+ self._repository = JsonConfigRepository(
160
+ path=path,
161
+ defaults=defaults,
162
+ merge=merge,
163
+ migrate=migrate,
164
+ normalize=normalize,
165
+ )
166
+ return self._repository
167
+
168
+
169
+ __all__ = [
170
+ "ConfigRepositoryProvider",
171
+ "JsonConfigRepository",
172
+ "build_default_config",
173
+ "deep_merge",
174
+ "normalize_loaded_config",
175
+ ]
@@ -0,0 +1,64 @@
1
+ """Scalar parsing and validation for environment, CLI, and persisted config."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ from typing import Any
8
+
9
+
10
+ def positive_int(value: Any) -> int | None:
11
+ try:
12
+ parsed = int(value)
13
+ except Exception:
14
+ return None
15
+ return parsed if parsed > 0 else None
16
+
17
+
18
+ def finite_float(value: Any) -> float | None:
19
+ try:
20
+ parsed = float(value)
21
+ except Exception:
22
+ return None
23
+ return parsed if math.isfinite(parsed) else None
24
+
25
+
26
+ def parse_config_value(value: str) -> Any:
27
+ text = value.strip()
28
+ normalized = text.lower()
29
+ if normalized in {"true", "yes", "on"}:
30
+ return True
31
+ if normalized in {"false", "no", "off"}:
32
+ return False
33
+ if normalized in {"none", "null"}:
34
+ return None
35
+ try:
36
+ return json.loads(text)
37
+ except Exception:
38
+ pass
39
+ try:
40
+ return int(text)
41
+ except Exception:
42
+ pass
43
+ try:
44
+ return float(text)
45
+ except Exception:
46
+ return text
47
+
48
+
49
+ def parse_bool(value: Any, default: bool = False) -> bool:
50
+ if isinstance(value, bool):
51
+ return value
52
+ if value is None:
53
+ return default
54
+ if isinstance(value, (int, float)):
55
+ return bool(value)
56
+ normalized = str(value).strip().lower()
57
+ if normalized in {"true", "yes", "on", "1", "enable", "enabled"}:
58
+ return True
59
+ if normalized in {"false", "no", "off", "0", "disable", "disabled"}:
60
+ return False
61
+ return default
62
+
63
+
64
+ __all__ = ["finite_float", "parse_bool", "parse_config_value", "positive_int"]