@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,195 @@
|
|
|
1
|
+
"""Codex MCP discovery, capability filtering, and proxy configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class CodexMcpConfigPorts:
|
|
14
|
+
discover: Callable[..., dict[str, dict[str, Any]]]
|
|
15
|
+
log: Callable[[str, str], None]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class CodexMcpArtifactPorts:
|
|
20
|
+
config_path: Callable[[], Path]
|
|
21
|
+
save_json: Callable[[Path, dict[str, Any], str], None]
|
|
22
|
+
unlink: Callable[[Path], None]
|
|
23
|
+
load_json: Callable[[Path], Any]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class CodexMcpCapabilityPorts:
|
|
28
|
+
ensure_probe_cache: Callable[..., Any]
|
|
29
|
+
read_servers: Callable[[Path, Path], list[dict[str, Any]]]
|
|
30
|
+
cached_probe_servers: Callable[[], list[dict[str, Any]]]
|
|
31
|
+
path_key: Callable[[Path], str]
|
|
32
|
+
cwd: Callable[[], Path]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class CodexMcpProjectionPorts:
|
|
37
|
+
dedupe_strings: Callable[[Iterable[str]], list[str]]
|
|
38
|
+
public_name: Callable[[str], str]
|
|
39
|
+
is_streamable_http: Callable[[dict[str, Any]], bool]
|
|
40
|
+
split_proxy_url: Callable[[str], str]
|
|
41
|
+
toml_string: Callable[[str], str]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class CodexMcpPolicy:
|
|
46
|
+
native_channel_names: frozenset[str]
|
|
47
|
+
builtin_channel_url: Callable[[], str]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class CodexMcpIntegrationService:
|
|
52
|
+
config: CodexMcpConfigPorts
|
|
53
|
+
artifact: CodexMcpArtifactPorts
|
|
54
|
+
capability: CodexMcpCapabilityPorts
|
|
55
|
+
projection: CodexMcpProjectionPorts
|
|
56
|
+
policy: CodexMcpPolicy
|
|
57
|
+
|
|
58
|
+
def discovered_servers(
|
|
59
|
+
self,
|
|
60
|
+
passthrough: list[str] | None = None,
|
|
61
|
+
env: dict[str, str] | None = None,
|
|
62
|
+
cwd: Path | None = None,
|
|
63
|
+
) -> dict[str, dict[str, Any]]:
|
|
64
|
+
return self.config.discover(passthrough, env, cwd, log=self.config.log)
|
|
65
|
+
|
|
66
|
+
def write_discovery_config(
|
|
67
|
+
self,
|
|
68
|
+
passthrough: list[str] | None = None,
|
|
69
|
+
env: dict[str, str] | None = None,
|
|
70
|
+
cwd: Path | None = None,
|
|
71
|
+
) -> Path | None:
|
|
72
|
+
servers = self.discovered_servers(passthrough or [], env=env, cwd=cwd)
|
|
73
|
+
path = self.artifact.config_path()
|
|
74
|
+
if not servers:
|
|
75
|
+
try:
|
|
76
|
+
self.artifact.unlink(path)
|
|
77
|
+
except FileNotFoundError:
|
|
78
|
+
pass
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
self.config.log(
|
|
81
|
+
"WARN",
|
|
82
|
+
"codex_mcp_config_remove_failed "
|
|
83
|
+
f"error={type(exc).__name__}: {exc}",
|
|
84
|
+
)
|
|
85
|
+
return None
|
|
86
|
+
self.artifact.save_json(path, {"mcpServers": servers}, "codex_mcp_config")
|
|
87
|
+
self.config.log(
|
|
88
|
+
"INFO", f"codex_mcp_config_written servers={','.join(sorted(servers))}"
|
|
89
|
+
)
|
|
90
|
+
return path
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def config_bare_key(name: str) -> str | None:
|
|
94
|
+
text = str(name or "").strip()
|
|
95
|
+
return text if re.fullmatch(r"[A-Za-z0-9_-]+", text) else None
|
|
96
|
+
|
|
97
|
+
def channel_capable_server_names(
|
|
98
|
+
self, cfg: dict[str, Any], config_path: Path | None
|
|
99
|
+
) -> list[str]:
|
|
100
|
+
if not config_path or not config_path.exists() or not config_path.is_file():
|
|
101
|
+
return []
|
|
102
|
+
self.capability.ensure_probe_cache(
|
|
103
|
+
cfg, [], extra_config_paths=[config_path]
|
|
104
|
+
)
|
|
105
|
+
candidate_names = [
|
|
106
|
+
str(server.get("channel") or "").strip()
|
|
107
|
+
for server in self.capability.read_servers(
|
|
108
|
+
config_path, self.capability.cwd()
|
|
109
|
+
)
|
|
110
|
+
if str(server.get("channel") or "").strip()
|
|
111
|
+
]
|
|
112
|
+
source_key = self.capability.path_key(config_path)
|
|
113
|
+
capable = {
|
|
114
|
+
str(record.get("name") or "").strip()
|
|
115
|
+
for record in self.capability.cached_probe_servers()
|
|
116
|
+
if record.get("capable")
|
|
117
|
+
and str(record.get("name") or "").strip()
|
|
118
|
+
and self.capability.path_key(
|
|
119
|
+
Path(str(record.get("source_path") or ""))
|
|
120
|
+
)
|
|
121
|
+
== source_key
|
|
122
|
+
}
|
|
123
|
+
return self.projection.dedupe_strings(
|
|
124
|
+
name
|
|
125
|
+
for name in candidate_names
|
|
126
|
+
if name in capable and name.casefold() not in self.policy.native_channel_names
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def streamable_http_servers(
|
|
130
|
+
self, config_path: Path | None
|
|
131
|
+
) -> dict[str, dict[str, Any]]:
|
|
132
|
+
if not config_path or not config_path.exists() or not config_path.is_file():
|
|
133
|
+
return {}
|
|
134
|
+
try:
|
|
135
|
+
data = self.artifact.load_json(config_path)
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
self.config.log(
|
|
138
|
+
"WARN",
|
|
139
|
+
"codex_mcp_compat_source_read_failed "
|
|
140
|
+
f"error={type(exc).__name__}: {exc}",
|
|
141
|
+
)
|
|
142
|
+
return {}
|
|
143
|
+
servers = data.get("mcpServers") if isinstance(data, dict) else None
|
|
144
|
+
if not isinstance(servers, dict):
|
|
145
|
+
return {}
|
|
146
|
+
return {
|
|
147
|
+
name: raw_server
|
|
148
|
+
for raw_name, raw_server in servers.items()
|
|
149
|
+
if (name := str(raw_name).strip())
|
|
150
|
+
and isinstance(raw_server, dict)
|
|
151
|
+
and self.projection.is_streamable_http(raw_server)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
def native_http_compat_args(
|
|
155
|
+
self,
|
|
156
|
+
config_path: Path | None,
|
|
157
|
+
*,
|
|
158
|
+
split_http_proxy: bool = False,
|
|
159
|
+
channel_owned_server_names: Iterable[str] | None = None,
|
|
160
|
+
include_builtin_channel: bool = False,
|
|
161
|
+
) -> list[str]:
|
|
162
|
+
servers = self.streamable_http_servers(config_path)
|
|
163
|
+
args: list[str] = []
|
|
164
|
+
active: list[str] = []
|
|
165
|
+
if include_builtin_channel:
|
|
166
|
+
key = "ciel-runtime-router"
|
|
167
|
+
url = self.projection.toml_string(self.policy.builtin_channel_url())
|
|
168
|
+
args.extend(["-c", f"mcp_servers.{key}.url={url}"])
|
|
169
|
+
active.append(key)
|
|
170
|
+
channel_owned = {
|
|
171
|
+
self.projection.public_name(str(name or "").strip())
|
|
172
|
+
for name in channel_owned_server_names or []
|
|
173
|
+
if str(name or "").strip()
|
|
174
|
+
}
|
|
175
|
+
for name in sorted(servers):
|
|
176
|
+
key = self.config_bare_key(name)
|
|
177
|
+
if not key:
|
|
178
|
+
self.config.log(
|
|
179
|
+
"WARN", f"codex_mcp_compat_skipped_unsafe_name server={name}"
|
|
180
|
+
)
|
|
181
|
+
continue
|
|
182
|
+
if split_http_proxy or self.projection.public_name(name) in channel_owned:
|
|
183
|
+
url = self.projection.toml_string(
|
|
184
|
+
self.projection.split_proxy_url(name)
|
|
185
|
+
)
|
|
186
|
+
args.extend(["-c", f"mcp_servers.{key}.url={url}"])
|
|
187
|
+
if name not in active:
|
|
188
|
+
active.append(name)
|
|
189
|
+
if active:
|
|
190
|
+
self.config.log(
|
|
191
|
+
"INFO",
|
|
192
|
+
"codex_mcp_native_http_compat servers=%s split_http_proxy=%s"
|
|
193
|
+
% (",".join(active), str(bool(split_http_proxy)).lower()),
|
|
194
|
+
)
|
|
195
|
+
return args
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Conservative restoration of missing native Codex MCP configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .mcp_inventory import McpInventoryService
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_SCALAR_KEYS = (
|
|
18
|
+
"command",
|
|
19
|
+
"cwd",
|
|
20
|
+
"url",
|
|
21
|
+
"bearer_token_env_var",
|
|
22
|
+
"enabled",
|
|
23
|
+
"experimental_environment",
|
|
24
|
+
"oauth_resource",
|
|
25
|
+
"required",
|
|
26
|
+
"startup_timeout_ms",
|
|
27
|
+
"startup_timeout_sec",
|
|
28
|
+
"tool_timeout_sec",
|
|
29
|
+
)
|
|
30
|
+
_LIST_KEYS = (
|
|
31
|
+
"args",
|
|
32
|
+
"env_vars",
|
|
33
|
+
"enabled_tools",
|
|
34
|
+
"disabled_tools",
|
|
35
|
+
"scopes",
|
|
36
|
+
)
|
|
37
|
+
_TABLE_KEYS = ("env", "http_headers", "env_http_headers")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class CodexMcpRestorePorts:
|
|
42
|
+
config_paths: Callable[..., list[Path]]
|
|
43
|
+
discover_managed: Callable[[Path], dict[str, dict[str, Any]]]
|
|
44
|
+
log: Callable[[str, str], None]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class CodexMcpRestoreService:
|
|
49
|
+
ports: CodexMcpRestorePorts
|
|
50
|
+
|
|
51
|
+
def restore(
|
|
52
|
+
self,
|
|
53
|
+
passthrough: list[str] | None = None,
|
|
54
|
+
env: dict[str, str] | None = None,
|
|
55
|
+
cwd: Path | None = None,
|
|
56
|
+
) -> list[str]:
|
|
57
|
+
working_directory = (cwd or Path.cwd()).resolve()
|
|
58
|
+
paths = self.ports.config_paths(
|
|
59
|
+
passthrough or [], env=env, cwd=working_directory
|
|
60
|
+
)
|
|
61
|
+
if not paths:
|
|
62
|
+
return []
|
|
63
|
+
config_path = paths[0]
|
|
64
|
+
reserved: set[str] = set()
|
|
65
|
+
for path in paths[1:]:
|
|
66
|
+
if not path.is_file():
|
|
67
|
+
continue
|
|
68
|
+
try:
|
|
69
|
+
reserved.update(
|
|
70
|
+
self._existing_names(path.read_text(encoding="utf-8"))
|
|
71
|
+
)
|
|
72
|
+
except (OSError, UnicodeError, ValueError, TypeError) as exc:
|
|
73
|
+
self.ports.log(
|
|
74
|
+
"WARN",
|
|
75
|
+
"codex_mcp_restore_config_read_failed "
|
|
76
|
+
f"path={path} error={type(exc).__name__}: {exc}",
|
|
77
|
+
)
|
|
78
|
+
return []
|
|
79
|
+
managed = self.ports.discover_managed(working_directory)
|
|
80
|
+
return self._restore_missing(config_path, managed, reserved=reserved)
|
|
81
|
+
|
|
82
|
+
def _restore_missing(
|
|
83
|
+
self,
|
|
84
|
+
config_path: Path,
|
|
85
|
+
managed: dict[str, dict[str, Any]],
|
|
86
|
+
*,
|
|
87
|
+
reserved: set[str] | None = None,
|
|
88
|
+
) -> list[str]:
|
|
89
|
+
try:
|
|
90
|
+
before = self._signature(config_path)
|
|
91
|
+
original = config_path.read_text(encoding="utf-8") if before else ""
|
|
92
|
+
existing = self._existing_names(original)
|
|
93
|
+
except (OSError, UnicodeError, ValueError, TypeError, RuntimeError) as exc:
|
|
94
|
+
self.ports.log(
|
|
95
|
+
"WARN",
|
|
96
|
+
"codex_mcp_restore_config_read_failed "
|
|
97
|
+
f"path={config_path} error={type(exc).__name__}: {exc}",
|
|
98
|
+
)
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
projected: dict[str, dict[str, Any]] = {}
|
|
102
|
+
for raw_name, raw_server in managed.items():
|
|
103
|
+
name = str(raw_name or "").strip()
|
|
104
|
+
if not name or not isinstance(raw_server, dict):
|
|
105
|
+
continue
|
|
106
|
+
server = self._project_server(raw_server)
|
|
107
|
+
if server is None:
|
|
108
|
+
self.ports.log(
|
|
109
|
+
"WARN", f"codex_mcp_restore_skipped_unsupported server={name}"
|
|
110
|
+
)
|
|
111
|
+
continue
|
|
112
|
+
projected[name] = server
|
|
113
|
+
merged = McpInventoryService.merge(
|
|
114
|
+
{name: {} for name in existing}, projected, reserved=reserved
|
|
115
|
+
)
|
|
116
|
+
additions = [(name, projected[name]) for name in merged.added]
|
|
117
|
+
|
|
118
|
+
if not additions:
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
suffix = "" if not original or original.endswith("\n") else "\n"
|
|
122
|
+
blocks = "\n".join(
|
|
123
|
+
self._server_toml(name, server) for name, server in additions
|
|
124
|
+
)
|
|
125
|
+
restored = original + suffix + ("\n" if original else "") + blocks
|
|
126
|
+
if not restored.endswith("\n"):
|
|
127
|
+
restored += "\n"
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
self._validate(restored)
|
|
131
|
+
self._atomic_replace(config_path, restored, before)
|
|
132
|
+
except (OSError, UnicodeError, ValueError, TypeError) as exc:
|
|
133
|
+
self.ports.log(
|
|
134
|
+
"WARN",
|
|
135
|
+
"codex_mcp_restore_write_failed "
|
|
136
|
+
f"path={config_path} error={type(exc).__name__}: {exc}",
|
|
137
|
+
)
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
names = [name for name, _server in additions]
|
|
141
|
+
self.ports.log(
|
|
142
|
+
"INFO",
|
|
143
|
+
"codex_mcp_config_restored "
|
|
144
|
+
f"path={config_path} servers={','.join(sorted(names))}",
|
|
145
|
+
)
|
|
146
|
+
return names
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _signature(path: Path) -> tuple[int, int, int] | None:
|
|
150
|
+
try:
|
|
151
|
+
stat = path.stat()
|
|
152
|
+
except FileNotFoundError:
|
|
153
|
+
return None
|
|
154
|
+
return stat.st_mtime_ns, stat.st_size, getattr(stat, "st_ino", 0)
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _validate(text: str) -> dict[str, Any]:
|
|
158
|
+
import tomllib
|
|
159
|
+
|
|
160
|
+
data = tomllib.loads(text)
|
|
161
|
+
if not isinstance(data, dict):
|
|
162
|
+
raise ValueError("Codex config must be a TOML table")
|
|
163
|
+
return data
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def _existing_names(cls, text: str) -> set[str]:
|
|
167
|
+
if not text.strip():
|
|
168
|
+
return set()
|
|
169
|
+
data = cls._validate(text)
|
|
170
|
+
servers = data.get("mcp_servers")
|
|
171
|
+
if not isinstance(servers, dict):
|
|
172
|
+
return set()
|
|
173
|
+
return {str(name).strip() for name in servers if str(name).strip()}
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def _project_server(cls, server: dict[str, Any]) -> dict[str, Any] | None:
|
|
177
|
+
projected: dict[str, Any] = {}
|
|
178
|
+
command = str(server.get("command") or "").strip()
|
|
179
|
+
url = str(server.get("url") or server.get("endpoint") or "").strip()
|
|
180
|
+
raw_args = server.get("args")
|
|
181
|
+
arguments = [str(item) for item in raw_args] if isinstance(raw_args, list) else []
|
|
182
|
+
if "mcp-proxy" in arguments and "--server-config" in arguments:
|
|
183
|
+
return None
|
|
184
|
+
transport = str(server.get("type") or server.get("transport") or "").strip().casefold()
|
|
185
|
+
if command:
|
|
186
|
+
projected["command"] = command
|
|
187
|
+
elif url.startswith(("http://", "https://")) and transport != "sse":
|
|
188
|
+
projected["url"] = url
|
|
189
|
+
else:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
aliases = {
|
|
193
|
+
"token_env_var": "bearer_token_env_var",
|
|
194
|
+
"headers": "http_headers",
|
|
195
|
+
}
|
|
196
|
+
normalized = dict(server)
|
|
197
|
+
for source, target in aliases.items():
|
|
198
|
+
if target not in normalized and source in normalized:
|
|
199
|
+
normalized[target] = normalized[source]
|
|
200
|
+
for key in _SCALAR_KEYS:
|
|
201
|
+
if key in {"command", "url"} or normalized.get(key) is None:
|
|
202
|
+
continue
|
|
203
|
+
value = normalized[key]
|
|
204
|
+
if isinstance(value, (str, bool, int)) or (
|
|
205
|
+
isinstance(value, float) and math.isfinite(value)
|
|
206
|
+
):
|
|
207
|
+
projected[key] = value
|
|
208
|
+
for key in _LIST_KEYS:
|
|
209
|
+
value = normalized.get(key)
|
|
210
|
+
if isinstance(value, list) and all(
|
|
211
|
+
cls._supported_list_item(item) for item in value
|
|
212
|
+
):
|
|
213
|
+
projected[key] = list(value)
|
|
214
|
+
for key in _TABLE_KEYS:
|
|
215
|
+
value = normalized.get(key)
|
|
216
|
+
if isinstance(value, dict):
|
|
217
|
+
projected[key] = {
|
|
218
|
+
str(item_key): str(item_value)
|
|
219
|
+
for item_key, item_value in value.items()
|
|
220
|
+
if str(item_key).strip()
|
|
221
|
+
}
|
|
222
|
+
return projected
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def _supported_list_item(value: Any) -> bool:
|
|
226
|
+
if isinstance(value, (str, bool, int)):
|
|
227
|
+
return True
|
|
228
|
+
if isinstance(value, float):
|
|
229
|
+
return math.isfinite(value)
|
|
230
|
+
if isinstance(value, dict):
|
|
231
|
+
return all(
|
|
232
|
+
str(key).strip()
|
|
233
|
+
and CodexMcpRestoreService._supported_list_item(item)
|
|
234
|
+
and not isinstance(item, (list, dict))
|
|
235
|
+
for key, item in value.items()
|
|
236
|
+
)
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def _toml_value(value: Any) -> str:
|
|
241
|
+
if isinstance(value, bool):
|
|
242
|
+
return "true" if value else "false"
|
|
243
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
244
|
+
return repr(value)
|
|
245
|
+
if isinstance(value, dict):
|
|
246
|
+
entries = (
|
|
247
|
+
f"{json.dumps(str(key), ensure_ascii=False)} = {CodexMcpRestoreService._toml_value(item)}"
|
|
248
|
+
for key, item in value.items()
|
|
249
|
+
)
|
|
250
|
+
return "{ " + ", ".join(entries) + " }"
|
|
251
|
+
if isinstance(value, list):
|
|
252
|
+
return "[" + ", ".join(CodexMcpRestoreService._toml_value(item) for item in value) + "]"
|
|
253
|
+
return json.dumps(str(value), ensure_ascii=False)
|
|
254
|
+
|
|
255
|
+
@classmethod
|
|
256
|
+
def _server_toml(cls, name: str, server: dict[str, Any]) -> str:
|
|
257
|
+
table_name = json.dumps(name, ensure_ascii=False)
|
|
258
|
+
lines = [
|
|
259
|
+
"# Restored by Ciel Runtime from managed MCP state; existing Codex entries take precedence.",
|
|
260
|
+
f"[mcp_servers.{table_name}]",
|
|
261
|
+
]
|
|
262
|
+
for key in _SCALAR_KEYS:
|
|
263
|
+
if key in server:
|
|
264
|
+
lines.append(f"{key} = {cls._toml_value(server[key])}")
|
|
265
|
+
for key in _LIST_KEYS:
|
|
266
|
+
if key in server:
|
|
267
|
+
lines.append(f"{key} = {cls._toml_value(server[key])}")
|
|
268
|
+
for key in _TABLE_KEYS:
|
|
269
|
+
values = server.get(key)
|
|
270
|
+
if not isinstance(values, dict) or not values:
|
|
271
|
+
continue
|
|
272
|
+
lines.extend(("", f"[mcp_servers.{table_name}.{key}]"))
|
|
273
|
+
for item_key, item_value in sorted(values.items()):
|
|
274
|
+
encoded_key = json.dumps(str(item_key), ensure_ascii=False)
|
|
275
|
+
lines.append(f"{encoded_key} = {cls._toml_value(item_value)}")
|
|
276
|
+
return "\n".join(lines) + "\n"
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def _atomic_replace(
|
|
280
|
+
cls,
|
|
281
|
+
path: Path,
|
|
282
|
+
text: str,
|
|
283
|
+
expected: tuple[int, int, int] | None,
|
|
284
|
+
) -> None:
|
|
285
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
286
|
+
if cls._signature(path) != expected:
|
|
287
|
+
raise RuntimeError("Codex config changed while MCP restoration was running")
|
|
288
|
+
temporary = path.with_name(
|
|
289
|
+
f"{path.name}.{os.getpid()}.{time.time_ns()}.tmp"
|
|
290
|
+
)
|
|
291
|
+
try:
|
|
292
|
+
temporary.write_text(text, encoding="utf-8")
|
|
293
|
+
os.chmod(temporary, 0o600)
|
|
294
|
+
if cls._signature(path) != expected:
|
|
295
|
+
raise RuntimeError("Codex config changed while MCP restoration was running")
|
|
296
|
+
temporary.replace(path)
|
|
297
|
+
finally:
|
|
298
|
+
try:
|
|
299
|
+
temporary.unlink()
|
|
300
|
+
except FileNotFoundError:
|
|
301
|
+
pass
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
__all__ = ["CodexMcpRestorePorts", "CodexMcpRestoreService"]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Codex bundled-model catalog projection and atomic persistence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class CodexModelCatalogSpec:
|
|
14
|
+
alias: str
|
|
15
|
+
provider_label: str
|
|
16
|
+
context_window: int
|
|
17
|
+
effort: str = ""
|
|
18
|
+
auto_compact_token_limit: int | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CodexModelCatalogService:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
config_dir: Path,
|
|
25
|
+
run: Callable[..., Any],
|
|
26
|
+
log: Callable[[str, str], None],
|
|
27
|
+
) -> None:
|
|
28
|
+
self.config_dir = config_dir
|
|
29
|
+
self.run = run
|
|
30
|
+
self.log = log
|
|
31
|
+
|
|
32
|
+
def write(
|
|
33
|
+
self,
|
|
34
|
+
codex: str,
|
|
35
|
+
spec: CodexModelCatalogSpec,
|
|
36
|
+
environment: dict[str, str],
|
|
37
|
+
) -> Path | None:
|
|
38
|
+
try:
|
|
39
|
+
result = self.run(
|
|
40
|
+
[codex, "debug", "models", "--bundled"],
|
|
41
|
+
capture_output=True,
|
|
42
|
+
text=True,
|
|
43
|
+
encoding="utf-8",
|
|
44
|
+
errors="replace",
|
|
45
|
+
timeout=15,
|
|
46
|
+
check=False,
|
|
47
|
+
env=environment,
|
|
48
|
+
)
|
|
49
|
+
if result.returncode != 0:
|
|
50
|
+
detail = result.stderr or result.stdout or f"exit {result.returncode}"
|
|
51
|
+
raise RuntimeError(detail.strip())
|
|
52
|
+
catalog = json.loads(result.stdout)
|
|
53
|
+
models = catalog.get("models") if isinstance(catalog, dict) else None
|
|
54
|
+
if not isinstance(models, list) or not models:
|
|
55
|
+
raise ValueError("bundled catalog contains no models")
|
|
56
|
+
template = next(
|
|
57
|
+
(
|
|
58
|
+
item
|
|
59
|
+
for item in models
|
|
60
|
+
if isinstance(item, dict) and item.get("slug") == "gpt-5.2"
|
|
61
|
+
),
|
|
62
|
+
None,
|
|
63
|
+
)
|
|
64
|
+
if template is None:
|
|
65
|
+
template = next((item for item in models if isinstance(item, dict)), None)
|
|
66
|
+
if template is None:
|
|
67
|
+
raise ValueError("bundled catalog contains no model metadata")
|
|
68
|
+
routed = self._routed_model(template, spec)
|
|
69
|
+
catalog["models"] = [
|
|
70
|
+
item
|
|
71
|
+
for item in models
|
|
72
|
+
if not (isinstance(item, dict) and item.get("slug") == spec.alias)
|
|
73
|
+
] + [routed]
|
|
74
|
+
return self._save(catalog)
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
self.log(
|
|
77
|
+
"WARN",
|
|
78
|
+
f"codex_model_catalog_generation_failed "
|
|
79
|
+
f"error={type(exc).__name__}: {exc}",
|
|
80
|
+
)
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _routed_model(
|
|
85
|
+
template: dict[str, Any], spec: CodexModelCatalogSpec
|
|
86
|
+
) -> dict[str, Any]:
|
|
87
|
+
routed = json.loads(json.dumps(template))
|
|
88
|
+
auto_compact_token_limit = (
|
|
89
|
+
spec.auto_compact_token_limit
|
|
90
|
+
if spec.auto_compact_token_limit is not None
|
|
91
|
+
else max(1, (spec.context_window * 9) // 10)
|
|
92
|
+
)
|
|
93
|
+
routed.update(
|
|
94
|
+
{
|
|
95
|
+
"slug": spec.alias,
|
|
96
|
+
"display_name": f"Ciel Runtime {spec.provider_label}",
|
|
97
|
+
"description": f"{spec.provider_label} routed through Ciel Runtime.",
|
|
98
|
+
"visibility": "none",
|
|
99
|
+
"supported_in_api": True,
|
|
100
|
+
"priority": 99,
|
|
101
|
+
"context_window": spec.context_window,
|
|
102
|
+
"max_context_window": spec.context_window,
|
|
103
|
+
"auto_compact_token_limit": min(
|
|
104
|
+
spec.context_window,
|
|
105
|
+
max(1, auto_compact_token_limit),
|
|
106
|
+
),
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
if spec.effort:
|
|
110
|
+
routed["default_reasoning_level"] = spec.effort
|
|
111
|
+
supported = routed.get("supported_reasoning_levels")
|
|
112
|
+
if not isinstance(supported, list):
|
|
113
|
+
supported = []
|
|
114
|
+
if not any(
|
|
115
|
+
isinstance(item, dict) and item.get("effort") == spec.effort
|
|
116
|
+
for item in supported
|
|
117
|
+
):
|
|
118
|
+
supported.append(
|
|
119
|
+
{
|
|
120
|
+
"effort": spec.effort,
|
|
121
|
+
"description": f"{spec.effort.title()} reasoning effort",
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
routed["supported_reasoning_levels"] = supported
|
|
125
|
+
return routed
|
|
126
|
+
|
|
127
|
+
def _save(self, catalog: dict[str, Any]) -> Path:
|
|
128
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
path = self.config_dir / "codex-model-catalog.json"
|
|
130
|
+
temporary = path.with_suffix(".json.tmp")
|
|
131
|
+
temporary.write_text(json.dumps(catalog, ensure_ascii=False), encoding="utf-8")
|
|
132
|
+
temporary.replace(path)
|
|
133
|
+
return path
|