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