@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.
- package/README.md +132 -0
- package/ciel-runtime-menu.py +56 -6
- package/ciel_runtime.py +9379 -35909
- package/ciel_runtime_support/advisor_client.py +193 -0
- package/ciel_runtime_support/advisor_policy.py +320 -0
- package/ciel_runtime_support/advisor_refinement.py +160 -0
- package/ciel_runtime_support/advisor_request_builder.py +261 -0
- package/ciel_runtime_support/agy_installer.py +169 -0
- package/ciel_runtime_support/agy_mcp_restore.py +182 -0
- package/ciel_runtime_support/anthropic_model_policy.py +186 -0
- package/ciel_runtime_support/anthropic_response_writer.py +255 -0
- package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
- package/ciel_runtime_support/api_key_cooldown.py +159 -0
- package/ciel_runtime_support/architecture.py +488 -1
- package/ciel_runtime_support/architecture_budget.py +42 -0
- package/ciel_runtime_support/channel_backlog.py +90 -0
- package/ciel_runtime_support/channel_cli.py +119 -0
- package/ciel_runtime_support/channel_compact_injection.py +82 -0
- package/ciel_runtime_support/channel_compact_poll.py +67 -0
- package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
- package/ciel_runtime_support/channel_config_service.py +281 -0
- package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
- package/ciel_runtime_support/channel_connection_registry.py +128 -0
- package/ciel_runtime_support/channel_connection_worker.py +284 -0
- package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
- package/ciel_runtime_support/channel_cursor_repository.py +89 -0
- package/ciel_runtime_support/channel_cursor_service.py +178 -0
- package/ciel_runtime_support/channel_event_identity.py +212 -0
- package/ciel_runtime_support/channel_event_projection.py +315 -0
- package/ciel_runtime_support/channel_inflight.py +127 -0
- package/ciel_runtime_support/channel_injection.py +115 -0
- package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
- package/ciel_runtime_support/channel_launch_policy.py +180 -0
- package/ciel_runtime_support/channel_llm_context.py +156 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
- package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
- package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
- package/ciel_runtime_support/channel_mcp_tools.py +240 -0
- package/ciel_runtime_support/channel_mcp_transport.py +394 -0
- package/ciel_runtime_support/channel_message_dedupe.py +65 -0
- package/ciel_runtime_support/channel_message_policy.py +256 -0
- package/ciel_runtime_support/channel_message_prompt.py +305 -0
- package/ciel_runtime_support/channel_message_repository.py +234 -0
- package/ciel_runtime_support/channel_notification_projection.py +217 -0
- package/ciel_runtime_support/channel_panel.py +162 -0
- package/ciel_runtime_support/channel_pending_injection.py +209 -0
- package/ciel_runtime_support/channel_pending_poll.py +109 -0
- package/ciel_runtime_support/channel_probe_cache.py +433 -0
- package/ciel_runtime_support/channel_probe_report.py +101 -0
- package/ciel_runtime_support/channel_runtime_environment.py +181 -0
- package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
- package/ciel_runtime_support/channel_session_repository.py +90 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
- package/ciel_runtime_support/channel_terminal_input.py +277 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
- package/ciel_runtime_support/channel_tool_context.py +166 -0
- package/ciel_runtime_support/channel_transcript.py +414 -0
- package/ciel_runtime_support/channel_transcript_repository.py +96 -0
- package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
- package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
- package/ciel_runtime_support/chat_files.py +138 -0
- package/ciel_runtime_support/chat_http_controller.py +235 -0
- package/ciel_runtime_support/claude_environment.py +375 -0
- package/ciel_runtime_support/claude_router.py +247 -193
- package/ciel_runtime_support/cli_dispatch.py +792 -0
- package/ciel_runtime_support/cli_parser.py +165 -0
- package/ciel_runtime_support/cli_usage.py +100 -0
- package/ciel_runtime_support/codex_app_server.py +20 -5
- package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
- package/ciel_runtime_support/codex_cli.py +42 -6
- package/ciel_runtime_support/codex_config.py +323 -0
- package/ciel_runtime_support/codex_launch_configuration.py +240 -0
- package/ciel_runtime_support/codex_launch_policy.py +66 -0
- package/ciel_runtime_support/codex_mcp_integration.py +195 -0
- package/ciel_runtime_support/codex_mcp_restore.py +304 -0
- package/ciel_runtime_support/codex_model_catalog.py +133 -0
- package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
- package/ciel_runtime_support/codex_router.py +147 -1
- package/ciel_runtime_support/codex_session_repository.py +115 -0
- package/ciel_runtime_support/codex_session_selection.py +114 -0
- package/ciel_runtime_support/command_asset_installer.py +103 -0
- package/ciel_runtime_support/compatibility_probe.py +295 -0
- package/ciel_runtime_support/compatibility_protocol.py +251 -0
- package/ciel_runtime_support/compatibility_runtime.py +166 -0
- package/ciel_runtime_support/compatibility_test.py +370 -0
- package/ciel_runtime_support/config_migrations.py +307 -0
- package/ciel_runtime_support/config_repository.py +175 -0
- package/ciel_runtime_support/config_value_codec.py +64 -0
- package/ciel_runtime_support/configuration_cli.py +374 -0
- package/ciel_runtime_support/context_compaction.py +280 -0
- package/ciel_runtime_support/context_setup.py +208 -0
- package/ciel_runtime_support/context_summary_policy.py +392 -0
- package/ciel_runtime_support/credential_cli.py +104 -0
- package/ciel_runtime_support/credential_management.py +261 -0
- package/ciel_runtime_support/credentials.py +269 -0
- package/ciel_runtime_support/executable_discovery.py +141 -0
- package/ciel_runtime_support/github_copilot_oauth.py +335 -0
- package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
- package/ciel_runtime_support/header_forwarding.py +73 -0
- package/ciel_runtime_support/headless_config.py +221 -0
- package/ciel_runtime_support/http_response.py +129 -0
- package/ciel_runtime_support/install_diagnostics.py +149 -0
- package/ciel_runtime_support/kimi_identity.py +123 -0
- package/ciel_runtime_support/launch_diagnostics.py +204 -0
- package/ciel_runtime_support/launch_state.py +127 -0
- package/ciel_runtime_support/live_api_key_controller.py +58 -0
- package/ciel_runtime_support/llm_config_http.py +148 -0
- package/ciel_runtime_support/llm_option_config.py +259 -0
- package/ciel_runtime_support/llm_presentation_data.py +447 -0
- package/ciel_runtime_support/llm_presets.py +773 -0
- package/ciel_runtime_support/lm_studio_runtime.py +401 -0
- package/ciel_runtime_support/managed_mcp_config.py +144 -0
- package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
- package/ciel_runtime_support/managed_service_cleanup.py +89 -0
- package/ciel_runtime_support/mcp_config_reader.py +230 -0
- package/ciel_runtime_support/mcp_http_proxy.py +607 -0
- package/ciel_runtime_support/mcp_inventory.py +59 -0
- package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
- package/ciel_runtime_support/mcp_probe_codec.py +136 -0
- package/ciel_runtime_support/mcp_probe_transport.py +328 -0
- package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
- package/ciel_runtime_support/mcp_proxy_config.py +107 -0
- package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
- package/ciel_runtime_support/mcp_proxy_process.py +560 -0
- package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
- package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
- package/ciel_runtime_support/mcp_transport.py +146 -0
- package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
- package/ciel_runtime_support/model_catalog_projection.py +61 -0
- package/ciel_runtime_support/model_context_hints.py +109 -0
- package/ciel_runtime_support/model_panel.py +147 -0
- package/ciel_runtime_support/model_registry_repository.py +231 -0
- package/ciel_runtime_support/npm_runtime.py +191 -0
- package/ciel_runtime_support/ollama_catalog.py +462 -0
- package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
- package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
- package/ciel_runtime_support/ollama_context_sync.py +87 -0
- package/ciel_runtime_support/ollama_forwarding.py +449 -0
- package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
- package/ciel_runtime_support/openai_chat_router.py +64 -0
- package/ciel_runtime_support/openai_forwarding.py +194 -0
- package/ciel_runtime_support/openai_responses_router.py +291 -0
- package/ciel_runtime_support/openai_responses_stream.py +135 -0
- package/ciel_runtime_support/output_budget.py +89 -0
- package/ciel_runtime_support/package_lifecycle.py +217 -0
- package/ciel_runtime_support/plan_artifact_controller.py +104 -0
- package/ciel_runtime_support/prelaunch.py +959 -0
- package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
- package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
- package/ciel_runtime_support/prelaunch_terminal.py +764 -0
- package/ciel_runtime_support/process_control.py +708 -0
- package/ciel_runtime_support/prompt_compaction.py +322 -0
- package/ciel_runtime_support/prompt_injection.py +176 -0
- package/ciel_runtime_support/protocols/__init__.py +24 -0
- package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
- package/ciel_runtime_support/protocols/chat_projection.py +315 -0
- package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
- package/ciel_runtime_support/protocols/ollama_response.py +231 -0
- package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
- package/ciel_runtime_support/protocols/openai_responses.py +271 -0
- package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
- package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
- package/ciel_runtime_support/provider_adapters.py +160 -0
- package/ciel_runtime_support/provider_catalog_sources.py +316 -0
- package/ciel_runtime_support/provider_choice.py +201 -0
- package/ciel_runtime_support/provider_compatibility.py +165 -0
- package/ciel_runtime_support/provider_config_mutations.py +361 -0
- package/ciel_runtime_support/provider_configuration_service.py +162 -0
- package/ciel_runtime_support/provider_context.py +308 -0
- package/ciel_runtime_support/provider_contract_projection.py +73 -0
- package/ciel_runtime_support/provider_descriptor.py +82 -0
- package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
- package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
- package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
- package/ciel_runtime_support/provider_limits.py +457 -0
- package/ciel_runtime_support/provider_model_identity.py +139 -0
- package/ciel_runtime_support/provider_model_selection.py +431 -0
- package/ciel_runtime_support/provider_model_specs.py +142 -0
- package/ciel_runtime_support/provider_models.py +263 -0
- package/ciel_runtime_support/provider_network.py +176 -0
- package/ciel_runtime_support/provider_option_cli.py +238 -0
- package/ciel_runtime_support/provider_option_panel.py +275 -0
- package/ciel_runtime_support/provider_option_status.py +192 -0
- package/ciel_runtime_support/provider_policy.py +101 -0
- package/ciel_runtime_support/provider_query_policy.py +67 -0
- package/ciel_runtime_support/provider_readiness.py +112 -0
- package/ciel_runtime_support/provider_request_access.py +131 -0
- package/ciel_runtime_support/provider_request_builder.py +250 -0
- package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
- package/ciel_runtime_support/provider_runtime_info.py +113 -0
- package/ciel_runtime_support/provider_runtime_modes.py +150 -0
- package/ciel_runtime_support/provider_sampling_policy.py +46 -0
- package/ciel_runtime_support/provider_status.py +145 -0
- package/ciel_runtime_support/provider_timeout_policy.py +184 -0
- package/ciel_runtime_support/provider_tool_policy.py +145 -0
- package/ciel_runtime_support/providers/__init__.py +65 -0
- package/ciel_runtime_support/providers/anthropic.py +160 -0
- package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
- package/ciel_runtime_support/providers/base.py +232 -0
- package/ciel_runtime_support/providers/catalog.py +326 -0
- package/ciel_runtime_support/providers/cloud.py +194 -0
- package/ciel_runtime_support/providers/constants.py +54 -0
- package/ciel_runtime_support/providers/deepseek.py +115 -0
- package/ciel_runtime_support/providers/fireworks.py +158 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
- package/ciel_runtime_support/providers/kimi.py +297 -0
- package/ciel_runtime_support/providers/lm_studio.py +76 -0
- package/ciel_runtime_support/providers/meta.py +257 -0
- package/ciel_runtime_support/providers/native.py +194 -0
- package/ciel_runtime_support/providers/nim.py +69 -0
- package/ciel_runtime_support/providers/nvidia.py +158 -0
- package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
- package/ciel_runtime_support/providers/ollama.py +181 -0
- package/ciel_runtime_support/providers/ollama_context.py +195 -0
- package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
- package/ciel_runtime_support/providers/opencode.py +220 -0
- package/ciel_runtime_support/providers/opencode_go.py +37 -0
- package/ciel_runtime_support/providers/openrouter.py +57 -0
- package/ciel_runtime_support/providers/vllm.py +65 -0
- package/ciel_runtime_support/providers/zai.py +119 -0
- package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
- package/ciel_runtime_support/rate_limit_policy.py +117 -0
- package/ciel_runtime_support/rate_limit_repository.py +154 -0
- package/ciel_runtime_support/registry.py +46 -0
- package/ciel_runtime_support/request_shortcuts.py +253 -0
- package/ciel_runtime_support/request_trace.py +323 -0
- package/ciel_runtime_support/response_collection.py +209 -0
- package/ciel_runtime_support/router_access.py +238 -0
- package/ciel_runtime_support/router_client_lifecycle.py +366 -0
- package/ciel_runtime_support/router_health_policy.py +101 -0
- package/ciel_runtime_support/router_http.py +513 -0
- package/ciel_runtime_support/router_process_lifecycle.py +401 -0
- package/ciel_runtime_support/router_rate_limit_service.py +285 -0
- package/ciel_runtime_support/router_server_runtime.py +103 -0
- package/ciel_runtime_support/router_shortcuts.py +201 -0
- package/ciel_runtime_support/routing_fallback.py +73 -0
- package/ciel_runtime_support/runtime_activity_repository.py +143 -0
- package/ciel_runtime_support/runtime_adapters.py +104 -0
- package/ciel_runtime_support/runtime_command_factory.py +73 -0
- package/ciel_runtime_support/runtime_compatibility.py +50 -0
- package/ciel_runtime_support/runtime_constants.py +178 -0
- package/ciel_runtime_support/runtime_launch.py +1602 -0
- package/ciel_runtime_support/runtime_llm_options.py +312 -0
- package/ciel_runtime_support/runtime_logging.py +161 -0
- package/ciel_runtime_support/runtime_paths.py +157 -0
- package/ciel_runtime_support/runtime_restart.py +84 -0
- package/ciel_runtime_support/runtime_upgrade.py +149 -0
- package/ciel_runtime_support/secure_json_repository.py +55 -0
- package/ciel_runtime_support/session_import.py +356 -0
- package/ciel_runtime_support/settings_repository.py +8 -0
- package/ciel_runtime_support/slash_command_assets.py +211 -0
- package/ciel_runtime_support/sse_stream.py +57 -0
- package/ciel_runtime_support/sse_trace.py +225 -0
- package/ciel_runtime_support/statusline_script.py +593 -0
- package/ciel_runtime_support/statusline_settings.py +53 -0
- package/ciel_runtime_support/stream_chunk_policy.py +18 -0
- package/ciel_runtime_support/streaming_anthropic.py +1955 -0
- package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
- package/ciel_runtime_support/terminal_platform_io.py +127 -0
- package/ciel_runtime_support/timeout_profile.py +196 -0
- package/ciel_runtime_support/tool_dialects.py +85 -0
- package/ciel_runtime_support/tool_exposure_policy.py +63 -0
- package/ciel_runtime_support/tool_guard_hooks.py +218 -0
- package/ciel_runtime_support/tool_request_projection.py +96 -0
- package/ciel_runtime_support/tool_schema.py +483 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
- package/ciel_runtime_support/ui_text.py +266 -0
- package/ciel_runtime_support/upstream_error_policy.py +104 -0
- package/ciel_runtime_support/upstream_retry.py +419 -0
- package/ciel_runtime_support/upstream_stream_io.py +106 -0
- package/ciel_runtime_support/usage_events.py +96 -0
- package/ciel_runtime_support/visible_stream_filters.py +130 -0
- package/ciel_runtime_support/web_endpoints.py +447 -0
- package/ciel_runtime_support/web_ui.py +915 -0
- package/ciel_runtime_support/web_ui_controller.py +189 -0
- package/ciel_runtime_support/windows_console_input.py +137 -0
- package/ciel_runtime_support/windows_console_mode.py +112 -0
- package/docs/Architecture.md +54 -0
- package/docs/Configuration.md +17 -0
- package/docs/Module-Map.md +1093 -26
- package/docs/Providers.md +46 -1
- package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
- package/npm-bin/run-ciel-runtime.js +22 -2
- package/package.json +9 -2
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Per-credential rate-limit cooldown policy and application service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from . import rate_limit_policy
|
|
12
|
+
from .rate_limit_repository import RateLimitRepository
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
RATE_LIMIT_RESET_HEADER_NAMES = (
|
|
16
|
+
"x-ratelimit-reset-requests",
|
|
17
|
+
"x-rate-limit-reset-requests",
|
|
18
|
+
"ratelimit-reset",
|
|
19
|
+
"rate-limit-reset",
|
|
20
|
+
"x-ratelimit-reset",
|
|
21
|
+
"x-rate-limit-reset",
|
|
22
|
+
)
|
|
23
|
+
API_KEY_COOLDOWN_MAX_SECONDS = 90_000.0
|
|
24
|
+
API_KEY_COOLDOWN_DEFAULT_SECONDS = 60.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class ApiKeyCooldownPorts:
|
|
29
|
+
repository: RateLimitRepository
|
|
30
|
+
rotation_name: Callable[[str, dict[str, Any]], str]
|
|
31
|
+
config_keys: Callable[[str, dict[str, Any]], list[str]]
|
|
32
|
+
meaningful_key: Callable[[str], bool]
|
|
33
|
+
log: Callable[[str, str], None]
|
|
34
|
+
now: Callable[[], float] = time.time
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class ApiKeyCooldownService:
|
|
39
|
+
ports: ApiKeyCooldownPorts
|
|
40
|
+
|
|
41
|
+
def state_key(self, provider: str, config: dict[str, Any], key: str) -> str:
|
|
42
|
+
digest = hashlib.sha256(str(key).encode("utf-8")).hexdigest()[:12]
|
|
43
|
+
return f"{self.ports.rotation_name(provider, config)}:__key__:{digest}"
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def reset_seconds(headers: Any) -> float:
|
|
47
|
+
reset = rate_limit_policy.reset_seconds(
|
|
48
|
+
rate_limit_policy.first_header(headers, list(RATE_LIMIT_RESET_HEADER_NAMES))
|
|
49
|
+
)
|
|
50
|
+
if reset is None or reset <= 0:
|
|
51
|
+
reset = rate_limit_policy.retry_after_seconds(
|
|
52
|
+
rate_limit_policy.first_header(headers, ["Retry-After", "retry-after"])
|
|
53
|
+
)
|
|
54
|
+
if reset is None or reset <= 0:
|
|
55
|
+
reset = API_KEY_COOLDOWN_DEFAULT_SECONDS
|
|
56
|
+
return max(1.0, min(float(reset), API_KEY_COOLDOWN_MAX_SECONDS))
|
|
57
|
+
|
|
58
|
+
def register(
|
|
59
|
+
self,
|
|
60
|
+
provider: str,
|
|
61
|
+
config: dict[str, Any],
|
|
62
|
+
key: str,
|
|
63
|
+
headers: Any,
|
|
64
|
+
) -> float:
|
|
65
|
+
if not self.ports.meaningful_key(key):
|
|
66
|
+
return 0.0
|
|
67
|
+
reset = self.reset_seconds(headers)
|
|
68
|
+
state_key = self.state_key(provider, config, key)
|
|
69
|
+
self.ports.repository.register_cooldown(state_key, reset)
|
|
70
|
+
self.ports.log(
|
|
71
|
+
"WARN",
|
|
72
|
+
f"api_key_cooldown provider={provider} "
|
|
73
|
+
f"key_hash={state_key.rsplit(':', 1)[-1]} rest={reset:.0f}s",
|
|
74
|
+
)
|
|
75
|
+
return reset
|
|
76
|
+
|
|
77
|
+
def cooldown_until(self, provider: str, config: dict[str, Any], key: str) -> float:
|
|
78
|
+
if not self.ports.meaningful_key(key):
|
|
79
|
+
return 0.0
|
|
80
|
+
return self.ports.repository.cooldown_until(self.state_key(provider, config, key))
|
|
81
|
+
|
|
82
|
+
def live_key_count(self, provider: str, config: dict[str, Any]) -> int:
|
|
83
|
+
keys = self.ports.config_keys(provider, config)
|
|
84
|
+
if len(keys) <= 1:
|
|
85
|
+
return len(keys)
|
|
86
|
+
now = self.ports.now()
|
|
87
|
+
return sum(1 for key in keys if self.cooldown_until(provider, config, key) <= now)
|
|
88
|
+
|
|
89
|
+
def has_live_key(self, provider: str, config: dict[str, Any]) -> bool:
|
|
90
|
+
return self.live_key_count(provider, config) > 0
|
|
91
|
+
|
|
92
|
+
def reset_for_router_start(self) -> int:
|
|
93
|
+
removed = self.ports.repository.reset_key_cooldowns()
|
|
94
|
+
if removed:
|
|
95
|
+
self.ports.log(
|
|
96
|
+
"INFO",
|
|
97
|
+
f"api_key_cooldown_reset_on_router_start removed={removed}",
|
|
98
|
+
)
|
|
99
|
+
return removed
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True, slots=True)
|
|
103
|
+
class ApiKeyCooldownCompatibilityApi:
|
|
104
|
+
"""Explicit facade surface that resolves the current repository per call."""
|
|
105
|
+
|
|
106
|
+
service_factory: Callable[[], ApiKeyCooldownService]
|
|
107
|
+
|
|
108
|
+
def state_key(self, provider: str, config: dict[str, Any], key: str) -> str:
|
|
109
|
+
return self.service_factory().state_key(provider, config, key)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def reset_seconds(headers: Any) -> float:
|
|
113
|
+
return ApiKeyCooldownService.reset_seconds(headers)
|
|
114
|
+
|
|
115
|
+
def register(
|
|
116
|
+
self,
|
|
117
|
+
provider: str,
|
|
118
|
+
config: dict[str, Any],
|
|
119
|
+
key: str,
|
|
120
|
+
headers: Any,
|
|
121
|
+
) -> float:
|
|
122
|
+
return self.service_factory().register(provider, config, key, headers)
|
|
123
|
+
|
|
124
|
+
def cooldown_until(
|
|
125
|
+
self, provider: str, config: dict[str, Any], key: str
|
|
126
|
+
) -> float:
|
|
127
|
+
return self.service_factory().cooldown_until(provider, config, key)
|
|
128
|
+
|
|
129
|
+
def live_key_count(self, provider: str, config: dict[str, Any]) -> int:
|
|
130
|
+
return self.service_factory().live_key_count(provider, config)
|
|
131
|
+
|
|
132
|
+
def has_live_key(self, provider: str, config: dict[str, Any]) -> bool:
|
|
133
|
+
return self.service_factory().has_live_key(provider, config)
|
|
134
|
+
|
|
135
|
+
def reset_for_router_start(self) -> int:
|
|
136
|
+
return self.service_factory().reset_for_router_start()
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def retry_after_exceeds_request_timeout(
|
|
140
|
+
headers: Any, timeout: float
|
|
141
|
+
) -> tuple[bool, float | None]:
|
|
142
|
+
retry_after = rate_limit_policy.first_header(
|
|
143
|
+
headers, ["Retry-After", "retry-after"]
|
|
144
|
+
)
|
|
145
|
+
seconds = rate_limit_policy.retry_after_seconds(retry_after)
|
|
146
|
+
if seconds is None:
|
|
147
|
+
return False, None
|
|
148
|
+
threshold = max(1.0, float(timeout) - 1.0)
|
|
149
|
+
return seconds >= threshold, seconds
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
__all__ = [
|
|
153
|
+
"API_KEY_COOLDOWN_DEFAULT_SECONDS",
|
|
154
|
+
"API_KEY_COOLDOWN_MAX_SECONDS",
|
|
155
|
+
"ApiKeyCooldownCompatibilityApi",
|
|
156
|
+
"ApiKeyCooldownPorts",
|
|
157
|
+
"ApiKeyCooldownService",
|
|
158
|
+
"RATE_LIMIT_RESET_HEADER_NAMES",
|
|
159
|
+
]
|
|
@@ -19,11 +19,18 @@ from __future__ import annotations
|
|
|
19
19
|
from abc import ABC, abstractmethod
|
|
20
20
|
from dataclasses import dataclass, field
|
|
21
21
|
from pathlib import Path
|
|
22
|
+
import re
|
|
22
23
|
from typing import Any, Literal, Mapping, Sequence
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
LaunchMode = Literal["native", "routed", "router"]
|
|
26
|
-
MessageProtocol = Literal[
|
|
27
|
+
MessageProtocol = Literal[
|
|
28
|
+
"anthropic_messages",
|
|
29
|
+
"openai_chat",
|
|
30
|
+
"openai_responses",
|
|
31
|
+
"ollama_chat",
|
|
32
|
+
"google_generative",
|
|
33
|
+
]
|
|
27
34
|
|
|
28
35
|
|
|
29
36
|
@dataclass(frozen=True)
|
|
@@ -100,6 +107,132 @@ class RateLimitState:
|
|
|
100
107
|
detail: str | None = None
|
|
101
108
|
|
|
102
109
|
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class ProviderCapabilities:
|
|
112
|
+
"""Provider-owned features that affect routing without naming providers."""
|
|
113
|
+
|
|
114
|
+
upstream_protocol: MessageProtocol = "openai_chat"
|
|
115
|
+
supports_streaming: bool = True
|
|
116
|
+
supports_tools: bool = True
|
|
117
|
+
supports_tool_choice: bool = True
|
|
118
|
+
supports_thinking: bool = False
|
|
119
|
+
preserves_anthropic_thinking: bool = False
|
|
120
|
+
requires_api_key: bool = False
|
|
121
|
+
local: bool = False
|
|
122
|
+
blocks_default_tools: bool = True
|
|
123
|
+
repairs_anthropic_tool_input: bool = False
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def supported_protocols(self) -> frozenset[MessageProtocol]:
|
|
127
|
+
return frozenset({self.upstream_protocol})
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class ProviderRequestPolicy:
|
|
132
|
+
"""Provider-owned endpoint and transport defaults."""
|
|
133
|
+
|
|
134
|
+
chat_path: str
|
|
135
|
+
models_path: str
|
|
136
|
+
model_info_path: str | None = None
|
|
137
|
+
default_timeout_seconds: float = 60.0
|
|
138
|
+
model_alias_strategy: Literal["identity", "ncp"] = "identity"
|
|
139
|
+
credential_strategy: Literal["adapter", "anthropic_inbound"] = "adapter"
|
|
140
|
+
stream_required: bool = False
|
|
141
|
+
normalize_historical_tool_turns: bool = True
|
|
142
|
+
managed_service: Literal["none", "nvidia_proxy"] = "none"
|
|
143
|
+
probe_strategy: Literal["anthropic", "ollama", "opencode"] = "anthropic"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass(frozen=True)
|
|
147
|
+
class ProviderModelCatalogPolicy:
|
|
148
|
+
"""Select a reusable model-catalog strategy without provider-name branching."""
|
|
149
|
+
|
|
150
|
+
kind: Literal["configured", "anthropic", "openai", "ollama", "lm_studio", "nvidia", "fireworks"] = "openai"
|
|
151
|
+
fallback_models: tuple[str, ...] = ()
|
|
152
|
+
allow_configured_fallback: bool = False
|
|
153
|
+
allow_public_without_auth: bool = False
|
|
154
|
+
use_bundled_catalog_fallback: bool = False
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(frozen=True)
|
|
158
|
+
class ProviderConfigurationPolicy:
|
|
159
|
+
"""Provider-owned configuration mutation capabilities."""
|
|
160
|
+
|
|
161
|
+
mutation_strategy: Literal["common", "ollama"] = "common"
|
|
162
|
+
supports_route_through_router: bool = False
|
|
163
|
+
supports_model_endpoint_overrides: bool = False
|
|
164
|
+
native_compat_error: str | None = None
|
|
165
|
+
text_option_aliases: Mapping[str, str] = field(default_factory=dict)
|
|
166
|
+
strip_trailing_slash_fields: frozenset[str] = frozenset()
|
|
167
|
+
status_fields: tuple[str, ...] = ()
|
|
168
|
+
uses_ollama_status: bool = False
|
|
169
|
+
runtime_owns_model: bool = False
|
|
170
|
+
restricts_runtime_options: bool = False
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class ProviderStatusPolicy:
|
|
175
|
+
"""Provider-owned base URL status projection strategy."""
|
|
176
|
+
|
|
177
|
+
kind: Literal["generic", "native_codex", "native_agy", "nvidia", "configured", "catalog"] = "generic"
|
|
178
|
+
label: str = ""
|
|
179
|
+
configured_description: str = ""
|
|
180
|
+
catalog_path: str = ""
|
|
181
|
+
catalog_count_key: Literal["data", "models"] = "data"
|
|
182
|
+
catalog_scope: Literal["configured", "fireworks_management"] = "configured"
|
|
183
|
+
catalog_count_label: str = "models"
|
|
184
|
+
unreachable_hint: str = "Set a reachable Base URL before launching Claude Code."
|
|
185
|
+
readiness_validation: Literal["none", "lm_studio"] = "none"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass(frozen=True)
|
|
189
|
+
class ProviderContextPolicy:
|
|
190
|
+
"""Provider-owned context capacity and configuration strategy."""
|
|
191
|
+
|
|
192
|
+
capacity_strategy: Literal[
|
|
193
|
+
"managed", "nvidia", "remote_first", "hint_first", "configured_first", "ollama", "hint_configured", "anthropic_hint"
|
|
194
|
+
] = "managed"
|
|
195
|
+
settings_strategy: Literal["managed", "ollama", "standard"] = "managed"
|
|
196
|
+
hosted_timeout: bool = False
|
|
197
|
+
timeout_weight: float = 1.0
|
|
198
|
+
uses_catalog_timeout: bool = False
|
|
199
|
+
managed_preset_inference: bool = False
|
|
200
|
+
context_family_before_size_markers: bool = False
|
|
201
|
+
preset_context_profile: Literal["default", "ollama", "nvidia"] = "default"
|
|
202
|
+
status_capacity_strategy: Literal[
|
|
203
|
+
"configured", "ollama_budget", "openai_budget", "provider"
|
|
204
|
+
] = "configured"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass(frozen=True)
|
|
208
|
+
class ProviderOptionPresentationPolicy:
|
|
209
|
+
"""Provider-owned option status capabilities."""
|
|
210
|
+
|
|
211
|
+
show_rate_limit: bool = False
|
|
212
|
+
show_native: bool = False
|
|
213
|
+
show_route: bool = False
|
|
214
|
+
show_tool_choice: bool = False
|
|
215
|
+
show_sampling: bool = False
|
|
216
|
+
show_stream: bool = False
|
|
217
|
+
show_ip_family: bool = False
|
|
218
|
+
show_rate_limit_controls: bool = False
|
|
219
|
+
show_sampling_controls: bool = False
|
|
220
|
+
show_ip_family_control: bool = False
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@dataclass(frozen=True)
|
|
224
|
+
class ProviderUiPolicy:
|
|
225
|
+
"""Provider-owned labels used by shared menus."""
|
|
226
|
+
|
|
227
|
+
menu_label: str = ""
|
|
228
|
+
routed_menu_label: str = ""
|
|
229
|
+
native_choice: str = ""
|
|
230
|
+
routed_choice: str = ""
|
|
231
|
+
model_placeholder: str = ""
|
|
232
|
+
advisor_placeholder: str = ""
|
|
233
|
+
uses_native_advisor: bool = False
|
|
234
|
+
|
|
235
|
+
|
|
103
236
|
class RuntimeAdapter(ABC):
|
|
104
237
|
"""Adapter for a local/interactive coding runtime.
|
|
105
238
|
|
|
@@ -136,6 +269,22 @@ class ProviderAdapter(ABC):
|
|
|
136
269
|
def default_base_url(self) -> str:
|
|
137
270
|
"""Return the provider default API base URL."""
|
|
138
271
|
|
|
272
|
+
def normalize_base_url(self, value: str) -> str:
|
|
273
|
+
"""Normalize a user-supplied endpoint before it is persisted."""
|
|
274
|
+
|
|
275
|
+
return str(value or "").rstrip("/")
|
|
276
|
+
|
|
277
|
+
def default_configuration(self) -> Mapping[str, Any]:
|
|
278
|
+
"""Return the minimal persisted configuration for a newly registered provider."""
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
"base_url": self.default_base_url(),
|
|
282
|
+
"api_key": "",
|
|
283
|
+
"current_model": "",
|
|
284
|
+
"advisor_model": "",
|
|
285
|
+
"custom_models": [],
|
|
286
|
+
}
|
|
287
|
+
|
|
139
288
|
@abstractmethod
|
|
140
289
|
def list_models(self, config: ProviderConfig) -> Sequence[ModelInfo]:
|
|
141
290
|
"""Return known models for this provider."""
|
|
@@ -149,6 +298,343 @@ class ProviderAdapter(ABC):
|
|
|
149
298
|
|
|
150
299
|
return None
|
|
151
300
|
|
|
301
|
+
def capabilities(self, config: ProviderConfig) -> ProviderCapabilities:
|
|
302
|
+
"""Return routing capabilities for this configured provider."""
|
|
303
|
+
|
|
304
|
+
del config
|
|
305
|
+
return ProviderCapabilities()
|
|
306
|
+
|
|
307
|
+
def request_policy(self, config: ProviderConfig) -> ProviderRequestPolicy:
|
|
308
|
+
"""Return endpoint and transport defaults for this provider."""
|
|
309
|
+
|
|
310
|
+
del config
|
|
311
|
+
return ProviderRequestPolicy(chat_path="/v1/chat/completions", models_path="/v1/models")
|
|
312
|
+
|
|
313
|
+
def resolve_endpoint(self, operation: str, config: ProviderConfig) -> str:
|
|
314
|
+
"""Resolve a provider operation to a path without exposing provider conditionals."""
|
|
315
|
+
|
|
316
|
+
policy = self.request_policy(config)
|
|
317
|
+
paths = {
|
|
318
|
+
"chat": policy.chat_path,
|
|
319
|
+
"models": policy.models_path,
|
|
320
|
+
"model_info": policy.model_info_path,
|
|
321
|
+
"anthropic_messages": "/v1/messages",
|
|
322
|
+
"openai_chat": "/v1/chat/completions",
|
|
323
|
+
"openai_responses": "/v1/responses",
|
|
324
|
+
"ollama_chat": "/api/chat",
|
|
325
|
+
}
|
|
326
|
+
path = paths.get(operation)
|
|
327
|
+
if not path:
|
|
328
|
+
raise KeyError(f"{self.name} does not support provider operation: {operation}")
|
|
329
|
+
return path
|
|
330
|
+
|
|
331
|
+
def build_model_headers(self, config: ProviderConfig, api_key: str | None) -> Mapping[str, str]:
|
|
332
|
+
"""Build headers for model discovery; providers may override request auth."""
|
|
333
|
+
|
|
334
|
+
return self.build_headers(config, api_key)
|
|
335
|
+
|
|
336
|
+
def model_paths(self, config: ProviderConfig) -> tuple[str, ...]:
|
|
337
|
+
"""Return model discovery paths in provider-preferred fallback order."""
|
|
338
|
+
|
|
339
|
+
primary = self.request_policy(config).models_path
|
|
340
|
+
return (primary,) if primary == "/models" else (primary, "/models")
|
|
341
|
+
|
|
342
|
+
def supported_protocols(self, config: ProviderConfig, model: str | None = None) -> frozenset[MessageProtocol]:
|
|
343
|
+
"""Return protocols this provider can use for the selected model."""
|
|
344
|
+
|
|
345
|
+
del model
|
|
346
|
+
return self.capabilities(config).supported_protocols
|
|
347
|
+
|
|
348
|
+
def select_protocol(
|
|
349
|
+
self,
|
|
350
|
+
operation: MessageProtocol,
|
|
351
|
+
config: ProviderConfig,
|
|
352
|
+
model: str | None = None,
|
|
353
|
+
) -> MessageProtocol:
|
|
354
|
+
"""Choose the upstream protocol for an inbound protocol operation."""
|
|
355
|
+
|
|
356
|
+
supported = self.supported_protocols(config, model)
|
|
357
|
+
if operation in supported:
|
|
358
|
+
return operation
|
|
359
|
+
return self.capabilities(config).upstream_protocol
|
|
360
|
+
|
|
361
|
+
def supports_tool_choice(self, config: ProviderConfig, model: str | None = None) -> bool:
|
|
362
|
+
configured = config.options.get("supports_tool_choice")
|
|
363
|
+
if configured is not None:
|
|
364
|
+
return bool(configured)
|
|
365
|
+
del model
|
|
366
|
+
return self.capabilities(config).supports_tool_choice
|
|
367
|
+
|
|
368
|
+
def normalizes_anthropic_tool_use(
|
|
369
|
+
self,
|
|
370
|
+
config: ProviderConfig,
|
|
371
|
+
) -> bool:
|
|
372
|
+
configured = config.options.get("normalize_anthropic_tool_use")
|
|
373
|
+
if configured is not None:
|
|
374
|
+
return bool(configured)
|
|
375
|
+
capabilities = self.capabilities(config)
|
|
376
|
+
return (
|
|
377
|
+
capabilities.upstream_protocol != "anthropic_messages"
|
|
378
|
+
and not capabilities.preserves_anthropic_thinking
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def preserves_anthropic_thinking(self, config: ProviderConfig) -> bool:
|
|
382
|
+
configured = config.options.get("preserve_anthropic_thinking")
|
|
383
|
+
if configured is not None:
|
|
384
|
+
return bool(configured)
|
|
385
|
+
return self.capabilities(config).preserves_anthropic_thinking
|
|
386
|
+
|
|
387
|
+
def openai_reasoning_passback_enabled(
|
|
388
|
+
self, config: ProviderConfig, model: str | None = None
|
|
389
|
+
) -> bool:
|
|
390
|
+
"""Whether OpenAI reasoning_content should survive history conversion."""
|
|
391
|
+
|
|
392
|
+
del config, model
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
def model_catalog_policy(self, config: ProviderConfig) -> ProviderModelCatalogPolicy:
|
|
396
|
+
del config
|
|
397
|
+
return ProviderModelCatalogPolicy()
|
|
398
|
+
|
|
399
|
+
def project_model_metadata(self, raw: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
400
|
+
"""Project provider response fields into the shared model metadata shape."""
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
key: raw[key]
|
|
404
|
+
for key in ("owned_by", "root", "object")
|
|
405
|
+
if raw.get(key) is not None
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
def project_router_model_metadata(
|
|
409
|
+
self, config: ProviderConfig, model_id: str
|
|
410
|
+
) -> Mapping[str, Any]:
|
|
411
|
+
"""Project provider-specific metadata exposed by the router model catalog."""
|
|
412
|
+
|
|
413
|
+
del config, model_id
|
|
414
|
+
return {}
|
|
415
|
+
|
|
416
|
+
def normalize_model_id(self, model_id: str) -> str:
|
|
417
|
+
"""Normalize a configured/catalog model id for shared runtime use."""
|
|
418
|
+
|
|
419
|
+
text = str(model_id or "").strip()
|
|
420
|
+
return re.sub(r"\[(?:1m)\]\s*$", "", text, flags=re.IGNORECASE).strip()
|
|
421
|
+
|
|
422
|
+
def upstream_api_model_id(self, model_id: str) -> str:
|
|
423
|
+
"""Return the provider's wire-level model id."""
|
|
424
|
+
|
|
425
|
+
return str(model_id or "").strip()
|
|
426
|
+
|
|
427
|
+
def preserves_claude_model_alias(self, model_id: str) -> bool:
|
|
428
|
+
"""Whether an already Claude-facing model id must remain unchanged."""
|
|
429
|
+
|
|
430
|
+
del model_id
|
|
431
|
+
return False
|
|
432
|
+
|
|
433
|
+
def display_model_name(self, model_id: str, provider_label: str) -> str:
|
|
434
|
+
"""Project a provider model id into the shared human-readable label."""
|
|
435
|
+
|
|
436
|
+
cleaned = str(model_id).replace("/", " ").replace("-", " ").replace("_", " ")
|
|
437
|
+
return (
|
|
438
|
+
f"{provider_label.replace('-', ' ')} {cleaned}"
|
|
439
|
+
.title()
|
|
440
|
+
.replace("Vllm", "vLLM")
|
|
441
|
+
.replace("Nvidia", "Nvidia")
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
def launch_model_strategy(self, config: ProviderConfig) -> str:
|
|
445
|
+
"""Return the provider-owned launch alias strategy."""
|
|
446
|
+
|
|
447
|
+
del config
|
|
448
|
+
return "alias"
|
|
449
|
+
|
|
450
|
+
def requires_catalog_model_selection(self, config: ProviderConfig) -> bool:
|
|
451
|
+
"""Whether placeholder model ids must be replaced from provider discovery."""
|
|
452
|
+
|
|
453
|
+
del config
|
|
454
|
+
return False
|
|
455
|
+
|
|
456
|
+
def placeholder_model_ids(self) -> frozenset[str]:
|
|
457
|
+
"""Return non-routable placeholder model ids accepted in configuration."""
|
|
458
|
+
|
|
459
|
+
return frozenset({"", "model"})
|
|
460
|
+
|
|
461
|
+
def routing_mode_update(self, enabled: bool) -> tuple[str, ...]:
|
|
462
|
+
"""Describe a persisted route-through-router mode change."""
|
|
463
|
+
|
|
464
|
+
state = "routed through ciel-runtime router" if enabled else "direct provider mode"
|
|
465
|
+
return ("Provider routing mode updated.", f"mode: {state}")
|
|
466
|
+
|
|
467
|
+
def selection_config_updates(self, config: ProviderConfig) -> Mapping[str, Any]:
|
|
468
|
+
"""Return provider-owned defaults applied when this provider is selected."""
|
|
469
|
+
|
|
470
|
+
del config
|
|
471
|
+
return {}
|
|
472
|
+
|
|
473
|
+
def selection_update_status_lines(
|
|
474
|
+
self, config: ProviderConfig, updates: Mapping[str, Any]
|
|
475
|
+
) -> tuple[str, ...]:
|
|
476
|
+
"""Describe provider-owned normalization performed during selection."""
|
|
477
|
+
|
|
478
|
+
del config, updates
|
|
479
|
+
return ()
|
|
480
|
+
|
|
481
|
+
def model_selection_config_updates(
|
|
482
|
+
self, config: ProviderConfig, model_id: str
|
|
483
|
+
) -> Mapping[str, Any]:
|
|
484
|
+
"""Return provider-owned config updates for an explicit model choice."""
|
|
485
|
+
|
|
486
|
+
del config, model_id
|
|
487
|
+
return {}
|
|
488
|
+
|
|
489
|
+
def selection_status_lines(self, config: ProviderConfig) -> tuple[str, ...]:
|
|
490
|
+
"""Return provider-owned status details after selection."""
|
|
491
|
+
|
|
492
|
+
del config
|
|
493
|
+
return ()
|
|
494
|
+
|
|
495
|
+
def configuration_policy(self, config: ProviderConfig) -> ProviderConfigurationPolicy:
|
|
496
|
+
"""Return provider-owned option mutation behavior."""
|
|
497
|
+
|
|
498
|
+
del config
|
|
499
|
+
return ProviderConfigurationPolicy()
|
|
500
|
+
|
|
501
|
+
def status_policy(self, config: ProviderConfig) -> ProviderStatusPolicy:
|
|
502
|
+
"""Return provider-owned base URL status behavior."""
|
|
503
|
+
|
|
504
|
+
request = self.request_policy(config)
|
|
505
|
+
count_key = "models" if request.models_path == "/api/tags" else "data"
|
|
506
|
+
return ProviderStatusPolicy(catalog_path=request.models_path, catalog_count_key=count_key)
|
|
507
|
+
|
|
508
|
+
def context_policy(self, config: ProviderConfig) -> ProviderContextPolicy:
|
|
509
|
+
"""Return provider-owned context capacity and mutation behavior."""
|
|
510
|
+
|
|
511
|
+
del config
|
|
512
|
+
return ProviderContextPolicy()
|
|
513
|
+
|
|
514
|
+
def option_presentation_policy(self, config: ProviderConfig) -> ProviderOptionPresentationPolicy:
|
|
515
|
+
del config
|
|
516
|
+
return ProviderOptionPresentationPolicy()
|
|
517
|
+
|
|
518
|
+
def ui_policy(self, config: ProviderConfig) -> ProviderUiPolicy:
|
|
519
|
+
"""Return provider-owned presentation and runtime compatibility metadata."""
|
|
520
|
+
|
|
521
|
+
del config
|
|
522
|
+
return ProviderUiPolicy(menu_label=self.name)
|
|
523
|
+
|
|
524
|
+
def shows_claude_workflow_options(self, config: ProviderConfig) -> bool:
|
|
525
|
+
del config
|
|
526
|
+
return True
|
|
527
|
+
|
|
528
|
+
def intercepts_advisor_shortcut(self, config: ProviderConfig) -> bool:
|
|
529
|
+
"""Whether the router should handle /advisor instead of the native runtime."""
|
|
530
|
+
|
|
531
|
+
del config
|
|
532
|
+
return True
|
|
533
|
+
|
|
534
|
+
def option_timeout_default(self) -> str:
|
|
535
|
+
return "default"
|
|
536
|
+
|
|
537
|
+
def model_configuration_profile(
|
|
538
|
+
self, config: ProviderConfig
|
|
539
|
+
) -> tuple[Mapping[str, Any], str | None]:
|
|
540
|
+
"""Return provider/model-specific configuration updates and an optional notice."""
|
|
541
|
+
|
|
542
|
+
del config
|
|
543
|
+
return {}, None
|
|
544
|
+
|
|
545
|
+
def propagates_inbound_beta_query(self, config: ProviderConfig) -> bool:
|
|
546
|
+
"""Whether an inbound Claude beta query should be forwarded upstream."""
|
|
547
|
+
|
|
548
|
+
del config
|
|
549
|
+
return False
|
|
550
|
+
|
|
551
|
+
def api_key_display_name(self) -> str:
|
|
552
|
+
"""Return the provider-owned name used in API-key status text."""
|
|
553
|
+
|
|
554
|
+
return self.name
|
|
555
|
+
|
|
556
|
+
def api_key_status(self, config: ProviderConfig, *, key_count: int, primary_detail: str) -> str:
|
|
557
|
+
"""Format API-key readiness without central provider-name branching."""
|
|
558
|
+
|
|
559
|
+
scope = self.api_key_display_name()
|
|
560
|
+
round_robin = f"{key_count} keys, round-robin"
|
|
561
|
+
if key_count > 1:
|
|
562
|
+
detail = f"{scope}{primary_detail}" if scope else primary_detail.lstrip("; ")
|
|
563
|
+
return f"API keys: {round_robin} ({detail})"
|
|
564
|
+
if key_count:
|
|
565
|
+
detail = f"{scope}{primary_detail}" if scope else primary_detail.lstrip("; ")
|
|
566
|
+
return f"API key: set ({detail})"
|
|
567
|
+
if self.capabilities(config).requires_api_key:
|
|
568
|
+
return f"API key: missing ({scope} required)"
|
|
569
|
+
if self.capabilities(config).local:
|
|
570
|
+
return f"API key: not required for {scope}"
|
|
571
|
+
return "API key: optional or not configured"
|
|
572
|
+
|
|
573
|
+
def launch_api_key_error(self, config: ProviderConfig) -> str | None:
|
|
574
|
+
"""Return a launch blocker when this provider requires an absent key."""
|
|
575
|
+
|
|
576
|
+
if self.capabilities(config).requires_api_key and not config.api_keys:
|
|
577
|
+
return f"Launch blocked: {self.api_key_display_name()} requires an API key."
|
|
578
|
+
return None
|
|
579
|
+
|
|
580
|
+
def model_panel_badge(self, config: ProviderConfig, model: str) -> str:
|
|
581
|
+
"""Return an optional provider-owned model label for selection UIs."""
|
|
582
|
+
|
|
583
|
+
del config, model
|
|
584
|
+
return ""
|
|
585
|
+
|
|
586
|
+
def supports_server_advisor_tool(self, config: ProviderConfig) -> bool:
|
|
587
|
+
"""Whether the upstream executes Claude's server-side advisor tool."""
|
|
588
|
+
|
|
589
|
+
del config
|
|
590
|
+
return False
|
|
591
|
+
|
|
592
|
+
def context_compaction_available(self, config: ProviderConfig) -> bool:
|
|
593
|
+
"""Whether this configured provider can run an auxiliary summary request."""
|
|
594
|
+
|
|
595
|
+
del config
|
|
596
|
+
return True
|
|
597
|
+
|
|
598
|
+
def router_native_anthropic_enabled(self, config: ProviderConfig, model: str | None = None) -> bool:
|
|
599
|
+
"""Whether the generic router may send Anthropic Messages directly upstream."""
|
|
600
|
+
|
|
601
|
+
del config, model
|
|
602
|
+
return False
|
|
603
|
+
|
|
604
|
+
def advisor_panel_notice(self, config: ProviderConfig) -> tuple[tuple[str, ...], tuple[str, ...]] | None:
|
|
605
|
+
"""Return a provider-owned advisor panel replacement, when applicable."""
|
|
606
|
+
|
|
607
|
+
del config
|
|
608
|
+
return None
|
|
609
|
+
|
|
610
|
+
def advisor_model_badge(self, config: ProviderConfig, model: str) -> str:
|
|
611
|
+
"""Return an optional provider-owned advisor model annotation."""
|
|
612
|
+
|
|
613
|
+
del config, model
|
|
614
|
+
return ""
|
|
615
|
+
|
|
616
|
+
def normalize_request_options(self, config: ProviderConfig, request: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
617
|
+
del config
|
|
618
|
+
return request
|
|
619
|
+
|
|
620
|
+
def openai_reasoning_effort(
|
|
621
|
+
self, config: ProviderConfig, model: str, request: Mapping[str, Any]
|
|
622
|
+
) -> str | None:
|
|
623
|
+
"""Return a provider-normalized Chat Completions reasoning effort."""
|
|
624
|
+
|
|
625
|
+
del config, model, request
|
|
626
|
+
return None
|
|
627
|
+
|
|
628
|
+
def allows_sampling_overrides(self, config: ProviderConfig) -> bool:
|
|
629
|
+
"""Whether user-provided sampling controls are valid for this provider."""
|
|
630
|
+
|
|
631
|
+
del config
|
|
632
|
+
return True
|
|
633
|
+
|
|
634
|
+
def normalize_tool_choice(self, config: ProviderConfig, model: str, tool_choice: Any) -> Any:
|
|
635
|
+
del config, model
|
|
636
|
+
return tool_choice
|
|
637
|
+
|
|
152
638
|
|
|
153
639
|
class MessageProtocolAdapter(ABC):
|
|
154
640
|
"""Adapter for upstream request/response wire formats."""
|
|
@@ -191,6 +677,7 @@ __all__ = [
|
|
|
191
677
|
"ModelInfo",
|
|
192
678
|
"ProviderAdapter",
|
|
193
679
|
"ProviderConfig",
|
|
680
|
+
"ProviderUiPolicy",
|
|
194
681
|
"RateLimitState",
|
|
195
682
|
"RuntimeAdapter",
|
|
196
683
|
"RuntimeCommand",
|