@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,431 @@
1
+ """Provider model selection, alias resolution, and request projection."""
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 ModelIdentityPorts:
11
+ normalize: Callable[[str, str], str]
12
+ model_map: Callable[[str, dict[str, Any]], dict[str, str]]
13
+ unslug: Callable[[str, str, dict[str, str]], str | None]
14
+ api_model_id: Callable[[str, str], str]
15
+ strip_context_suffix: Callable[[str | None], str]
16
+ alias: Callable[[str, str], str]
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class ModelSelectionPorts:
21
+ adapter: Callable[[str, dict[str, Any]], Any]
22
+ contract: Callable[[str, dict[str, Any]], Any]
23
+ placeholders: Callable[[str], set[str]]
24
+ upstream_ids: Callable[..., list[str]]
25
+ unique_ids: Callable[[str, list[str]], list[str]]
26
+ apply_specs: Callable[[str, dict[str, Any]], list[str]]
27
+ apply_timeout: Callable[..., list[str]]
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class ModelCatalogPorts:
32
+ model_object: Callable[[str, str, dict[str, Any]], dict[str, Any]]
33
+ headers: Callable[[str, dict[str, Any], Any | None], dict[str, str]]
34
+ fetch_anthropic: Callable[..., tuple[list[str], str]]
35
+ sorted_ids: Callable[[list[str]], list[str]]
36
+ routed_anthropic: Callable[[str, dict[str, Any]], bool]
37
+ log: Callable[[str, str], None]
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class ModelMutationConfigPorts:
42
+ load_config: Callable[[], dict[str, Any]]
43
+ current_provider: Callable[[dict[str, Any]], tuple[str, dict[str, Any]]]
44
+ save_config: Callable[[dict[str, Any]], None]
45
+ clear_model_cache: Callable[[], None]
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class ModelMutationPolicyPorts:
50
+ model_map: Callable[..., dict[str, str]]
51
+ unslug: Callable[[str, str, dict[str, str]], str | None]
52
+ normalize: Callable[[str, str], str]
53
+ apply_profile: Callable[[str, dict[str, Any]], list[str]]
54
+ read_model_info: Callable[[str, dict[str, Any]], dict[str, Any]]
55
+ positive_int: Callable[[Any], int | None]
56
+ model_preset: Callable[[str], dict[str, Any]]
57
+ apply_selection_updates: Callable[[str, dict[str, Any], str], None]
58
+ alias: Callable[[str, str], str]
59
+ format_context: Callable[[int], str]
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class ModelMutationEffectPorts:
64
+ sync_context_limit: Callable[[str, dict[str, Any], str], list[str]]
65
+ cap_context_settings: Callable[[str, dict[str, Any]], list[str]]
66
+ apply_recommended_preset: Callable[[str, dict[str, Any], str], list[str]]
67
+ apply_recommended_timeout: Callable[..., list[str]]
68
+ read_model_list: Callable[[str, dict[str, Any]], list[str] | None]
69
+
70
+
71
+ @dataclass(frozen=True, slots=True)
72
+ class AdvisorModelMutationPorts:
73
+ load_config: Callable[[], dict[str, Any]]
74
+ current_provider: Callable[[dict[str, Any]], tuple[str, dict[str, Any]]]
75
+ save_config: Callable[[dict[str, Any]], None]
76
+ clear_model_cache: Callable[[], None]
77
+ normalize: Callable[[str, str], str]
78
+ read_model_list: Callable[[str, dict[str, Any]], list[str] | None]
79
+ uses_native_advisor: Callable[[str, dict[str, Any]], bool]
80
+
81
+
82
+ class AdvisorModelSelectionController:
83
+ def __init__(self, ports: AdvisorModelMutationPorts) -> None:
84
+ self._ports = ports
85
+
86
+ def select(self, value: str) -> list[str]:
87
+ config = self._ports.load_config()
88
+ provider, provider_config = self._ports.current_provider(config)
89
+ if self._ports.uses_native_advisor(provider, provider_config):
90
+ return [
91
+ f"{provider.title()} modes use Claude Code's built-in /advisor; "
92
+ "run /advisor in the session to pick its model."
93
+ ]
94
+ stripped = value.strip()
95
+ model_id = self._ports.normalize(provider, stripped) if stripped else ""
96
+ provider_config["advisor_model"] = model_id
97
+ if model_id:
98
+ known = self._ports.read_model_list(provider, provider_config) or []
99
+ custom = provider_config.setdefault("custom_models", [])
100
+ if model_id not in custom and model_id not in known:
101
+ custom.append(model_id)
102
+ self._ports.save_config(config)
103
+ self._ports.clear_model_cache()
104
+ if not model_id:
105
+ return [f"Advisor Model for {provider} disabled."]
106
+ return [f"Advisor Model for {provider} set to {model_id}."]
107
+
108
+
109
+ class ModelSelectionController:
110
+ def __init__(
111
+ self,
112
+ config: ModelMutationConfigPorts,
113
+ policy: ModelMutationPolicyPorts,
114
+ effects: ModelMutationEffectPorts,
115
+ ) -> None:
116
+ self._config = config
117
+ self._policy = policy
118
+ self._effects = effects
119
+
120
+ def select(self, value: str) -> list[str]:
121
+ config = self._config.load_config()
122
+ provider, provider_config = self._config.current_provider(config)
123
+ model_map = self._policy.model_map(provider, provider_config, fetch=False)
124
+ model_id = self._policy.normalize(
125
+ provider,
126
+ self._policy.unslug(provider, value, model_map) or value,
127
+ )
128
+ previous_model = str(provider_config.get("current_model") or "")
129
+ provider_config["current_model"] = model_id
130
+ self._policy.apply_selection_updates(provider, provider_config, model_id)
131
+ selected_info = self._policy.read_model_info(provider, provider_config).get(model_id) or {}
132
+ selected_context = self._policy.positive_int(selected_info.get("max_model_len"))
133
+ if selected_context:
134
+ provider_config["max_model_len"] = selected_context
135
+ profile_messages = self._policy.apply_profile(provider, provider_config)
136
+ preset = self._policy.model_preset(model_id)
137
+ if preset.get("num_ctx_min"):
138
+ provider_config["num_ctx_min"] = preset["num_ctx_min"]
139
+ if preset.get("num_ctx_max"):
140
+ provider_config["num_ctx_max"] = preset["num_ctx_max"]
141
+ context_messages = self._effects.sync_context_limit(provider, provider_config, model_id)
142
+ context_messages.extend(self._effects.cap_context_settings(provider, provider_config))
143
+ preset_messages = self._effects.apply_recommended_preset(
144
+ provider,
145
+ provider_config,
146
+ config.get("language", "en"),
147
+ )
148
+ timeout_messages = self._effects.apply_recommended_timeout(
149
+ provider,
150
+ provider_config,
151
+ use_context_fallback=False,
152
+ )
153
+ known = self._effects.read_model_list(provider, provider_config) or []
154
+ custom = provider_config.setdefault("custom_models", [])
155
+ if model_id not in custom and model_id not in known:
156
+ custom.append(model_id)
157
+ self._config.save_config(config)
158
+ self._config.clear_model_cache()
159
+ messages = [
160
+ f"Model for {provider} set to {model_id}.",
161
+ f"Claude Code alias: {self._policy.alias(provider, model_id)}",
162
+ *profile_messages,
163
+ ]
164
+ if model_id != previous_model:
165
+ messages.append(
166
+ "Start a new session after changing models to avoid context-cache re-prefill overhead."
167
+ )
168
+ if selected_context:
169
+ messages.append(
170
+ f"Model context size: {self._policy.format_context(selected_context)} "
171
+ f"({selected_context:,} tokens)."
172
+ )
173
+ messages.extend(context_messages)
174
+ messages.extend(preset_messages)
175
+ messages.extend(timeout_messages)
176
+ if preset.get("thinking"):
177
+ messages.append("Note: this is a thinking model; compatibility test uses extended token budget.")
178
+ return messages
179
+
180
+
181
+ class ProviderModelSelection:
182
+ def __init__(
183
+ self,
184
+ identity: ModelIdentityPorts,
185
+ selection: ModelSelectionPorts,
186
+ catalog: ModelCatalogPorts,
187
+ ) -> None:
188
+ self.identity = identity
189
+ self.selection = selection
190
+ self.catalog = catalog
191
+
192
+ def current_upstream_id(self, provider: str, config: dict[str, Any]) -> str:
193
+ current = self.identity.normalize(provider, config.get("current_model") or "model")
194
+ if current.startswith(f"ciel-runtime-{provider}-"):
195
+ try:
196
+ return (
197
+ self.identity.unslug(
198
+ provider, current, self.identity.model_map(provider, config)
199
+ )
200
+ or current
201
+ )
202
+ except Exception:
203
+ return current
204
+ return current
205
+
206
+ def needs_selection(self, provider: str, config: dict[str, Any]) -> bool:
207
+ adapter = self.selection.adapter(provider, config)
208
+ if not adapter.requires_catalog_model_selection(
209
+ self.selection.contract(provider, config)
210
+ ):
211
+ return False
212
+ current = self.identity.normalize(provider, str(config.get("current_model") or ""))
213
+ return current in self.selection.placeholders(provider)
214
+
215
+ def ensure_selected(
216
+ self,
217
+ provider: str,
218
+ config: dict[str, Any],
219
+ *,
220
+ force_refresh: bool = False,
221
+ ) -> tuple[bool, list[str]]:
222
+ adapter = self.selection.adapter(provider, config)
223
+ if not adapter.requires_catalog_model_selection(
224
+ self.selection.contract(provider, config)
225
+ ):
226
+ return True, []
227
+ current = self.identity.normalize(provider, str(config.get("current_model") or ""))
228
+ placeholders = self.selection.placeholders(provider)
229
+ if current and current not in placeholders:
230
+ return True, []
231
+ try:
232
+ ids = self.selection.unique_ids(
233
+ provider,
234
+ self.selection.upstream_ids(provider, config, force_refresh=force_refresh),
235
+ )
236
+ except Exception as exc:
237
+ if current:
238
+ return True, [
239
+ f"Model list unavailable for {provider}; keeping configured model "
240
+ f"{current} ({type(exc).__name__}: {exc})."
241
+ ]
242
+ return False, [
243
+ f"Model selection required for {provider}: model list unavailable "
244
+ f"({type(exc).__name__}: {exc})."
245
+ ]
246
+ if current and current in ids:
247
+ return True, []
248
+ candidates = [
249
+ model_id
250
+ for model_id in ids
251
+ if self.identity.normalize(provider, model_id) not in placeholders
252
+ ]
253
+ if len(candidates) == 1:
254
+ selected = self.identity.normalize(provider, candidates[0])
255
+ config["current_model"] = selected
256
+ context = self.selection.apply_specs(provider, config)
257
+ timeout = self.selection.apply_timeout(
258
+ provider, config, use_context_fallback=False
259
+ )
260
+ return True, [
261
+ f"Model auto-selected from provider list: {selected}.",
262
+ *context,
263
+ *timeout,
264
+ ]
265
+ if len(candidates) > 1:
266
+ return False, [
267
+ f"Model selection required for {provider}: provider returned "
268
+ f"{len(candidates)} models; choose one before launch/test."
269
+ ]
270
+ if current:
271
+ return True, [
272
+ f"Model list for {provider} did not include a non-placeholder model; "
273
+ f"keeping configured model {current}."
274
+ ]
275
+ return False, [
276
+ f"Model selection required for {provider}: provider returned no usable model ids."
277
+ ]
278
+
279
+ def launch_id(self, provider: str, config: dict[str, Any]) -> str:
280
+ current = self.identity.normalize(provider, config.get("current_model") or "model")
281
+ adapter = self.selection.adapter(provider, config)
282
+ contract = self.selection.contract(provider, config)
283
+ if adapter.launch_model_strategy(contract) != "ollama_unslug":
284
+ return current if adapter.preserves_claude_model_alias(current) else self.identity.alias(
285
+ provider, current
286
+ )
287
+ if not current.startswith(f"ciel-runtime-{provider}-"):
288
+ return current
289
+ try:
290
+ return (
291
+ self.identity.unslug(
292
+ provider, current, self.identity.model_map(provider, config)
293
+ )
294
+ or current
295
+ )
296
+ except Exception:
297
+ return current
298
+
299
+ def resolve_requested(
300
+ self, provider: str, config: dict[str, Any], requested: str | None
301
+ ) -> str:
302
+ requested = self.identity.strip_context_suffix(requested)
303
+ fallback = self.identity.normalize(provider, config.get("current_model") or "model")
304
+ adapter = self.selection.adapter(provider, config)
305
+ model_map = self.identity.model_map(provider, config)
306
+ if not requested:
307
+ return self.identity.api_model_id(provider, fallback)
308
+ resolved = self.identity.unslug(provider, requested, model_map)
309
+ if resolved:
310
+ return self.identity.api_model_id(provider, resolved)
311
+ if requested in set(model_map.values()):
312
+ return self.identity.api_model_id(provider, requested)
313
+ if adapter.preserves_claude_model_alias(requested):
314
+ return self.identity.api_model_id(provider, requested)
315
+ if requested.startswith(("claude-", "ciel-runtime-")):
316
+ return self.identity.api_model_id(provider, fallback)
317
+ return self.identity.api_model_id(
318
+ provider, self.identity.normalize(provider, requested)
319
+ )
320
+
321
+ def resolve_tool_models(
322
+ self, provider: str, config: dict[str, Any], body: dict[str, Any]
323
+ ) -> dict[str, Any]:
324
+ tools = body.get("tools")
325
+ if not isinstance(tools, list) or not tools:
326
+ return body
327
+ projected: list[Any] = []
328
+ changed = 0
329
+ for tool in tools:
330
+ model = tool.get("model") if isinstance(tool, dict) else None
331
+ if not isinstance(model, str) or not model.strip():
332
+ projected.append(tool)
333
+ continue
334
+ resolved = self.resolve_requested(provider, config, model)
335
+ if resolved and resolved != model:
336
+ projected.append({**tool, "model": resolved})
337
+ changed += 1
338
+ else:
339
+ projected.append(tool)
340
+ if not changed:
341
+ return body
342
+ self.catalog.log(
343
+ "INFO", f"resolved upstream tool model references for {provider}: {changed}"
344
+ )
345
+ return {**body, "tools": projected}
346
+
347
+ def list_objects(self, provider: str, config: dict[str, Any]) -> list[dict[str, Any]]:
348
+ return [
349
+ self.catalog.model_object(provider, model_id, config)
350
+ for model_id in self.selection.upstream_ids(provider, config)
351
+ ]
352
+
353
+ def list_objects_for_request(
354
+ self, provider: str, config: dict[str, Any], inbound_headers: Any | None = None
355
+ ) -> list[dict[str, Any]]:
356
+ if self.catalog.routed_anthropic(provider, config):
357
+ try:
358
+ ids, source = self.catalog.fetch_anthropic(
359
+ config,
360
+ self.catalog.headers(
361
+ provider,
362
+ config,
363
+ inbound_headers,
364
+ "anthropic_messages",
365
+ ),
366
+ timeout=6.0,
367
+ )
368
+ if ids:
369
+ ids.extend(
370
+ model_id
371
+ for value in config.get("custom_models", []) or []
372
+ if (model_id := self.identity.normalize(provider, value))
373
+ and model_id not in ids
374
+ )
375
+ current = self.identity.normalize(
376
+ provider, config.get("current_model") or ""
377
+ )
378
+ if current and current not in ids:
379
+ ids.append(current)
380
+ self.catalog.log(
381
+ "DEBUG",
382
+ f"anthropic routed model discovery source={source} count={len(ids)}",
383
+ )
384
+ models = self.catalog.sorted_ids(
385
+ self.selection.unique_ids(provider, ids)
386
+ )
387
+ return [
388
+ self.catalog.model_object(provider, model_id, config)
389
+ for model_id in models
390
+ ]
391
+ except Exception as exc:
392
+ self.catalog.log(
393
+ "DEBUG",
394
+ "anthropic routed model discovery fallback "
395
+ f"error={type(exc).__name__}: {exc}",
396
+ )
397
+ return self.list_objects(provider, config)
398
+
399
+
400
+ @dataclass(frozen=True, slots=True)
401
+ class ProviderModelSelectionApi:
402
+ """Explicit public adapter for late-bound model-selection services."""
403
+
404
+ selection_factory: Callable[[], ProviderModelSelection]
405
+
406
+ def current_upstream_model_id(self, provider: str, pcfg: dict[str, Any]) -> str:
407
+ return self.selection_factory().current_upstream_id(provider, pcfg)
408
+
409
+ def provider_placeholder_model_ids(self, provider: str) -> set[str]:
410
+ return self.selection_factory().selection.placeholders(provider)
411
+
412
+ def current_model_needs_provider_selection(self, provider: str, pcfg: dict[str, Any]) -> bool:
413
+ return self.selection_factory().needs_selection(provider, pcfg)
414
+
415
+ def ensure_current_model_from_provider_list(self, provider: str, pcfg: dict[str, Any], *, force_refresh: bool = False) -> tuple[bool, list[str]]:
416
+ return self.selection_factory().ensure_selected(provider, pcfg, force_refresh=force_refresh)
417
+
418
+ def launch_model_id(self, provider: str, pcfg: dict[str, Any]) -> str:
419
+ return self.selection_factory().launch_id(provider, pcfg)
420
+
421
+ def resolve_requested_model(self, provider: str, pcfg: dict[str, Any], requested: str | None) -> str:
422
+ return self.selection_factory().resolve_requested(provider, pcfg, requested)
423
+
424
+ def resolve_tool_model_references(self, provider: str, pcfg: dict[str, Any], body: dict[str, Any]) -> dict[str, Any]:
425
+ return self.selection_factory().resolve_tool_models(provider, pcfg, body)
426
+
427
+ def list_model_objects(self, provider: str, pcfg: dict[str, Any]) -> list[dict[str, Any]]:
428
+ return self.selection_factory().list_objects(provider, pcfg)
429
+
430
+ def list_model_objects_for_request(self, provider: str, pcfg: dict[str, Any], inbound_headers: Any | None = None) -> list[dict[str, Any]]:
431
+ return self.selection_factory().list_objects_for_request(provider, pcfg, inbound_headers)
@@ -0,0 +1,142 @@
1
+ """Current-model specification lookup, projection, and refresh service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping, Sequence
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from .architecture import ProviderContextPolicy
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class ModelSpecLookupPorts:
14
+ read_cache: Callable[[str, dict[str, Any]], Mapping[str, dict[str, Any]]]
15
+ normalize_model: Callable[[str, str], str]
16
+ upstream_model: Callable[[str, dict[str, Any]], str]
17
+ strip_context_suffix: Callable[[str], str]
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class ModelSpecMutationPorts:
22
+ positive_int: Callable[[Any], int | None]
23
+ apply_model_profile: Callable[[str, dict[str, Any]], list[str]]
24
+ context_policy: Callable[[str, dict[str, Any]], ProviderContextPolicy]
25
+ ollama_model_matches: Callable[[str, str | None], bool]
26
+ preserve_ollama_cap: Callable[[dict[str, Any]], bool]
27
+ format_context: Callable[[int | None], str]
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class ModelSpecRefreshPorts:
32
+ refresh_models: Callable[..., Sequence[str]]
33
+
34
+
35
+ class ProviderModelSpecService:
36
+ def __init__(
37
+ self,
38
+ lookup: ModelSpecLookupPorts,
39
+ mutation: ModelSpecMutationPorts,
40
+ refresh: ModelSpecRefreshPorts,
41
+ ) -> None:
42
+ self.lookup = lookup
43
+ self.mutation = mutation
44
+ self.refresh_ports = refresh
45
+
46
+ def current_info(
47
+ self,
48
+ provider: str,
49
+ config: dict[str, Any],
50
+ ) -> dict[str, Any]:
51
+ info = self.lookup.read_cache(provider, config)
52
+ if not info:
53
+ return {}
54
+ upstream = self.lookup.upstream_model(provider, config)
55
+ configured = str(config.get("current_model") or "")
56
+ normalized_upstream = self.lookup.normalize_model(provider, upstream)
57
+ normalized_configured = self.lookup.normalize_model(provider, configured)
58
+ candidates = (
59
+ normalized_upstream,
60
+ normalized_configured,
61
+ self.lookup.strip_context_suffix(normalized_configured),
62
+ )
63
+ for model_id in candidates:
64
+ if model_id and model_id in info:
65
+ return info[model_id]
66
+ folded_current = normalized_upstream.casefold()
67
+ for model_id, model_info in info.items():
68
+ if (
69
+ self.lookup.normalize_model(provider, model_id).casefold()
70
+ == folded_current
71
+ ):
72
+ return model_info
73
+ return {}
74
+
75
+ def apply(self, provider: str, config: dict[str, Any]) -> list[str]:
76
+ messages = self.mutation.apply_model_profile(provider, config)
77
+ info = self.current_info(provider, config)
78
+ max_context = (
79
+ self.mutation.positive_int(info.get("max_model_len")) if info else None
80
+ )
81
+ if not max_context:
82
+ return messages
83
+ model = self.lookup.normalize_model(
84
+ provider,
85
+ self.lookup.upstream_model(provider, config),
86
+ )
87
+ strategy = self.mutation.context_policy(
88
+ provider,
89
+ config,
90
+ ).settings_strategy
91
+ if strategy == "ollama":
92
+ cached_model = str(config.get("model_context_model") or "")
93
+ cached_context = self.mutation.positive_int(config.get("model_context_max"))
94
+ if (
95
+ not self.mutation.ollama_model_matches(model, cached_model)
96
+ or cached_context != max_context
97
+ ):
98
+ config["model_context_max"] = max_context
99
+ config["model_context_model"] = model
100
+ messages.append(self._context_notice(max_context))
101
+ current_max = self.mutation.positive_int(config.get("num_ctx_max"))
102
+ if not (
103
+ current_max
104
+ and current_max <= max_context
105
+ and self.mutation.preserve_ollama_cap(config)
106
+ ):
107
+ config["num_ctx_max"] = (
108
+ min(current_max, max_context)
109
+ if current_max and current_max > max_context
110
+ else max_context
111
+ )
112
+ elif strategy == "standard":
113
+ if self.mutation.positive_int(config.get("max_model_len")) != max_context:
114
+ config["max_model_len"] = max_context
115
+ messages.append(self._context_notice(max_context))
116
+ return messages
117
+
118
+ def refresh(self, provider: str, config: dict[str, Any]) -> list[str]:
119
+ messages: list[str] = []
120
+ try:
121
+ models = self.refresh_ports.refresh_models(
122
+ provider,
123
+ config,
124
+ force_refresh=True,
125
+ )
126
+ if models:
127
+ messages.append(
128
+ f"Model specs refreshed from provider: {len(models)} model(s)."
129
+ )
130
+ except Exception as exc:
131
+ messages.append(
132
+ f"Model specs refresh failed: {type(exc).__name__}: {exc}"
133
+ )
134
+ messages.extend(self.apply(provider, config))
135
+ return messages
136
+
137
+ def _context_notice(self, max_context: int) -> str:
138
+ return (
139
+ "Model context size from provider specs: "
140
+ f"{self.mutation.format_context(max_context)} "
141
+ f"({max_context:,} tokens)."
142
+ )