@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,316 @@
1
+ """Provider model-catalog response projection and remote source adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import re
7
+ import urllib.parse
8
+ import urllib.request
9
+ from collections.abc import Callable
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+
13
+ from ciel_runtime_support.model_catalog_projection import (
14
+ ModelCatalogProjectionServices,
15
+ project_model_info,
16
+ )
17
+ from ciel_runtime_support.runtime_constants import (
18
+ ANTHROPIC_LIMITED_ACCESS_MODEL_IDS,
19
+ ANTHROPIC_MODEL_DOCS_URLS,
20
+ ANTHROPIC_PUBLIC_MODEL_DEFAULT_IDS,
21
+ ANTHROPIC_PUBLIC_MODEL_FALLBACK_IDS,
22
+ FIREWORKS_API_BASE_URL,
23
+ FIREWORKS_DEFAULT_ACCOUNT_ID,
24
+ FIREWORKS_INFERENCE_BASE_URL,
25
+ )
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class ModelCatalogProjectionPorts:
30
+ normalize_model_id: Callable[[str, Any], str]
31
+ model_context: Callable[[dict[str, Any]], Any]
32
+ positive_int: Callable[[Any], int | None]
33
+ provider_metadata: Callable[[str], Callable[..., dict[str, Any]]]
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class ProviderCatalogHttpPorts:
38
+ http_json: Callable[..., Any]
39
+ join_url: Callable[[str, str], str]
40
+ upstream_base: Callable[[str, dict[str, Any]], str]
41
+ request_headers: Callable[[], dict[str, str]]
42
+ urlopen: Callable[..., Any]
43
+
44
+
45
+ @dataclass(frozen=True, slots=True)
46
+ class ProviderCatalogPolicyPorts:
47
+ unique_model_ids: Callable[[str, list[str]], list[str]]
48
+ log: Callable[[str, str], None]
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class AnthropicCatalogPolicy:
53
+ docs_urls: tuple[str, ...]
54
+ default_ids: tuple[str, ...]
55
+ limited_ids: tuple[str, ...]
56
+ fallback_ids: tuple[str, ...]
57
+ public_id_pattern: re.Pattern[str]
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class FireworksCatalogPolicy:
62
+ default_account_id: str
63
+ api_base_url: str
64
+ inference_base_url: str
65
+ max_pages: int = 20
66
+
67
+
68
+ def build_default_provider_catalog_source_service(
69
+ projection: ModelCatalogProjectionPorts,
70
+ http: ProviderCatalogHttpPorts,
71
+ policy: ProviderCatalogPolicyPorts,
72
+ ) -> ProviderCatalogSourceService:
73
+ """Compose catalog sources with the runtime's immutable provider defaults."""
74
+ return ProviderCatalogSourceService(
75
+ projection=projection,
76
+ http=http,
77
+ policy=policy,
78
+ anthropic=AnthropicCatalogPolicy(
79
+ docs_urls=ANTHROPIC_MODEL_DOCS_URLS,
80
+ default_ids=ANTHROPIC_PUBLIC_MODEL_DEFAULT_IDS,
81
+ limited_ids=ANTHROPIC_LIMITED_ACCESS_MODEL_IDS,
82
+ fallback_ids=ANTHROPIC_PUBLIC_MODEL_FALLBACK_IDS,
83
+ public_id_pattern=ANTHROPIC_PUBLIC_MODEL_ID_RE,
84
+ ),
85
+ fireworks=FireworksCatalogPolicy(
86
+ default_account_id=FIREWORKS_DEFAULT_ACCOUNT_ID,
87
+ api_base_url=FIREWORKS_API_BASE_URL,
88
+ inference_base_url=FIREWORKS_INFERENCE_BASE_URL,
89
+ ),
90
+ )
91
+
92
+
93
+ @dataclass(frozen=True, slots=True)
94
+ class ProviderCatalogSourceService:
95
+ projection: ModelCatalogProjectionPorts
96
+ http: ProviderCatalogHttpPorts
97
+ policy: ProviderCatalogPolicyPorts
98
+ anthropic: AnthropicCatalogPolicy
99
+ fireworks: FireworksCatalogPolicy
100
+
101
+ @staticmethod
102
+ def model_ids_from_response(data: Any) -> list[str]:
103
+ ids: list[str] = []
104
+ candidates: Any
105
+ if isinstance(data, dict):
106
+ candidates = data.get("data")
107
+ if candidates is None:
108
+ candidates = data.get("models")
109
+ if candidates is None:
110
+ candidates = data.get("model")
111
+ else:
112
+ candidates = data
113
+ if isinstance(candidates, str):
114
+ candidates = [candidates]
115
+ if not isinstance(candidates, list):
116
+ return ids
117
+ for item in candidates:
118
+ if isinstance(item, str):
119
+ model_id = item
120
+ elif isinstance(item, dict):
121
+ model_id = (
122
+ item.get("id")
123
+ or item.get("key")
124
+ or item.get("name")
125
+ or item.get("model")
126
+ )
127
+ else:
128
+ model_id = None
129
+ if model_id and str(model_id).strip():
130
+ ids.append(str(model_id).strip())
131
+ return ids
132
+
133
+ def model_info_from_response(
134
+ self, provider: str, data: Any
135
+ ) -> dict[str, dict[str, Any]]:
136
+ return project_model_info(
137
+ provider,
138
+ data,
139
+ ModelCatalogProjectionServices(
140
+ normalize_model_id=self.projection.normalize_model_id,
141
+ model_context=self.projection.model_context,
142
+ positive_int=self.projection.positive_int,
143
+ project_metadata=self.projection.provider_metadata(provider),
144
+ ),
145
+ )
146
+
147
+ def fireworks_account_id(self, provider_config: dict[str, Any]) -> str:
148
+ configured = str(provider_config.get("account_id") or "").strip()
149
+ if configured:
150
+ return configured
151
+ values = (
152
+ provider_config.get("current_model"),
153
+ *(provider_config.get("custom_models", []) or []),
154
+ )
155
+ for value in values:
156
+ match = re.match(
157
+ r"^accounts/([^/]+)/models/[^/]+$", str(value or "")
158
+ )
159
+ if match:
160
+ return match.group(1)
161
+ return self.fireworks.default_account_id
162
+
163
+ def fireworks_management_base_url(
164
+ self, provider_config: dict[str, Any]
165
+ ) -> str:
166
+ configured = str(
167
+ provider_config.get("model_api_base_url") or ""
168
+ ).strip().rstrip("/")
169
+ base = str(
170
+ provider_config.get("base_url") or self.fireworks.inference_base_url
171
+ ).strip().rstrip("/")
172
+ parsed = urllib.parse.urlparse(base)
173
+ if configured and (
174
+ configured != self.fireworks.api_base_url
175
+ or not (parsed.scheme and parsed.netloc)
176
+ or parsed.netloc.endswith("fireworks.ai")
177
+ ):
178
+ return configured
179
+ if parsed.scheme and parsed.netloc:
180
+ return f"{parsed.scheme}://{parsed.netloc}"
181
+ return configured or self.fireworks.api_base_url
182
+
183
+ def fetch_fireworks_model_ids(
184
+ self,
185
+ provider_config: dict[str, Any],
186
+ headers: dict[str, str],
187
+ timeout: float = 8.0,
188
+ ) -> tuple[list[str], dict[str, dict[str, Any]], str]:
189
+ account_id = self.fireworks_account_id(provider_config)
190
+ base = self.fireworks_management_base_url(provider_config)
191
+ models: list[str] = []
192
+ model_info: dict[str, dict[str, Any]] = {}
193
+ page_token = ""
194
+ source = f"fireworks:{account_id}"
195
+ for _ in range(self.fireworks.max_pages):
196
+ query = {"pageSize": "200"}
197
+ if page_token:
198
+ query["pageToken"] = page_token
199
+ account = urllib.parse.quote(account_id, safe="")
200
+ encoded_query = urllib.parse.urlencode(query)
201
+ path = f"/v1/accounts/{account}/models?{encoded_query}"
202
+ data = self.http.http_json(
203
+ self.http.join_url(base, path),
204
+ headers=headers,
205
+ timeout=timeout,
206
+ provider="fireworks",
207
+ pcfg=provider_config,
208
+ )
209
+ ids = [
210
+ self.projection.normalize_model_id("fireworks", model_id)
211
+ for model_id in self.model_ids_from_response(data)
212
+ ]
213
+ for model_id in ids:
214
+ if model_id and model_id not in models:
215
+ models.append(model_id)
216
+ model_info.update(self.model_info_from_response("fireworks", data))
217
+ if not isinstance(data, dict):
218
+ break
219
+ page_token = str(data.get("nextPageToken") or "").strip()
220
+ if not page_token:
221
+ break
222
+ return models, model_info, source
223
+
224
+ def fetch_text_url(self, url: str, timeout: float = 8.0) -> str:
225
+ request = urllib.request.Request(url, headers=self.http.request_headers())
226
+ with self.http.urlopen(request, timeout=timeout) as response:
227
+ return response.read(5_000_000).decode("utf-8", errors="replace")
228
+
229
+ def anthropic_model_ids_from_docs_text(self, text: str) -> list[str]:
230
+ ids: list[str] = []
231
+ seen: set[str] = set()
232
+ for match in self.anthropic.public_id_pattern.finditer(html.unescape(text or "")):
233
+ model_id = match.group(0)
234
+ key = model_id.casefold()
235
+ if key in seen:
236
+ continue
237
+ seen.add(key)
238
+ ids.append(model_id)
239
+ return ids
240
+
241
+ def filter_anthropic_default_model_ids(self, ids: list[str]) -> list[str]:
242
+ allowed = set(self.anthropic.default_ids)
243
+ limited = set(self.anthropic.limited_ids)
244
+ out: list[str] = []
245
+ seen: set[str] = set()
246
+ for raw in ids:
247
+ model_id = self.projection.normalize_model_id("anthropic", raw)
248
+ key = model_id.casefold()
249
+ if not model_id or key in seen or model_id in limited or model_id not in allowed:
250
+ continue
251
+ out.append(model_id)
252
+ seen.add(key)
253
+ return out
254
+
255
+ def fetch_anthropic_public_model_ids(self, timeout: float = 8.0) -> list[str]:
256
+ ids: list[str] = []
257
+ errors: list[str] = []
258
+ for url in self.anthropic.docs_urls:
259
+ try:
260
+ text = self.fetch_text_url(url, timeout=timeout)
261
+ ids.extend(self.anthropic_model_ids_from_docs_text(text))
262
+ except Exception as exc:
263
+ errors.append(f"{url}: {type(exc).__name__}: {exc}")
264
+ unique = self.policy.unique_model_ids("anthropic", ids)
265
+ out = self.filter_anthropic_default_model_ids(unique)
266
+ if out:
267
+ return out
268
+ if errors:
269
+ self.policy.log(
270
+ "WARN", "anthropic model docs fetch failed: " + " ; ".join(errors)
271
+ )
272
+ return list(self.anthropic.fallback_ids)
273
+
274
+ def fetch_anthropic_api_model_ids(
275
+ self,
276
+ provider_config: dict[str, Any],
277
+ headers: dict[str, str],
278
+ timeout: float = 6.0,
279
+ ) -> tuple[list[str], str]:
280
+ base = self.http.upstream_base("anthropic", provider_config)
281
+ errors: list[str] = []
282
+ for path in ("/v1/models", "/models"):
283
+ try:
284
+ data = self.http.http_json(
285
+ self.http.join_url(base, path),
286
+ headers=headers,
287
+ timeout=timeout,
288
+ )
289
+ ids = self.policy.unique_model_ids(
290
+ "anthropic", self.model_ids_from_response(data)
291
+ )
292
+ if ids:
293
+ return ids, f"api:{path}"
294
+ except Exception as exc:
295
+ errors.append(f"{path}: {type(exc).__name__}: {exc}")
296
+ if errors:
297
+ self.policy.log(
298
+ "DEBUG", "anthropic model API fetch failed: " + " ; ".join(errors)
299
+ )
300
+ return [], ""
301
+
302
+
303
+ ANTHROPIC_PUBLIC_MODEL_ID_RE = re.compile(
304
+ r"(?<![A-Za-z0-9_.@:-])"
305
+ r"(?:"
306
+ r"claude-(?:fable|mythos)-\d+(?:-\d+)?(?:-\d{8})?|"
307
+ r"claude-mythos-preview|"
308
+ r"claude-(?:opus|sonnet|haiku)-\d+-\d+-\d{8}|"
309
+ r"claude-(?:opus|sonnet|haiku)-\d+-\d{8}|"
310
+ r"claude-(?:opus|sonnet|haiku)-\d+-\d+|"
311
+ r"claude-(?:opus|sonnet|haiku)-\d+(?:-\d+)?-latest|"
312
+ r"claude-\d+(?:-\d+){0,2}-(?:opus|sonnet|haiku)-(?:\d{8}|latest)|"
313
+ r"claude-(?:opus|sonnet|haiku)-\d+"
314
+ r")"
315
+ r"(?![A-Za-z0-9_.@:-])"
316
+ )
@@ -0,0 +1,201 @@
1
+ """Provider/runtime choice normalization and selection strategies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+ from .architecture import ProviderAdapter, ProviderConfig
9
+
10
+
11
+ ANTHROPIC_NATIVE_PROVIDER_CHOICE = "anthropic:native"
12
+ ANTHROPIC_ROUTED_PROVIDER_CHOICE = "anthropic:routed"
13
+ AGY_NATIVE_PROVIDER_CHOICE = "agy:native"
14
+ AGY_ROUTED_PROVIDER_CHOICE = "agy:routed"
15
+ CODEX_NATIVE_PROVIDER_CHOICE = "codex:native"
16
+ CODEX_ROUTED_PROVIDER_CHOICE = "codex:routed"
17
+ KIMI_NATIVE_PROVIDER_CHOICE = "kimi:native"
18
+ KIMI_ROUTED_PROVIDER_CHOICE = "kimi:routed"
19
+
20
+
21
+ CHOICE_ALIASES = {
22
+ "anthropic-native": ANTHROPIC_NATIVE_PROVIDER_CHOICE,
23
+ "claude-native": ANTHROPIC_NATIVE_PROVIDER_CHOICE,
24
+ "native": ANTHROPIC_NATIVE_PROVIDER_CHOICE,
25
+ "claude-code": ANTHROPIC_NATIVE_PROVIDER_CHOICE,
26
+ "anthropic-routed": ANTHROPIC_ROUTED_PROVIDER_CHOICE,
27
+ "anthropic-router": ANTHROPIC_ROUTED_PROVIDER_CHOICE,
28
+ "claude-routed": ANTHROPIC_ROUTED_PROVIDER_CHOICE,
29
+ "claude-router": ANTHROPIC_ROUTED_PROVIDER_CHOICE,
30
+ "agy": AGY_NATIVE_PROVIDER_CHOICE,
31
+ "agy-native": AGY_NATIVE_PROVIDER_CHOICE,
32
+ "native-agy": AGY_NATIVE_PROVIDER_CHOICE,
33
+ "antigravity": AGY_NATIVE_PROVIDER_CHOICE,
34
+ "google-antigravity": AGY_NATIVE_PROVIDER_CHOICE,
35
+ "agy-routed": AGY_ROUTED_PROVIDER_CHOICE,
36
+ "agy-router": AGY_ROUTED_PROVIDER_CHOICE,
37
+ "routed-agy": AGY_ROUTED_PROVIDER_CHOICE,
38
+ "antigravity-routed": AGY_ROUTED_PROVIDER_CHOICE,
39
+ "codex": CODEX_NATIVE_PROVIDER_CHOICE,
40
+ "codex-native": CODEX_NATIVE_PROVIDER_CHOICE,
41
+ "native-codex": CODEX_NATIVE_PROVIDER_CHOICE,
42
+ "codex-routed": CODEX_ROUTED_PROVIDER_CHOICE,
43
+ "codex-router": CODEX_ROUTED_PROVIDER_CHOICE,
44
+ "routed-codex": CODEX_ROUTED_PROVIDER_CHOICE,
45
+ "kimi-native": KIMI_NATIVE_PROVIDER_CHOICE,
46
+ "native-kimi": KIMI_NATIVE_PROVIDER_CHOICE,
47
+ "kimi-code": KIMI_NATIVE_PROVIDER_CHOICE,
48
+ "kimi-routed": KIMI_ROUTED_PROVIDER_CHOICE,
49
+ "kimi-router": KIMI_ROUTED_PROVIDER_CHOICE,
50
+ }
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class ProviderChoiceStrategy:
55
+ provider: str
56
+ routed: bool
57
+ status_lines: tuple[str, ...]
58
+ missing_api_key_line: str = ""
59
+
60
+
61
+ CHOICE_STRATEGIES = {
62
+ ANTHROPIC_NATIVE_PROVIDER_CHOICE: ProviderChoiceStrategy(
63
+ provider="anthropic",
64
+ routed=False,
65
+ status_lines=(
66
+ "Provider set to anthropic (Claude Native).",
67
+ "mode: anthropic-native",
68
+ "Claude Code OAuth/Max can be used directly, but ciel-runtime router features such as /advisor are unavailable.",
69
+ ),
70
+ ),
71
+ ANTHROPIC_ROUTED_PROVIDER_CHOICE: ProviderChoiceStrategy(
72
+ provider="anthropic",
73
+ routed=True,
74
+ status_lines=(
75
+ "Provider set to anthropic (Anthropic routed).",
76
+ "mode: anthropic-routed",
77
+ ),
78
+ missing_api_key_line="Anthropic routed mode will use Claude Code OAuth/API auth headers when available.",
79
+ ),
80
+ AGY_NATIVE_PROVIDER_CHOICE: ProviderChoiceStrategy(
81
+ provider="agy",
82
+ routed=False,
83
+ status_lines=(
84
+ "Provider set to agy (AGY).",
85
+ "mode: agy-native",
86
+ "AGY runs with its own native settings; Ciel Runtime router features are unavailable.",
87
+ ),
88
+ ),
89
+ AGY_ROUTED_PROVIDER_CHOICE: ProviderChoiceStrategy(
90
+ provider="agy",
91
+ routed=True,
92
+ status_lines=(
93
+ "Provider set to agy (AGY routed).",
94
+ "mode: agy-routed",
95
+ "AGY uses native Google Antigravity auth/settings; Ciel Runtime adds channel/PTY wake support only.",
96
+ ),
97
+ ),
98
+ CODEX_NATIVE_PROVIDER_CHOICE: ProviderChoiceStrategy(
99
+ provider="codex",
100
+ routed=False,
101
+ status_lines=(
102
+ "Provider set to codex (Codex Native).",
103
+ "mode: codex-native",
104
+ "Codex runs with its own native settings; ciel-runtime router features are unavailable.",
105
+ ),
106
+ ),
107
+ CODEX_ROUTED_PROVIDER_CHOICE: ProviderChoiceStrategy(
108
+ provider="codex",
109
+ routed=True,
110
+ status_lines=(
111
+ "Provider set to codex (Codex routed).",
112
+ "mode: codex-routed",
113
+ "Codex uses its native OpenAI account/config, with base URL routed through ciel-runtime.",
114
+ ),
115
+ ),
116
+ KIMI_NATIVE_PROVIDER_CHOICE: ProviderChoiceStrategy(
117
+ provider="kimi", routed=False,
118
+ status_lines=("Provider set to kimi (Kimi Native).", "mode: kimi-native", "Kimi Code uses its official OAuth/config directly."),
119
+ ),
120
+ KIMI_ROUTED_PROVIDER_CHOICE: ProviderChoiceStrategy(
121
+ provider="kimi", routed=True,
122
+ status_lines=("Provider set to kimi (Kimi Routed).", "mode: kimi-routed", "Kimi Code requests are routed through ciel-runtime."),
123
+ missing_api_key_line="Kimi Routed requires Kimi OAuth login or a Kimi API key; open API key setup to authenticate.",
124
+ ),
125
+ }
126
+
127
+
128
+ def normalize_provider_choice(name: str) -> str | None:
129
+ raw = str(name or "").strip().lower().replace("_", "-").replace(" ", "-")
130
+ if raw in CHOICE_STRATEGIES:
131
+ return raw
132
+ return CHOICE_ALIASES.get(raw.replace(":", "-"))
133
+
134
+
135
+ @dataclass(frozen=True, slots=True)
136
+ class ProviderChoicePorts:
137
+ load_config: Callable[[], dict[str, Any]]
138
+ save_config: Callable[[dict[str, Any]], None]
139
+ clear_model_cache: Callable[[], None]
140
+ provider_has_api_key: Callable[[str, dict[str, Any]], bool]
141
+ configured_adapter: Callable[[str, dict[str, Any]], ProviderAdapter]
142
+ contract_config: Callable[[str, dict[str, Any]], ProviderConfig]
143
+ provider_label: Callable[[str], str]
144
+
145
+
146
+ class ProviderChoiceController:
147
+ def __init__(self, ports: ProviderChoicePorts) -> None:
148
+ self._ports = ports
149
+
150
+ def select(self, choice: str) -> list[str]:
151
+ normalized = normalize_provider_choice(choice) or choice
152
+ strategy = CHOICE_STRATEGIES.get(normalized)
153
+ if strategy is None:
154
+ return self.select_standard(normalized)
155
+ config = self._ports.load_config()
156
+ config["current_provider"] = strategy.provider
157
+ provider_config = config["providers"][strategy.provider]
158
+ provider_config["route_through_router"] = strategy.routed
159
+ self._ports.save_config(config)
160
+ self._ports.clear_model_cache()
161
+ lines = list(strategy.status_lines)
162
+ if (
163
+ strategy.missing_api_key_line
164
+ and not self._ports.provider_has_api_key(strategy.provider, provider_config)
165
+ ):
166
+ lines.append(strategy.missing_api_key_line)
167
+ return lines
168
+
169
+ def select_standard(self, provider: str) -> list[str]:
170
+ """Select a provider while delegating normalization to its adapter."""
171
+
172
+ config = self._ports.load_config()
173
+ config["current_provider"] = provider
174
+ provider_config = config["providers"][provider]
175
+ adapter = self._ports.configured_adapter(provider, provider_config)
176
+ contract = self._ports.contract_config(provider, provider_config)
177
+ updates = adapter.selection_config_updates(contract)
178
+ provider_config.update(updates)
179
+ self._ports.save_config(config)
180
+ self._ports.clear_model_cache()
181
+ selected_contract = self._ports.contract_config(provider, provider_config)
182
+ lines = [f"Provider set to {provider} ({self._ports.provider_label(provider)})."]
183
+ lines.extend(adapter.selection_status_lines(selected_contract))
184
+ lines.extend(adapter.selection_update_status_lines(selected_contract, updates))
185
+ return lines
186
+
187
+
188
+ __all__ = [
189
+ "AGY_NATIVE_PROVIDER_CHOICE",
190
+ "AGY_ROUTED_PROVIDER_CHOICE",
191
+ "ANTHROPIC_NATIVE_PROVIDER_CHOICE",
192
+ "ANTHROPIC_ROUTED_PROVIDER_CHOICE",
193
+ "CODEX_NATIVE_PROVIDER_CHOICE",
194
+ "CODEX_ROUTED_PROVIDER_CHOICE",
195
+ "KIMI_NATIVE_PROVIDER_CHOICE",
196
+ "KIMI_ROUTED_PROVIDER_CHOICE",
197
+ "ProviderChoiceController",
198
+ "ProviderChoicePorts",
199
+ "ProviderChoiceStrategy",
200
+ "normalize_provider_choice",
201
+ ]
@@ -0,0 +1,165 @@
1
+ """Provider compatibility strategies kept separate from transport adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Callable, Mapping
7
+
8
+ from .architecture import ProviderConfig
9
+
10
+
11
+ FailureDiagnosis = Callable[[int | None, str], str | None]
12
+ ToolUseBlocker = Callable[[str], str]
13
+ RuntimeMetadataProjection = Callable[[Mapping[str, object]], tuple[str, ...]]
14
+ AutoWebSearchPolicy = Callable[[ProviderConfig], bool]
15
+
16
+
17
+ def _no_diagnosis(code: int | None, message: str) -> str | None:
18
+ del code, message
19
+ return None
20
+
21
+
22
+ def _no_tool_use_blocker(model: str) -> str:
23
+ del model
24
+ return ""
25
+
26
+
27
+ def _no_runtime_metadata(info: Mapping[str, object]) -> tuple[str, ...]:
28
+ del info
29
+ return ()
30
+
31
+
32
+ def _allow_auto_web_search(config: ProviderConfig) -> bool:
33
+ del config
34
+ return True
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ProviderCompatibilityPolicy:
39
+ advisor_transport: str = ""
40
+ runtime_model_info_strategy: str = ""
41
+ auto_web_search: AutoWebSearchPolicy = _allow_auto_web_search
42
+ requires_compat_prompt: bool = True
43
+ failure_diagnosis: FailureDiagnosis = _no_diagnosis
44
+ tool_use_blocker: ToolUseBlocker = _no_tool_use_blocker
45
+ runtime_metadata: RuntimeMetadataProjection = _no_runtime_metadata
46
+
47
+ @property
48
+ def exposes_runtime_info(self) -> bool:
49
+ return bool(self.runtime_model_info_strategy)
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class ProviderCompatibilityRegistry:
54
+ policies: Mapping[str, ProviderCompatibilityPolicy] = field(default_factory=dict)
55
+ default: ProviderCompatibilityPolicy = ProviderCompatibilityPolicy()
56
+
57
+ def resolve(self, provider: str) -> ProviderCompatibilityPolicy:
58
+ return self.policies.get(provider, self.default)
59
+
60
+
61
+ def _vllm_failure_diagnosis(code: int | None, message: str) -> str | None:
62
+ del code
63
+ lower = message.lower()
64
+ if "tool" not in lower and "parse" not in lower and "parser" not in lower:
65
+ return None
66
+ return (
67
+ "Diagnosis: vLLM tool calling depends on the server's model-specific --tool-call-parser and chat template. "
68
+ "For Qwen3-Coder models, current vLLM docs recommend --tool-call-parser qwen3_xml; Hermes is for Hermes-style models "
69
+ "and some older Qwen tool templates."
70
+ )
71
+
72
+
73
+ def _nvidia_failure_diagnosis(code: int | None, message: str) -> str | None:
74
+ lower = message.lower()
75
+ if code == 404:
76
+ return (
77
+ "Diagnosis: NVIDIA API Catalog does not expose this request path/model for the current account. "
78
+ "Use the default router mode for nvidia-hosted, or pick another hosted model."
79
+ )
80
+ if code in (502, 503, 504):
81
+ return (
82
+ "Diagnosis: NVIDIA API Catalog or the hosted model backend returned a transient upstream error. "
83
+ "Retry the compatibility test, or choose another NVIDIA hosted model if it repeats."
84
+ )
85
+ if any(
86
+ marker in lower
87
+ for marker in (
88
+ "remotedisconnected",
89
+ "remote end closed connection",
90
+ "connection reset",
91
+ "gateway timeout",
92
+ )
93
+ ):
94
+ return (
95
+ "Diagnosis: the NVIDIA hosted upstream closed the request without a complete response. "
96
+ "This is usually a transient API Catalog/backend issue rather than a local ciel-runtime configuration error. "
97
+ "Retry the test, or choose another hosted model if it repeats."
98
+ )
99
+ if "function" in lower and "not found" in lower:
100
+ return (
101
+ "Diagnosis: NVIDIA returned a missing function for this hosted model. The model is visible in /v1/models "
102
+ "but is not callable with the current account."
103
+ )
104
+ return None
105
+
106
+
107
+ def _zai_tool_use_blocker(model: str) -> str:
108
+ if str(model or "").strip().lower() != "glm-4.7-flash":
109
+ return ""
110
+ return (
111
+ "Z.AI GLM-4.7-Flash responds to text requests but direct Anthropic tool-use probes time out. "
112
+ "Claude Code requires tool calling for normal work; use glm-4.5-flash for a Flash/free model, "
113
+ "or use glm-4.7/glm-5.2 if your Z.AI account can access them."
114
+ )
115
+
116
+
117
+ def _lm_studio_runtime_metadata(info: Mapping[str, object]) -> tuple[str, ...]:
118
+ lines: list[str] = []
119
+ if info.get("loaded_context_len"):
120
+ lines.append(f"Runtime loaded_context_length: {info['loaded_context_len']}")
121
+ if info.get("state"):
122
+ lines.append(f"Runtime model state: {info['state']}")
123
+ return tuple(lines)
124
+
125
+
126
+ def _zai_auto_web_search(config: ProviderConfig) -> bool:
127
+ return not bool(config.options.get("managed_mcp", True))
128
+
129
+
130
+ PROVIDER_COMPATIBILITY = ProviderCompatibilityRegistry(
131
+ policies={
132
+ "anthropic": ProviderCompatibilityPolicy(
133
+ advisor_transport="anthropic",
134
+ auto_web_search=lambda _config: False,
135
+ requires_compat_prompt=False,
136
+ ),
137
+ "ollama": ProviderCompatibilityPolicy(advisor_transport="ollama"),
138
+ "ollama-cloud": ProviderCompatibilityPolicy(advisor_transport="ollama"),
139
+ "lm-studio": ProviderCompatibilityPolicy(
140
+ advisor_transport="openai-compatible",
141
+ runtime_model_info_strategy="lm_studio",
142
+ runtime_metadata=_lm_studio_runtime_metadata,
143
+ ),
144
+ "vllm": ProviderCompatibilityPolicy(
145
+ runtime_model_info_strategy="openai",
146
+ failure_diagnosis=_vllm_failure_diagnosis,
147
+ ),
148
+ "nvidia-hosted": ProviderCompatibilityPolicy(
149
+ advisor_transport="openai-compatible",
150
+ failure_diagnosis=_nvidia_failure_diagnosis,
151
+ ),
152
+ "self-hosted-nim": ProviderCompatibilityPolicy(runtime_model_info_strategy="openai"),
153
+ "zai": ProviderCompatibilityPolicy(
154
+ auto_web_search=_zai_auto_web_search,
155
+ tool_use_blocker=_zai_tool_use_blocker,
156
+ ),
157
+ }
158
+ )
159
+
160
+
161
+ __all__ = [
162
+ "PROVIDER_COMPATIBILITY",
163
+ "ProviderCompatibilityPolicy",
164
+ "ProviderCompatibilityRegistry",
165
+ ]