@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,150 @@
1
+ """Runtime routing and provider-native compatibility policies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from ciel_runtime_support.providers.constants import OPENCODE_PROVIDER_NAMES
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class RuntimeModePolicy:
14
+ parse_bool: Callable[..., bool]
15
+ runtime_providers: Mapping[str, str]
16
+
17
+ def native_enabled(self, runtime: str, provider: str) -> bool:
18
+ return provider == self.runtime_providers.get(runtime)
19
+
20
+ def routed_enabled(
21
+ self, runtime: str, provider: str, config: dict[str, Any]
22
+ ) -> bool:
23
+ return self.native_enabled(runtime, provider) and self.parse_bool(
24
+ config.get("route_through_router"), default=False
25
+ )
26
+
27
+ def direct_enabled(
28
+ self, runtime: str, provider: str, config: dict[str, Any]
29
+ ) -> bool:
30
+ return self.native_enabled(runtime, provider) and not self.routed_enabled(
31
+ runtime, provider, config
32
+ )
33
+
34
+ def label(self, provider: str, config: dict[str, Any]) -> str:
35
+ for runtime in ("anthropic", "agy", "codex"):
36
+ if self.direct_enabled(runtime, provider, config):
37
+ return f"{runtime}-native"
38
+ if self.routed_enabled(runtime, provider, config):
39
+ return f"{runtime}-routed"
40
+ return "ciel-runtime-router"
41
+
42
+ def native_anthropic(self, provider: str) -> bool:
43
+ return self.native_enabled("anthropic", provider)
44
+
45
+ def anthropic_routed(
46
+ self, provider: str, config: dict[str, Any]
47
+ ) -> bool:
48
+ return self.routed_enabled("anthropic", provider, config)
49
+
50
+ def direct_anthropic(
51
+ self, provider: str, config: dict[str, Any]
52
+ ) -> bool:
53
+ return self.direct_enabled("anthropic", provider, config)
54
+
55
+ def native_agy(self, provider: str) -> bool:
56
+ return self.native_enabled("agy", provider)
57
+
58
+ def agy_routed(self, provider: str, config: dict[str, Any]) -> bool:
59
+ return self.routed_enabled("agy", provider, config)
60
+
61
+ def direct_agy(self, provider: str, config: dict[str, Any]) -> bool:
62
+ return self.direct_enabled("agy", provider, config)
63
+
64
+ def native_codex(self, provider: str) -> bool:
65
+ return self.native_enabled("codex", provider)
66
+
67
+ def codex_routed(self, provider: str, config: dict[str, Any]) -> bool:
68
+ return self.routed_enabled("codex", provider, config)
69
+
70
+ def direct_codex(self, provider: str, config: dict[str, Any]) -> bool:
71
+ return self.direct_enabled("codex", provider, config)
72
+
73
+
74
+ def build_default_runtime_mode_policy(
75
+ parse_bool: Callable[..., bool],
76
+ ) -> RuntimeModePolicy:
77
+ """Build the standard native runtime-to-provider mapping."""
78
+ return RuntimeModePolicy(
79
+ parse_bool=parse_bool,
80
+ runtime_providers={
81
+ "anthropic": "anthropic",
82
+ "agy": "agy",
83
+ "codex": "codex",
84
+ },
85
+ )
86
+
87
+
88
+ @dataclass(frozen=True, slots=True)
89
+ class ProviderNativeCompatibilityPolicy:
90
+ native_enabled: Callable[[str, dict[str, Any]], bool]
91
+ compatibility_groups: Mapping[str, frozenset[str]]
92
+
93
+ def group_enabled(
94
+ self, group: str, provider: str, config: dict[str, Any]
95
+ ) -> bool:
96
+ return (
97
+ provider in self.compatibility_groups[group]
98
+ and self.native_enabled(provider, config)
99
+ )
100
+
101
+ def ollama(self, provider: str, config: dict[str, Any]) -> bool:
102
+ return self.group_enabled("ollama", provider, config)
103
+
104
+ def vllm(self, provider: str, config: dict[str, Any]) -> bool:
105
+ return self.group_enabled("vllm", provider, config)
106
+
107
+ def nim(self, provider: str, config: dict[str, Any]) -> bool:
108
+ return self.group_enabled("nim", provider, config)
109
+
110
+ def lm_studio(self, provider: str, config: dict[str, Any]) -> bool:
111
+ return self.group_enabled("lm_studio", provider, config)
112
+
113
+ def nvidia(self, provider: str, config: dict[str, Any]) -> bool:
114
+ return self.group_enabled("nvidia", provider, config)
115
+
116
+ def deepseek(self, provider: str, config: dict[str, Any]) -> bool:
117
+ return self.group_enabled("deepseek", provider, config)
118
+
119
+ def opencode(self, provider: str, config: dict[str, Any]) -> bool:
120
+ return self.group_enabled("opencode", provider, config)
121
+
122
+ def kimi(self, provider: str, config: dict[str, Any]) -> bool:
123
+ return self.group_enabled("kimi", provider, config)
124
+
125
+ def zai(self, provider: str, config: dict[str, Any]) -> bool:
126
+ return self.group_enabled("zai", provider, config)
127
+
128
+ def fireworks(self, provider: str, config: dict[str, Any]) -> bool:
129
+ return self.group_enabled("fireworks", provider, config)
130
+
131
+
132
+ def build_default_native_compatibility_policy(
133
+ native_enabled: Callable[[str, dict[str, Any]], bool],
134
+ ) -> ProviderNativeCompatibilityPolicy:
135
+ """Build the provider families supported by native compatibility mode."""
136
+ return ProviderNativeCompatibilityPolicy(
137
+ native_enabled=native_enabled,
138
+ compatibility_groups={
139
+ "ollama": frozenset({"ollama"}),
140
+ "vllm": frozenset({"vllm"}),
141
+ "nim": frozenset({"self-hosted-nim"}),
142
+ "lm_studio": frozenset({"lm-studio"}),
143
+ "nvidia": frozenset({"nvidia-hosted"}),
144
+ "deepseek": frozenset({"deepseek"}),
145
+ "opencode": frozenset(OPENCODE_PROVIDER_NAMES),
146
+ "kimi": frozenset({"kimi"}),
147
+ "zai": frozenset({"zai"}),
148
+ "fireworks": frozenset({"fireworks"}),
149
+ },
150
+ )
@@ -0,0 +1,46 @@
1
+ """Provider sampling option normalization and validation policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .config_value_codec import finite_float, positive_int
8
+
9
+
10
+ class ProviderSamplingPolicy:
11
+ """Canonicalize and validate provider-independent sampling options."""
12
+
13
+ _ALIASES = {
14
+ "temp": "temperature",
15
+ "temperature": "temperature",
16
+ "top": "top_p",
17
+ "top_p": "top_p",
18
+ "topp": "top_p",
19
+ "topk": "top_k",
20
+ "top_k": "top_k",
21
+ }
22
+
23
+ def option_key(self, key: str) -> str | None:
24
+ normalized = key.strip().lower().replace("-", "_")
25
+ return self._ALIASES.get(normalized)
26
+
27
+ def validate(self, key: str, value: Any) -> float | int:
28
+ if key == "temperature":
29
+ fixed = finite_float(value)
30
+ if fixed is None or fixed < 0 or fixed > 2:
31
+ raise SystemExit("temperature must be a number from 0 to 2")
32
+ return fixed
33
+ if key == "top_p":
34
+ fixed = finite_float(value)
35
+ if fixed is None or fixed <= 0 or fixed > 1:
36
+ raise SystemExit("top_p must be a number greater than 0 and up to 1")
37
+ return fixed
38
+ if key == "top_k":
39
+ fixed = positive_int(value)
40
+ if not fixed:
41
+ raise SystemExit("top_k must be a positive integer")
42
+ return fixed
43
+ raise SystemExit(f"Unknown provider option: {key}")
44
+
45
+
46
+ __all__ = ["ProviderSamplingPolicy"]
@@ -0,0 +1,145 @@
1
+ """Provider base URL status projection application service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import json
7
+ from typing import Any, Callable
8
+ import urllib.error
9
+ import urllib.request
10
+
11
+ from .architecture import ProviderStatusPolicy
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class ProviderStatusRouting:
16
+ codex_routed: Callable[..., bool]
17
+ agy_routed: Callable[..., bool]
18
+ nvidia_native: Callable[..., bool]
19
+ native_anthropic_base: Callable[..., str]
20
+ router_up: Callable[[], bool]
21
+ router_base: str
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class ProviderStatusCatalog:
26
+ model_headers: Callable[..., dict[str, str]]
27
+ http_json: Callable[..., Any]
28
+ join_url: Callable[..., str]
29
+ management_base: Callable[..., str]
30
+ model_ids: Callable[..., list[str]]
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class ProviderStatusGeneric:
35
+ primary_api_key: Callable[..., str]
36
+ meaningful_key: Callable[..., bool]
37
+ with_user_agent: Callable[..., dict[str, str]]
38
+ provider_urlopen: Callable[..., Any]
39
+ model_context_limit: Callable[..., int | None]
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class ProviderStatusServices:
44
+ routing: ProviderStatusRouting
45
+ catalog: ProviderStatusCatalog
46
+ generic: ProviderStatusGeneric
47
+
48
+
49
+ def base_url_status_line(
50
+ provider: str,
51
+ pcfg: dict[str, Any],
52
+ policy: ProviderStatusPolicy,
53
+ *,
54
+ services: ProviderStatusServices,
55
+ ) -> str:
56
+ base = str(pcfg.get("base_url") or "").rstrip("/")
57
+ if not base:
58
+ return "Base URL: missing"
59
+ if "your-" in base:
60
+ return f"Base URL: placeholder ({base})"
61
+ routing = services.routing
62
+ if policy.kind == "native_codex":
63
+ if routing.codex_routed(provider, pcfg):
64
+ return f"Base URL: Codex routed through local router ({routing.router_base}/backend-api/codex)"
65
+ return "Base URL: native Codex config (ciel-runtime does not override it)"
66
+ if policy.kind == "native_agy":
67
+ if routing.agy_routed(provider, pcfg):
68
+ return "Base URL: AGY routed uses native Antigravity model upstream; Ciel routes channel/PTY wake only"
69
+ return "Base URL: native AGY config (ciel-runtime does not override it)"
70
+ if policy.kind == "nvidia":
71
+ if routing.nvidia_native(provider, pcfg):
72
+ return f"Base URL: NVIDIA hosted native ({routing.native_anthropic_base(provider, pcfg)}/v1/messages)"
73
+ state = "ready" if routing.router_up() else "starts on launch"
74
+ return f"Base URL: NVIDIA hosted ({base}); local router {routing.router_base} {state}"
75
+ if policy.kind == "configured":
76
+ return f"Base URL: {policy.configured_description} ({base})"
77
+ if policy.kind == "catalog":
78
+ return _catalog_status(provider, pcfg, base, policy, services.catalog)
79
+ return _generic_status(provider, pcfg, base, policy, services.generic, services.catalog.join_url)
80
+
81
+
82
+ def _catalog_status(
83
+ provider: str,
84
+ pcfg: dict[str, Any],
85
+ base: str,
86
+ policy: ProviderStatusPolicy,
87
+ catalog: ProviderStatusCatalog,
88
+ ) -> str:
89
+ probe_base = catalog.management_base(pcfg) if policy.catalog_scope == "fireworks_management" else base
90
+ scope = "model API" if policy.catalog_scope == "fireworks_management" else "model list"
91
+ try:
92
+ data = catalog.http_json(
93
+ catalog.join_url(probe_base, policy.catalog_path),
94
+ headers=catalog.model_headers(provider, pcfg),
95
+ timeout=2.5,
96
+ provider=provider,
97
+ pcfg=pcfg,
98
+ )
99
+ count = len(catalog.model_ids(data))
100
+ return f"Base URL: {policy.label} {scope} reachable ({policy.catalog_path}, {count} {policy.catalog_count_label})"
101
+ except urllib.error.HTTPError as exc:
102
+ if exc.code in (401, 403):
103
+ return f"Base URL: {policy.label} reachable, auth rejected ({exc.code})"
104
+ return f"Base URL: {policy.label} HTTP {exc.code}"
105
+ except Exception as exc:
106
+ return f"Base URL: {policy.label} {scope} unreachable ({type(exc).__name__})"
107
+
108
+
109
+ def _generic_status(
110
+ provider: str,
111
+ pcfg: dict[str, Any],
112
+ base: str,
113
+ policy: ProviderStatusPolicy,
114
+ generic: ProviderStatusGeneric,
115
+ join_url: Callable[..., str],
116
+ ) -> str:
117
+ headers: dict[str, str] = {}
118
+ key = generic.primary_api_key(provider, pcfg)
119
+ if generic.meaningful_key(key):
120
+ headers = {"x-api-key": key, "authorization": f"Bearer {key}"}
121
+ headers = generic.with_user_agent(headers)
122
+ path = policy.catalog_path or "/v1/models"
123
+ try:
124
+ request = urllib.request.Request(join_url(base, path), headers=headers)
125
+ with generic.provider_urlopen(request, timeout=2.5, provider=provider, pcfg=pcfg) as response:
126
+ body = response.read(131072).decode("utf-8", errors="ignore")
127
+ count = ""
128
+ try:
129
+ data = json.loads(body)
130
+ except (TypeError, ValueError):
131
+ data = {}
132
+ models = data.get(policy.catalog_count_key) if isinstance(data, dict) else None
133
+ if isinstance(models, list):
134
+ count = f", {len(models)} models"
135
+ if policy.catalog_count_key == "data":
136
+ limit = generic.model_context_limit(provider, pcfg, timeout=1.0)
137
+ if limit:
138
+ count += f", max_model_len {limit}"
139
+ return f"Base URL: model list reachable ({path}{count})"
140
+ except urllib.error.HTTPError as exc:
141
+ if exc.code in (401, 403):
142
+ return f"Base URL: model list reachable, auth rejected ({exc.code})"
143
+ return f"Base URL: HTTP {exc.code}"
144
+ except Exception as exc:
145
+ return f"Base URL: unreachable ({type(exc).__name__})"
@@ -0,0 +1,184 @@
1
+ """Context-aware provider request timeout calculation and application policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Callable, Mapping
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ from .architecture import ProviderContextPolicy
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class ProviderTimeoutSettings:
15
+ default_ms: int
16
+ minimum_ms: int
17
+ maximum_ms: int
18
+ round_ms: int
19
+ idle_max_ms: int
20
+ preset_timeouts: Mapping[str, int]
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class ProviderTimeoutPorts:
25
+ positive_int: Callable[[Any], int | None]
26
+ context_policy: Callable[[str, dict[str, Any]], ProviderContextPolicy]
27
+ context_capacity: Callable[[str, dict[str, Any]], int | None]
28
+ output_token_cap: Callable[[str, dict[str, Any], int | None], int | None]
29
+ ollama_options: Callable[[dict[str, Any]], dict[str, Any]]
30
+ catalog_timeout: Callable[[str], int | None]
31
+ model_preset: Callable[[str], Mapping[str, Any]]
32
+ timeout_for_context: Callable[[int | None], int]
33
+ format_context: Callable[[int | None], str]
34
+
35
+
36
+ class ProviderTimeoutPolicy:
37
+ def __init__(
38
+ self,
39
+ settings: ProviderTimeoutSettings,
40
+ ports: ProviderTimeoutPorts,
41
+ ) -> None:
42
+ self.settings = settings
43
+ self.ports = ports
44
+
45
+ def configured_context(
46
+ self,
47
+ provider: str,
48
+ config: dict[str, Any],
49
+ ) -> int | None:
50
+ strategy = self.ports.context_policy(provider, config).settings_strategy
51
+ positive_int = self.ports.positive_int
52
+ if strategy == "ollama":
53
+ fixed = positive_int(config.get("num_ctx"))
54
+ if fixed:
55
+ return fixed
56
+ return self.ports.context_capacity(provider, config) or positive_int(
57
+ config.get("num_ctx_max")
58
+ )
59
+ if strategy == "standard":
60
+ return positive_int(
61
+ config.get("context_window")
62
+ ) or self.ports.context_capacity(provider, config)
63
+ return self.ports.context_capacity(provider, config)
64
+
65
+ def configured_output(
66
+ self,
67
+ provider: str,
68
+ config: dict[str, Any],
69
+ ) -> int | None:
70
+ positive_int = self.ports.positive_int
71
+ if self.ports.context_policy(provider, config).settings_strategy == "ollama":
72
+ options = self.ports.ollama_options(config)
73
+ configured = positive_int(options.get("num_predict")) or positive_int(
74
+ config.get("max_output_tokens")
75
+ )
76
+ else:
77
+ configured = positive_int(
78
+ config.get("max_output_tokens")
79
+ ) or positive_int(config.get("num_predict"))
80
+ return self.ports.output_token_cap(provider, config, configured)
81
+
82
+ def clamp(self, milliseconds: int | float | None) -> int:
83
+ value = self.ports.positive_int(milliseconds) or self.settings.default_ms
84
+ value = max(self.settings.minimum_ms, min(self.settings.maximum_ms, value))
85
+ return int(
86
+ math.ceil(value / self.settings.round_ms) * self.settings.round_ms
87
+ )
88
+
89
+ def calculated(
90
+ self,
91
+ provider: str,
92
+ config: dict[str, Any],
93
+ timeout_candidates: list[int] | None = None,
94
+ ) -> int:
95
+ context_policy = self.ports.context_policy(provider, config)
96
+ context_tokens = self.ports.positive_int(
97
+ self.configured_context(provider, config)
98
+ )
99
+ output_tokens = self.ports.positive_int(
100
+ self.configured_output(provider, config)
101
+ )
102
+ timeout_ms = self.settings.minimum_ms
103
+ if context_tokens:
104
+ context_score = max(
105
+ 0.0,
106
+ min(
107
+ 1.0,
108
+ math.log2(max(context_tokens, 65536) / 65536) / 4.0,
109
+ ),
110
+ )
111
+ timeout_ms += int(240000 * context_score)
112
+ if output_tokens:
113
+ output_score = max(0.0, min(1.0, (output_tokens - 2048) / 6144))
114
+ timeout_ms += int(120000 * output_score)
115
+ if context_policy.hosted_timeout:
116
+ timeout_ms += 60000
117
+ for candidate in timeout_candidates or []:
118
+ fixed = self.ports.positive_int(candidate)
119
+ if fixed:
120
+ timeout_ms = max(timeout_ms, fixed)
121
+ timeout_ms *= context_policy.timeout_weight
122
+ return self.clamp(timeout_ms)
123
+
124
+ def recommended(
125
+ self,
126
+ provider: str,
127
+ config: dict[str, Any],
128
+ *,
129
+ use_context_fallback: bool = True,
130
+ ) -> int:
131
+ model = str(config.get("current_model") or "")
132
+ candidates: list[int] = []
133
+ context_policy = self.ports.context_policy(provider, config)
134
+ preset_id = str(config.get("llm_preset") or "").strip()
135
+ preset_timeout = self.ports.positive_int(
136
+ self.settings.preset_timeouts.get(preset_id)
137
+ )
138
+ if preset_timeout:
139
+ candidates.append(preset_timeout)
140
+ if context_policy.uses_catalog_timeout:
141
+ catalog_timeout = self.ports.catalog_timeout(model)
142
+ if catalog_timeout:
143
+ candidates.append(catalog_timeout)
144
+ model_timeout = self.ports.positive_int(
145
+ self.ports.model_preset(model).get("recommended_timeout_ms")
146
+ )
147
+ if model_timeout:
148
+ candidates.append(model_timeout)
149
+ if candidates:
150
+ return self.calculated(provider, config, candidates)
151
+ if not use_context_fallback:
152
+ return self.settings.default_ms
153
+ context_timeout = self.ports.timeout_for_context(
154
+ self.configured_context(provider, config)
155
+ )
156
+ return self.calculated(provider, config, [context_timeout])
157
+
158
+ def apply(
159
+ self,
160
+ provider: str,
161
+ config: dict[str, Any],
162
+ *,
163
+ use_context_fallback: bool = True,
164
+ ) -> list[str]:
165
+ timeout_ms = self.recommended(
166
+ provider,
167
+ config,
168
+ use_context_fallback=use_context_fallback,
169
+ )
170
+ idle_ms = min(timeout_ms, self.settings.idle_max_ms)
171
+ positive_int = self.ports.positive_int
172
+ changed = (
173
+ positive_int(config.get("request_timeout_ms")) != timeout_ms
174
+ or positive_int(config.get("stream_idle_timeout_ms")) != idle_ms
175
+ )
176
+ config["request_timeout_ms"] = timeout_ms
177
+ config["stream_idle_timeout_ms"] = idle_ms
178
+ if not changed:
179
+ return []
180
+ context = self.configured_context(provider, config)
181
+ return [
182
+ f"Auto timeout: {timeout_ms}ms for context {self.ports.format_context(context)}.",
183
+ f"stream_idle_timeout_ms: {idle_ms}",
184
+ ]
@@ -0,0 +1,145 @@
1
+ """Provider-owned tool exposure and tool-choice application policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+ from .protocols.anthropic_thinking_policy import (
9
+ ToolChoicePorts,
10
+ normalize_tool_choice,
11
+ )
12
+
13
+
14
+ ProviderConfig = dict[str, Any]
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class ProviderToolPolicy:
19
+ adapter_for: Callable[..., Any]
20
+ contract_for: Callable[..., Any]
21
+ current_model: Callable[[str, ProviderConfig], str]
22
+ strip_context_suffix: Callable[[str], str]
23
+ resolve_emitted_name: Callable[
24
+ [str, dict[str, Any] | None],
25
+ str,
26
+ ]
27
+ default_blocked_tools: frozenset[str]
28
+ repair_tools: frozenset[str]
29
+ log: Callable[[str, str], None]
30
+
31
+ def blocked_tools(
32
+ self,
33
+ provider: str,
34
+ config: ProviderConfig,
35
+ ) -> set[str]:
36
+ override = config.get("blocked_tools")
37
+ if override is False:
38
+ return set()
39
+ if isinstance(override, list):
40
+ return {
41
+ str(name).strip()
42
+ for name in override
43
+ if str(name).strip()
44
+ }
45
+ adapter, contract = self._adapter(provider, config)
46
+ if adapter.capabilities(contract).blocks_default_tools:
47
+ return set(self.default_blocked_tools)
48
+ return set()
49
+
50
+ def normalize_anthropic_stream_tool_use(
51
+ self,
52
+ provider: str,
53
+ config: ProviderConfig,
54
+ ) -> bool:
55
+ adapter, contract = self._adapter(provider, config)
56
+ return adapter.normalizes_anthropic_tool_use(contract)
57
+
58
+ def supports_tool_choice(
59
+ self,
60
+ provider: str,
61
+ config: ProviderConfig,
62
+ body: dict[str, Any],
63
+ ) -> bool:
64
+ raw_model = str(
65
+ body.get("model") or config.get("current_model") or ""
66
+ )
67
+ model = self.strip_context_suffix(raw_model).lower()
68
+ adapter, contract = self._adapter(provider, config)
69
+ return adapter.supports_tool_choice(contract, model)
70
+
71
+ def tool_choice_status(
72
+ self,
73
+ provider: str,
74
+ config: ProviderConfig,
75
+ ) -> str:
76
+ configured = config.get("supports_tool_choice")
77
+ if configured is not None:
78
+ return "on" if bool(configured) else "off"
79
+ model = self.current_model(provider, config)
80
+ enabled = self.supports_tool_choice(
81
+ provider,
82
+ config,
83
+ {"model": model},
84
+ )
85
+ return f"auto ({'on' if enabled else 'off'})"
86
+
87
+ def normalize_tool_choice(
88
+ self,
89
+ provider: str,
90
+ config: ProviderConfig,
91
+ body: dict[str, Any],
92
+ ) -> dict[str, Any]:
93
+ return normalize_tool_choice(
94
+ provider,
95
+ config,
96
+ body,
97
+ ToolChoicePorts(
98
+ normalize=self._normalize_choice,
99
+ supports=self.supports_tool_choice,
100
+ log=self.log,
101
+ ),
102
+ )
103
+
104
+ def should_repair_passthrough_input(
105
+ self,
106
+ provider: str,
107
+ config: ProviderConfig,
108
+ raw_name: str,
109
+ source_body: dict[str, Any] | None,
110
+ ) -> bool:
111
+ adapter, contract = self._adapter(provider, config)
112
+ if not adapter.capabilities(contract).repairs_anthropic_tool_input:
113
+ return False
114
+ return self.resolve_emitted_name(
115
+ raw_name,
116
+ source_body,
117
+ ) in self.repair_tools
118
+
119
+ def _adapter(
120
+ self,
121
+ provider: str,
122
+ config: ProviderConfig,
123
+ ) -> tuple[Any, Any]:
124
+ return (
125
+ self.adapter_for(provider, config),
126
+ self.contract_for(provider, config),
127
+ )
128
+
129
+ def _normalize_choice(
130
+ self,
131
+ provider: str,
132
+ config: ProviderConfig,
133
+ request: dict[str, Any],
134
+ choice: Any,
135
+ ) -> Any:
136
+ adapter, contract = self._adapter(provider, config)
137
+ model = str(
138
+ request.get("model")
139
+ or config.get("current_model")
140
+ or ""
141
+ )
142
+ return adapter.normalize_tool_choice(contract, model, choice)
143
+
144
+
145
+ __all__ = ["ProviderToolPolicy"]