@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,361 @@
1
+ """Pure provider option mutations independent from CLI and persistence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+ from .architecture import ProviderConfigurationPolicy
9
+ from .provider_sampling_policy import ProviderSamplingPolicy
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class ProviderOptionPolicy:
14
+ normalize_claude_code_supported_capabilities: Callable[..., Any]
15
+ normalize_ip_family: Callable[..., Any]
16
+ normalize_model_id: Callable[..., Any]
17
+ normalize_opencode_endpoint_kind: Callable[..., Any]
18
+ parse_bool: Callable[..., Any]
19
+ parse_config_value: Callable[..., Any]
20
+ positive_int: Callable[..., Any]
21
+ sampling: ProviderSamplingPolicy
22
+
23
+
24
+ def apply_ollama_option(
25
+ pcfg: dict[str, Any], token: str, *, policy: ProviderOptionPolicy
26
+ ) -> None:
27
+ parse_bool = policy.parse_bool
28
+ parse_config_value = policy.parse_config_value
29
+ positive_int = policy.positive_int
30
+ if token.startswith("unset:"):
31
+ key = token.split(":", 1)[1].strip()
32
+ if key in ("num_ctx", "ctx"):
33
+ pcfg["num_ctx"] = "auto"
34
+ elif key in ("context_window", "context", "max_model_len"):
35
+ pcfg.pop("context_window", None)
36
+ pcfg["num_ctx"] = "auto"
37
+ pcfg.pop("num_ctx_max", None)
38
+ elif key in ("num_ctx_min", "ctx_min", "min"):
39
+ pcfg.pop("num_ctx_min", None)
40
+ elif key in ("num_ctx_max", "ctx_max", "max"):
41
+ pcfg.pop("num_ctx_max", None)
42
+ elif key in ("max_output_tokens", "max_tokens", "maxtoken", "max_token", "num_predict"):
43
+ pcfg.pop("max_output_tokens", None)
44
+ pcfg.setdefault("ollama_options", {}).pop("num_predict", None)
45
+ elif key in ("keep_alive", "keepalive"):
46
+ pcfg.pop("keep_alive", None)
47
+ elif key == "think":
48
+ pcfg["think"] = False
49
+ elif key in ("stream", "stream_enabled"):
50
+ pcfg["stream_enabled"] = True
51
+ elif key in ("stream_word_chunking", "word_chunking", "stream_chunk", "stream_words"):
52
+ pcfg["stream_word_chunking"] = False
53
+ elif key in ("stream_idle_timeout", "stream_idle_timeout_ms", "idle_timeout", "idle_timeout_ms"):
54
+ pcfg.pop("stream_idle_timeout_ms", None)
55
+ elif key in ("rate_limit", "rate_limit_rpm", "rpm"):
56
+ pcfg["rate_limit_rpm"] = 0
57
+ pcfg["rate_limit_status"] = False
58
+ elif key in ("rate_limit_status", "rpm_status"):
59
+ pcfg["rate_limit_status"] = False
60
+ else:
61
+ pcfg.setdefault("ollama_options", {}).pop(key, None)
62
+ return
63
+ if "=" not in token:
64
+ raise SystemExit(f"Expected key=value or unset:key, got: {token}")
65
+ key, raw_value = token.split("=", 1)
66
+ key = key.strip()
67
+ value = parse_config_value(raw_value)
68
+ if key in ("num_ctx", "ctx"):
69
+ if isinstance(value, str) and value.lower() in ("auto", "dynamic"):
70
+ pcfg["num_ctx"] = "auto"
71
+ else:
72
+ fixed = positive_int(value)
73
+ if not fixed:
74
+ raise SystemExit("num_ctx must be auto or a positive integer")
75
+ pcfg["num_ctx"] = fixed
76
+ return
77
+ if key in ("context_window", "context", "max_model_len"):
78
+ fixed = positive_int(value)
79
+ if not fixed:
80
+ raise SystemExit("context_window must be a positive integer")
81
+ pcfg["context_window"] = fixed
82
+ pcfg["num_ctx"] = "auto"
83
+ pcfg["num_ctx_max"] = fixed
84
+ pcfg["num_ctx_min"] = min(fixed, 32768 if fixed <= 65536 else 65536)
85
+ return
86
+ if key in ("num_ctx_min", "ctx_min", "min"):
87
+ fixed = positive_int(value)
88
+ if not fixed:
89
+ raise SystemExit("num_ctx_min must be a positive integer")
90
+ pcfg["num_ctx_min"] = fixed
91
+ return
92
+ if key in ("num_ctx_max", "ctx_max", "max"):
93
+ fixed = positive_int(value)
94
+ if not fixed:
95
+ raise SystemExit("num_ctx_max must be a positive integer")
96
+ pcfg["num_ctx_max"] = fixed
97
+ return
98
+ if key in ("keep_alive", "keepalive"):
99
+ if value is None:
100
+ pcfg.pop("keep_alive", None)
101
+ else:
102
+ pcfg["keep_alive"] = str(value)
103
+ return
104
+ if key in ("timeout", "timeout_ms", "request_timeout", "request_timeout_ms"):
105
+ fixed = positive_int(value)
106
+ if not fixed:
107
+ raise SystemExit("timeout must be a positive integer; values above 10000 are treated as milliseconds")
108
+ pcfg["request_timeout_ms"] = fixed if key.endswith("_ms") or fixed > 10000 else fixed * 1000
109
+ return
110
+ if key in ("stream_idle_timeout", "stream_idle_timeout_ms", "idle_timeout", "idle_timeout_ms"):
111
+ fixed = positive_int(value)
112
+ if not fixed:
113
+ raise SystemExit("stream_idle_timeout_ms must be a positive integer; values above 10000 are treated as milliseconds")
114
+ pcfg["stream_idle_timeout_ms"] = fixed if key.endswith("_ms") or fixed > 10000 else fixed * 1000
115
+ return
116
+ if key in ("max_output_tokens", "max_tokens", "maxtoken", "max_token", "num_predict"):
117
+ fixed = positive_int(value)
118
+ if not fixed:
119
+ raise SystemExit("max_tokens/num_predict must be a positive integer")
120
+ pcfg["max_output_tokens"] = fixed
121
+ pcfg.setdefault("ollama_options", {})["num_predict"] = fixed
122
+ return
123
+ if key == "think":
124
+ pcfg["think"] = bool(value)
125
+ return
126
+ if key in ("stream", "stream_enabled"):
127
+ pcfg["stream_enabled"] = parse_bool(value, default=True)
128
+ return
129
+ if key in ("stream_word_chunking", "word_chunking", "stream_chunk", "stream_words"):
130
+ pcfg["stream_word_chunking"] = parse_bool(value, default=False)
131
+ return
132
+ if key in ("rate_limit", "rate_limit_rpm", "rpm"):
133
+ fixed = positive_int(value)
134
+ if not fixed:
135
+ if str(value).lower() in ("0", "false", "off", "disable", "disabled", "none", "unset"):
136
+ pcfg["rate_limit_rpm"] = 0
137
+ pcfg["rate_limit_status"] = False
138
+ return
139
+ raise SystemExit("rate_limit_rpm must be a positive integer, or 0 to disable")
140
+ pcfg["rate_limit_rpm"] = fixed
141
+ return
142
+ if key in ("rate_limit_status", "rpm_status"):
143
+ pcfg["rate_limit_status"] = parse_bool(value, default=False)
144
+ return
145
+ opts = pcfg.setdefault("ollama_options", {})
146
+ if value is None:
147
+ opts.pop(key, None)
148
+ else:
149
+ opts[key] = value
150
+
151
+
152
+ def apply_provider_option(
153
+ provider: str,
154
+ pcfg: dict[str, Any],
155
+ token: str,
156
+ *,
157
+ policy: ProviderOptionPolicy,
158
+ capabilities: ProviderConfigurationPolicy,
159
+ ) -> None:
160
+ normalize_claude_code_supported_capabilities = policy.normalize_claude_code_supported_capabilities
161
+ normalize_ip_family = policy.normalize_ip_family
162
+ normalize_model_id = policy.normalize_model_id
163
+ normalize_opencode_endpoint_kind = policy.normalize_opencode_endpoint_kind
164
+ parse_bool = policy.parse_bool
165
+ parse_config_value = policy.parse_config_value
166
+ positive_int = policy.positive_int
167
+ sampling = policy.sampling
168
+ if capabilities.supports_model_endpoint_overrides and token.startswith("endpoint:") and "=" in token:
169
+ key, raw_value = token.split("=", 1)
170
+ model_id = key.split(":", 1)[1].strip()
171
+ endpoint = normalize_opencode_endpoint_kind(raw_value)
172
+ if not model_id:
173
+ raise SystemExit("endpoint override requires endpoint:<model-id>=<messages|chat|responses|gemini>")
174
+ if not endpoint:
175
+ raise SystemExit("endpoint override must be one of: messages, chat, responses, gemini")
176
+ endpoints = pcfg.setdefault("model_endpoints", {})
177
+ if not isinstance(endpoints, dict):
178
+ endpoints = {}
179
+ pcfg["model_endpoints"] = endpoints
180
+ endpoints[normalize_model_id(provider, model_id)] = endpoint
181
+ return
182
+ token_key = ""
183
+ if token.startswith("unset:"):
184
+ token_key = token.split(":", 1)[1].strip()
185
+ elif "=" in token:
186
+ token_key = token.split("=", 1)[0].strip()
187
+ provider_common_keys = {
188
+ "force_query_string",
189
+ "force_query",
190
+ "upstream_query",
191
+ "test_query_string",
192
+ "supports_tool_choice",
193
+ "tool_choice",
194
+ "tool-choice",
195
+ "auto_tool_choice",
196
+ }
197
+ if capabilities.mutation_strategy == "ollama" and token_key not in provider_common_keys:
198
+ apply_ollama_option(pcfg, token, policy=policy)
199
+ return
200
+ if token.startswith("unset:"):
201
+ key = token.split(":", 1)[1].strip()
202
+ if key in ("context_window", "context", "max_model_len"):
203
+ pcfg.pop("context_window", None)
204
+ elif key in ("context_reserve_tokens", "reserve"):
205
+ pcfg.pop("context_reserve_tokens", None)
206
+ elif key in ("max_output_tokens", "max_tokens", "maxtoken", "max_token"):
207
+ pcfg.pop("max_output_tokens", None)
208
+ elif key in ("timeout", "timeout_ms", "request_timeout", "request_timeout_ms"):
209
+ pcfg.pop("request_timeout_ms", None)
210
+ elif key in ("stream_idle_timeout", "stream_idle_timeout_ms", "idle_timeout", "idle_timeout_ms"):
211
+ pcfg.pop("stream_idle_timeout_ms", None)
212
+ elif key in ("rate_limit", "rate_limit_rpm", "rpm"):
213
+ pcfg["rate_limit_rpm"] = 0
214
+ pcfg["rate_limit_status"] = False
215
+ elif key in ("rate_limit_status", "rpm_status"):
216
+ pcfg["rate_limit_status"] = False
217
+ elif key in ("native", "native_compat"):
218
+ if capabilities.native_compat_error:
219
+ raise SystemExit(capabilities.native_compat_error)
220
+ pcfg["native_compat"] = True
221
+ elif key in ("route", "routed", "route_through_router", "router"):
222
+ pcfg["route_through_router"] = False
223
+ elif key in ("stream", "stream_enabled"):
224
+ pcfg["stream_enabled"] = True
225
+ elif key in ("stream_word_chunking", "word_chunking", "stream_chunk", "stream_words"):
226
+ pcfg["stream_word_chunking"] = False
227
+ elif key in ("ip_family", "network_family", "address_family", "addr_family"):
228
+ pcfg.pop("ip_family", None)
229
+ elif key in ("force_query_string", "force_query", "upstream_query", "test_query_string"):
230
+ pcfg.pop("force_query_string", None)
231
+ elif key in ("supports_tool_choice", "tool_choice", "tool-choice", "auto_tool_choice"):
232
+ pcfg.pop("supports_tool_choice", None)
233
+ elif key in capabilities.text_option_aliases:
234
+ pcfg.pop(capabilities.text_option_aliases[key], None)
235
+ elif key in ("workflows_enabled", "workflow", "workflows"):
236
+ pcfg["workflows_enabled"] = False
237
+ elif key in ("ultracode_enabled", "ultracode"):
238
+ pcfg["ultracode_enabled"] = False
239
+ elif key in ("claude_code_supported_capabilities", "supported_capabilities", "capabilities"):
240
+ pcfg.pop("claude_code_supported_capabilities", None)
241
+ elif capabilities.supports_model_endpoint_overrides and key.startswith("endpoint:"):
242
+ model_id = normalize_model_id(provider, key.split(":", 1)[1].strip())
243
+ endpoints = pcfg.get("model_endpoints")
244
+ if isinstance(endpoints, dict):
245
+ endpoints.pop(model_id, None)
246
+ elif sampling.option_key(key):
247
+ pcfg.pop(sampling.option_key(key), None)
248
+ else:
249
+ raise SystemExit(f"Unknown provider option: {key}")
250
+ return
251
+ if "=" not in token:
252
+ raise SystemExit(f"Expected key=value or unset:key, got: {token}")
253
+ key, raw_value = token.split("=", 1)
254
+ key = key.strip()
255
+ value = parse_config_value(raw_value)
256
+ if key in ("force_query_string", "force_query", "upstream_query", "test_query_string"):
257
+ # Operator-controlled raw query string for upstream /v1/messages. A
258
+ # leading "?" is tolerated and empty/default-like values clear it.
259
+ text = "" if value is None else str(value).strip().lstrip("?").strip()
260
+ if not text or text.lower() in ("default", "unset", "none", "null"):
261
+ pcfg.pop("force_query_string", None)
262
+ else:
263
+ pcfg["force_query_string"] = text
264
+ return
265
+ if key in ("supports_tool_choice", "tool_choice", "tool-choice", "auto_tool_choice"):
266
+ if value is None or str(value).strip().lower() in ("", "auto", "default", "unset", "none", "null"):
267
+ pcfg.pop("supports_tool_choice", None)
268
+ else:
269
+ pcfg["supports_tool_choice"] = parse_bool(value, default=True)
270
+ return
271
+ if key in ("context_window", "context", "max_model_len"):
272
+ fixed = positive_int(value)
273
+ if not fixed:
274
+ raise SystemExit("context_window must be a positive integer")
275
+ pcfg["context_window"] = fixed
276
+ return
277
+ if key in ("context_reserve_tokens", "reserve"):
278
+ fixed = positive_int(value)
279
+ if not fixed:
280
+ raise SystemExit("context_reserve_tokens must be a positive integer")
281
+ pcfg["context_reserve_tokens"] = fixed
282
+ return
283
+ if key in ("max_output_tokens", "max_tokens", "maxtoken", "max_token"):
284
+ fixed = positive_int(value)
285
+ if not fixed:
286
+ raise SystemExit("max_output_tokens must be a positive integer")
287
+ pcfg["max_output_tokens"] = fixed
288
+ return
289
+ if key in ("timeout", "timeout_ms", "request_timeout", "request_timeout_ms"):
290
+ fixed = positive_int(value)
291
+ if not fixed:
292
+ raise SystemExit("timeout must be a positive integer; values above 10000 are treated as milliseconds")
293
+ pcfg["request_timeout_ms"] = fixed if key.endswith("_ms") or fixed > 10000 else fixed * 1000
294
+ return
295
+ if key in ("stream_idle_timeout", "stream_idle_timeout_ms", "idle_timeout", "idle_timeout_ms"):
296
+ fixed = positive_int(value)
297
+ if not fixed:
298
+ raise SystemExit("stream_idle_timeout_ms must be a positive integer; values above 10000 are treated as milliseconds")
299
+ pcfg["stream_idle_timeout_ms"] = fixed if key.endswith("_ms") or fixed > 10000 else fixed * 1000
300
+ return
301
+ if key in ("rate_limit", "rate_limit_rpm", "rpm"):
302
+ fixed = positive_int(value)
303
+ if value in (0, "0", False, None) or str(value).lower() in ("false", "off", "disable", "disabled", "none", "unset"):
304
+ pcfg["rate_limit_rpm"] = 0
305
+ pcfg["rate_limit_status"] = False
306
+ return
307
+ if not fixed:
308
+ raise SystemExit("rate_limit_rpm must be a positive integer, or 0/unset to disable")
309
+ pcfg["rate_limit_rpm"] = fixed
310
+ return
311
+ if key in ("native", "native_compat"):
312
+ if capabilities.native_compat_error:
313
+ raise SystemExit(capabilities.native_compat_error)
314
+ pcfg["native_compat"] = bool(value)
315
+ return
316
+ if key in ("route", "routed", "route_through_router", "router"):
317
+ if not capabilities.supports_route_through_router:
318
+ raise SystemExit("route_through_router is only available for the Anthropic provider")
319
+ pcfg["route_through_router"] = parse_bool(value, default=False)
320
+ return
321
+ if key in ("stream", "stream_enabled"):
322
+ pcfg["stream_enabled"] = parse_bool(value, default=True)
323
+ return
324
+ if key in ("stream_word_chunking", "word_chunking", "stream_chunk", "stream_words"):
325
+ pcfg["stream_word_chunking"] = parse_bool(value, default=False)
326
+ return
327
+ if key in ("ip_family", "network_family", "address_family", "addr_family"):
328
+ pcfg["ip_family"] = normalize_ip_family(value)
329
+ return
330
+ if key in capabilities.text_option_aliases:
331
+ target = capabilities.text_option_aliases[key]
332
+ text = "" if value is None else str(value).strip()
333
+ if target in capabilities.strip_trailing_slash_fields:
334
+ text = text.rstrip("/")
335
+ if not text:
336
+ pcfg.pop(target, None)
337
+ else:
338
+ pcfg[target] = text
339
+ return
340
+ if key in ("rate_limit_status", "rpm_status"):
341
+ pcfg["rate_limit_status"] = parse_bool(value, default=False)
342
+ return
343
+ if key in ("workflows_enabled", "workflow", "workflows"):
344
+ pcfg["workflows_enabled"] = parse_bool(value, default=False)
345
+ return
346
+ if key in ("ultracode_enabled", "ultracode"):
347
+ pcfg["ultracode_enabled"] = parse_bool(value, default=False)
348
+ if pcfg["ultracode_enabled"]:
349
+ pcfg["workflows_enabled"] = True
350
+ return
351
+ if key in ("claude_code_supported_capabilities", "supported_capabilities", "capabilities"):
352
+ pcfg["claude_code_supported_capabilities"] = normalize_claude_code_supported_capabilities(value)
353
+ return
354
+ sample_key = sampling.option_key(key)
355
+ if sample_key:
356
+ if value is None:
357
+ pcfg.pop(sample_key, None)
358
+ else:
359
+ pcfg[sample_key] = sampling.validate(sample_key, value)
360
+ return
361
+ raise SystemExit(f"Unknown provider option: {key}")
@@ -0,0 +1,162 @@
1
+ """Provider endpoint mutation and runtime status projection services."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Any, Callable
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class ProviderEndpointPolicy:
10
+ keep_native_default: Callable[[str], bool]
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class ProviderEndpointPorts:
15
+ load_config: Callable[[], dict[str, Any]]
16
+ save_config: Callable[[dict[str, Any]], None]
17
+ clear_model_cache: Callable[[], None]
18
+ normalize_base_url: Callable[[str, dict[str, Any], str], str]
19
+ detect_native_compat: Callable[..., tuple[bool | None, str]]
20
+ ensure_current_model: Callable[..., tuple[str | None, list[str]]]
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class ProviderEndpointService:
25
+ policy: ProviderEndpointPolicy
26
+ ports: ProviderEndpointPorts
27
+
28
+ def set_base_url(self, provider: str, url: str) -> list[str]:
29
+ config = self.ports.load_config()
30
+ provider_config = config["providers"][provider]
31
+ old_url = str(provider_config.get("base_url") or "").rstrip("/")
32
+ new_url = self.ports.normalize_base_url(provider, provider_config, url)
33
+ provider_config["base_url"] = new_url
34
+ endpoint_changed = old_url != new_url
35
+ lines = [f"Base URL for {provider} set to {new_url}."]
36
+ if endpoint_changed:
37
+ lines.extend(self._reset_endpoint_model(provider, provider_config))
38
+ self.ports.save_config(config)
39
+ if not endpoint_changed:
40
+ self.ports.clear_model_cache()
41
+ return lines
42
+
43
+ def _reset_endpoint_model(self, provider: str, provider_config: dict[str, Any]) -> list[str]:
44
+ provider_config["current_model"] = ""
45
+ provider_config["custom_models"] = []
46
+ self.ports.clear_model_cache()
47
+ lines = ["Model selection was reset because the provider endpoint changed."]
48
+ detected, reason = self.ports.detect_native_compat(provider, provider_config)
49
+ if detected is None:
50
+ if self.policy.keep_native_default(provider):
51
+ provider_config["native_compat"] = True
52
+ lines.append(
53
+ f"Endpoint auto-detect inconclusive ({reason}); "
54
+ "Native compatibility kept on as the Anthropic default."
55
+ )
56
+ else:
57
+ provider_config["native_compat"] = bool(detected)
58
+ mode = "enabled" if detected else "disabled"
59
+ lines.append(f"Endpoint auto-detected ({reason}); Native compatibility {mode}.")
60
+ selected, selection_lines = self.ports.ensure_current_model(
61
+ provider, provider_config, force_refresh=True
62
+ )
63
+ lines.extend(selection_lines)
64
+ if not selected:
65
+ lines.append("Choose a model before running compatibility test or Launch Claude Code.")
66
+ return lines
67
+
68
+
69
+ @dataclass(frozen=True, slots=True)
70
+ class ProviderStatusProjectionPorts:
71
+ get_current_provider: Callable[[dict[str, Any]], tuple[str, dict[str, Any]]]
72
+ mode_label: Callable[[str, dict[str, Any]], str]
73
+ direct_native_anthropic: Callable[[str, dict[str, Any]], bool]
74
+ configured_adapter: Callable[..., Any]
75
+ contract_config: Callable[..., Any]
76
+ ollama_num_ctx_status: Callable[[dict[str, Any]], str]
77
+ ollama_options_status: Callable[[dict[str, Any]], str]
78
+ ollama_think_status: Callable[[str, dict[str, Any]], str]
79
+ current_upstream_model: Callable[[str, dict[str, Any]], str]
80
+ current_alias: Callable[[dict[str, Any]], str]
81
+
82
+
83
+ @dataclass(frozen=True, slots=True)
84
+ class RuntimeStatusPorts:
85
+ load_config: Callable[[], dict[str, Any]]
86
+ log_level_status: Callable[[], str]
87
+ channel_status_text: Callable[[dict[str, Any]], str]
88
+ channel_delivery_mode: Callable[[dict[str, Any]], str]
89
+ router_up: Callable[[], bool]
90
+ router_base: str
91
+ config_path: Any
92
+ web_status_lines: Callable[[], list[str]]
93
+
94
+
95
+ @dataclass(frozen=True, slots=True)
96
+ class ProviderStatusService:
97
+ projection: ProviderStatusProjectionPorts
98
+ runtime: RuntimeStatusPorts
99
+
100
+ def lines(self) -> list[str]:
101
+ config = self.runtime.load_config()
102
+ provider, provider_config = self.projection.get_current_provider(config)
103
+ direct_native = self.projection.direct_native_anthropic(provider, provider_config)
104
+ policy = self.projection.configured_adapter(provider, provider_config).configuration_policy(
105
+ self.projection.contract_config(provider, provider_config)
106
+ )
107
+ provider_lines = self._provider_lines(provider, provider_config, policy)
108
+ claude_model = self._claude_model(config, provider, provider_config, direct_native, policy)
109
+ router = (
110
+ "bypassed for native provider compatibility"
111
+ if direct_native
112
+ else f"{'up' if self.runtime.router_up() else 'down'} {self.runtime.router_base}"
113
+ )
114
+ return [
115
+ f"provider: {provider}",
116
+ f"language: {config.get('language', 'en')}",
117
+ f"mode: {self.projection.mode_label(provider, provider_config)}",
118
+ f"base_url: {provider_config.get('base_url')}",
119
+ f"model: {provider_config.get('current_model')}",
120
+ *provider_lines,
121
+ f"claude_model: {claude_model}",
122
+ f"log_level: {self.runtime.log_level_status()}",
123
+ f"channels: {self.runtime.channel_status_text(config)}",
124
+ f"channel_delivery: {self.runtime.channel_delivery_mode(config)}",
125
+ f"router: {router}",
126
+ *self.runtime.web_status_lines(),
127
+ f"config: {self.runtime.config_path}",
128
+ ]
129
+
130
+ def _provider_lines(self, provider: str, provider_config: dict[str, Any], policy: Any) -> list[str]:
131
+ lines: list[str] = []
132
+ if policy.uses_ollama_status:
133
+ model = self.projection.current_upstream_model(provider, provider_config)
134
+ lines.extend(
135
+ (
136
+ f"num_ctx: {self.projection.ollama_num_ctx_status(provider_config)}",
137
+ f"ollama_options: {self.projection.ollama_options_status(provider_config)}",
138
+ f"keep_alive: {provider_config.get('keep_alive', 'default')}",
139
+ f"think: {self.projection.ollama_think_status(model, provider_config)}",
140
+ f"request_timeout_ms: {provider_config.get('request_timeout_ms', 'default')}",
141
+ f"stream_idle_timeout_ms: {provider_config.get('stream_idle_timeout_ms', 'auto')}",
142
+ )
143
+ )
144
+ for field_name in policy.status_fields:
145
+ default = "auto" if field_name == "stream_idle_timeout_ms" else "default"
146
+ lines.append(f"{field_name}: {provider_config.get(field_name, default)}")
147
+ return lines
148
+
149
+ def _claude_model(
150
+ self, config: dict[str, Any], provider: str, provider_config: dict[str, Any], direct_native: bool, policy: Any
151
+ ) -> str:
152
+ if policy.runtime_owns_model:
153
+ return "disabled for native runtime provider"
154
+ if direct_native:
155
+ return self.projection.current_upstream_model(provider, provider_config)
156
+ return self.projection.current_alias(config)
157
+
158
+
159
+ __all__ = [
160
+ "ProviderEndpointPolicy", "ProviderEndpointPorts", "ProviderEndpointService",
161
+ "ProviderStatusProjectionPorts", "ProviderStatusService", "RuntimeStatusPorts",
162
+ ]