@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,103 @@
|
|
|
1
|
+
"""Ownership-aware installer for Claude commands and Codex prompts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class CommandAsset:
|
|
13
|
+
content: str
|
|
14
|
+
ownership_markers: tuple[str, ...]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_owned_command_file(path: Path, markers: tuple[str, ...]) -> bool:
|
|
18
|
+
try:
|
|
19
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
20
|
+
except Exception:
|
|
21
|
+
return False
|
|
22
|
+
return any(marker in text for marker in markers)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class CommandAssetInstaller:
|
|
27
|
+
directory: Path
|
|
28
|
+
warn: Callable[[str], None]
|
|
29
|
+
|
|
30
|
+
def install_one(self, name: str, asset: CommandAsset) -> None:
|
|
31
|
+
try:
|
|
32
|
+
self.directory.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
path = self.directory / name
|
|
34
|
+
if path.exists():
|
|
35
|
+
existing = path.read_text(encoding="utf-8", errors="replace")
|
|
36
|
+
if existing == asset.content:
|
|
37
|
+
return
|
|
38
|
+
if not is_owned_command_file(path, asset.ownership_markers):
|
|
39
|
+
return
|
|
40
|
+
path.write_text(asset.content, encoding="utf-8")
|
|
41
|
+
try:
|
|
42
|
+
os.chmod(path, 0o600)
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
except Exception as error:
|
|
46
|
+
self.warn(f"could not install {name} ({type(error).__name__}: {error}).")
|
|
47
|
+
|
|
48
|
+
def remove_one(self, name: str, markers: tuple[str, ...]) -> None:
|
|
49
|
+
try:
|
|
50
|
+
path = self.directory / name
|
|
51
|
+
if path.exists() and is_owned_command_file(path, markers):
|
|
52
|
+
path.unlink()
|
|
53
|
+
except Exception as error:
|
|
54
|
+
self.warn(f"could not remove {name} ({type(error).__name__}: {error}).")
|
|
55
|
+
|
|
56
|
+
def install_all(
|
|
57
|
+
self,
|
|
58
|
+
assets: dict[str, CommandAsset],
|
|
59
|
+
stale_glob: str | None = None,
|
|
60
|
+
stale_markers: tuple[str, ...] = (),
|
|
61
|
+
) -> None:
|
|
62
|
+
try:
|
|
63
|
+
self.directory.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
if stale_glob:
|
|
65
|
+
for path in self.directory.glob(stale_glob):
|
|
66
|
+
if path.name not in assets and is_owned_command_file(path, stale_markers):
|
|
67
|
+
try:
|
|
68
|
+
path.unlink()
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
stale_channel = self.directory / "channel.md"
|
|
72
|
+
if stale_channel.exists() and is_owned_command_file(
|
|
73
|
+
stale_channel,
|
|
74
|
+
("CIEL_RUNTIME_CHANNEL_BRIDGE", "ciel-runtime channel bridge"),
|
|
75
|
+
):
|
|
76
|
+
try:
|
|
77
|
+
stale_channel.unlink()
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
for name, asset in assets.items():
|
|
81
|
+
self.install_one(name, asset)
|
|
82
|
+
except Exception as error:
|
|
83
|
+
self.warn(f"could not install command assets ({type(error).__name__}: {error}).")
|
|
84
|
+
|
|
85
|
+
def remove_all(
|
|
86
|
+
self,
|
|
87
|
+
assets: dict[str, CommandAsset],
|
|
88
|
+
stale_glob: str | None = None,
|
|
89
|
+
stale_markers: tuple[str, ...] = (),
|
|
90
|
+
) -> None:
|
|
91
|
+
try:
|
|
92
|
+
if not self.directory.exists():
|
|
93
|
+
return
|
|
94
|
+
for name, asset in assets.items():
|
|
95
|
+
path = self.directory / name
|
|
96
|
+
if path.exists() and is_owned_command_file(path, asset.ownership_markers):
|
|
97
|
+
path.unlink()
|
|
98
|
+
if stale_glob:
|
|
99
|
+
for path in self.directory.glob(stale_glob):
|
|
100
|
+
if path.name not in assets and is_owned_command_file(path, stale_markers):
|
|
101
|
+
path.unlink()
|
|
102
|
+
except Exception as error:
|
|
103
|
+
self.warn(f"could not remove command assets ({type(error).__name__}: {error}).")
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Provider-aware API-key compatibility probe request builder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Callable, Protocol
|
|
7
|
+
import urllib.error
|
|
8
|
+
|
|
9
|
+
from .architecture import ProviderRequestPolicy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ProviderConfig = dict[str, Any]
|
|
13
|
+
RequestBody = dict[str, Any]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CompatibilityApiKeyProbeError(Exception):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
message: str,
|
|
20
|
+
code: int | None = None,
|
|
21
|
+
diagnosis: str = "",
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.code = code
|
|
25
|
+
self.diagnosis = diagnosis
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OllamaProbeRequest(Protocol):
|
|
29
|
+
def __call__(
|
|
30
|
+
self,
|
|
31
|
+
model: str,
|
|
32
|
+
body: RequestBody,
|
|
33
|
+
config: ProviderConfig,
|
|
34
|
+
*,
|
|
35
|
+
stream: bool,
|
|
36
|
+
provider: str,
|
|
37
|
+
) -> RequestBody: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OpenAiProbeRequest(Protocol):
|
|
41
|
+
def __call__(
|
|
42
|
+
self,
|
|
43
|
+
provider: str,
|
|
44
|
+
model: str,
|
|
45
|
+
body: RequestBody,
|
|
46
|
+
config: ProviderConfig,
|
|
47
|
+
*,
|
|
48
|
+
stream: bool,
|
|
49
|
+
) -> RequestBody: ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ProbePost(Protocol):
|
|
53
|
+
def __call__(
|
|
54
|
+
self,
|
|
55
|
+
url: str,
|
|
56
|
+
body: RequestBody,
|
|
57
|
+
*,
|
|
58
|
+
headers: dict[str, str],
|
|
59
|
+
timeout: float,
|
|
60
|
+
provider: str,
|
|
61
|
+
pcfg: ProviderConfig,
|
|
62
|
+
) -> Any: ...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class CompatibilityProbeProjectionPorts:
|
|
67
|
+
normalize_thinking: Callable[
|
|
68
|
+
[str, ProviderConfig, RequestBody],
|
|
69
|
+
RequestBody,
|
|
70
|
+
]
|
|
71
|
+
normalize_tool_choice: Callable[
|
|
72
|
+
[str, ProviderConfig, RequestBody],
|
|
73
|
+
RequestBody,
|
|
74
|
+
]
|
|
75
|
+
resolve_model: Callable[[str, ProviderConfig, str], str]
|
|
76
|
+
headers: Callable[[str, ProviderConfig], dict[str, str]]
|
|
77
|
+
request_policy: Callable[
|
|
78
|
+
[str, ProviderConfig],
|
|
79
|
+
ProviderRequestPolicy,
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True, slots=True)
|
|
84
|
+
class CompatibilityProbeRoutingPorts:
|
|
85
|
+
ollama_request: OllamaProbeRequest
|
|
86
|
+
openai_request: OpenAiProbeRequest
|
|
87
|
+
endpoint: Callable[[str, ProviderConfig, str], str]
|
|
88
|
+
opencode_endpoint_kind: Callable[[str, str, ProviderConfig], str]
|
|
89
|
+
openai_router_enabled: Callable[[str, ProviderConfig], bool]
|
|
90
|
+
request_base: Callable[[str, ProviderConfig], str]
|
|
91
|
+
join_url: Callable[[str, str], str]
|
|
92
|
+
ncp_model_id: Callable[[str], str]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True, slots=True)
|
|
96
|
+
class CompatibilityProbeAnthropicPorts:
|
|
97
|
+
cap_body: Callable[[str, ProviderConfig, RequestBody], RequestBody]
|
|
98
|
+
apply_options: Callable[[str, ProviderConfig, RequestBody], RequestBody]
|
|
99
|
+
resolve_tool_models: Callable[
|
|
100
|
+
[str, ProviderConfig, RequestBody],
|
|
101
|
+
RequestBody,
|
|
102
|
+
]
|
|
103
|
+
native_compat_enabled: Callable[[str, ProviderConfig], bool]
|
|
104
|
+
native_base_url: Callable[[str, ProviderConfig], str]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True, slots=True)
|
|
108
|
+
class CompatibilityApiKeyProbeBuilder:
|
|
109
|
+
projection: CompatibilityProbeProjectionPorts
|
|
110
|
+
routing: CompatibilityProbeRoutingPorts
|
|
111
|
+
anthropic: CompatibilityProbeAnthropicPorts
|
|
112
|
+
|
|
113
|
+
def build(
|
|
114
|
+
self,
|
|
115
|
+
provider: str,
|
|
116
|
+
config: ProviderConfig,
|
|
117
|
+
model: str,
|
|
118
|
+
request_body: RequestBody,
|
|
119
|
+
) -> tuple[str, RequestBody, dict[str, str]]:
|
|
120
|
+
body = self.projection.normalize_thinking(
|
|
121
|
+
provider,
|
|
122
|
+
config,
|
|
123
|
+
request_body,
|
|
124
|
+
)
|
|
125
|
+
body = self.projection.normalize_tool_choice(provider, config, body)
|
|
126
|
+
upstream_model = self.projection.resolve_model(provider, config, model)
|
|
127
|
+
headers = self.projection.headers(provider, config)
|
|
128
|
+
policy = self.projection.request_policy(provider, config)
|
|
129
|
+
|
|
130
|
+
if policy.probe_strategy == "ollama":
|
|
131
|
+
probe_body = self.routing.ollama_request(
|
|
132
|
+
upstream_model,
|
|
133
|
+
body,
|
|
134
|
+
config,
|
|
135
|
+
stream=False,
|
|
136
|
+
provider=provider,
|
|
137
|
+
)
|
|
138
|
+
return (
|
|
139
|
+
self.routing.endpoint(provider, config, "ollama_chat"),
|
|
140
|
+
probe_body,
|
|
141
|
+
headers,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if policy.probe_strategy == "opencode":
|
|
145
|
+
endpoint_kind = self.routing.opencode_endpoint_kind(
|
|
146
|
+
provider,
|
|
147
|
+
upstream_model,
|
|
148
|
+
config,
|
|
149
|
+
)
|
|
150
|
+
if endpoint_kind == "openai-chat":
|
|
151
|
+
probe_body = self.routing.openai_request(
|
|
152
|
+
provider,
|
|
153
|
+
upstream_model,
|
|
154
|
+
body,
|
|
155
|
+
config,
|
|
156
|
+
stream=False,
|
|
157
|
+
)
|
|
158
|
+
url = self.routing.join_url(
|
|
159
|
+
self.routing.request_base(provider, config),
|
|
160
|
+
"/v1/chat/completions",
|
|
161
|
+
)
|
|
162
|
+
return url, probe_body, headers
|
|
163
|
+
if endpoint_kind != "anthropic-messages":
|
|
164
|
+
raise CompatibilityApiKeyProbeError(
|
|
165
|
+
f"model {upstream_model!r} uses unsupported endpoint family "
|
|
166
|
+
f"{endpoint_kind!r} for API-key probing"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if self.routing.openai_router_enabled(provider, config):
|
|
170
|
+
if policy.model_alias_strategy == "ncp":
|
|
171
|
+
upstream_model = self.routing.ncp_model_id(upstream_model)
|
|
172
|
+
probe_body = self.routing.openai_request(
|
|
173
|
+
provider,
|
|
174
|
+
upstream_model,
|
|
175
|
+
body,
|
|
176
|
+
config,
|
|
177
|
+
stream=False,
|
|
178
|
+
)
|
|
179
|
+
return (
|
|
180
|
+
self.routing.endpoint(provider, config, "openai_chat"),
|
|
181
|
+
probe_body,
|
|
182
|
+
headers,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
body = self.anthropic.cap_body(provider, config, body)
|
|
186
|
+
body = self.anthropic.apply_options(provider, config, body)
|
|
187
|
+
body = dict(body)
|
|
188
|
+
body["model"] = upstream_model
|
|
189
|
+
body = self.anthropic.resolve_tool_models(provider, config, body)
|
|
190
|
+
if self.anthropic.native_compat_enabled(provider, config):
|
|
191
|
+
base = self.anthropic.native_base_url(provider, config)
|
|
192
|
+
else:
|
|
193
|
+
base = self.routing.request_base(provider, config)
|
|
194
|
+
return self.routing.join_url(base, "/v1/messages"), body, headers
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(frozen=True, slots=True)
|
|
198
|
+
class CompatibilityApiKeyProbeRunnerPorts:
|
|
199
|
+
api_keys: Callable[[str, ProviderConfig], list[str]]
|
|
200
|
+
mask_secret: Callable[[str], str]
|
|
201
|
+
build_request: Callable[
|
|
202
|
+
[str, ProviderConfig, str, RequestBody],
|
|
203
|
+
tuple[str, RequestBody, dict[str, str]],
|
|
204
|
+
]
|
|
205
|
+
post: ProbePost
|
|
206
|
+
http_error_message: Callable[[urllib.error.HTTPError], str]
|
|
207
|
+
failure_diagnosis: Callable[[str, int | None, str], str | None]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass(frozen=True, slots=True)
|
|
211
|
+
class CompatibilityApiKeyProbeRunner:
|
|
212
|
+
ports: CompatibilityApiKeyProbeRunnerPorts
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def single_key_config(
|
|
216
|
+
config: ProviderConfig,
|
|
217
|
+
key: str,
|
|
218
|
+
) -> ProviderConfig:
|
|
219
|
+
keyed = dict(config)
|
|
220
|
+
keyed["api_key"] = key
|
|
221
|
+
keyed["api_keys"] = []
|
|
222
|
+
return keyed
|
|
223
|
+
|
|
224
|
+
def run(
|
|
225
|
+
self,
|
|
226
|
+
provider: str,
|
|
227
|
+
config: ProviderConfig,
|
|
228
|
+
model: str,
|
|
229
|
+
request_body: RequestBody,
|
|
230
|
+
timeout: float,
|
|
231
|
+
) -> list[str]:
|
|
232
|
+
keys = self.ports.api_keys(provider, config)
|
|
233
|
+
if len(keys) <= 1:
|
|
234
|
+
return []
|
|
235
|
+
lines = [f"API key checks: running {len(keys)} configured keys"]
|
|
236
|
+
for index, key in enumerate(keys, start=1):
|
|
237
|
+
label = (
|
|
238
|
+
f"API key {index}/{len(keys)} "
|
|
239
|
+
f"({self.ports.mask_secret(key)})"
|
|
240
|
+
)
|
|
241
|
+
keyed_config = self.single_key_config(config, key)
|
|
242
|
+
try:
|
|
243
|
+
url, body, headers = self.ports.build_request(
|
|
244
|
+
provider,
|
|
245
|
+
keyed_config,
|
|
246
|
+
model,
|
|
247
|
+
request_body,
|
|
248
|
+
)
|
|
249
|
+
self.ports.post(
|
|
250
|
+
url,
|
|
251
|
+
body,
|
|
252
|
+
headers=headers,
|
|
253
|
+
timeout=timeout,
|
|
254
|
+
provider=provider,
|
|
255
|
+
pcfg=keyed_config,
|
|
256
|
+
)
|
|
257
|
+
except CompatibilityApiKeyProbeError:
|
|
258
|
+
raise
|
|
259
|
+
except urllib.error.HTTPError as exc:
|
|
260
|
+
message = self.ports.http_error_message(exc)
|
|
261
|
+
diagnosis = (
|
|
262
|
+
self.ports.failure_diagnosis(
|
|
263
|
+
provider,
|
|
264
|
+
exc.code,
|
|
265
|
+
message,
|
|
266
|
+
)
|
|
267
|
+
or ""
|
|
268
|
+
)
|
|
269
|
+
raise CompatibilityApiKeyProbeError(
|
|
270
|
+
f"{label}: {message}",
|
|
271
|
+
exc.code,
|
|
272
|
+
diagnosis,
|
|
273
|
+
) from exc
|
|
274
|
+
except TimeoutError as exc:
|
|
275
|
+
raise CompatibilityApiKeyProbeError(
|
|
276
|
+
f"{label}: timed out before the {timeout:g}s "
|
|
277
|
+
"API-key probe timeout"
|
|
278
|
+
) from exc
|
|
279
|
+
except Exception as exc:
|
|
280
|
+
raise CompatibilityApiKeyProbeError(
|
|
281
|
+
f"{label}: {type(exc).__name__}: {exc}"
|
|
282
|
+
) from exc
|
|
283
|
+
lines.append(f"{label}: OK")
|
|
284
|
+
return lines
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
__all__ = [
|
|
288
|
+
"CompatibilityApiKeyProbeBuilder",
|
|
289
|
+
"CompatibilityApiKeyProbeError",
|
|
290
|
+
"CompatibilityApiKeyProbeRunner",
|
|
291
|
+
"CompatibilityApiKeyProbeRunnerPorts",
|
|
292
|
+
"CompatibilityProbeAnthropicPorts",
|
|
293
|
+
"CompatibilityProbeProjectionPorts",
|
|
294
|
+
"CompatibilityProbeRoutingPorts",
|
|
295
|
+
]
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Protocol codec for provider compatibility probes."""
|
|
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
|
+
import urllib.error
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class CompatibilityProtocolPorts:
|
|
15
|
+
max_tokens_for_model: Callable[[str], int]
|
|
16
|
+
first_header: Callable[[Any, list[str]], str | None]
|
|
17
|
+
parse_retry_after: Callable[[str], float | None]
|
|
18
|
+
format_duration: Callable[[float], str]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CompatibilityProtocolCodec:
|
|
22
|
+
def __init__(self, tool_name: str, ports: CompatibilityProtocolPorts) -> None:
|
|
23
|
+
self.tool_name = tool_name
|
|
24
|
+
self.ports = ports
|
|
25
|
+
|
|
26
|
+
def tool_schema(self) -> dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
"name": self.tool_name,
|
|
29
|
+
"description": "A minimal compatibility test tool. It echoes one required text argument.",
|
|
30
|
+
"input_schema": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {"text": {"type": "string"}},
|
|
33
|
+
"required": ["text"],
|
|
34
|
+
"additionalProperties": False,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def text_request(self, model: str) -> dict[str, Any]:
|
|
39
|
+
return {
|
|
40
|
+
"model": model,
|
|
41
|
+
"max_tokens": self.ports.max_tokens_for_model(model),
|
|
42
|
+
"stream": False,
|
|
43
|
+
"messages": [
|
|
44
|
+
{
|
|
45
|
+
"role": "user",
|
|
46
|
+
"content": "Compatibility text test. Reply with exactly OK and do not call tools.",
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def tool_request(self, model: str) -> dict[str, Any]:
|
|
52
|
+
return {
|
|
53
|
+
"model": model,
|
|
54
|
+
"max_tokens": 128,
|
|
55
|
+
"stream": False,
|
|
56
|
+
"messages": [
|
|
57
|
+
{
|
|
58
|
+
"role": "user",
|
|
59
|
+
"content": "Compatibility tool test. Use the compat_echo tool exactly once with text set to ping.",
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"tools": [self.tool_schema()],
|
|
63
|
+
"tool_choice": {"type": "tool", "name": self.tool_name},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def tool_result_request(
|
|
67
|
+
self,
|
|
68
|
+
model: str,
|
|
69
|
+
tool_use: dict[str, Any],
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
tool_id = str(tool_use.get("id") or "toolu_compat_echo_1")
|
|
72
|
+
tool_input = (
|
|
73
|
+
tool_use.get("input")
|
|
74
|
+
if isinstance(tool_use.get("input"), dict)
|
|
75
|
+
else {"text": "ping"}
|
|
76
|
+
)
|
|
77
|
+
return {
|
|
78
|
+
"model": model,
|
|
79
|
+
"max_tokens": 64,
|
|
80
|
+
"stream": False,
|
|
81
|
+
"messages": [
|
|
82
|
+
{
|
|
83
|
+
"role": "user",
|
|
84
|
+
"content": "Compatibility tool test. Use the compat_echo tool exactly once with text set to ping.",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"role": "assistant",
|
|
88
|
+
"content": [
|
|
89
|
+
{
|
|
90
|
+
"type": "tool_use",
|
|
91
|
+
"id": tool_id,
|
|
92
|
+
"name": self.tool_name,
|
|
93
|
+
"input": tool_input,
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"role": "user",
|
|
99
|
+
"content": [
|
|
100
|
+
{
|
|
101
|
+
"type": "tool_result",
|
|
102
|
+
"tool_use_id": tool_id,
|
|
103
|
+
"content": "pong",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"type": "text",
|
|
107
|
+
"text": "Now reply with FINAL_OK and do not call tools.",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
"tools": [self.tool_schema()],
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def content_blocks(data: Any) -> list[dict[str, Any]]:
|
|
117
|
+
if not isinstance(data, dict):
|
|
118
|
+
return []
|
|
119
|
+
content = data.get("content")
|
|
120
|
+
if not isinstance(content, list):
|
|
121
|
+
return []
|
|
122
|
+
return [block for block in content if isinstance(block, dict)]
|
|
123
|
+
|
|
124
|
+
def content_types(self, data: Any) -> list[str]:
|
|
125
|
+
return [str(block.get("type", "?")) for block in self.content_blocks(data)]
|
|
126
|
+
|
|
127
|
+
def text_preview(self, data: Any) -> str:
|
|
128
|
+
parts = [
|
|
129
|
+
block["text"].strip()
|
|
130
|
+
for block in self.content_blocks(data)
|
|
131
|
+
if block.get("type") == "text" and isinstance(block.get("text"), str)
|
|
132
|
+
]
|
|
133
|
+
return " ".join(parts).strip()[:300]
|
|
134
|
+
|
|
135
|
+
def find_tool_use(self, data: Any) -> tuple[dict[str, Any] | None, str]:
|
|
136
|
+
for block in self.content_blocks(data):
|
|
137
|
+
if block.get("type") != "tool_use":
|
|
138
|
+
continue
|
|
139
|
+
if block.get("name") != self.tool_name:
|
|
140
|
+
return None, f"unexpected tool name {block.get('name')!r}"
|
|
141
|
+
tool_input = block.get("input")
|
|
142
|
+
if not isinstance(tool_input, dict):
|
|
143
|
+
return None, "tool input was not a JSON object"
|
|
144
|
+
if tool_input.get("text") != "ping":
|
|
145
|
+
return None, (
|
|
146
|
+
f"tool input text was {tool_input.get('text')!r}, expected 'ping'"
|
|
147
|
+
)
|
|
148
|
+
if not block.get("id"):
|
|
149
|
+
return None, "tool_use block did not include an id"
|
|
150
|
+
return block, ""
|
|
151
|
+
types = ", ".join(self.content_types(data)) or "none"
|
|
152
|
+
preview = self.text_preview(data)
|
|
153
|
+
suffix = f"; text={preview!r}" if preview else ""
|
|
154
|
+
return None, (
|
|
155
|
+
f"no {self.tool_name} tool_use block returned; "
|
|
156
|
+
f"content blocks: {types}{suffix}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def summarize_response(self, data: Any, label: str) -> list[str]:
|
|
160
|
+
lines = [f"{label}: OK"]
|
|
161
|
+
if not isinstance(data, dict):
|
|
162
|
+
return lines
|
|
163
|
+
stop = data.get("stop_reason")
|
|
164
|
+
if stop:
|
|
165
|
+
lines.append(f"Stop reason: {stop}")
|
|
166
|
+
types = self.content_types(data)
|
|
167
|
+
if types:
|
|
168
|
+
lines.append("Content blocks: " + ", ".join(types[:6]))
|
|
169
|
+
usage = data.get("usage")
|
|
170
|
+
if isinstance(usage, dict):
|
|
171
|
+
tokens = []
|
|
172
|
+
if "input_tokens" in usage:
|
|
173
|
+
tokens.append(f"in={usage['input_tokens']}")
|
|
174
|
+
if "output_tokens" in usage:
|
|
175
|
+
tokens.append(f"out={usage['output_tokens']}")
|
|
176
|
+
if tokens:
|
|
177
|
+
lines.append("Tokens: " + ", ".join(tokens))
|
|
178
|
+
return lines
|
|
179
|
+
|
|
180
|
+
def http_error_message(self, exc: urllib.error.HTTPError) -> str:
|
|
181
|
+
raw = exc.read().decode("utf-8", errors="ignore")
|
|
182
|
+
message = raw.strip()
|
|
183
|
+
error_type = ""
|
|
184
|
+
try:
|
|
185
|
+
error = json.loads(raw)
|
|
186
|
+
if isinstance(error, dict):
|
|
187
|
+
if isinstance(error.get("error"), dict):
|
|
188
|
+
error_object = error["error"]
|
|
189
|
+
error_type = str(error_object.get("type") or "").strip()
|
|
190
|
+
message = str(
|
|
191
|
+
error_object.get("message")
|
|
192
|
+
or json.dumps(error_object, ensure_ascii=False)
|
|
193
|
+
)
|
|
194
|
+
elif error.get("message"):
|
|
195
|
+
message = str(error["message"])
|
|
196
|
+
error_type = str(error.get("type") or "").strip()
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
if error_type and error_type not in message:
|
|
200
|
+
message = f"{error_type}: {message}"
|
|
201
|
+
retry_after = self.ports.first_header(
|
|
202
|
+
exc.headers,
|
|
203
|
+
["Retry-After", "retry-after"],
|
|
204
|
+
)
|
|
205
|
+
if not retry_after:
|
|
206
|
+
return message
|
|
207
|
+
retry_text = retry_after.strip()
|
|
208
|
+
retry_seconds = self.ports.parse_retry_after(retry_text)
|
|
209
|
+
if retry_seconds is None:
|
|
210
|
+
return f"{message} Retry-After: {retry_text}"
|
|
211
|
+
display = self.ports.format_duration(retry_seconds)
|
|
212
|
+
if not retry_text:
|
|
213
|
+
return f"{message} Retry-After: {display}"
|
|
214
|
+
raw_suffix = f"{retry_text}s" if re.fullmatch(r"\d+(?:\.\d+)?", retry_text) else retry_text
|
|
215
|
+
return f"{message} Retry-After: {display} ({raw_suffix})"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass(frozen=True, slots=True)
|
|
219
|
+
class CompatibilityProtocolApi:
|
|
220
|
+
"""Explicit compatibility facade backed by a late-bound protocol codec."""
|
|
221
|
+
|
|
222
|
+
codec_factory: Callable[[], CompatibilityProtocolCodec]
|
|
223
|
+
|
|
224
|
+
def tool_schema(self) -> dict[str, Any]:
|
|
225
|
+
return self.codec_factory().tool_schema()
|
|
226
|
+
|
|
227
|
+
def text_request(self, model: str) -> dict[str, Any]:
|
|
228
|
+
return self.codec_factory().text_request(model)
|
|
229
|
+
|
|
230
|
+
def tool_request(self, model: str) -> dict[str, Any]:
|
|
231
|
+
return self.codec_factory().tool_request(model)
|
|
232
|
+
|
|
233
|
+
def tool_result_request(
|
|
234
|
+
self, model: str, tool_use: dict[str, Any]
|
|
235
|
+
) -> dict[str, Any]:
|
|
236
|
+
return self.codec_factory().tool_result_request(model, tool_use)
|
|
237
|
+
|
|
238
|
+
def content_types(self, data: Any) -> list[str]:
|
|
239
|
+
return self.codec_factory().content_types(data)
|
|
240
|
+
|
|
241
|
+
def text_preview(self, data: Any) -> str:
|
|
242
|
+
return self.codec_factory().text_preview(data)
|
|
243
|
+
|
|
244
|
+
def find_tool_use(self, data: Any) -> tuple[dict[str, Any] | None, str]:
|
|
245
|
+
return self.codec_factory().find_tool_use(data)
|
|
246
|
+
|
|
247
|
+
def summarize_response(self, data: Any, label: str) -> list[str]:
|
|
248
|
+
return self.codec_factory().summarize_response(data, label)
|
|
249
|
+
|
|
250
|
+
def http_error_message(self, exc: urllib.error.HTTPError) -> str:
|
|
251
|
+
return self.codec_factory().http_error_message(exc)
|