@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,109 @@
1
+ """Pure model-identity heuristics for context-capacity hints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable, Mapping, Sequence
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class ModelContextHintPorts:
13
+ strip_context_suffix: Callable[[str], str]
14
+ catalog_context: Callable[[str], tuple[int | None, str | None, str | None]]
15
+ model_preset: Callable[[str], Mapping[str, Any]]
16
+ positive_int: Callable[[Any], int | None]
17
+
18
+
19
+ class ModelContextHintPolicy:
20
+ def __init__(
21
+ self,
22
+ zai_hints: Sequence[tuple[str, int]],
23
+ ports: ModelContextHintPorts,
24
+ ) -> None:
25
+ self.zai_hints = tuple(zai_hints)
26
+ self.ports = ports
27
+
28
+ @staticmethod
29
+ def is_qwen36_plus(model_id: str) -> bool:
30
+ compact = re.sub(r"[^a-z0-9]+", "", (model_id or "").lower())
31
+ return "qwen36plus" in compact
32
+
33
+ def is_kimi_k3(self, model_id: str) -> bool:
34
+ normalized = (
35
+ self.ports.strip_context_suffix(model_id)
36
+ .strip()
37
+ .lower()
38
+ .replace("_", "-")
39
+ )
40
+ if normalized.startswith("ciel-runtime-kimi-"):
41
+ normalized = normalized[len("ciel-runtime-kimi-") :]
42
+ return normalized in {"k3", "kimi-k3", "kimi/k3", "kimi-code/k3"}
43
+
44
+ def zai_hint(self, model_id: str) -> int | None:
45
+ model = (
46
+ self.ports.strip_context_suffix(model_id)
47
+ .strip()
48
+ .lower()
49
+ .replace("_", "-")
50
+ )
51
+ if not model:
52
+ return None
53
+ for prefix, limit in self.zai_hints:
54
+ if model == prefix or model.startswith(prefix + "-"):
55
+ return limit
56
+ return None
57
+
58
+ def resolve(self, model_id: str) -> int | None:
59
+ model = (model_id or "").lower()
60
+ if not model:
61
+ return None
62
+ zai_hint = self.zai_hint(model_id)
63
+ if zai_hint:
64
+ return zai_hint
65
+ if self.is_qwen36_plus(model_id):
66
+ return 1048576
67
+ if self.is_kimi_k3(model_id):
68
+ return 1048576
69
+ catalog_limit, _family, _source = self.ports.catalog_context(model_id)
70
+ if catalog_limit:
71
+ return catalog_limit
72
+ if any(
73
+ marker in model
74
+ for marker in (
75
+ "deepseek-v4-pro",
76
+ "deepseek-v4-flash",
77
+ "deepseek-v4",
78
+ "v4-pro",
79
+ "v4-flash",
80
+ "1m",
81
+ "million",
82
+ )
83
+ ):
84
+ return 1048576
85
+ if any(
86
+ marker in model
87
+ for marker in (
88
+ "kimi-for-coding",
89
+ "kimi-code",
90
+ "kimi-k2.7",
91
+ "kimi_k2.7",
92
+ "kimi2.7",
93
+ "k2.7",
94
+ "kimi-k2.6",
95
+ "kimi_k2.6",
96
+ "kimi2.6",
97
+ "kimi-k2",
98
+ )
99
+ ):
100
+ return 262144
101
+ if "qwen3.6" in model:
102
+ return 262144
103
+ if "glm-4.7" in model or "glm-5.1" in model:
104
+ return 200000
105
+ if "deepseek-r1" in model or "llama3.3" in model:
106
+ return 131072
107
+ return self.ports.positive_int(
108
+ self.ports.model_preset(model_id).get("num_ctx_max")
109
+ )
@@ -0,0 +1,147 @@
1
+ """Provider-registry-backed model panel projections."""
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 ModelPanelCatalog:
11
+ alias_for: Callable[..., Any]
12
+ cached_or_configured_model_ids: Callable[..., Any]
13
+ read_model_info_cache: Callable[..., Any]
14
+ read_model_list_cache: Callable[..., Any]
15
+ unique_model_ids: Callable[..., Any]
16
+ upstream_model_ids: Callable[..., Any]
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class ModelPanelPresentation:
21
+ advisor_model_badge: Callable[..., Any]
22
+ advisor_panel_notice: Callable[..., Any]
23
+ format_context_tokens: Callable[..., Any]
24
+ format_parameter_count: Callable[..., Any]
25
+ model_panel_badge: Callable[..., Any]
26
+ normalize_model_id: Callable[..., Any]
27
+ positive_int: Callable[..., Any]
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class ModelPanelServices:
32
+ catalog: ModelPanelCatalog
33
+ presentation: ModelPanelPresentation
34
+
35
+
36
+ def model_panel_rows(
37
+ provider: str,
38
+ pcfg: dict[str, Any],
39
+ fetch: bool = True,
40
+ force_refresh: bool = False,
41
+ *,
42
+ services: ModelPanelServices,
43
+ ) -> tuple[list[str], list[str]]:
44
+ catalog = services.catalog
45
+ presentation = services.presentation
46
+ alias_for = catalog.alias_for
47
+ cached_or_configured_model_ids = catalog.cached_or_configured_model_ids
48
+ format_context_tokens = presentation.format_context_tokens
49
+ format_parameter_count = presentation.format_parameter_count
50
+ model_panel_badge = presentation.model_panel_badge
51
+ normalize_model_id = presentation.normalize_model_id
52
+ positive_int = presentation.positive_int
53
+ read_model_info_cache = catalog.read_model_info_cache
54
+ read_model_list_cache = catalog.read_model_list_cache
55
+ unique_model_ids = catalog.unique_model_ids
56
+ upstream_model_ids = catalog.upstream_model_ids
57
+ values = unique_model_ids(
58
+ provider,
59
+ upstream_model_ids(provider, pcfg, force_refresh=force_refresh)
60
+ if fetch else cached_or_configured_model_ids(provider, pcfg),
61
+ )
62
+ rows: list[str] = []
63
+ current = pcfg.get("current_model")
64
+ seen_aliases: set[str] = set()
65
+ deduped_values: list[str] = []
66
+ cache = read_model_list_cache(provider, pcfg)
67
+ cached_info = read_model_info_cache(provider, pcfg)
68
+ rows.append("Refresh provider model list..." if cache is None else "Refresh provider model list")
69
+ deduped_values.append("__refresh_models__")
70
+ for mid in values:
71
+ alias = alias_for(provider, mid)
72
+ suffix = ""
73
+ info = cached_info.get(normalize_model_id(provider, mid), {})
74
+ max_context = positive_int(info.get("max_model_len"))
75
+ if max_context:
76
+ suffix += f" [ctx {format_context_tokens(max_context)}]"
77
+ parameter_count = format_parameter_count(info.get("parameter_count"))
78
+ if parameter_count:
79
+ suffix += f" [{parameter_count} params]"
80
+ badge = model_panel_badge(provider, pcfg, mid)
81
+ if badge:
82
+ suffix += f" [{badge}]"
83
+ alias_key = alias.casefold()
84
+ if alias_key in seen_aliases:
85
+ continue
86
+ seen_aliases.add(alias_key)
87
+ deduped_values.append(mid)
88
+ mark = "*" if mid == current else " "
89
+ rows.append(f"{mark} {mid} {alias}{suffix}")
90
+ rows.append("+ Custom model id...")
91
+ deduped_values.append("__custom__")
92
+ rows.append("Back")
93
+ deduped_values.append("back")
94
+ return rows, deduped_values
95
+
96
+
97
+ def advisor_model_panel_rows(
98
+ provider: str,
99
+ pcfg: dict[str, Any],
100
+ fetch: bool = True,
101
+ force_refresh: bool = False,
102
+ *,
103
+ services: ModelPanelServices,
104
+ ) -> tuple[list[str], list[str]]:
105
+ catalog = services.catalog
106
+ presentation = services.presentation
107
+ advisor_model_badge = presentation.advisor_model_badge
108
+ advisor_panel_notice = presentation.advisor_panel_notice
109
+ cached_or_configured_model_ids = catalog.cached_or_configured_model_ids
110
+ normalize_model_id = presentation.normalize_model_id
111
+ read_model_list_cache = catalog.read_model_list_cache
112
+ unique_model_ids = catalog.unique_model_ids
113
+ upstream_model_ids = catalog.upstream_model_ids
114
+ notice = advisor_panel_notice(provider, pcfg)
115
+ if notice:
116
+ return list(notice[0]), list(notice[1])
117
+ current = normalize_model_id(provider, pcfg.get("advisor_model", ""))
118
+ values = unique_model_ids(
119
+ provider,
120
+ (
121
+ upstream_model_ids(provider, pcfg, force_refresh=force_refresh)
122
+ if fetch else cached_or_configured_model_ids(provider, pcfg)
123
+ )
124
+ + ([current] if current else []),
125
+ )
126
+ rows: list[str] = []
127
+ rows.append(("* Disable Advisor Model" if not current else " Disable Advisor Model"))
128
+ deduped_values = [""]
129
+ cache = read_model_list_cache(provider, pcfg)
130
+ rows.append("Refresh provider model list..." if cache is None else "Refresh provider model list")
131
+ deduped_values.append("__refresh_models__")
132
+ seen: set[str] = set()
133
+ for mid in values:
134
+ if not mid or mid in seen:
135
+ continue
136
+ seen.add(mid)
137
+ mark = "*" if mid == current else " "
138
+ badge = advisor_model_badge(provider, pcfg, mid)
139
+ suffix = f" {badge}" if badge else ""
140
+ rows.append(f"{mark} {mid}{suffix}")
141
+ deduped_values.append(mid)
142
+ rows.append("+ Custom advisor model id...")
143
+ deduped_values.append("__custom__")
144
+ rows.append("Back")
145
+ deduped_values.append("back")
146
+ return rows, deduped_values
147
+
@@ -0,0 +1,231 @@
1
+ """Persistent provider model registry and short-lived model-list cache."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import time
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any, Callable
11
+
12
+ from .runtime_constants import MODEL_CACHE_TTL_SECONDS
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class ModelRegistryPaths:
17
+ config_dir: Path
18
+ registry: Path
19
+ list_cache: Path
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class ModelRegistryPolicy:
24
+ cache_key: Callable[[str, dict[str, Any]], str]
25
+ unique_ids: Callable[[str, list[str]], list[str]]
26
+ normalize_id: Callable[[str, str], str]
27
+ positive_int: Callable[[Any], int | None]
28
+ recommendations: Callable[[str, list[str]], dict[str, Any]]
29
+ log: Callable[[str, str], None]
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class ModelRegistryRepository:
34
+ paths: ModelRegistryPaths
35
+ policy: ModelRegistryPolicy
36
+ ttl_seconds: float
37
+
38
+ def read_registry(
39
+ self,
40
+ provider: str,
41
+ config: dict[str, Any],
42
+ max_age_seconds: float | None = None,
43
+ ) -> dict[str, Any] | None:
44
+ try:
45
+ data = json.loads(self.paths.registry.read_text(encoding="utf-8"))
46
+ except Exception:
47
+ return None
48
+ providers = data.get("providers") if isinstance(data, dict) else None
49
+ entry = providers.get(provider) if isinstance(providers, dict) else None
50
+ if not isinstance(entry, dict) or not self._key_matches(provider, config, entry):
51
+ return None
52
+ max_age = self.ttl_seconds if max_age_seconds is None else max_age_seconds
53
+ try:
54
+ if max_age > 0 and time.time() - float(entry.get("time", 0)) > max_age:
55
+ return None
56
+ except Exception:
57
+ return None
58
+ return entry if isinstance(entry.get("models"), list) else None
59
+
60
+ def _key_matches(
61
+ self,
62
+ provider: str,
63
+ config: dict[str, Any],
64
+ entry: dict[str, Any],
65
+ ) -> bool:
66
+ current = self.policy.cache_key(provider, config)
67
+ if entry.get("key") == current:
68
+ return True
69
+ if provider != "anthropic" or entry.get("source") != "anthropic-docs":
70
+ return False
71
+ try:
72
+ saved_key = json.loads(str(entry.get("key") or "{}"))
73
+ current_key = json.loads(current)
74
+ saved_key.pop("api", None)
75
+ current_key.pop("api", None)
76
+ return saved_key == current_key
77
+ except Exception:
78
+ return False
79
+
80
+ def read_registry_models(
81
+ self,
82
+ provider: str,
83
+ config: dict[str, Any],
84
+ max_age_seconds: float | None = None,
85
+ ) -> list[str] | None:
86
+ entry = self.read_registry(provider, config, max_age_seconds)
87
+ models = entry.get("models") if entry else None
88
+ if not isinstance(models, list):
89
+ return None
90
+ return self.policy.unique_ids(provider, [str(item) for item in models if str(item).strip()])
91
+
92
+ def read_registry_info(
93
+ self,
94
+ provider: str,
95
+ config: dict[str, Any],
96
+ max_age_seconds: float | None = None,
97
+ ) -> dict[str, dict[str, Any]]:
98
+ entry = self.read_registry(provider, config, max_age_seconds)
99
+ metadata = entry.get("metadata") if entry else None
100
+ return self._normalize_info(provider, metadata)
101
+
102
+ def write_registry(
103
+ self,
104
+ provider: str,
105
+ config: dict[str, Any],
106
+ models: list[str],
107
+ source: str = "provider",
108
+ metadata: dict[str, Any] | None = None,
109
+ ) -> None:
110
+ self.paths.config_dir.mkdir(parents=True, exist_ok=True)
111
+ try:
112
+ data = json.loads(self.paths.registry.read_text(encoding="utf-8")) if self.paths.registry.exists() else {}
113
+ except Exception:
114
+ data = {}
115
+ if not isinstance(data, dict):
116
+ data = {}
117
+ providers = data.get("providers")
118
+ if not isinstance(providers, dict):
119
+ providers = {}
120
+ providers[provider] = {
121
+ "time": time.time(),
122
+ "key": self.policy.cache_key(provider, config),
123
+ "source": source,
124
+ "models": self.policy.unique_ids(provider, models),
125
+ "recommendations": self.policy.recommendations(provider, models),
126
+ "metadata": metadata or {},
127
+ }
128
+ data.update(schema=1, providers=providers)
129
+ try:
130
+ self.paths.registry.write_text(
131
+ json.dumps(data, indent=2, sort_keys=True) + "\n",
132
+ encoding="utf-8",
133
+ )
134
+ os.chmod(self.paths.registry, 0o600)
135
+ except Exception as error:
136
+ self.policy.log(
137
+ "WARN",
138
+ f"model_registry_write_failed provider={provider} error={type(error).__name__}: {error}",
139
+ )
140
+
141
+ def read_list_cache(self, provider: str, config: dict[str, Any]) -> list[str] | None:
142
+ try:
143
+ data = json.loads(self.paths.list_cache.read_text())
144
+ except Exception:
145
+ return self.read_registry_models(provider, config, 0)
146
+ if not isinstance(data, dict) or data.get("key") != self.policy.cache_key(provider, config):
147
+ return self.read_registry_models(provider, config, 0)
148
+ if time.time() - float(data.get("time", 0)) > self.ttl_seconds:
149
+ if registry_models := self.read_registry_models(provider, config, 0):
150
+ return registry_models
151
+ models = data.get("models")
152
+ if not isinstance(models, list):
153
+ return None
154
+ return self.policy.unique_ids(provider, [str(item) for item in models if str(item).strip()])
155
+
156
+ def read_info_cache(self, provider: str, config: dict[str, Any]) -> dict[str, dict[str, Any]]:
157
+ try:
158
+ data = json.loads(self.paths.list_cache.read_text())
159
+ except Exception:
160
+ return self.read_registry_info(provider, config, 0)
161
+ if not isinstance(data, dict) or data.get("key") != self.policy.cache_key(provider, config):
162
+ return self.read_registry_info(provider, config, 0)
163
+ info = self._normalize_info(provider, data.get("metadata"))
164
+ return info or self.read_registry_info(provider, config, 0)
165
+
166
+ def _normalize_info(self, provider: str, metadata: Any) -> dict[str, dict[str, Any]]:
167
+ raw_info = metadata.get("model_info") if isinstance(metadata, dict) else None
168
+ if not isinstance(raw_info, dict):
169
+ return {}
170
+ result: dict[str, dict[str, Any]] = {}
171
+ for raw_id, raw_value in raw_info.items():
172
+ model_id = self.policy.normalize_id(provider, str(raw_id))
173
+ if not model_id or not isinstance(raw_value, dict):
174
+ continue
175
+ info = dict(raw_value)
176
+ if context := self.policy.positive_int(info.get("max_model_len")):
177
+ info["max_model_len"] = context
178
+ result[model_id] = info
179
+ return result
180
+
181
+ def write_list_cache(
182
+ self,
183
+ provider: str,
184
+ config: dict[str, Any],
185
+ models: list[str],
186
+ metadata: dict[str, Any] | None = None,
187
+ ) -> None:
188
+ self.paths.config_dir.mkdir(parents=True, exist_ok=True)
189
+ data = {
190
+ "time": time.time(),
191
+ "key": self.policy.cache_key(provider, config),
192
+ "models": models,
193
+ "metadata": metadata or {},
194
+ }
195
+ try:
196
+ self.paths.list_cache.write_text(json.dumps(data, indent=2) + "\n")
197
+ os.chmod(self.paths.list_cache, 0o600)
198
+ except Exception as error:
199
+ self.policy.log(
200
+ "WARN",
201
+ f"model_list_cache_write_failed provider={provider} error={type(error).__name__}: {error}",
202
+ )
203
+ self.write_registry(provider, config, models, "provider", metadata)
204
+
205
+
206
+ @dataclass(frozen=True, slots=True)
207
+ class ModelRegistryApi:
208
+ """Explicit public adapter for late-bound model registry repositories."""
209
+
210
+ repository_factory: Callable[[], ModelRegistryRepository]
211
+
212
+ def read_registry(self, provider: str, pcfg: dict[str, Any], max_age_seconds: float = MODEL_CACHE_TTL_SECONDS) -> dict[str, Any] | None:
213
+ return self.repository_factory().read_registry(provider, pcfg, max_age_seconds)
214
+
215
+ def read_registry_models(self, provider: str, pcfg: dict[str, Any], max_age_seconds: float = MODEL_CACHE_TTL_SECONDS) -> list[str] | None:
216
+ return self.repository_factory().read_registry_models(provider, pcfg, max_age_seconds)
217
+
218
+ def read_registry_info(self, provider: str, pcfg: dict[str, Any], max_age_seconds: float = MODEL_CACHE_TTL_SECONDS) -> dict[str, dict[str, Any]]:
219
+ return self.repository_factory().read_registry_info(provider, pcfg, max_age_seconds)
220
+
221
+ def write_registry(self, provider: str, pcfg: dict[str, Any], models: list[str], source: str = "provider", metadata: dict[str, Any] | None = None) -> None:
222
+ self.repository_factory().write_registry(provider, pcfg, models, source, metadata)
223
+
224
+ def read_list_cache(self, provider: str, pcfg: dict[str, Any]) -> list[str] | None:
225
+ return self.repository_factory().read_list_cache(provider, pcfg)
226
+
227
+ def read_info_cache(self, provider: str, pcfg: dict[str, Any]) -> dict[str, dict[str, Any]]:
228
+ return self.repository_factory().read_info_cache(provider, pcfg)
229
+
230
+ def write_list_cache(self, provider: str, pcfg: dict[str, Any], models: list[str], metadata: dict[str, Any] | None = None) -> None:
231
+ self.repository_factory().write_list_cache(provider, pcfg, models, metadata)
@@ -0,0 +1,191 @@
1
+ """npm process, version, and installed-package path utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import subprocess
8
+ from collections.abc import Mapping
9
+ from pathlib import Path
10
+
11
+
12
+ def parse_version_tuple(value: str) -> tuple[int, ...]:
13
+ return tuple(int(item) for item in re.split(r"[^0-9]+", value.strip()) if item)
14
+
15
+
16
+ def version_newer(latest: str, current: str) -> bool:
17
+ left = list(parse_version_tuple(latest))
18
+ right = list(parse_version_tuple(current))
19
+ size = max(len(left), len(right), 1)
20
+ left.extend([0] * (size - len(left)))
21
+ right.extend([0] * (size - len(right)))
22
+ return tuple(left) > tuple(right)
23
+
24
+
25
+ def npm_latest_package_version(
26
+ npm: str, package_spec: str, timeout: float = 8.0
27
+ ) -> str:
28
+ try:
29
+ process = subprocess.run(
30
+ [npm, "view", package_spec, "version"],
31
+ text=True,
32
+ stdout=subprocess.PIPE,
33
+ stderr=subprocess.DEVNULL,
34
+ timeout=timeout,
35
+ )
36
+ except Exception:
37
+ return ""
38
+ if process.returncode != 0:
39
+ return ""
40
+ output = (process.stdout or "").strip()
41
+ return output.splitlines()[-1].strip() if output else ""
42
+
43
+
44
+ def npm_global_package_root(
45
+ npm: str,
46
+ package_name: str = "@oneciel-ai/ciel-runtime",
47
+ timeout: float = 8.0,
48
+ ) -> Path | None:
49
+ try:
50
+ process = subprocess.run(
51
+ [npm, "root", "-g"],
52
+ text=True,
53
+ stdout=subprocess.PIPE,
54
+ stderr=subprocess.DEVNULL,
55
+ timeout=timeout,
56
+ )
57
+ except Exception:
58
+ return None
59
+ if process.returncode != 0:
60
+ return None
61
+ root = (process.stdout or "").strip()
62
+ if not root:
63
+ return None
64
+ package_path = Path(root)
65
+ for part in package_name.split("/"):
66
+ if part:
67
+ package_path /= part
68
+ return package_path
69
+
70
+
71
+ def npm_prefix_from_package_root(package_root: Path) -> Path | None:
72
+ """Infer the npm install prefix from a global installed package root."""
73
+
74
+ parts = package_root.parts
75
+ for index, part in enumerate(parts):
76
+ if part != "node_modules":
77
+ continue
78
+ try:
79
+ node_modules = Path(*parts[: index + 1])
80
+ except Exception:
81
+ return None
82
+ parent = node_modules.parent
83
+ return parent.parent if parent.name == "lib" else parent
84
+ return None
85
+
86
+
87
+ def npm_global_install_command(
88
+ npm: str, package_spec: str, prefix: Path | None = None
89
+ ) -> list[str]:
90
+ command = [npm, "install", "-g"]
91
+ if prefix is not None:
92
+ command.extend(["--prefix", str(prefix)])
93
+ command.append(package_spec)
94
+ return command
95
+
96
+
97
+ def npm_install_runtime_command(
98
+ npm: str, package_spec: str, prefix: Path | None = None
99
+ ) -> list[str]:
100
+ command = npm_global_install_command(npm, package_spec, prefix)
101
+ command.insert(3, "--prefer-online")
102
+ return command
103
+
104
+
105
+ def npm_global_bin_dir_from_prefix(prefix: Path) -> Path:
106
+ return prefix if os.name == "nt" else prefix / "bin"
107
+
108
+
109
+ def executable_version(executable: str, timeout: float = 8.0) -> str:
110
+ try:
111
+ process = subprocess.run(
112
+ [executable, "--version"],
113
+ text=True,
114
+ stdout=subprocess.PIPE,
115
+ stderr=subprocess.STDOUT,
116
+ timeout=timeout,
117
+ )
118
+ except Exception:
119
+ return ""
120
+ if process.returncode != 0:
121
+ return ""
122
+ match = re.search(r"\d+(?:\.\d+)+", process.stdout or "")
123
+ return match.group(0) if match else ""
124
+
125
+
126
+ def run_upgrade_command(
127
+ command: list[str],
128
+ environ: Mapping[str, str],
129
+ timeout: float = 300.0,
130
+ ) -> tuple[int, str]:
131
+ try:
132
+ process = subprocess.run(
133
+ command,
134
+ text=True,
135
+ input="y\n",
136
+ stdout=subprocess.PIPE,
137
+ stderr=subprocess.STDOUT,
138
+ env=dict(environ),
139
+ timeout=timeout,
140
+ )
141
+ except subprocess.TimeoutExpired:
142
+ return 124, "timed out"
143
+ except Exception as exc:
144
+ return 1, f"{type(exc).__name__}: {exc}"
145
+ return process.returncode, (process.stdout or "").strip()
146
+
147
+
148
+ def claude_code_current_version(claude: str) -> str:
149
+ return executable_version(claude)
150
+
151
+
152
+ def codex_current_version(codex: str) -> str:
153
+ return executable_version(codex)
154
+
155
+
156
+ def package_root_from_installed_path(path: Path) -> Path | None:
157
+ """Return the npm package root when a path lives inside this package."""
158
+
159
+ try:
160
+ resolved = path.resolve(strict=False)
161
+ except Exception:
162
+ resolved = path
163
+ parts = resolved.parts
164
+ for index in range(0, max(0, len(parts) - 2)):
165
+ if (
166
+ parts[index] == "node_modules"
167
+ and parts[index + 1] == "@oneciel-ai"
168
+ and parts[index + 2] == "ciel-runtime"
169
+ ):
170
+ try:
171
+ return Path(*parts[: index + 3])
172
+ except Exception:
173
+ return None
174
+ return None
175
+
176
+
177
+ __all__ = [
178
+ "claude_code_current_version",
179
+ "codex_current_version",
180
+ "executable_version",
181
+ "npm_global_bin_dir_from_prefix",
182
+ "npm_global_install_command",
183
+ "npm_global_package_root",
184
+ "npm_install_runtime_command",
185
+ "npm_latest_package_version",
186
+ "npm_prefix_from_package_root",
187
+ "package_root_from_installed_path",
188
+ "parse_version_tuple",
189
+ "run_upgrade_command",
190
+ "version_newer",
191
+ ]