@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,109 @@
|
|
|
1
|
+
"""Pure model-identity heuristics for context-capacity hints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class ModelContextHintPorts:
|
|
13
|
+
strip_context_suffix: Callable[[str], str]
|
|
14
|
+
catalog_context: Callable[[str], tuple[int | None, str | None, str | None]]
|
|
15
|
+
model_preset: Callable[[str], Mapping[str, Any]]
|
|
16
|
+
positive_int: Callable[[Any], int | None]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ModelContextHintPolicy:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
zai_hints: Sequence[tuple[str, int]],
|
|
23
|
+
ports: ModelContextHintPorts,
|
|
24
|
+
) -> None:
|
|
25
|
+
self.zai_hints = tuple(zai_hints)
|
|
26
|
+
self.ports = ports
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def is_qwen36_plus(model_id: str) -> bool:
|
|
30
|
+
compact = re.sub(r"[^a-z0-9]+", "", (model_id or "").lower())
|
|
31
|
+
return "qwen36plus" in compact
|
|
32
|
+
|
|
33
|
+
def is_kimi_k3(self, model_id: str) -> bool:
|
|
34
|
+
normalized = (
|
|
35
|
+
self.ports.strip_context_suffix(model_id)
|
|
36
|
+
.strip()
|
|
37
|
+
.lower()
|
|
38
|
+
.replace("_", "-")
|
|
39
|
+
)
|
|
40
|
+
if normalized.startswith("ciel-runtime-kimi-"):
|
|
41
|
+
normalized = normalized[len("ciel-runtime-kimi-") :]
|
|
42
|
+
return normalized in {"k3", "kimi-k3", "kimi/k3", "kimi-code/k3"}
|
|
43
|
+
|
|
44
|
+
def zai_hint(self, model_id: str) -> int | None:
|
|
45
|
+
model = (
|
|
46
|
+
self.ports.strip_context_suffix(model_id)
|
|
47
|
+
.strip()
|
|
48
|
+
.lower()
|
|
49
|
+
.replace("_", "-")
|
|
50
|
+
)
|
|
51
|
+
if not model:
|
|
52
|
+
return None
|
|
53
|
+
for prefix, limit in self.zai_hints:
|
|
54
|
+
if model == prefix or model.startswith(prefix + "-"):
|
|
55
|
+
return limit
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def resolve(self, model_id: str) -> int | None:
|
|
59
|
+
model = (model_id or "").lower()
|
|
60
|
+
if not model:
|
|
61
|
+
return None
|
|
62
|
+
zai_hint = self.zai_hint(model_id)
|
|
63
|
+
if zai_hint:
|
|
64
|
+
return zai_hint
|
|
65
|
+
if self.is_qwen36_plus(model_id):
|
|
66
|
+
return 1048576
|
|
67
|
+
if self.is_kimi_k3(model_id):
|
|
68
|
+
return 1048576
|
|
69
|
+
catalog_limit, _family, _source = self.ports.catalog_context(model_id)
|
|
70
|
+
if catalog_limit:
|
|
71
|
+
return catalog_limit
|
|
72
|
+
if any(
|
|
73
|
+
marker in model
|
|
74
|
+
for marker in (
|
|
75
|
+
"deepseek-v4-pro",
|
|
76
|
+
"deepseek-v4-flash",
|
|
77
|
+
"deepseek-v4",
|
|
78
|
+
"v4-pro",
|
|
79
|
+
"v4-flash",
|
|
80
|
+
"1m",
|
|
81
|
+
"million",
|
|
82
|
+
)
|
|
83
|
+
):
|
|
84
|
+
return 1048576
|
|
85
|
+
if any(
|
|
86
|
+
marker in model
|
|
87
|
+
for marker in (
|
|
88
|
+
"kimi-for-coding",
|
|
89
|
+
"kimi-code",
|
|
90
|
+
"kimi-k2.7",
|
|
91
|
+
"kimi_k2.7",
|
|
92
|
+
"kimi2.7",
|
|
93
|
+
"k2.7",
|
|
94
|
+
"kimi-k2.6",
|
|
95
|
+
"kimi_k2.6",
|
|
96
|
+
"kimi2.6",
|
|
97
|
+
"kimi-k2",
|
|
98
|
+
)
|
|
99
|
+
):
|
|
100
|
+
return 262144
|
|
101
|
+
if "qwen3.6" in model:
|
|
102
|
+
return 262144
|
|
103
|
+
if "glm-4.7" in model or "glm-5.1" in model:
|
|
104
|
+
return 200000
|
|
105
|
+
if "deepseek-r1" in model or "llama3.3" in model:
|
|
106
|
+
return 131072
|
|
107
|
+
return self.ports.positive_int(
|
|
108
|
+
self.ports.model_preset(model_id).get("num_ctx_max")
|
|
109
|
+
)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Provider-registry-backed model panel projections."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class ModelPanelCatalog:
|
|
11
|
+
alias_for: Callable[..., Any]
|
|
12
|
+
cached_or_configured_model_ids: Callable[..., Any]
|
|
13
|
+
read_model_info_cache: Callable[..., Any]
|
|
14
|
+
read_model_list_cache: Callable[..., Any]
|
|
15
|
+
unique_model_ids: Callable[..., Any]
|
|
16
|
+
upstream_model_ids: Callable[..., Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class ModelPanelPresentation:
|
|
21
|
+
advisor_model_badge: Callable[..., Any]
|
|
22
|
+
advisor_panel_notice: Callable[..., Any]
|
|
23
|
+
format_context_tokens: Callable[..., Any]
|
|
24
|
+
format_parameter_count: Callable[..., Any]
|
|
25
|
+
model_panel_badge: Callable[..., Any]
|
|
26
|
+
normalize_model_id: Callable[..., Any]
|
|
27
|
+
positive_int: Callable[..., Any]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class ModelPanelServices:
|
|
32
|
+
catalog: ModelPanelCatalog
|
|
33
|
+
presentation: ModelPanelPresentation
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def model_panel_rows(
|
|
37
|
+
provider: str,
|
|
38
|
+
pcfg: dict[str, Any],
|
|
39
|
+
fetch: bool = True,
|
|
40
|
+
force_refresh: bool = False,
|
|
41
|
+
*,
|
|
42
|
+
services: ModelPanelServices,
|
|
43
|
+
) -> tuple[list[str], list[str]]:
|
|
44
|
+
catalog = services.catalog
|
|
45
|
+
presentation = services.presentation
|
|
46
|
+
alias_for = catalog.alias_for
|
|
47
|
+
cached_or_configured_model_ids = catalog.cached_or_configured_model_ids
|
|
48
|
+
format_context_tokens = presentation.format_context_tokens
|
|
49
|
+
format_parameter_count = presentation.format_parameter_count
|
|
50
|
+
model_panel_badge = presentation.model_panel_badge
|
|
51
|
+
normalize_model_id = presentation.normalize_model_id
|
|
52
|
+
positive_int = presentation.positive_int
|
|
53
|
+
read_model_info_cache = catalog.read_model_info_cache
|
|
54
|
+
read_model_list_cache = catalog.read_model_list_cache
|
|
55
|
+
unique_model_ids = catalog.unique_model_ids
|
|
56
|
+
upstream_model_ids = catalog.upstream_model_ids
|
|
57
|
+
values = unique_model_ids(
|
|
58
|
+
provider,
|
|
59
|
+
upstream_model_ids(provider, pcfg, force_refresh=force_refresh)
|
|
60
|
+
if fetch else cached_or_configured_model_ids(provider, pcfg),
|
|
61
|
+
)
|
|
62
|
+
rows: list[str] = []
|
|
63
|
+
current = pcfg.get("current_model")
|
|
64
|
+
seen_aliases: set[str] = set()
|
|
65
|
+
deduped_values: list[str] = []
|
|
66
|
+
cache = read_model_list_cache(provider, pcfg)
|
|
67
|
+
cached_info = read_model_info_cache(provider, pcfg)
|
|
68
|
+
rows.append("Refresh provider model list..." if cache is None else "Refresh provider model list")
|
|
69
|
+
deduped_values.append("__refresh_models__")
|
|
70
|
+
for mid in values:
|
|
71
|
+
alias = alias_for(provider, mid)
|
|
72
|
+
suffix = ""
|
|
73
|
+
info = cached_info.get(normalize_model_id(provider, mid), {})
|
|
74
|
+
max_context = positive_int(info.get("max_model_len"))
|
|
75
|
+
if max_context:
|
|
76
|
+
suffix += f" [ctx {format_context_tokens(max_context)}]"
|
|
77
|
+
parameter_count = format_parameter_count(info.get("parameter_count"))
|
|
78
|
+
if parameter_count:
|
|
79
|
+
suffix += f" [{parameter_count} params]"
|
|
80
|
+
badge = model_panel_badge(provider, pcfg, mid)
|
|
81
|
+
if badge:
|
|
82
|
+
suffix += f" [{badge}]"
|
|
83
|
+
alias_key = alias.casefold()
|
|
84
|
+
if alias_key in seen_aliases:
|
|
85
|
+
continue
|
|
86
|
+
seen_aliases.add(alias_key)
|
|
87
|
+
deduped_values.append(mid)
|
|
88
|
+
mark = "*" if mid == current else " "
|
|
89
|
+
rows.append(f"{mark} {mid} {alias}{suffix}")
|
|
90
|
+
rows.append("+ Custom model id...")
|
|
91
|
+
deduped_values.append("__custom__")
|
|
92
|
+
rows.append("Back")
|
|
93
|
+
deduped_values.append("back")
|
|
94
|
+
return rows, deduped_values
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def advisor_model_panel_rows(
|
|
98
|
+
provider: str,
|
|
99
|
+
pcfg: dict[str, Any],
|
|
100
|
+
fetch: bool = True,
|
|
101
|
+
force_refresh: bool = False,
|
|
102
|
+
*,
|
|
103
|
+
services: ModelPanelServices,
|
|
104
|
+
) -> tuple[list[str], list[str]]:
|
|
105
|
+
catalog = services.catalog
|
|
106
|
+
presentation = services.presentation
|
|
107
|
+
advisor_model_badge = presentation.advisor_model_badge
|
|
108
|
+
advisor_panel_notice = presentation.advisor_panel_notice
|
|
109
|
+
cached_or_configured_model_ids = catalog.cached_or_configured_model_ids
|
|
110
|
+
normalize_model_id = presentation.normalize_model_id
|
|
111
|
+
read_model_list_cache = catalog.read_model_list_cache
|
|
112
|
+
unique_model_ids = catalog.unique_model_ids
|
|
113
|
+
upstream_model_ids = catalog.upstream_model_ids
|
|
114
|
+
notice = advisor_panel_notice(provider, pcfg)
|
|
115
|
+
if notice:
|
|
116
|
+
return list(notice[0]), list(notice[1])
|
|
117
|
+
current = normalize_model_id(provider, pcfg.get("advisor_model", ""))
|
|
118
|
+
values = unique_model_ids(
|
|
119
|
+
provider,
|
|
120
|
+
(
|
|
121
|
+
upstream_model_ids(provider, pcfg, force_refresh=force_refresh)
|
|
122
|
+
if fetch else cached_or_configured_model_ids(provider, pcfg)
|
|
123
|
+
)
|
|
124
|
+
+ ([current] if current else []),
|
|
125
|
+
)
|
|
126
|
+
rows: list[str] = []
|
|
127
|
+
rows.append(("* Disable Advisor Model" if not current else " Disable Advisor Model"))
|
|
128
|
+
deduped_values = [""]
|
|
129
|
+
cache = read_model_list_cache(provider, pcfg)
|
|
130
|
+
rows.append("Refresh provider model list..." if cache is None else "Refresh provider model list")
|
|
131
|
+
deduped_values.append("__refresh_models__")
|
|
132
|
+
seen: set[str] = set()
|
|
133
|
+
for mid in values:
|
|
134
|
+
if not mid or mid in seen:
|
|
135
|
+
continue
|
|
136
|
+
seen.add(mid)
|
|
137
|
+
mark = "*" if mid == current else " "
|
|
138
|
+
badge = advisor_model_badge(provider, pcfg, mid)
|
|
139
|
+
suffix = f" {badge}" if badge else ""
|
|
140
|
+
rows.append(f"{mark} {mid}{suffix}")
|
|
141
|
+
deduped_values.append(mid)
|
|
142
|
+
rows.append("+ Custom advisor model id...")
|
|
143
|
+
deduped_values.append("__custom__")
|
|
144
|
+
rows.append("Back")
|
|
145
|
+
deduped_values.append("back")
|
|
146
|
+
return rows, deduped_values
|
|
147
|
+
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Persistent provider model registry and short-lived model-list cache."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
from .runtime_constants import MODEL_CACHE_TTL_SECONDS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class ModelRegistryPaths:
|
|
17
|
+
config_dir: Path
|
|
18
|
+
registry: Path
|
|
19
|
+
list_cache: Path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class ModelRegistryPolicy:
|
|
24
|
+
cache_key: Callable[[str, dict[str, Any]], str]
|
|
25
|
+
unique_ids: Callable[[str, list[str]], list[str]]
|
|
26
|
+
normalize_id: Callable[[str, str], str]
|
|
27
|
+
positive_int: Callable[[Any], int | None]
|
|
28
|
+
recommendations: Callable[[str, list[str]], dict[str, Any]]
|
|
29
|
+
log: Callable[[str, str], None]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class ModelRegistryRepository:
|
|
34
|
+
paths: ModelRegistryPaths
|
|
35
|
+
policy: ModelRegistryPolicy
|
|
36
|
+
ttl_seconds: float
|
|
37
|
+
|
|
38
|
+
def read_registry(
|
|
39
|
+
self,
|
|
40
|
+
provider: str,
|
|
41
|
+
config: dict[str, Any],
|
|
42
|
+
max_age_seconds: float | None = None,
|
|
43
|
+
) -> dict[str, Any] | None:
|
|
44
|
+
try:
|
|
45
|
+
data = json.loads(self.paths.registry.read_text(encoding="utf-8"))
|
|
46
|
+
except Exception:
|
|
47
|
+
return None
|
|
48
|
+
providers = data.get("providers") if isinstance(data, dict) else None
|
|
49
|
+
entry = providers.get(provider) if isinstance(providers, dict) else None
|
|
50
|
+
if not isinstance(entry, dict) or not self._key_matches(provider, config, entry):
|
|
51
|
+
return None
|
|
52
|
+
max_age = self.ttl_seconds if max_age_seconds is None else max_age_seconds
|
|
53
|
+
try:
|
|
54
|
+
if max_age > 0 and time.time() - float(entry.get("time", 0)) > max_age:
|
|
55
|
+
return None
|
|
56
|
+
except Exception:
|
|
57
|
+
return None
|
|
58
|
+
return entry if isinstance(entry.get("models"), list) else None
|
|
59
|
+
|
|
60
|
+
def _key_matches(
|
|
61
|
+
self,
|
|
62
|
+
provider: str,
|
|
63
|
+
config: dict[str, Any],
|
|
64
|
+
entry: dict[str, Any],
|
|
65
|
+
) -> bool:
|
|
66
|
+
current = self.policy.cache_key(provider, config)
|
|
67
|
+
if entry.get("key") == current:
|
|
68
|
+
return True
|
|
69
|
+
if provider != "anthropic" or entry.get("source") != "anthropic-docs":
|
|
70
|
+
return False
|
|
71
|
+
try:
|
|
72
|
+
saved_key = json.loads(str(entry.get("key") or "{}"))
|
|
73
|
+
current_key = json.loads(current)
|
|
74
|
+
saved_key.pop("api", None)
|
|
75
|
+
current_key.pop("api", None)
|
|
76
|
+
return saved_key == current_key
|
|
77
|
+
except Exception:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def read_registry_models(
|
|
81
|
+
self,
|
|
82
|
+
provider: str,
|
|
83
|
+
config: dict[str, Any],
|
|
84
|
+
max_age_seconds: float | None = None,
|
|
85
|
+
) -> list[str] | None:
|
|
86
|
+
entry = self.read_registry(provider, config, max_age_seconds)
|
|
87
|
+
models = entry.get("models") if entry else None
|
|
88
|
+
if not isinstance(models, list):
|
|
89
|
+
return None
|
|
90
|
+
return self.policy.unique_ids(provider, [str(item) for item in models if str(item).strip()])
|
|
91
|
+
|
|
92
|
+
def read_registry_info(
|
|
93
|
+
self,
|
|
94
|
+
provider: str,
|
|
95
|
+
config: dict[str, Any],
|
|
96
|
+
max_age_seconds: float | None = None,
|
|
97
|
+
) -> dict[str, dict[str, Any]]:
|
|
98
|
+
entry = self.read_registry(provider, config, max_age_seconds)
|
|
99
|
+
metadata = entry.get("metadata") if entry else None
|
|
100
|
+
return self._normalize_info(provider, metadata)
|
|
101
|
+
|
|
102
|
+
def write_registry(
|
|
103
|
+
self,
|
|
104
|
+
provider: str,
|
|
105
|
+
config: dict[str, Any],
|
|
106
|
+
models: list[str],
|
|
107
|
+
source: str = "provider",
|
|
108
|
+
metadata: dict[str, Any] | None = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
self.paths.config_dir.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
try:
|
|
112
|
+
data = json.loads(self.paths.registry.read_text(encoding="utf-8")) if self.paths.registry.exists() else {}
|
|
113
|
+
except Exception:
|
|
114
|
+
data = {}
|
|
115
|
+
if not isinstance(data, dict):
|
|
116
|
+
data = {}
|
|
117
|
+
providers = data.get("providers")
|
|
118
|
+
if not isinstance(providers, dict):
|
|
119
|
+
providers = {}
|
|
120
|
+
providers[provider] = {
|
|
121
|
+
"time": time.time(),
|
|
122
|
+
"key": self.policy.cache_key(provider, config),
|
|
123
|
+
"source": source,
|
|
124
|
+
"models": self.policy.unique_ids(provider, models),
|
|
125
|
+
"recommendations": self.policy.recommendations(provider, models),
|
|
126
|
+
"metadata": metadata or {},
|
|
127
|
+
}
|
|
128
|
+
data.update(schema=1, providers=providers)
|
|
129
|
+
try:
|
|
130
|
+
self.paths.registry.write_text(
|
|
131
|
+
json.dumps(data, indent=2, sort_keys=True) + "\n",
|
|
132
|
+
encoding="utf-8",
|
|
133
|
+
)
|
|
134
|
+
os.chmod(self.paths.registry, 0o600)
|
|
135
|
+
except Exception as error:
|
|
136
|
+
self.policy.log(
|
|
137
|
+
"WARN",
|
|
138
|
+
f"model_registry_write_failed provider={provider} error={type(error).__name__}: {error}",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def read_list_cache(self, provider: str, config: dict[str, Any]) -> list[str] | None:
|
|
142
|
+
try:
|
|
143
|
+
data = json.loads(self.paths.list_cache.read_text())
|
|
144
|
+
except Exception:
|
|
145
|
+
return self.read_registry_models(provider, config, 0)
|
|
146
|
+
if not isinstance(data, dict) or data.get("key") != self.policy.cache_key(provider, config):
|
|
147
|
+
return self.read_registry_models(provider, config, 0)
|
|
148
|
+
if time.time() - float(data.get("time", 0)) > self.ttl_seconds:
|
|
149
|
+
if registry_models := self.read_registry_models(provider, config, 0):
|
|
150
|
+
return registry_models
|
|
151
|
+
models = data.get("models")
|
|
152
|
+
if not isinstance(models, list):
|
|
153
|
+
return None
|
|
154
|
+
return self.policy.unique_ids(provider, [str(item) for item in models if str(item).strip()])
|
|
155
|
+
|
|
156
|
+
def read_info_cache(self, provider: str, config: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
157
|
+
try:
|
|
158
|
+
data = json.loads(self.paths.list_cache.read_text())
|
|
159
|
+
except Exception:
|
|
160
|
+
return self.read_registry_info(provider, config, 0)
|
|
161
|
+
if not isinstance(data, dict) or data.get("key") != self.policy.cache_key(provider, config):
|
|
162
|
+
return self.read_registry_info(provider, config, 0)
|
|
163
|
+
info = self._normalize_info(provider, data.get("metadata"))
|
|
164
|
+
return info or self.read_registry_info(provider, config, 0)
|
|
165
|
+
|
|
166
|
+
def _normalize_info(self, provider: str, metadata: Any) -> dict[str, dict[str, Any]]:
|
|
167
|
+
raw_info = metadata.get("model_info") if isinstance(metadata, dict) else None
|
|
168
|
+
if not isinstance(raw_info, dict):
|
|
169
|
+
return {}
|
|
170
|
+
result: dict[str, dict[str, Any]] = {}
|
|
171
|
+
for raw_id, raw_value in raw_info.items():
|
|
172
|
+
model_id = self.policy.normalize_id(provider, str(raw_id))
|
|
173
|
+
if not model_id or not isinstance(raw_value, dict):
|
|
174
|
+
continue
|
|
175
|
+
info = dict(raw_value)
|
|
176
|
+
if context := self.policy.positive_int(info.get("max_model_len")):
|
|
177
|
+
info["max_model_len"] = context
|
|
178
|
+
result[model_id] = info
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
def write_list_cache(
|
|
182
|
+
self,
|
|
183
|
+
provider: str,
|
|
184
|
+
config: dict[str, Any],
|
|
185
|
+
models: list[str],
|
|
186
|
+
metadata: dict[str, Any] | None = None,
|
|
187
|
+
) -> None:
|
|
188
|
+
self.paths.config_dir.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
data = {
|
|
190
|
+
"time": time.time(),
|
|
191
|
+
"key": self.policy.cache_key(provider, config),
|
|
192
|
+
"models": models,
|
|
193
|
+
"metadata": metadata or {},
|
|
194
|
+
}
|
|
195
|
+
try:
|
|
196
|
+
self.paths.list_cache.write_text(json.dumps(data, indent=2) + "\n")
|
|
197
|
+
os.chmod(self.paths.list_cache, 0o600)
|
|
198
|
+
except Exception as error:
|
|
199
|
+
self.policy.log(
|
|
200
|
+
"WARN",
|
|
201
|
+
f"model_list_cache_write_failed provider={provider} error={type(error).__name__}: {error}",
|
|
202
|
+
)
|
|
203
|
+
self.write_registry(provider, config, models, "provider", metadata)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(frozen=True, slots=True)
|
|
207
|
+
class ModelRegistryApi:
|
|
208
|
+
"""Explicit public adapter for late-bound model registry repositories."""
|
|
209
|
+
|
|
210
|
+
repository_factory: Callable[[], ModelRegistryRepository]
|
|
211
|
+
|
|
212
|
+
def read_registry(self, provider: str, pcfg: dict[str, Any], max_age_seconds: float = MODEL_CACHE_TTL_SECONDS) -> dict[str, Any] | None:
|
|
213
|
+
return self.repository_factory().read_registry(provider, pcfg, max_age_seconds)
|
|
214
|
+
|
|
215
|
+
def read_registry_models(self, provider: str, pcfg: dict[str, Any], max_age_seconds: float = MODEL_CACHE_TTL_SECONDS) -> list[str] | None:
|
|
216
|
+
return self.repository_factory().read_registry_models(provider, pcfg, max_age_seconds)
|
|
217
|
+
|
|
218
|
+
def read_registry_info(self, provider: str, pcfg: dict[str, Any], max_age_seconds: float = MODEL_CACHE_TTL_SECONDS) -> dict[str, dict[str, Any]]:
|
|
219
|
+
return self.repository_factory().read_registry_info(provider, pcfg, max_age_seconds)
|
|
220
|
+
|
|
221
|
+
def write_registry(self, provider: str, pcfg: dict[str, Any], models: list[str], source: str = "provider", metadata: dict[str, Any] | None = None) -> None:
|
|
222
|
+
self.repository_factory().write_registry(provider, pcfg, models, source, metadata)
|
|
223
|
+
|
|
224
|
+
def read_list_cache(self, provider: str, pcfg: dict[str, Any]) -> list[str] | None:
|
|
225
|
+
return self.repository_factory().read_list_cache(provider, pcfg)
|
|
226
|
+
|
|
227
|
+
def read_info_cache(self, provider: str, pcfg: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
228
|
+
return self.repository_factory().read_info_cache(provider, pcfg)
|
|
229
|
+
|
|
230
|
+
def write_list_cache(self, provider: str, pcfg: dict[str, Any], models: list[str], metadata: dict[str, Any] | None = None) -> None:
|
|
231
|
+
self.repository_factory().write_list_cache(provider, pcfg, models, metadata)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""npm process, version, and installed-package path utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_version_tuple(value: str) -> tuple[int, ...]:
|
|
13
|
+
return tuple(int(item) for item in re.split(r"[^0-9]+", value.strip()) if item)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def version_newer(latest: str, current: str) -> bool:
|
|
17
|
+
left = list(parse_version_tuple(latest))
|
|
18
|
+
right = list(parse_version_tuple(current))
|
|
19
|
+
size = max(len(left), len(right), 1)
|
|
20
|
+
left.extend([0] * (size - len(left)))
|
|
21
|
+
right.extend([0] * (size - len(right)))
|
|
22
|
+
return tuple(left) > tuple(right)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def npm_latest_package_version(
|
|
26
|
+
npm: str, package_spec: str, timeout: float = 8.0
|
|
27
|
+
) -> str:
|
|
28
|
+
try:
|
|
29
|
+
process = subprocess.run(
|
|
30
|
+
[npm, "view", package_spec, "version"],
|
|
31
|
+
text=True,
|
|
32
|
+
stdout=subprocess.PIPE,
|
|
33
|
+
stderr=subprocess.DEVNULL,
|
|
34
|
+
timeout=timeout,
|
|
35
|
+
)
|
|
36
|
+
except Exception:
|
|
37
|
+
return ""
|
|
38
|
+
if process.returncode != 0:
|
|
39
|
+
return ""
|
|
40
|
+
output = (process.stdout or "").strip()
|
|
41
|
+
return output.splitlines()[-1].strip() if output else ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def npm_global_package_root(
|
|
45
|
+
npm: str,
|
|
46
|
+
package_name: str = "@oneciel-ai/ciel-runtime",
|
|
47
|
+
timeout: float = 8.0,
|
|
48
|
+
) -> Path | None:
|
|
49
|
+
try:
|
|
50
|
+
process = subprocess.run(
|
|
51
|
+
[npm, "root", "-g"],
|
|
52
|
+
text=True,
|
|
53
|
+
stdout=subprocess.PIPE,
|
|
54
|
+
stderr=subprocess.DEVNULL,
|
|
55
|
+
timeout=timeout,
|
|
56
|
+
)
|
|
57
|
+
except Exception:
|
|
58
|
+
return None
|
|
59
|
+
if process.returncode != 0:
|
|
60
|
+
return None
|
|
61
|
+
root = (process.stdout or "").strip()
|
|
62
|
+
if not root:
|
|
63
|
+
return None
|
|
64
|
+
package_path = Path(root)
|
|
65
|
+
for part in package_name.split("/"):
|
|
66
|
+
if part:
|
|
67
|
+
package_path /= part
|
|
68
|
+
return package_path
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def npm_prefix_from_package_root(package_root: Path) -> Path | None:
|
|
72
|
+
"""Infer the npm install prefix from a global installed package root."""
|
|
73
|
+
|
|
74
|
+
parts = package_root.parts
|
|
75
|
+
for index, part in enumerate(parts):
|
|
76
|
+
if part != "node_modules":
|
|
77
|
+
continue
|
|
78
|
+
try:
|
|
79
|
+
node_modules = Path(*parts[: index + 1])
|
|
80
|
+
except Exception:
|
|
81
|
+
return None
|
|
82
|
+
parent = node_modules.parent
|
|
83
|
+
return parent.parent if parent.name == "lib" else parent
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def npm_global_install_command(
|
|
88
|
+
npm: str, package_spec: str, prefix: Path | None = None
|
|
89
|
+
) -> list[str]:
|
|
90
|
+
command = [npm, "install", "-g"]
|
|
91
|
+
if prefix is not None:
|
|
92
|
+
command.extend(["--prefix", str(prefix)])
|
|
93
|
+
command.append(package_spec)
|
|
94
|
+
return command
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def npm_install_runtime_command(
|
|
98
|
+
npm: str, package_spec: str, prefix: Path | None = None
|
|
99
|
+
) -> list[str]:
|
|
100
|
+
command = npm_global_install_command(npm, package_spec, prefix)
|
|
101
|
+
command.insert(3, "--prefer-online")
|
|
102
|
+
return command
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def npm_global_bin_dir_from_prefix(prefix: Path) -> Path:
|
|
106
|
+
return prefix if os.name == "nt" else prefix / "bin"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def executable_version(executable: str, timeout: float = 8.0) -> str:
|
|
110
|
+
try:
|
|
111
|
+
process = subprocess.run(
|
|
112
|
+
[executable, "--version"],
|
|
113
|
+
text=True,
|
|
114
|
+
stdout=subprocess.PIPE,
|
|
115
|
+
stderr=subprocess.STDOUT,
|
|
116
|
+
timeout=timeout,
|
|
117
|
+
)
|
|
118
|
+
except Exception:
|
|
119
|
+
return ""
|
|
120
|
+
if process.returncode != 0:
|
|
121
|
+
return ""
|
|
122
|
+
match = re.search(r"\d+(?:\.\d+)+", process.stdout or "")
|
|
123
|
+
return match.group(0) if match else ""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def run_upgrade_command(
|
|
127
|
+
command: list[str],
|
|
128
|
+
environ: Mapping[str, str],
|
|
129
|
+
timeout: float = 300.0,
|
|
130
|
+
) -> tuple[int, str]:
|
|
131
|
+
try:
|
|
132
|
+
process = subprocess.run(
|
|
133
|
+
command,
|
|
134
|
+
text=True,
|
|
135
|
+
input="y\n",
|
|
136
|
+
stdout=subprocess.PIPE,
|
|
137
|
+
stderr=subprocess.STDOUT,
|
|
138
|
+
env=dict(environ),
|
|
139
|
+
timeout=timeout,
|
|
140
|
+
)
|
|
141
|
+
except subprocess.TimeoutExpired:
|
|
142
|
+
return 124, "timed out"
|
|
143
|
+
except Exception as exc:
|
|
144
|
+
return 1, f"{type(exc).__name__}: {exc}"
|
|
145
|
+
return process.returncode, (process.stdout or "").strip()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def claude_code_current_version(claude: str) -> str:
|
|
149
|
+
return executable_version(claude)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def codex_current_version(codex: str) -> str:
|
|
153
|
+
return executable_version(codex)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def package_root_from_installed_path(path: Path) -> Path | None:
|
|
157
|
+
"""Return the npm package root when a path lives inside this package."""
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
resolved = path.resolve(strict=False)
|
|
161
|
+
except Exception:
|
|
162
|
+
resolved = path
|
|
163
|
+
parts = resolved.parts
|
|
164
|
+
for index in range(0, max(0, len(parts) - 2)):
|
|
165
|
+
if (
|
|
166
|
+
parts[index] == "node_modules"
|
|
167
|
+
and parts[index + 1] == "@oneciel-ai"
|
|
168
|
+
and parts[index + 2] == "ciel-runtime"
|
|
169
|
+
):
|
|
170
|
+
try:
|
|
171
|
+
return Path(*parts[: index + 3])
|
|
172
|
+
except Exception:
|
|
173
|
+
return None
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
__all__ = [
|
|
178
|
+
"claude_code_current_version",
|
|
179
|
+
"codex_current_version",
|
|
180
|
+
"executable_version",
|
|
181
|
+
"npm_global_bin_dir_from_prefix",
|
|
182
|
+
"npm_global_install_command",
|
|
183
|
+
"npm_global_package_root",
|
|
184
|
+
"npm_install_runtime_command",
|
|
185
|
+
"npm_latest_package_version",
|
|
186
|
+
"npm_prefix_from_package_root",
|
|
187
|
+
"package_root_from_installed_path",
|
|
188
|
+
"parse_version_tuple",
|
|
189
|
+
"run_upgrade_command",
|
|
190
|
+
"version_newer",
|
|
191
|
+
]
|