@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,263 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable
5
+
6
+
7
+ @dataclass(frozen=True, slots=True)
8
+ class ModelCatalogStorage:
9
+ read_model_list_cache: Callable[..., Any]
10
+ write_model_list_cache: Callable[..., Any]
11
+ write_model_registry: Callable[..., Any]
12
+ router_log: Callable[..., Any]
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class ModelCatalogHttp:
17
+ http_json: Callable[..., Any]
18
+ join_url: Callable[..., Any]
19
+ with_upstream_user_agent: Callable[..., Any]
20
+ lm_studio_api_base: Callable[..., Any]
21
+ nvidia_hosted_list_headers: Callable[..., Any]
22
+ nvidia_upstream_base_url: Callable[..., Any]
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class ProviderCatalogSources:
27
+ ANTHROPIC_MODEL_DOCS_URLS: Any
28
+ fetch_anthropic_api_model_ids: Callable[..., Any]
29
+ fetch_anthropic_public_model_ids: Callable[..., Any]
30
+ fetch_fireworks_model_ids: Callable[..., Any]
31
+ fireworks_account_id: Callable[..., Any]
32
+ fireworks_management_base_url: Callable[..., Any]
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class ModelCatalogResponseCodec:
37
+ model_ids_from_response: Callable[..., Any]
38
+ model_info_from_response: Callable[..., Any]
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class ModelCatalogPolicy:
43
+ normalize_model_id: Callable[..., Any]
44
+ ollama_catalog_model_ids: Callable[..., Any]
45
+ provider_has_api_key: Callable[..., Any]
46
+ provider_model_catalog_policy: Callable[..., Any]
47
+ provider_model_paths: Callable[..., Any]
48
+ provider_model_list_headers: Callable[..., Any]
49
+ provider_upstream_request_base: Callable[..., Any]
50
+ sorted_model_ids: Callable[..., Any]
51
+ unique_model_ids: Callable[..., Any]
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class ProviderModelServices:
56
+ storage: ModelCatalogStorage
57
+ http: ModelCatalogHttp
58
+ sources: ProviderCatalogSources
59
+ response_codec: ModelCatalogResponseCodec
60
+ policy: ModelCatalogPolicy
61
+
62
+
63
+ def fetch_upstream_model_ids(provider: str, pcfg: dict[str, Any], force_refresh: bool = False,
64
+ *,
65
+ services: ProviderModelServices,
66
+ ) -> list[str]:
67
+ ANTHROPIC_MODEL_DOCS_URLS = services.sources.ANTHROPIC_MODEL_DOCS_URLS
68
+ fetch_anthropic_api_model_ids = services.sources.fetch_anthropic_api_model_ids
69
+ fetch_anthropic_public_model_ids = services.sources.fetch_anthropic_public_model_ids
70
+ fetch_fireworks_model_ids = services.sources.fetch_fireworks_model_ids
71
+ fireworks_account_id = services.sources.fireworks_account_id
72
+ fireworks_management_base_url = services.sources.fireworks_management_base_url
73
+ http_json = services.http.http_json
74
+ join_url = services.http.join_url
75
+ lm_studio_api_base = services.http.lm_studio_api_base
76
+ model_ids_from_response = services.response_codec.model_ids_from_response
77
+ model_info_from_response = services.response_codec.model_info_from_response
78
+ normalize_model_id = services.policy.normalize_model_id
79
+ nvidia_hosted_list_headers = services.http.nvidia_hosted_list_headers
80
+ nvidia_upstream_base_url = services.http.nvidia_upstream_base_url
81
+ ollama_catalog_model_ids = services.policy.ollama_catalog_model_ids
82
+ provider_has_api_key = services.policy.provider_has_api_key
83
+ provider_model_catalog_policy = services.policy.provider_model_catalog_policy
84
+ provider_model_paths = services.policy.provider_model_paths
85
+ provider_model_list_headers = services.policy.provider_model_list_headers
86
+ provider_upstream_request_base = services.policy.provider_upstream_request_base
87
+ read_model_list_cache = services.storage.read_model_list_cache
88
+ router_log = services.storage.router_log
89
+ sorted_model_ids = services.policy.sorted_model_ids
90
+ unique_model_ids = services.policy.unique_model_ids
91
+ with_upstream_user_agent = services.http.with_upstream_user_agent
92
+ write_model_list_cache = services.storage.write_model_list_cache
93
+ write_model_registry = services.storage.write_model_registry
94
+ cached = None if force_refresh else read_model_list_cache(provider, pcfg)
95
+ if cached is not None:
96
+ return cached
97
+ catalog_policy = provider_model_catalog_policy(provider, pcfg)
98
+ if catalog_policy.kind == "configured":
99
+ ids = unique_model_ids(provider, [
100
+ *catalog_policy.fallback_models,
101
+ *(pcfg.get("custom_models", []) or []),
102
+ pcfg.get("current_model") or "",
103
+ ])
104
+ sorted_ids = sorted_model_ids(ids)
105
+ if sorted_ids:
106
+ write_model_list_cache(provider, pcfg, sorted_ids)
107
+ return sorted_ids
108
+ if catalog_policy.kind == "anthropic":
109
+ ids: list[str] = []
110
+ source = ""
111
+ if provider_has_api_key(provider, pcfg):
112
+ ids, source = fetch_anthropic_api_model_ids(pcfg, provider_model_list_headers(provider, pcfg), timeout=6.0)
113
+ if not ids:
114
+ ids = fetch_anthropic_public_model_ids()
115
+ source = "anthropic-docs"
116
+ if not ids:
117
+ return []
118
+ for mid in pcfg.get("custom_models", []) or []:
119
+ mid = normalize_model_id(provider, mid)
120
+ if mid and mid not in ids:
121
+ ids.append(mid)
122
+ cur = normalize_model_id(provider, pcfg.get("current_model") or "")
123
+ if cur and cur not in ids:
124
+ ids.insert(0, cur)
125
+ sorted_ids = unique_model_ids(provider, ids)
126
+ write_model_list_cache(provider, pcfg, sorted_ids)
127
+ write_model_registry(provider, pcfg, sorted_ids, source, {"urls": list(ANTHROPIC_MODEL_DOCS_URLS) if source == "anthropic-docs" else []})
128
+ return sorted_ids
129
+ if catalog_policy.kind == "fireworks":
130
+ headers = provider_model_list_headers(provider, pcfg)
131
+ model_info: dict[str, dict[str, Any]] = {}
132
+ source = ""
133
+ try:
134
+ ids, model_info, source = fetch_fireworks_model_ids(pcfg, headers, timeout=8.0)
135
+ fetched = bool(ids)
136
+ except Exception as exc:
137
+ router_log("DEBUG", f"fireworks model API fetch failed: {type(exc).__name__}: {exc}")
138
+ ids = []
139
+ fetched = False
140
+ if not fetched:
141
+ base = provider_upstream_request_base(provider, pcfg)
142
+ try:
143
+ data = http_json(join_url(base, "/v1/models"), headers=headers, timeout=6.0, provider=provider, pcfg=pcfg)
144
+ ids = [normalize_model_id(provider, mid) for mid in model_ids_from_response(data)]
145
+ model_info.update(model_info_from_response(provider, data))
146
+ fetched = bool(ids)
147
+ source = "fireworks:/v1/models"
148
+ except Exception as exc:
149
+ router_log("DEBUG", f"fireworks inference model list fetch failed: {type(exc).__name__}: {exc}")
150
+ if not fetched:
151
+ ids = unique_model_ids(provider, [
152
+ *(pcfg.get("custom_models", []) or []),
153
+ pcfg.get("current_model") or "",
154
+ ])
155
+ sorted_ids = sorted_model_ids(ids)
156
+ if sorted_ids:
157
+ write_model_list_cache(provider, pcfg, sorted_ids)
158
+ return sorted_ids
159
+ for mid in pcfg.get("custom_models", []) or []:
160
+ mid = normalize_model_id(provider, mid)
161
+ if mid and mid not in ids:
162
+ ids.append(mid)
163
+ cur = normalize_model_id(provider, pcfg.get("current_model") or "")
164
+ if cur and cur not in ids:
165
+ ids.insert(0, cur)
166
+ sorted_ids = sorted_model_ids(unique_model_ids(provider, ids))
167
+ metadata = {
168
+ "model_info": model_info,
169
+ "source": source,
170
+ "account_id": fireworks_account_id(pcfg),
171
+ "model_api_base_url": fireworks_management_base_url(pcfg),
172
+ }
173
+ write_model_list_cache(provider, pcfg, sorted_ids, metadata)
174
+ return sorted_ids
175
+ if catalog_policy.kind == "nvidia":
176
+ base = (pcfg.get("base_url") or nvidia_upstream_base_url()).rstrip("/")
177
+ else:
178
+ base = provider_upstream_request_base(provider, pcfg)
179
+ ids: list[str] = []
180
+ model_info: dict[str, dict[str, Any]] = {}
181
+ fetched = False
182
+ try:
183
+ if catalog_policy.kind == "nvidia":
184
+ data = http_json(join_url(base, "/v1/models"), headers=nvidia_hosted_list_headers(), timeout=8.0, provider=provider, pcfg=pcfg)
185
+ ids = model_ids_from_response(data)
186
+ model_info.update(model_info_from_response(provider, data))
187
+ fetched = True
188
+ else:
189
+ headers = provider_model_list_headers(provider, pcfg)
190
+ for path in provider_model_paths(provider, pcfg):
191
+ try:
192
+ request_base = lm_studio_api_base(pcfg) if catalog_policy.kind == "lm_studio" and path.startswith("/api/") else base
193
+ timeout = 2.0 if catalog_policy.kind == "lm_studio" else (4.0 if catalog_policy.kind == "ollama" else 6.0)
194
+ data = http_json(join_url(request_base, path), headers=headers, timeout=timeout, provider=provider, pcfg=pcfg)
195
+ ids = [normalize_model_id(provider, mid) for mid in model_ids_from_response(data)]
196
+ model_info.update(model_info_from_response(provider, data))
197
+ fetched = True
198
+ if ids:
199
+ break
200
+ except Exception:
201
+ continue
202
+ if not fetched and catalog_policy.allow_public_without_auth:
203
+ # OpenCode publishes the model catalog at /v1/models. Keep the
204
+ # picker independent from key-specific auth/rate-limit failures.
205
+ try:
206
+ public_headers = with_upstream_user_agent({"content-type": "application/json"})
207
+ data = http_json(join_url(base, "/v1/models"), headers=public_headers, timeout=6.0, provider=provider, pcfg=pcfg)
208
+ ids = model_ids_from_response(data)
209
+ model_info.update(model_info_from_response(provider, data))
210
+ fetched = True
211
+ except Exception as exc:
212
+ router_log("DEBUG", f"{provider} public model catalog fetch failed: {type(exc).__name__}: {exc}")
213
+ except Exception:
214
+ ids = []
215
+ if catalog_policy.use_bundled_catalog_fallback and not ids:
216
+ ids = ollama_catalog_model_ids(provider)
217
+ fetched = bool(ids)
218
+ if not fetched and catalog_policy.allow_configured_fallback:
219
+ ids = unique_model_ids(provider, [
220
+ *(pcfg.get("custom_models", []) or []),
221
+ pcfg.get("current_model") or "",
222
+ ])
223
+ sorted_ids = sorted_model_ids(ids)
224
+ if sorted_ids:
225
+ write_model_list_cache(provider, pcfg, sorted_ids)
226
+ return sorted_ids
227
+ if not fetched and catalog_policy.fallback_models:
228
+ ids = list(catalog_policy.fallback_models)
229
+ fetched = True
230
+ if not fetched:
231
+ return []
232
+ for mid in catalog_policy.fallback_models:
233
+ mid = normalize_model_id(provider, mid)
234
+ if mid and mid not in ids:
235
+ ids.append(mid)
236
+ for mid in pcfg.get("custom_models", []) or []:
237
+ mid = normalize_model_id(provider, mid)
238
+ if mid and mid not in ids:
239
+ ids.append(mid)
240
+ cur = normalize_model_id(provider, pcfg.get("current_model") or "")
241
+ if cur and catalog_policy.kind != "nvidia" and cur.startswith(f"ciel-runtime-{provider}-"):
242
+ pass
243
+ elif cur and cur not in ids and not (catalog_policy.kind == "nvidia" and cur.startswith("claude-")):
244
+ ids.insert(0, cur)
245
+ if catalog_policy.kind == "nvidia" and cur and cur not in ids:
246
+ ids.insert(0, cur)
247
+ sorted_ids = unique_model_ids(provider, ids)
248
+ if catalog_policy.kind != "anthropic":
249
+ sorted_ids = sorted_model_ids(sorted_ids)
250
+ metadata = {"model_info": model_info} if model_info else None
251
+ write_model_list_cache(provider, pcfg, sorted_ids, metadata)
252
+ return sorted_ids
253
+
254
+
255
+ __all__ = [
256
+ "ModelCatalogHttp",
257
+ "ModelCatalogPolicy",
258
+ "ModelCatalogResponseCodec",
259
+ "ModelCatalogStorage",
260
+ "ProviderCatalogSources",
261
+ "ProviderModelServices",
262
+ "fetch_upstream_model_ids",
263
+ ]
@@ -0,0 +1,176 @@
1
+ """Provider HTTP identity, IP-family selection, and connectivity policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import os
7
+ import socket
8
+ import threading
9
+ import urllib.parse
10
+ import urllib.request
11
+ from collections.abc import Iterable
12
+ from typing import Any, Callable
13
+
14
+ from ciel_runtime_support.providers.constants import OPENCODE_PROVIDER_NAMES
15
+
16
+
17
+ DEFAULT_UPSTREAM_USER_AGENT = "claude-cli"
18
+ IP_FAMILY_ALIASES = {
19
+ "": "auto",
20
+ "auto": "auto",
21
+ "default": "auto",
22
+ "system": "auto",
23
+ "any": "auto",
24
+ "4": "ipv4",
25
+ "v4": "ipv4",
26
+ "ipv4": "ipv4",
27
+ "inet": "ipv4",
28
+ "6": "ipv6",
29
+ "v6": "ipv6",
30
+ "ipv6": "ipv6",
31
+ "inet6": "ipv6",
32
+ "prefer4": "ipv4-preferred",
33
+ "prefer-v4": "ipv4-preferred",
34
+ "preferred4": "ipv4-preferred",
35
+ "ipv4-preferred": "ipv4-preferred",
36
+ "ipv4_preferred": "ipv4-preferred",
37
+ "prefer-ipv4": "ipv4-preferred",
38
+ "prefer6": "ipv6-preferred",
39
+ "prefer-v6": "ipv6-preferred",
40
+ "preferred6": "ipv6-preferred",
41
+ "ipv6-preferred": "ipv6-preferred",
42
+ "ipv6_preferred": "ipv6-preferred",
43
+ "prefer-ipv6": "ipv6-preferred",
44
+ }
45
+ IP_FAMILY_CHOICES = ("auto", "ipv4", "ipv6", "ipv4-preferred", "ipv6-preferred")
46
+
47
+ _IP_FAMILY_LOCK = threading.Lock()
48
+
49
+
50
+ def upstream_user_agent(environ: dict[str, str] | None = None) -> str:
51
+ values = os.environ if environ is None else environ
52
+ configured = str(values.get("CIEL_RUNTIME_UPSTREAM_USER_AGENT") or "").strip()
53
+ return configured or DEFAULT_UPSTREAM_USER_AGENT
54
+
55
+
56
+ def with_upstream_user_agent(headers: dict[str, str] | None = None) -> dict[str, str]:
57
+ result = dict(headers or {})
58
+ if not any(str(key).lower() == "user-agent" for key in result):
59
+ result["user-agent"] = upstream_user_agent()
60
+ return result
61
+
62
+
63
+ def normalize_ip_family(value: Any, default: str = "auto") -> str:
64
+ text = str(value if value is not None else default).strip().lower().replace("_", "-")
65
+ if family := IP_FAMILY_ALIASES.get(text):
66
+ return family
67
+ raise SystemExit(f"ip_family must be one of: {', '.join(IP_FAMILY_CHOICES)}")
68
+
69
+
70
+ def default_provider_ip_family(provider: str) -> str:
71
+ return "ipv6-preferred" if provider in OPENCODE_PROVIDER_NAMES else "auto"
72
+
73
+
74
+ def provider_ip_family(provider: str | None, config: dict[str, Any] | None) -> str:
75
+ if not provider or not isinstance(config, dict):
76
+ return "auto"
77
+ return normalize_ip_family(config.get("ip_family"), default_provider_ip_family(provider))
78
+
79
+
80
+ @contextlib.contextmanager
81
+ def socket_ip_family_policy(ip_family: str) -> Iterable[None]:
82
+ policy = normalize_ip_family(ip_family)
83
+ if policy == "auto":
84
+ yield
85
+ return
86
+ strict = policy in ("ipv4", "ipv6")
87
+ desired = socket.AF_INET6 if policy in ("ipv6", "ipv6-preferred") else socket.AF_INET
88
+ with _IP_FAMILY_LOCK:
89
+ original_getaddrinfo = socket.getaddrinfo
90
+
91
+ def filtered_getaddrinfo(
92
+ host: Any,
93
+ port: Any,
94
+ family: int = 0,
95
+ type: int = 0,
96
+ proto: int = 0,
97
+ flags: int = 0,
98
+ ) -> Any:
99
+ infos = original_getaddrinfo(host, port, family, type, proto, flags)
100
+ matches = [info for info in infos if info and info[0] == desired]
101
+ if strict:
102
+ if not matches:
103
+ raise socket.gaierror(socket.EAI_NONAME, f"no {policy} address for {host}")
104
+ return matches
105
+ others = [info for info in infos if not info or info[0] != desired]
106
+ return sorted(matches, key=lambda info: (0 if info[0] == desired else 1, info[0])) + others
107
+
108
+ socket.getaddrinfo = filtered_getaddrinfo
109
+ try:
110
+ yield
111
+ finally:
112
+ socket.getaddrinfo = original_getaddrinfo
113
+
114
+
115
+ def provider_urlopen(
116
+ request: urllib.request.Request,
117
+ timeout: float,
118
+ provider: str | None,
119
+ config: dict[str, Any] | None,
120
+ log: Callable[[str, str], None],
121
+ ) -> Any:
122
+ policy = provider_ip_family(provider, config)
123
+ if policy != "auto":
124
+ log("DEBUG", f"upstream_ip_family provider={provider or ''} policy={policy}")
125
+ with socket_ip_family_policy(policy):
126
+ return urllib.request.urlopen(request, timeout=timeout)
127
+
128
+
129
+ def ip_family_connectivity(
130
+ host: str,
131
+ port: int,
132
+ family: int,
133
+ timeout: float = 1.5,
134
+ ) -> tuple[bool, str]:
135
+ try:
136
+ infos = socket.getaddrinfo(host, port, family, socket.SOCK_STREAM, socket.IPPROTO_TCP)
137
+ except Exception as error:
138
+ return False, f"dns:{type(error).__name__}"
139
+ if not infos:
140
+ return False, "dns:no-address"
141
+ last_error = ""
142
+ for address_family, socktype, protocol, _canonical_name, address in infos:
143
+ connection = socket.socket(address_family, socktype, protocol)
144
+ connection.settimeout(timeout)
145
+ try:
146
+ connection.connect(address)
147
+ return True, "connect:ok"
148
+ except Exception as error:
149
+ last_error = f"connect:{type(error).__name__}"
150
+ finally:
151
+ try:
152
+ connection.close()
153
+ except Exception:
154
+ pass
155
+ return False, last_error or "connect:failed"
156
+
157
+
158
+ def provider_ip_family_probe_lines(
159
+ provider: str,
160
+ config: dict[str, Any],
161
+ default_base_url: Callable[[str], str | None],
162
+ ) -> list[str]:
163
+ if provider not in OPENCODE_PROVIDER_NAMES:
164
+ return []
165
+ base = str(config.get("base_url") or default_base_url(provider) or "").strip()
166
+ parsed = urllib.parse.urlparse(base)
167
+ if not parsed.hostname:
168
+ return [f"IP family: {provider_ip_family(provider, config)}; probe unavailable (base URL host missing)"]
169
+ port = parsed.port or (443 if parsed.scheme == "https" else 80)
170
+ ipv4_ok, ipv4_reason = ip_family_connectivity(parsed.hostname, port, socket.AF_INET)
171
+ ipv6_ok, ipv6_reason = ip_family_connectivity(parsed.hostname, port, socket.AF_INET6)
172
+ return [
173
+ f"IP family: {provider_ip_family(provider, config)}",
174
+ f"IPv4 connectivity: {'OK' if ipv4_ok else 'FAIL'} ({ipv4_reason})",
175
+ f"IPv6 connectivity: {'OK' if ipv6_ok else 'FAIL'} ({ipv6_reason})",
176
+ ]
@@ -0,0 +1,238 @@
1
+ """CLI controller for provider and Ollama option configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Collection, Mapping, Sequence
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class ProviderOptionCliConfig:
12
+ load: Callable[[], dict[str, Any]]
13
+ save: Callable[[dict[str, Any]], None]
14
+ normalize_provider: Callable[[str], str]
15
+ clear_model_cache: Callable[[], None]
16
+ output: Callable[[str], None]
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class OllamaOptionCommands:
21
+ apply: Callable[[dict[str, Any], str], None]
22
+ apply_timeout: Callable[[str, dict[str, Any]], list[str]]
23
+ context_status: Callable[[dict[str, Any]], str]
24
+ rate_usage: Callable[[str, dict[str, Any]], tuple[int, int | None]]
25
+ options_status: Callable[[dict[str, Any]], str]
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class ProviderOptionCommands:
30
+ apply: Callable[[str, dict[str, Any], str], None]
31
+ cap_context: Callable[[str, dict[str, Any]], list[str]]
32
+ cap_output: Callable[[str, dict[str, Any]], list[str]]
33
+ apply_timeout: Callable[[str, dict[str, Any]], list[str]]
34
+ status: Callable[[str, dict[str, Any]], str]
35
+
36
+
37
+ _CONTEXT_KEYS = frozenset(("context_window", "context", "max_model_len"))
38
+ _OLLAMA_CONTEXT_KEYS = frozenset(
39
+ (
40
+ "num_ctx",
41
+ "ctx",
42
+ "num_ctx_min",
43
+ "ctx_min",
44
+ "min",
45
+ "num_ctx_max",
46
+ "ctx_max",
47
+ "max",
48
+ )
49
+ )
50
+ _TIMEOUT_KEYS = frozenset(
51
+ (
52
+ "timeout",
53
+ "timeout_ms",
54
+ "request_timeout",
55
+ "request_timeout_ms",
56
+ "stream_idle_timeout",
57
+ "stream_idle_timeout_ms",
58
+ "idle_timeout",
59
+ "idle_timeout_ms",
60
+ )
61
+ )
62
+ _COMMON_PROVIDER_NOTES = (
63
+ " max_output_tokens is passed to Claude Code as CLAUDE_CODE_MAX_OUTPUT_TOKENS.",
64
+ " context_window is a ciel-runtime/router cap; native mode still cannot raise the real server limit.",
65
+ " temperature/top_p/top_k are injected by ciel-runtime router mode when the provider supports them.",
66
+ )
67
+ _PROVIDER_EXAMPLES = (
68
+ " ciel-runtimectl provider-options deepseek max_output_tokens=8192 context_window=1048576",
69
+ " ciel-runtimectl provider-options opencode-go endpoint:custom-model=chat",
70
+ " ciel-runtimectl provider-options opencode ip_family=ipv6-preferred",
71
+ " ciel-runtimectl provider-options fireworks account_id=fireworks model_api_base_url=https://api.fireworks.ai",
72
+ " ciel-runtimectl provider-options nvidia-hosted max_output_tokens=4096 temperature=0.7 top_p=0.8 timeout=300000 rate_limit_rpm=40",
73
+ " ciel-runtimectl provider-options vllm max_output_tokens=4096 context_window=65536 timeout=300000",
74
+ " ciel-runtimectl provider-options self-hosted-nim native=true max_output_tokens=4096",
75
+ )
76
+ _OLLAMA_EXAMPLES = (
77
+ " ciel-runtimectl ollama-options num_ctx=auto min=32768 max=131072",
78
+ " ciel-runtimectl ollama-options num_ctx=65536 temperature=0.7 top_p=0.8 max_tokens=32768 timeout=300000",
79
+ " ciel-runtime --ca-ollama-option temperature=0.7 --ca-ollama-num-ctx 65536",
80
+ )
81
+
82
+
83
+ class ProviderOptionCliController:
84
+ def __init__(
85
+ self,
86
+ config: ProviderOptionCliConfig,
87
+ ollama: OllamaOptionCommands,
88
+ provider: ProviderOptionCommands,
89
+ *,
90
+ supported_providers: Collection[str],
91
+ ollama_providers: Collection[str],
92
+ provider_notes: Mapping[str, Sequence[str]],
93
+ unsupported_message: str,
94
+ ) -> None:
95
+ self.config = config
96
+ self.ollama = ollama
97
+ self.provider = provider
98
+ self.supported_providers = frozenset(supported_providers)
99
+ self.ollama_providers = frozenset(ollama_providers)
100
+ self.provider_notes = provider_notes
101
+ self.unsupported_message = unsupported_message
102
+
103
+ def native(self, args: Any) -> None:
104
+ config = self.config.load()
105
+ provider_config = config["providers"]["ollama"]
106
+ raw = getattr(args, "value", None)
107
+ if raw:
108
+ value = raw.lower()
109
+ if value in ("on", "enable", "enabled", "true", "1"):
110
+ provider_config["native_compat"] = True
111
+ elif value in ("off", "disable", "disabled", "false", "0"):
112
+ provider_config["native_compat"] = False
113
+ else:
114
+ raise SystemExit("Use: ciel-runtime ollama-native on|off|status")
115
+ self.config.save(config)
116
+ state = "on" if provider_config.get("native_compat", True) else "off"
117
+ self._lines(
118
+ (
119
+ f"ollama_native_compat: {state}",
120
+ f"base_url: {provider_config.get('base_url')}",
121
+ f"model: {provider_config.get('current_model')}",
122
+ 'launch_env: ANTHROPIC_BASE_URL=<ollama>, ANTHROPIC_AUTH_TOKEN=ollama, ANTHROPIC_API_KEY=""',
123
+ )
124
+ )
125
+
126
+ def ollama_options(self, args: Any) -> None:
127
+ config = self.config.load()
128
+ values = list(getattr(args, "values", []) or [])
129
+ provider = self._selected_provider(
130
+ values,
131
+ str(config.get("current_provider", "ollama")),
132
+ self.ollama_providers,
133
+ "ollama",
134
+ )
135
+ provider_config = config["providers"][provider]
136
+ if values:
137
+ context_changed = self._contains_key(values, _OLLAMA_CONTEXT_KEYS)
138
+ explicit_timeout = self._contains_key(values, _TIMEOUT_KEYS)
139
+ for token in values:
140
+ self.ollama.apply(provider_config, token)
141
+ timeout_lines = (
142
+ self.ollama.apply_timeout(provider, provider_config)
143
+ if context_changed and not explicit_timeout
144
+ else []
145
+ )
146
+ self._persist(config)
147
+ self.config.output(f"Ollama options updated for {provider}.")
148
+ self._lines(timeout_lines)
149
+ self._ollama_status(provider, provider_config)
150
+
151
+ def provider_options(self, args: Any) -> None:
152
+ config = self.config.load()
153
+ values = list(getattr(args, "values", []) or [])
154
+ provider = self._selected_provider(
155
+ values,
156
+ str(config.get("current_provider", "vllm")),
157
+ self.supported_providers,
158
+ )
159
+ if provider not in self.supported_providers:
160
+ raise SystemExit(self.unsupported_message)
161
+ provider_config = config["providers"][provider]
162
+ if values:
163
+ context_changed = self._contains_key(values, _CONTEXT_KEYS)
164
+ explicit_timeout = self._contains_key(values, _TIMEOUT_KEYS)
165
+ for token in values:
166
+ self.provider.apply(provider, provider_config, token)
167
+ cap_lines = self.provider.cap_context(provider, provider_config)
168
+ cap_lines.extend(self.provider.cap_output(provider, provider_config))
169
+ timeout_lines = (
170
+ self.provider.apply_timeout(provider, provider_config)
171
+ if context_changed and not explicit_timeout
172
+ else []
173
+ )
174
+ self._persist(config)
175
+ self.config.output(f"Provider options updated for {provider}.")
176
+ self._lines((*cap_lines, *timeout_lines))
177
+ self.config.output(f"provider: {provider}")
178
+ self.config.output(
179
+ f"provider_options: {self.provider.status(provider, provider_config)}"
180
+ )
181
+ self.config.output("Notes:")
182
+ self._lines(_COMMON_PROVIDER_NOTES)
183
+ self._lines(self.provider_notes.get(provider, ()))
184
+ self.config.output("Examples:")
185
+ self._lines(_PROVIDER_EXAMPLES)
186
+
187
+ def _ollama_status(self, provider: str, config: dict[str, Any]) -> None:
188
+ self._lines(
189
+ (
190
+ f"provider: {provider}",
191
+ f"num_ctx: {self.ollama.context_status(config)}",
192
+ f"keep_alive: {config.get('keep_alive', 'default')}",
193
+ f"think: {bool(config.get('think', False))}",
194
+ f"request_timeout_ms: {config.get('request_timeout_ms', 'default')}",
195
+ )
196
+ )
197
+ used, limit = self.ollama.rate_usage(provider, config)
198
+ if limit is not None:
199
+ self.config.output(f"rate_limit_rpm: {limit}")
200
+ if bool(config.get("rate_limit_status", False)):
201
+ suffix = f"{used}/{limit}" if limit > 0 else f"{used}/min (unmanaged)"
202
+ self.config.output(f"rpm_used: {suffix}")
203
+ self.config.output(f"ollama_options: {self.ollama.options_status(config)}")
204
+ self.config.output("Examples:")
205
+ self._lines(_OLLAMA_EXAMPLES)
206
+
207
+ def _selected_provider(
208
+ self,
209
+ values: list[str],
210
+ current: str,
211
+ allowed: Collection[str],
212
+ fallback: str | None = None,
213
+ ) -> str:
214
+ provider = current if current in allowed else fallback or current
215
+ if values:
216
+ try:
217
+ candidate = self.config.normalize_provider(values[0])
218
+ if candidate in allowed:
219
+ provider = candidate
220
+ values.pop(0)
221
+ except SystemExit:
222
+ pass
223
+ return provider
224
+
225
+ @staticmethod
226
+ def _contains_key(values: Sequence[str], keys: Collection[str]) -> bool:
227
+ return any(
228
+ token.split("=", 1)[0].replace("unset:", "").strip() in keys
229
+ for token in values
230
+ )
231
+
232
+ def _persist(self, config: dict[str, Any]) -> None:
233
+ self.config.save(config)
234
+ self.config.clear_model_cache()
235
+
236
+ def _lines(self, lines: Sequence[str]) -> None:
237
+ for line in lines:
238
+ self.config.output(line)