@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,457 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class ProviderKeyServices:
11
+ _API_KEY_ROTATION_CURSOR: Any
12
+ _API_KEY_ROTATION_LOCK: Any
13
+ api_key_cooldown_until: Callable[..., Any]
14
+ provider_api_key_rotation_name: Callable[..., Any]
15
+ provider_config_api_keys: Callable[..., Any]
16
+ router_log: Callable[..., Any]
17
+
18
+
19
+ def choose_provider_api_key(provider: str, pcfg: dict[str, Any], *, rotate: bool = True,
20
+ services: ProviderKeyServices,
21
+ ) -> str:
22
+ _API_KEY_ROTATION_CURSOR = services._API_KEY_ROTATION_CURSOR
23
+ _API_KEY_ROTATION_LOCK = services._API_KEY_ROTATION_LOCK
24
+ api_key_cooldown_until = services.api_key_cooldown_until
25
+ provider_api_key_rotation_name = services.provider_api_key_rotation_name
26
+ provider_config_api_keys = services.provider_config_api_keys
27
+ router_log = services.router_log
28
+ keys = provider_config_api_keys(provider, pcfg)
29
+ if not keys:
30
+ return ""
31
+ if not rotate or len(keys) == 1:
32
+ return keys[0]
33
+ name = provider_api_key_rotation_name(provider, pcfg)
34
+ # Skip keys that are resting after a 429; round-robin over the live ones so a
35
+ # rate-limited key rests while the rest keep serving. If every key is cooling,
36
+ # use the one that frees up soonest.
37
+ now = time.time()
38
+ live = [k for k in keys if api_key_cooldown_until(provider, pcfg, k) <= now]
39
+ if not live:
40
+ soonest = min(keys, key=lambda k: api_key_cooldown_until(provider, pcfg, k))
41
+ router_log("DEBUG", f"api_key_round_robin provider={provider} all_cooling={len(keys)} using_soonest")
42
+ return soonest
43
+ with _API_KEY_ROTATION_LOCK:
44
+ counter = _API_KEY_ROTATION_CURSOR.get(name, 0)
45
+ _API_KEY_ROTATION_CURSOR[name] = counter + 1
46
+ idx = counter % len(live)
47
+ if len(live) < len(keys):
48
+ router_log("DEBUG", f"api_key_round_robin provider={provider} key_index={idx + 1}/{len(live)} cooling={len(keys) - len(live)}")
49
+ else:
50
+ router_log("DEBUG", f"api_key_round_robin provider={provider} key_index={idx + 1}/{len(keys)}")
51
+ return live[idx]
52
+
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class RateLimitStateStore:
57
+ CONFIG_DIR: Any
58
+ RATE_LIMIT_STATE_PATH: Any
59
+ _RATE_LIMIT_LOCK: Any
60
+ router_log: Callable[..., Any]
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class RateLimitLearningPolicy:
65
+ current_upstream_model_id: Callable[..., Any]
66
+ first_header: Callable[..., Any]
67
+ first_int_in_header: Callable[..., Any]
68
+ provider_api_key_count: Callable[..., Any]
69
+ rate_limit_reset_seconds: Callable[..., Any]
70
+ router_rate_limit_configured_rpm: Callable[..., Any]
71
+ router_rate_limit_key: Callable[..., Any]
72
+ router_rate_limit_recent: Callable[..., Any]
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class RateLimitLearningServices:
77
+ state_store: RateLimitStateStore
78
+ policy: RateLimitLearningPolicy
79
+
80
+
81
+ def learn_rate_limit_headers(provider: str, pcfg: dict[str, Any], model: str | None, headers: Any,
82
+ *,
83
+ services: RateLimitLearningServices,
84
+ ) -> None:
85
+ CONFIG_DIR = services.state_store.CONFIG_DIR
86
+ RATE_LIMIT_STATE_PATH = services.state_store.RATE_LIMIT_STATE_PATH
87
+ _RATE_LIMIT_LOCK = services.state_store._RATE_LIMIT_LOCK
88
+ router_log = services.state_store.router_log
89
+ current_upstream_model_id = services.policy.current_upstream_model_id
90
+ first_header = services.policy.first_header
91
+ first_int_in_header = services.policy.first_int_in_header
92
+ provider_api_key_count = services.policy.provider_api_key_count
93
+ rate_limit_reset_seconds = services.policy.rate_limit_reset_seconds
94
+ router_rate_limit_configured_rpm = services.policy.router_rate_limit_configured_rpm
95
+ router_rate_limit_key = services.policy.router_rate_limit_key
96
+ router_rate_limit_recent = services.policy.router_rate_limit_recent
97
+ limit = first_int_in_header(first_header(headers, [
98
+ "x-ratelimit-limit-requests",
99
+ "x-rate-limit-limit-requests",
100
+ "ratelimit-limit",
101
+ "rate-limit-limit",
102
+ "x-ratelimit-limit",
103
+ "x-rate-limit-limit",
104
+ ]))
105
+ remaining = first_int_in_header(first_header(headers, [
106
+ "x-ratelimit-remaining-requests",
107
+ "x-rate-limit-remaining-requests",
108
+ "ratelimit-remaining",
109
+ "rate-limit-remaining",
110
+ "x-ratelimit-remaining",
111
+ "x-rate-limit-remaining",
112
+ ]))
113
+ reset = rate_limit_reset_seconds(first_header(headers, [
114
+ "x-ratelimit-reset-requests",
115
+ "x-rate-limit-reset-requests",
116
+ "ratelimit-reset",
117
+ "rate-limit-reset",
118
+ "x-ratelimit-reset",
119
+ "x-rate-limit-reset",
120
+ ]))
121
+ max_concurrent = first_int_in_header(first_header(headers, [
122
+ "x-ratelimit-max-concurrent",
123
+ "x-rate-limit-max-concurrent",
124
+ "ratelimit-max-concurrent",
125
+ "rate-limit-max-concurrent",
126
+ ]))
127
+ active = first_int_in_header(first_header(headers, [
128
+ "x-ratelimit-active",
129
+ "x-rate-limit-active",
130
+ "ratelimit-active",
131
+ "rate-limit-active",
132
+ ]))
133
+ queue_limit = first_int_in_header(first_header(headers, [
134
+ "x-ratelimit-queue-limit",
135
+ "x-rate-limit-queue-limit",
136
+ "ratelimit-queue-limit",
137
+ "rate-limit-queue-limit",
138
+ ]))
139
+ queued = first_int_in_header(first_header(headers, [
140
+ "x-ratelimit-queued",
141
+ "x-rate-limit-queued",
142
+ "ratelimit-queued",
143
+ "rate-limit-queued",
144
+ ]))
145
+ if (
146
+ limit is None
147
+ and remaining is None
148
+ and reset is None
149
+ and max_concurrent is None
150
+ and active is None
151
+ and queue_limit is None
152
+ and queued is None
153
+ ):
154
+ return
155
+ configured = router_rate_limit_configured_rpm(provider, pcfg)
156
+ rpm = limit if limit and limit > 0 else configured
157
+ if rpm is None:
158
+ rpm = 0
159
+ multi_key = provider_api_key_count(provider, pcfg) > 1
160
+ key = router_rate_limit_key(provider, pcfg, model)
161
+ with _RATE_LIMIT_LOCK:
162
+ try:
163
+ state = json.loads(RATE_LIMIT_STATE_PATH.read_text(encoding="utf-8")) if RATE_LIMIT_STATE_PATH.exists() else {}
164
+ if not isinstance(state, dict):
165
+ state = {}
166
+ except Exception:
167
+ state = {}
168
+ now = time.time()
169
+ entry = state.get(key)
170
+ if not isinstance(entry, dict):
171
+ legacy_key = f"{provider}:{model or current_upstream_model_id(provider, pcfg)}"
172
+ entry = state.get(legacy_key)
173
+ timestamps = entry.get("timestamps") if isinstance(entry, dict) else []
174
+ recent = router_rate_limit_recent(timestamps, now, 60.0, include_future=True)
175
+ penalty_until = 0.0 if multi_key else (float(entry.get("penalty_until") or 0.0) if isinstance(entry, dict) else 0.0)
176
+ if remaining == 0 and reset and reset > 0 and not multi_key:
177
+ penalty_until = max(penalty_until, now + reset)
178
+ new_entry: dict[str, Any] = {
179
+ "timestamps": recent[-max(int(rpm or 0), 240):],
180
+ "rpm": int(rpm or 0),
181
+ "updated_at": now,
182
+ "last_wait": float(entry.get("last_wait") or 0.0) if isinstance(entry, dict) else 0.0,
183
+ "server_remaining": remaining,
184
+ "server_reset_seconds": reset,
185
+ }
186
+ if max_concurrent is not None:
187
+ new_entry["server_max_concurrent"] = max_concurrent
188
+ elif isinstance(entry, dict) and entry.get("server_max_concurrent") is not None:
189
+ new_entry["server_max_concurrent"] = entry.get("server_max_concurrent")
190
+ if active is not None:
191
+ new_entry["server_active"] = active
192
+ elif isinstance(entry, dict) and entry.get("server_active") is not None:
193
+ new_entry["server_active"] = entry.get("server_active")
194
+ if queue_limit is not None:
195
+ new_entry["server_queue_limit"] = queue_limit
196
+ elif isinstance(entry, dict) and entry.get("server_queue_limit") is not None:
197
+ new_entry["server_queue_limit"] = entry.get("server_queue_limit")
198
+ if queued is not None:
199
+ new_entry["server_queued"] = queued
200
+ elif isinstance(entry, dict) and entry.get("server_queued") is not None:
201
+ new_entry["server_queued"] = entry.get("server_queued")
202
+ if limit and limit > 0:
203
+ new_entry["server_rpm"] = int(limit)
204
+ new_entry["server_rpm_updated_at"] = now
205
+ elif isinstance(entry, dict) and entry.get("server_rpm"):
206
+ new_entry["server_rpm"] = entry.get("server_rpm")
207
+ new_entry["server_rpm_updated_at"] = entry.get("server_rpm_updated_at")
208
+ if penalty_until > now:
209
+ new_entry["penalty_until"] = penalty_until
210
+ state[key] = new_entry
211
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
212
+ RATE_LIMIT_STATE_PATH.write_text(json.dumps(state, ensure_ascii=False) + "\n", encoding="utf-8")
213
+ extra = " multi_key_no_global_penalty=1" if multi_key and remaining == 0 and reset and reset > 0 else ""
214
+ router_log(
215
+ "INFO",
216
+ f"rate_limit_headers provider={provider} model={model or ''} limit={limit} remaining={remaining} reset={reset}"
217
+ f" max_concurrent={max_concurrent} active={active} queue_limit={queue_limit} queued={queued}{extra}",
218
+ )
219
+
220
+
221
+
222
+ @dataclass(frozen=True, slots=True)
223
+ class RateLimitBackoffPolicy:
224
+ current_upstream_model_id: Callable[..., Any]
225
+ parse_retry_after_seconds: Callable[..., Any]
226
+ provider_api_key_count: Callable[..., Any]
227
+ router_rate_limit_capacity: Callable[..., Any]
228
+ router_rate_limit_configured_rpm: Callable[..., Any]
229
+ router_rate_limit_effective_rpm: Callable[..., Any]
230
+ router_rate_limit_key: Callable[..., Any]
231
+ router_rate_limit_recent: Callable[..., Any]
232
+
233
+
234
+ @dataclass(frozen=True, slots=True)
235
+ class RateLimitBackoffServices:
236
+ state_store: RateLimitStateStore
237
+ policy: RateLimitBackoffPolicy
238
+
239
+
240
+ def register_rate_limit_backoff(provider: str, pcfg: dict[str, Any], model: str | None, retry_after: str | None = None,
241
+ *,
242
+ services: RateLimitBackoffServices,
243
+ ) -> float:
244
+ CONFIG_DIR = services.state_store.CONFIG_DIR
245
+ RATE_LIMIT_STATE_PATH = services.state_store.RATE_LIMIT_STATE_PATH
246
+ _RATE_LIMIT_LOCK = services.state_store._RATE_LIMIT_LOCK
247
+ router_log = services.state_store.router_log
248
+ current_upstream_model_id = services.policy.current_upstream_model_id
249
+ parse_retry_after_seconds = services.policy.parse_retry_after_seconds
250
+ provider_api_key_count = services.policy.provider_api_key_count
251
+ router_rate_limit_capacity = services.policy.router_rate_limit_capacity
252
+ router_rate_limit_configured_rpm = services.policy.router_rate_limit_configured_rpm
253
+ router_rate_limit_effective_rpm = services.policy.router_rate_limit_effective_rpm
254
+ router_rate_limit_key = services.policy.router_rate_limit_key
255
+ router_rate_limit_recent = services.policy.router_rate_limit_recent
256
+ rpm = router_rate_limit_effective_rpm(provider, pcfg, model)
257
+ fallback = 60.0 / float(rpm) if rpm and rpm > 0 else 15.0
258
+ wait = parse_retry_after_seconds(retry_after)
259
+ if wait is None:
260
+ wait = max(10.0, min(60.0, fallback * 4.0))
261
+ wait = max(1.0, min(300.0, wait))
262
+ key = router_rate_limit_key(provider, pcfg, model)
263
+ multi_key = provider_api_key_count(provider, pcfg) > 1
264
+ with _RATE_LIMIT_LOCK:
265
+ try:
266
+ state = json.loads(RATE_LIMIT_STATE_PATH.read_text(encoding="utf-8")) if RATE_LIMIT_STATE_PATH.exists() else {}
267
+ if not isinstance(state, dict):
268
+ state = {}
269
+ except Exception:
270
+ state = {}
271
+ now = time.time()
272
+ entry = state.get(key)
273
+ if not isinstance(entry, dict):
274
+ legacy_key = f"{provider}:{model or current_upstream_model_id(provider, pcfg)}"
275
+ entry = state.get(legacy_key)
276
+ timestamps = entry.get("timestamps") if isinstance(entry, dict) else []
277
+ recent = router_rate_limit_recent(timestamps, now, 60.0, include_future=True)
278
+ actual_recent = router_rate_limit_recent(timestamps, now, 60.0, include_future=False)
279
+ configured_rpm = router_rate_limit_configured_rpm(provider, pcfg)
280
+ inferred_rpm: int | None = None
281
+ if (
282
+ isinstance(entry, dict)
283
+ and not entry.get("server_rpm")
284
+ and configured_rpm
285
+ and configured_rpm > 0
286
+ and 0 < len(actual_recent) < configured_rpm
287
+ ):
288
+ inferred_rpm = max(1, len(actual_recent))
289
+ rpm = inferred_rpm
290
+ capacity = router_rate_limit_capacity(int(rpm or 0)) if rpm and rpm > 0 else int(rpm or 0)
291
+ if capacity and capacity > 0 and len(actual_recent) >= capacity and actual_recent:
292
+ wait = max(wait, max(0.0, actual_recent[0] + 60.0 - now))
293
+ existing_penalty_until = 0.0 if multi_key else (float(entry.get("penalty_until") or 0.0) if isinstance(entry, dict) else 0.0)
294
+ penalty_until = max(existing_penalty_until, now + wait) if not multi_key else 0.0
295
+ state[key] = {
296
+ "timestamps": recent[-max(int(rpm or 0), 240):],
297
+ "rpm": int(rpm or 0),
298
+ "updated_at": now,
299
+ "last_wait": wait,
300
+ "last_429_at": now,
301
+ }
302
+ if penalty_until > now:
303
+ state[key]["penalty_until"] = penalty_until
304
+ if isinstance(entry, dict):
305
+ for preserve_key in (
306
+ "server_rpm",
307
+ "server_rpm_updated_at",
308
+ "server_remaining",
309
+ "server_reset_seconds",
310
+ "server_max_concurrent",
311
+ "server_active",
312
+ "server_queue_limit",
313
+ "server_queued",
314
+ ):
315
+ if preserve_key in entry:
316
+ state[key][preserve_key] = entry[preserve_key]
317
+ if inferred_rpm:
318
+ state[key]["server_rpm"] = inferred_rpm
319
+ state[key]["server_rpm_updated_at"] = now
320
+ state[key]["server_rpm_reason"] = "inferred_from_429"
321
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
322
+ RATE_LIMIT_STATE_PATH.write_text(json.dumps(state, ensure_ascii=False) + "\n", encoding="utf-8")
323
+ extra = " multi_key_no_global_penalty=1" if multi_key else ""
324
+ router_log("WARN", f"rate_limit_429_backoff provider={provider} model={model or ''} wait={wait:.2f}s{extra}")
325
+ return wait
326
+
327
+
328
+
329
+ @dataclass(frozen=True, slots=True)
330
+ class RateLimitApplyPolicy:
331
+ current_upstream_model_id: Callable[..., Any]
332
+ provider_api_key_count: Callable[..., Any]
333
+ record_router_rate_usage: Callable[..., Any]
334
+ router_rate_limit_capacity: Callable[..., Any]
335
+ router_rate_limit_effective_rpm: Callable[..., Any]
336
+ router_rate_limit_key: Callable[..., Any]
337
+ router_rate_limit_recent: Callable[..., Any]
338
+ wait_for_router_rate_limit_penalty: Callable[..., Any]
339
+
340
+
341
+ @dataclass(frozen=True, slots=True)
342
+ class RateLimitApplyServices:
343
+ state_store: RateLimitStateStore
344
+ policy: RateLimitApplyPolicy
345
+
346
+
347
+ def apply_rate_limit(provider: str, pcfg: dict[str, Any], model: str | None = None,
348
+ *,
349
+ services: RateLimitApplyServices,
350
+ ) -> tuple[float, int, int | None]:
351
+ CONFIG_DIR = services.state_store.CONFIG_DIR
352
+ RATE_LIMIT_STATE_PATH = services.state_store.RATE_LIMIT_STATE_PATH
353
+ _RATE_LIMIT_LOCK = services.state_store._RATE_LIMIT_LOCK
354
+ router_log = services.state_store.router_log
355
+ current_upstream_model_id = services.policy.current_upstream_model_id
356
+ provider_api_key_count = services.policy.provider_api_key_count
357
+ record_router_rate_usage = services.policy.record_router_rate_usage
358
+ router_rate_limit_capacity = services.policy.router_rate_limit_capacity
359
+ router_rate_limit_effective_rpm = services.policy.router_rate_limit_effective_rpm
360
+ router_rate_limit_key = services.policy.router_rate_limit_key
361
+ router_rate_limit_recent = services.policy.router_rate_limit_recent
362
+ wait_for_router_rate_limit_penalty = services.policy.wait_for_router_rate_limit_penalty
363
+ rpm = router_rate_limit_effective_rpm(provider, pcfg, model)
364
+ if rpm is None:
365
+ waited = wait_for_router_rate_limit_penalty(provider, pcfg, model, rpm)
366
+ return waited, 0, None
367
+ if rpm <= 0:
368
+ waited = wait_for_router_rate_limit_penalty(provider, pcfg, model, rpm)
369
+ used, limit = record_router_rate_usage(provider, pcfg, model, rpm)
370
+ return waited, used, limit
371
+ window = 60.0
372
+ base_interval = window / float(rpm)
373
+ capacity = router_rate_limit_capacity(rpm)
374
+ key = router_rate_limit_key(provider, pcfg, model)
375
+ multi_key = provider_api_key_count(provider, pcfg) > 1
376
+ waited = 0.0
377
+ while True:
378
+ with _RATE_LIMIT_LOCK:
379
+ try:
380
+ state = json.loads(RATE_LIMIT_STATE_PATH.read_text(encoding="utf-8")) if RATE_LIMIT_STATE_PATH.exists() else {}
381
+ if not isinstance(state, dict):
382
+ state = {}
383
+ except Exception:
384
+ state = {}
385
+ now = time.time()
386
+ entry = state.get(key)
387
+ if not isinstance(entry, dict):
388
+ legacy_key = f"{provider}:{model or current_upstream_model_id(provider, pcfg)}"
389
+ entry = state.get(legacy_key)
390
+ if isinstance(entry, dict):
391
+ timestamps = entry.get("timestamps")
392
+ try:
393
+ penalty_until = 0.0 if multi_key else float(entry.get("penalty_until") or 0.0)
394
+ except Exception:
395
+ penalty_until = 0.0
396
+ elif isinstance(entry, (int, float)):
397
+ timestamps = [float(entry)]
398
+ penalty_until = 0.0
399
+ else:
400
+ timestamps = []
401
+ penalty_until = 0.0
402
+ recent = router_rate_limit_recent(timestamps, now, window, include_future=True)
403
+ used = len(recent)
404
+ usage_ratio = min(1.0, used / float(capacity))
405
+ wait = 0.0
406
+ if penalty_until > now:
407
+ wait = max(wait, penalty_until - now)
408
+ if used >= capacity and recent:
409
+ wait = max(0.0, recent[0] + window - now)
410
+ elif recent:
411
+ elapsed_since_last = max(0.0, now - recent[-1])
412
+ wait = max(0.0, base_interval - elapsed_since_last)
413
+ if usage_ratio >= 0.70:
414
+ pressure = (usage_ratio - 0.70) / 0.30
415
+ target_interval = base_interval * (1.0 + max(0.0, min(1.0, pressure)) * 3.0)
416
+ wait = max(wait, target_interval - elapsed_since_last)
417
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
418
+ if wait <= 0.001:
419
+ recent.append(now)
420
+ new_entry = {"timestamps": recent[-rpm:], "rpm": rpm, "updated_at": now, "last_wait": waited}
421
+ if penalty_until > now:
422
+ new_entry["penalty_until"] = penalty_until
423
+ state[key] = new_entry
424
+ RATE_LIMIT_STATE_PATH.write_text(json.dumps(state, ensure_ascii=False) + "\n", encoding="utf-8")
425
+ return waited, len(recent), rpm
426
+ if used < capacity:
427
+ scheduled = now + wait
428
+ recent.append(scheduled)
429
+ new_entry = {"timestamps": recent[-rpm:], "rpm": rpm, "updated_at": scheduled, "last_wait": wait}
430
+ if penalty_until > now:
431
+ new_entry["penalty_until"] = penalty_until
432
+ state[key] = new_entry
433
+ RATE_LIMIT_STATE_PATH.write_text(json.dumps(state, ensure_ascii=False) + "\n", encoding="utf-8")
434
+ router_log("INFO", f"rate_limit_soft_wait provider={provider} model={model or ''} rpm={rpm} wait={wait:.2f}s")
435
+ time.sleep(wait)
436
+ return waited + wait, len(recent), rpm
437
+ sleep_for = min(wait, 10.0)
438
+ router_log("INFO", f"rate_limit_wait provider={provider} model={model or ''} rpm={rpm} wait={wait:.2f}s waited={waited:.2f}s")
439
+ time.sleep(sleep_for)
440
+ waited += sleep_for
441
+
442
+
443
+
444
+ __all__ = [
445
+ "ProviderKeyServices",
446
+ "RateLimitApplyPolicy",
447
+ "RateLimitApplyServices",
448
+ "RateLimitBackoffPolicy",
449
+ "RateLimitBackoffServices",
450
+ "RateLimitLearningPolicy",
451
+ "RateLimitLearningServices",
452
+ "RateLimitStateStore",
453
+ "apply_rate_limit",
454
+ "choose_provider_api_key",
455
+ "learn_rate_limit_headers",
456
+ "register_rate_limit_backoff",
457
+ ]
@@ -0,0 +1,139 @@
1
+ """Provider-aware model identity and presentation policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from typing import Mapping, Protocol
8
+
9
+ from .architecture import ProviderAdapter
10
+
11
+
12
+ class ProviderAdapterRegistryPort(Protocol):
13
+ def create(self, name: str, /) -> ProviderAdapter: ...
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class ProviderModelIdentityService:
18
+ """Resolve provider names and model identities through registered strategies."""
19
+
20
+ adapters: ProviderAdapterRegistryPort
21
+ aliases: Mapping[str, str]
22
+ labels: Mapping[str, str]
23
+
24
+ def normalize_provider(self, name: str) -> str:
25
+ key = name.strip().lower().replace("_", "-").replace(" ", "-")
26
+ try:
27
+ return self.aliases[key]
28
+ except KeyError as exc:
29
+ known = ", ".join(self.labels)
30
+ raise SystemExit(f"Unknown provider: {name}\nKnown: {known}") from exc
31
+
32
+ @staticmethod
33
+ def slug(value: str) -> str:
34
+ normalized = re.sub(r"[^a-zA-Z0-9_.-]+", "-", value.lower())
35
+ return re.sub(r"-+", "-", normalized).strip("-") or "model"
36
+
37
+ @staticmethod
38
+ def sort_key(model_id: str) -> tuple[str, str]:
39
+ return (model_id.casefold(), model_id)
40
+
41
+ def sorted_ids(self, model_ids: list[str]) -> list[str]:
42
+ return sorted(model_ids, key=self.sort_key)
43
+
44
+ def unique_ids(self, provider: str, model_ids: list[str]) -> list[str]:
45
+ seen: set[str] = set()
46
+ unique: list[str] = []
47
+ for raw in model_ids:
48
+ model_id = self.normalize_model_id(provider, str(raw))
49
+ key = model_id.casefold()
50
+ if model_id and key not in seen:
51
+ seen.add(key)
52
+ unique.append(model_id)
53
+ return unique
54
+
55
+ def normalize_model_id(self, provider: str, model_id: str) -> str:
56
+ return self.adapters.create(provider).normalize_model_id(model_id)
57
+
58
+ @staticmethod
59
+ def strip_claude_context_suffix(model_id: str | None) -> str:
60
+ text = str(model_id or "").strip()
61
+ return re.sub(r"\[(?:1m)\]\s*$", "", text, flags=re.IGNORECASE)
62
+
63
+ def upstream_api_model_id(self, provider: str, model_id: str | None) -> str:
64
+ return self.adapters.create(provider).upstream_api_model_id(str(model_id or ""))
65
+
66
+ def alias_for(self, provider: str, model_id: str) -> str:
67
+ if self.adapters.create(provider).preserves_claude_model_alias(model_id):
68
+ return model_id
69
+ return f"ciel-runtime-{provider}-{self.slug(model_id)}"
70
+
71
+ def unslug_alias(
72
+ self,
73
+ provider: str,
74
+ alias: str,
75
+ model_map: Mapping[str, str],
76
+ ) -> str | None:
77
+ alias = self.strip_claude_context_suffix(alias)
78
+ if alias in model_map:
79
+ return model_map[alias]
80
+ prefix = f"ciel-runtime-{provider}-"
81
+ if alias.startswith(prefix):
82
+ target_slug = alias[len(prefix) :]
83
+ for model_id in model_map.values():
84
+ if self.slug(model_id) == target_slug:
85
+ return model_id
86
+ return None
87
+
88
+ def display_name(self, provider: str, model_id: str) -> str:
89
+ label = self.labels.get(provider, provider)
90
+ return self.adapters.create(provider).display_model_name(model_id, label)
91
+
92
+
93
+ @dataclass(frozen=True, slots=True)
94
+ class ProviderModelIdentityApi:
95
+ """Preserve the facade's public model-identity signatures explicitly."""
96
+
97
+ service: ProviderModelIdentityService
98
+
99
+ def normalize_provider(self, name: str) -> str:
100
+ return self.service.normalize_provider(name)
101
+
102
+ def slug(self, s: str) -> str:
103
+ return self.service.slug(s)
104
+
105
+ def model_sort_key(self, model_id: str) -> tuple[str, str]:
106
+ return self.service.sort_key(model_id)
107
+
108
+ def sorted_model_ids(self, ids: list[str]) -> list[str]:
109
+ return self.service.sorted_ids(ids)
110
+
111
+ def unique_model_ids(self, provider: str, ids: list[str]) -> list[str]:
112
+ return self.service.unique_ids(provider, ids)
113
+
114
+ def normalize_model_id(self, provider: str, model_id: str) -> str:
115
+ return self.service.normalize_model_id(provider, model_id)
116
+
117
+ def strip_claude_context_suffix(self, model_id: str | None) -> str:
118
+ return self.service.strip_claude_context_suffix(model_id)
119
+
120
+ def upstream_api_model_id(self, provider: str, model_id: str | None) -> str:
121
+ return self.service.upstream_api_model_id(provider, model_id)
122
+
123
+ def alias_for(self, provider: str, model_id: str) -> str:
124
+ return self.service.alias_for(provider, model_id)
125
+
126
+ def unslug_provider_alias(
127
+ self, provider: str, alias: str, model_map: dict[str, str]
128
+ ) -> str | None:
129
+ return self.service.unslug_alias(provider, alias, model_map)
130
+
131
+ def display_name(self, provider: str, model_id: str) -> str:
132
+ return self.service.display_name(provider, model_id)
133
+
134
+
135
+ __all__ = [
136
+ "ProviderAdapterRegistryPort",
137
+ "ProviderModelIdentityApi",
138
+ "ProviderModelIdentityService",
139
+ ]