@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,57 @@
1
+ """OpenRouter provider adapter."""
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from ..architecture import ProviderCapabilities, ProviderConfig, ProviderContextPolicy
6
+ from .base import OpenAICompatibleProviderAdapter, provider_configuration
7
+ from .constants import DEFAULT_REQUEST_TIMEOUT_MS, PROVIDER_DEFAULT_BASE_URLS
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class OpenRouterProviderAdapter(OpenAICompatibleProviderAdapter):
12
+ name: str = "OpenRouter"
13
+ base_url: str = PROVIDER_DEFAULT_BASE_URLS["openrouter"]
14
+ configuration_defaults_value: dict = field(
15
+ default_factory=lambda: provider_configuration(
16
+ "nvidia/nemotron-3-ultra-550b-a55b:free",
17
+ native_compat=False,
18
+ rate_limit_rpm=0,
19
+ rate_limit_status=False,
20
+ context_window=262144,
21
+ max_output_tokens=8192,
22
+ temperature=0.7,
23
+ top_p=0.8,
24
+ context_reserve_tokens=1024,
25
+ request_timeout_ms=DEFAULT_REQUEST_TIMEOUT_MS,
26
+ stream_enabled=True,
27
+ stream_word_chunking=False,
28
+ )
29
+ )
30
+ authorization_header: str = "Authorization"
31
+ require_api_key: bool = True
32
+ api_key_display_name_value: str = "OpenRouter"
33
+ api_key_launch_error_value: str = (
34
+ "Launch blocked: OpenRouter requires an OpenRouter API key."
35
+ )
36
+ capabilities_value: ProviderCapabilities = field(
37
+ default_factory=lambda: ProviderCapabilities(
38
+ upstream_protocol="openai_chat", requires_api_key=True
39
+ )
40
+ )
41
+
42
+ def context_policy(self, config: ProviderConfig) -> ProviderContextPolicy:
43
+ del config
44
+ return ProviderContextPolicy(
45
+ capacity_strategy="configured_first",
46
+ settings_strategy="standard",
47
+ hosted_timeout=True,
48
+ )
49
+
50
+ def router_native_anthropic_enabled(
51
+ self, config: ProviderConfig, model: str | None = None
52
+ ) -> bool:
53
+ del config, model
54
+ return False
55
+
56
+
57
+ __all__ = ["OpenRouterProviderAdapter"]
@@ -0,0 +1,65 @@
1
+ """vLLM provider adapter."""
2
+
3
+ from dataclasses import dataclass, field, replace
4
+
5
+ from ..architecture import (
6
+ ProviderCapabilities,
7
+ ProviderConfig,
8
+ ProviderContextPolicy,
9
+ ProviderStatusPolicy,
10
+ )
11
+ from .base import OpenAICompatibleProviderAdapter, provider_configuration
12
+ from .constants import DEFAULT_REQUEST_TIMEOUT_MS, PROVIDER_DEFAULT_BASE_URLS
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class VllmProviderAdapter(OpenAICompatibleProviderAdapter):
17
+ name: str = "vllm"
18
+ base_url: str = PROVIDER_DEFAULT_BASE_URLS["vllm"]
19
+ configuration_defaults_value: dict = field(
20
+ default_factory=lambda: provider_configuration(
21
+ "my-model",
22
+ api_key="dummy",
23
+ custom_models=("my-model",),
24
+ native_compat=True,
25
+ supports_tool_choice=False,
26
+ context_window=32768,
27
+ max_output_tokens=4096,
28
+ temperature=0.7,
29
+ top_p=0.8,
30
+ context_reserve_tokens=1024,
31
+ request_timeout_ms=DEFAULT_REQUEST_TIMEOUT_MS,
32
+ stream_enabled=True,
33
+ stream_word_chunking=False,
34
+ )
35
+ )
36
+ send_placeholder_key: bool = True
37
+ capabilities_value: ProviderCapabilities = field(
38
+ default_factory=lambda: ProviderCapabilities(
39
+ upstream_protocol="openai_chat", supports_tool_choice=False, local=True
40
+ )
41
+ )
42
+
43
+ def requires_catalog_model_selection(self, config: ProviderConfig) -> bool:
44
+ del config
45
+ return True
46
+
47
+ def placeholder_model_ids(self) -> frozenset[str]:
48
+ return super().placeholder_model_ids() | {"my-model"}
49
+
50
+ def context_policy(self, config: ProviderConfig) -> ProviderContextPolicy:
51
+ del config
52
+ return ProviderContextPolicy(
53
+ capacity_strategy="remote_first",
54
+ settings_strategy="standard",
55
+ status_capacity_strategy="openai_budget",
56
+ )
57
+
58
+ def status_policy(self, config: ProviderConfig) -> ProviderStatusPolicy:
59
+ return replace(
60
+ super().status_policy(config),
61
+ unreachable_hint="vLLM must be reachable from this machine and expose Anthropic-compatible /v1/messages.",
62
+ )
63
+
64
+
65
+ __all__ = ["VllmProviderAdapter"]
@@ -0,0 +1,119 @@
1
+ """Z.AI provider adapter."""
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from ..architecture import (
6
+ ProviderCapabilities,
7
+ ProviderConfig,
8
+ ProviderContextPolicy,
9
+ ProviderModelCatalogPolicy,
10
+ ProviderOptionPresentationPolicy,
11
+ ProviderRequestPolicy,
12
+ ProviderStatusPolicy,
13
+ )
14
+ from .base import HttpBearerProviderAdapter, provider_configuration
15
+ from .constants import PROVIDER_DEFAULT_BASE_URLS, ZAI_MODEL_FALLBACK_IDS
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ZaiProviderAdapter(HttpBearerProviderAdapter):
20
+ name: str = "zai"
21
+ base_url: str = PROVIDER_DEFAULT_BASE_URLS["zai"]
22
+ configuration_defaults_value: dict = field(
23
+ default_factory=lambda: provider_configuration(
24
+ "glm-5.2[1m]",
25
+ custom_models=ZAI_MODEL_FALLBACK_IDS,
26
+ native_compat=True,
27
+ preserve_anthropic_thinking=True,
28
+ claude_code_supported_capabilities=["effort", "thinking"],
29
+ context_window=1000000,
30
+ auto_compact_window=1000000,
31
+ max_output_tokens=8192,
32
+ context_reserve_tokens=8192,
33
+ request_timeout_ms=3000000,
34
+ stream_enabled=True,
35
+ stream_word_chunking=False,
36
+ effort_level="max",
37
+ opus_model="glm-5.2[1m]",
38
+ sonnet_model="glm-5.2[1m]",
39
+ haiku_model="glm-4.7",
40
+ subagent_model="glm-5.2[1m]",
41
+ managed_mcp=True,
42
+ )
43
+ )
44
+ send_placeholder_key: bool = True
45
+ api_key_display_name_value: str = "Z.AI GLM"
46
+ api_key_launch_error_value: str = (
47
+ "Launch blocked: Z.AI GLM requires a Z.AI API key."
48
+ )
49
+ capabilities_value: ProviderCapabilities = field(
50
+ default_factory=lambda: ProviderCapabilities(
51
+ upstream_protocol="anthropic_messages",
52
+ supports_thinking=True,
53
+ requires_api_key=True,
54
+ )
55
+ )
56
+ request_policy_value: ProviderRequestPolicy = field(
57
+ default_factory=lambda: ProviderRequestPolicy(
58
+ chat_path="/v1/messages", models_path="/v1/models"
59
+ )
60
+ )
61
+ model_catalog_policy_value: ProviderModelCatalogPolicy = field(
62
+ default_factory=lambda: ProviderModelCatalogPolicy(
63
+ kind="openai", fallback_models=ZAI_MODEL_FALLBACK_IDS
64
+ )
65
+ )
66
+
67
+ def normalize_model_id(self, model_id: str) -> str:
68
+ return str(model_id or "").strip()
69
+
70
+ def upstream_api_model_id(self, model_id: str) -> str:
71
+ return super().normalize_model_id(model_id)
72
+
73
+ def model_selection_config_updates(
74
+ self, config: ProviderConfig, model_id: str
75
+ ) -> dict[str, str]:
76
+ del config
77
+ return {
78
+ "haiku_model": model_id,
79
+ "opus_model": model_id,
80
+ "sonnet_model": model_id,
81
+ }
82
+
83
+ def context_policy(self, config: ProviderConfig) -> ProviderContextPolicy:
84
+ del config
85
+ return ProviderContextPolicy(
86
+ capacity_strategy="hint_first",
87
+ settings_strategy="standard",
88
+ hosted_timeout=True,
89
+ context_family_before_size_markers=True,
90
+ status_capacity_strategy="provider",
91
+ )
92
+
93
+ def router_native_anthropic_enabled(
94
+ self, config: ProviderConfig, model: str | None = None
95
+ ) -> bool:
96
+ del model
97
+ return bool(config.options.get("native_compat", True))
98
+
99
+ def option_presentation_policy(
100
+ self, config: ProviderConfig
101
+ ) -> ProviderOptionPresentationPolicy:
102
+ del config
103
+ return ProviderOptionPresentationPolicy(
104
+ show_native=True,
105
+ show_tool_choice=True,
106
+ show_stream=True,
107
+ show_rate_limit_controls=True,
108
+ show_sampling_controls=True,
109
+ show_ip_family_control=True,
110
+ )
111
+
112
+ def status_policy(self, config: ProviderConfig) -> ProviderStatusPolicy:
113
+ del config
114
+ return ProviderStatusPolicy(
115
+ kind="configured", configured_description="Z.AI Anthropic API configured"
116
+ )
117
+
118
+
119
+ __all__ = ["ZaiProviderAdapter"]
@@ -0,0 +1,115 @@
1
+ """Parser strategy for provider-emitted pseudo tool-call envelopes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass
9
+ from typing import Any
10
+
11
+
12
+ PSEUDO_TOOL_START = "<|tool_calls_section_begin|>"
13
+ PSEUDO_TOOL_END = "<|tool_calls_section_end|>"
14
+ PSEUDO_CALL_BEGIN = "<|tool_call_begin|>"
15
+ PSEUDO_ARG_BEGIN = "<|tool_call_argument_begin|>"
16
+ PSEUDO_CALL_END = "<|tool_call_end|>"
17
+
18
+
19
+ def normalize_tool_arguments(tool_name: str, arguments: Any) -> dict[str, Any]:
20
+ if isinstance(arguments, dict):
21
+ return arguments
22
+ if isinstance(arguments, str):
23
+ text = arguments.strip()
24
+ if not text:
25
+ return {}
26
+ try:
27
+ parsed = json.loads(text)
28
+ if isinstance(parsed, dict):
29
+ return parsed
30
+ except (TypeError, ValueError):
31
+ pass
32
+ if tool_name == "Bash":
33
+ return {"command": text}
34
+ return {}
35
+
36
+
37
+ def infer_tool_name_from_args(arguments: dict[str, Any]) -> str:
38
+ keys = set(arguments)
39
+ if "command" in keys:
40
+ return "Bash"
41
+ if {"file_path", "content"}.issubset(keys):
42
+ return "Write"
43
+ if {"file_path", "old_string", "new_string"}.issubset(keys):
44
+ return "Edit"
45
+ if "file_path" in keys:
46
+ return "Read"
47
+ if keys & {"taskId", "task_id", "addBlocks", "addBlockedBy"}:
48
+ return "TaskUpdate"
49
+ return "TaskList" if not arguments else "Write"
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class PseudoToolParserServices:
54
+ parse_xml: Callable[..., tuple[str, list[dict[str, Any]]]]
55
+ fuzzy_tool_name: Callable[[str], str]
56
+
57
+
58
+ def parse_pseudo_tool_calls(
59
+ text: str,
60
+ source_body: dict[str, Any] | None,
61
+ services: PseudoToolParserServices,
62
+ ) -> tuple[str, list[dict[str, Any]]]:
63
+ if PSEUDO_TOOL_START not in text:
64
+ return services.parse_xml(text, source_body)
65
+ visible_parts: list[str] = []
66
+ calls: list[dict[str, Any]] = []
67
+ position = 0
68
+ while True:
69
+ start = text.find(PSEUDO_TOOL_START, position)
70
+ if start < 0:
71
+ visible_parts.append(text[position:])
72
+ break
73
+ visible_parts.append(text[position:start])
74
+ end = text.find(PSEUDO_TOOL_END, start)
75
+ if end < 0:
76
+ section = text[start + len(PSEUDO_TOOL_START) :]
77
+ position = len(text)
78
+ else:
79
+ section = text[start + len(PSEUDO_TOOL_START) : end]
80
+ position = end + len(PSEUDO_TOOL_END)
81
+ pattern = (
82
+ re.escape(PSEUDO_CALL_BEGIN)
83
+ + r"(.*?)"
84
+ + re.escape(PSEUDO_ARG_BEGIN)
85
+ + r"(.*?)"
86
+ + re.escape(PSEUDO_CALL_END)
87
+ )
88
+ for match in re.finditer(pattern, section, flags=re.DOTALL):
89
+ raw_header = match.group(1).strip()
90
+ try:
91
+ arguments = json.loads(match.group(2).strip())
92
+ except (TypeError, ValueError):
93
+ continue
94
+ if not isinstance(arguments, dict):
95
+ continue
96
+ name = next(
97
+ (
98
+ candidate
99
+ for part in re.split(r"[\s:|,]+", raw_header)
100
+ if (candidate := services.fuzzy_tool_name(part))
101
+ ),
102
+ "",
103
+ )
104
+ if not name:
105
+ name = infer_tool_name_from_args(arguments)
106
+ calls.append(
107
+ {
108
+ "function": {"name": name, "arguments": arguments},
109
+ "id": raw_header,
110
+ }
111
+ )
112
+ if end < 0:
113
+ break
114
+ visible_text, xml_calls = services.parse_xml("".join(visible_parts), source_body)
115
+ return visible_text, calls + xml_calls
@@ -0,0 +1,117 @@
1
+ """Pure rate-limit configuration, window, and HTTP header policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import re
7
+ import time
8
+ from email.utils import parsedate_to_datetime
9
+ from typing import Any, Callable
10
+
11
+
12
+ def configured_rpm(config: dict[str, Any], positive_int: Callable[[Any], int | None]) -> int | None:
13
+ raw = config.get("rate_limit_rpm")
14
+ if raw is None:
15
+ return None
16
+ if isinstance(raw, str) and raw.strip().lower() in (
17
+ "0", "false", "off", "disable", "disabled", "none", "unset",
18
+ ):
19
+ return 0
20
+ try:
21
+ if int(raw) == 0:
22
+ return 0
23
+ except Exception:
24
+ pass
25
+ rpm = positive_int(raw)
26
+ return rpm if rpm and rpm > 0 else None
27
+
28
+
29
+ def capacity(rpm: int) -> int:
30
+ if rpm <= 1:
31
+ return 1
32
+ reserve = 1 if rpm <= 20 else max(1, math.ceil(rpm * 0.05))
33
+ return max(1, rpm - reserve)
34
+
35
+
36
+ def recent_timestamps(
37
+ timestamps: Any,
38
+ now: float,
39
+ window: float,
40
+ *,
41
+ include_future: bool,
42
+ ) -> list[float]:
43
+ result: list[float] = []
44
+ for timestamp in timestamps or []:
45
+ if not isinstance(timestamp, (int, float)):
46
+ continue
47
+ value = float(timestamp)
48
+ age = now - value
49
+ if age < window and (include_future or age >= 0.0):
50
+ result.append(value)
51
+ return sorted(result)
52
+
53
+
54
+ def retry_after_seconds(value: str | None, now: Callable[[], float] = time.time) -> float | None:
55
+ if not value:
56
+ return None
57
+ text = value.strip()
58
+ try:
59
+ return max(0.0, float(text))
60
+ except Exception:
61
+ pass
62
+ try:
63
+ return max(0.0, parsedate_to_datetime(text).timestamp() - now())
64
+ except Exception:
65
+ return None
66
+
67
+
68
+ def format_duration(seconds: float) -> str:
69
+ total = max(0, int(round(seconds)))
70
+ days, remainder = divmod(total, 86400)
71
+ hours, remainder = divmod(remainder, 3600)
72
+ minutes, secs = divmod(remainder, 60)
73
+ parts: list[str] = []
74
+ if days:
75
+ parts.append(f"{days}d")
76
+ if hours:
77
+ parts.append(f"{hours}h")
78
+ if minutes:
79
+ parts.append(f"{minutes}m")
80
+ if secs or not parts:
81
+ parts.append(f"{secs}s")
82
+ return " ".join(parts)
83
+
84
+
85
+ def first_header(headers: Any, names: list[str]) -> str | None:
86
+ for name in names:
87
+ try:
88
+ value = headers.get(name)
89
+ except Exception:
90
+ value = None
91
+ if value:
92
+ return str(value)
93
+ return None
94
+
95
+
96
+ def first_integer(value: str | None) -> int | None:
97
+ match = re.search(r"\d+", value) if value else None
98
+ try:
99
+ return int(match.group(0)) if match else None
100
+ except Exception:
101
+ return None
102
+
103
+
104
+ def reset_seconds(value: str | None, now: Callable[[], float] = time.time) -> float | None:
105
+ if not value:
106
+ return None
107
+ text = value.strip()
108
+ try:
109
+ numeric = float(text)
110
+ current = now()
111
+ if numeric > 1e12:
112
+ return max(0.0, numeric / 1000.0 - current)
113
+ if numeric > current + 60.0:
114
+ return max(0.0, numeric - current)
115
+ return max(0.0, numeric)
116
+ except Exception:
117
+ return retry_after_seconds(text, now)
@@ -0,0 +1,154 @@
1
+ """Persistent repository for provider rate usage and API-key cooldown state."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from collections.abc import Callable
8
+ from pathlib import Path
9
+ from threading import Lock, RLock
10
+ from typing import Any
11
+
12
+
13
+ class RateLimitRepository:
14
+ def __init__(
15
+ self,
16
+ config_dir: Path,
17
+ state_path: Path,
18
+ lock: Lock | RLock,
19
+ log: Callable[[str, str], None],
20
+ ) -> None:
21
+ self.config_dir = config_dir
22
+ self.state_path = state_path
23
+ self.lock = lock
24
+ self.log = log
25
+
26
+ def entry(self, key: str, legacy_key: str) -> dict[str, Any]:
27
+ state = self._read()
28
+ entry = state.get(key)
29
+ if not isinstance(entry, dict):
30
+ entry = state.get(legacy_key)
31
+ return entry if isinstance(entry, dict) else {}
32
+
33
+ def effective_rpm(self, key: str, legacy_key: str, configured: int | None) -> int | None:
34
+ if configured == 0:
35
+ return 0
36
+ entry = self.entry(key, legacy_key)
37
+ try:
38
+ server_rpm = int(entry.get("server_rpm") or 0)
39
+ updated_at = float(entry.get("server_rpm_updated_at") or 0.0)
40
+ if server_rpm > 0 and 0.0 <= time.time() - updated_at < 3600.0:
41
+ return server_rpm
42
+ except Exception:
43
+ pass
44
+ return configured
45
+
46
+ def usage(
47
+ self,
48
+ key: str,
49
+ legacy_key: str,
50
+ rpm: int | None,
51
+ recent: Callable[..., list[float]],
52
+ ) -> tuple[int, int | None]:
53
+ if rpm is None:
54
+ return 0, None
55
+ if rpm == 0:
56
+ return 0, 0
57
+ state = self._read()
58
+ entry = state.get(key)
59
+ if not isinstance(entry, dict):
60
+ entry = state.get(legacy_key)
61
+ timestamps = (
62
+ entry.get("timestamps")
63
+ if isinstance(entry, dict)
64
+ else [float(entry)]
65
+ if isinstance(entry, (int, float))
66
+ else []
67
+ )
68
+ used = len(recent(timestamps, time.time(), 60.0, include_future=False))
69
+ return used, rpm
70
+
71
+ def record_usage(
72
+ self,
73
+ key: str,
74
+ legacy_key: str,
75
+ rpm: int | None,
76
+ recent: Callable[..., list[float]],
77
+ ) -> tuple[int, int | None]:
78
+ if rpm is None:
79
+ return 0, None
80
+ with self.lock:
81
+ state = self._read()
82
+ now = time.time()
83
+ entry = state.get(key)
84
+ if not isinstance(entry, dict):
85
+ entry = state.get(legacy_key)
86
+ timestamps = (
87
+ entry.get("timestamps")
88
+ if isinstance(entry, dict)
89
+ else [float(entry)]
90
+ if isinstance(entry, (int, float))
91
+ else []
92
+ )
93
+ values = recent(timestamps, now, 60.0, include_future=True)
94
+ values.append(now)
95
+ new_entry: dict[str, Any] = {
96
+ "timestamps": values[-max(int(rpm or 0), 240) :],
97
+ "rpm": int(rpm or 0),
98
+ "updated_at": now,
99
+ "last_wait": 0.0,
100
+ }
101
+ existing_penalty = (
102
+ float(entry.get("penalty_until") or 0.0)
103
+ if isinstance(entry, dict)
104
+ else 0.0
105
+ )
106
+ if existing_penalty > now:
107
+ new_entry["penalty_until"] = existing_penalty
108
+ state[key] = new_entry
109
+ self._write(state)
110
+ return len(values), rpm
111
+
112
+ def register_cooldown(self, state_key: str, seconds: float) -> None:
113
+ with self.lock:
114
+ state = self._read()
115
+ now = time.time()
116
+ state[state_key] = {"cooldown_until": now + seconds, "last_429_at": now}
117
+ self._write(state)
118
+
119
+ def cooldown_until(self, state_key: str) -> float:
120
+ with self.lock:
121
+ entry = self._read().get(state_key)
122
+ if not isinstance(entry, dict):
123
+ return 0.0
124
+ try:
125
+ until = float(entry.get("cooldown_until") or 0.0)
126
+ except Exception:
127
+ return 0.0
128
+ return until if until > time.time() else 0.0
129
+
130
+ def reset_key_cooldowns(self) -> int:
131
+ with self.lock:
132
+ state = self._read()
133
+ kept = {key: value for key, value in state.items() if ":__key__:" not in str(key)}
134
+ removed = len(state) - len(kept)
135
+ if removed > 0:
136
+ self._write(kept)
137
+ return removed
138
+
139
+ def _read(self) -> dict[str, Any]:
140
+ try:
141
+ value = (
142
+ json.loads(self.state_path.read_text(encoding="utf-8"))
143
+ if self.state_path.exists()
144
+ else {}
145
+ )
146
+ return value if isinstance(value, dict) else {}
147
+ except Exception:
148
+ return {}
149
+
150
+ def _write(self, state: dict[str, Any]) -> None:
151
+ self.config_dir.mkdir(parents=True, exist_ok=True)
152
+ self.state_path.write_text(
153
+ json.dumps(state, ensure_ascii=False) + "\n", encoding="utf-8"
154
+ )
@@ -0,0 +1,46 @@
1
+ """Small typed registries used by runtime architecture extension points."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Iterable
6
+ from typing import Any, Generic, TypeVar
7
+
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ class AdapterRegistry(Generic[T]):
13
+ """Map stable names and aliases to adapter factories."""
14
+
15
+ def __init__(self) -> None:
16
+ self._factories: dict[str, Callable[..., T]] = {}
17
+
18
+ def register(self, name: str, factory: Callable[..., T], *, aliases: Iterable[str] = ()) -> None:
19
+ keys = tuple(dict.fromkeys(self._normalize(key) for key in (name, *aliases)))
20
+ for normalized in keys:
21
+ if not normalized:
22
+ raise ValueError("adapter name cannot be empty")
23
+ if normalized in self._factories:
24
+ raise ValueError(f"adapter already registered: {normalized}")
25
+ self._factories[normalized] = factory
26
+
27
+ def create(self, name: str, /, **kwargs: Any) -> T:
28
+ normalized = self._normalize(name)
29
+ try:
30
+ factory = self._factories[normalized]
31
+ except KeyError as exc:
32
+ raise KeyError(f"unknown adapter: {name}") from exc
33
+ return factory(**kwargs)
34
+
35
+ def contains(self, name: str) -> bool:
36
+ return self._normalize(name) in self._factories
37
+
38
+ def names(self) -> tuple[str, ...]:
39
+ return tuple(sorted(self._factories))
40
+
41
+ @staticmethod
42
+ def _normalize(name: str) -> str:
43
+ return str(name or "").strip().lower().replace("_", "-")
44
+
45
+
46
+ __all__ = ["AdapterRegistry"]