@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,105 @@
|
|
|
1
|
+
"""Policies and controller for locally synthesized Claude Code tool uses."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class SyntheticTasklistPorts:
|
|
10
|
+
provider_enabled: Callable[[str], bool]
|
|
11
|
+
content_to_text: Callable[[Any], str]
|
|
12
|
+
should_continue: Callable[..., bool]
|
|
13
|
+
now_ms: Callable[[], int]
|
|
14
|
+
log: Callable[[str, str], None]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class SyntheticTasklistPolicy:
|
|
19
|
+
ports: SyntheticTasklistPorts
|
|
20
|
+
|
|
21
|
+
def append(
|
|
22
|
+
self,
|
|
23
|
+
message: dict[str, Any],
|
|
24
|
+
model: str,
|
|
25
|
+
source_body: dict[str, Any],
|
|
26
|
+
reason: str,
|
|
27
|
+
provider: str = "",
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
if not self.ports.provider_enabled(provider):
|
|
30
|
+
return message
|
|
31
|
+
content = message.get("content")
|
|
32
|
+
if not isinstance(content, list):
|
|
33
|
+
content = [{"type": "text", "text": self.ports.content_to_text(content)}] if content else []
|
|
34
|
+
tool_calls = [block for block in content if isinstance(block, dict) and block.get("type") == "tool_use"]
|
|
35
|
+
if not self.ports.should_continue(source_body, self.ports.content_to_text(content), tool_calls):
|
|
36
|
+
return message
|
|
37
|
+
projected = dict(message)
|
|
38
|
+
projected["content"] = [
|
|
39
|
+
*content,
|
|
40
|
+
{
|
|
41
|
+
"type": "tool_use",
|
|
42
|
+
"id": f"toolu_ciel_runtime_TaskList_{reason}_{self.ports.now_ms()}",
|
|
43
|
+
"name": "TaskList",
|
|
44
|
+
"input": {},
|
|
45
|
+
},
|
|
46
|
+
]
|
|
47
|
+
projected["stop_reason"] = "tool_use"
|
|
48
|
+
projected.setdefault("model", model or message.get("model") or "ciel-runtime-router")
|
|
49
|
+
self.ports.log("WARN", f"auto-synthesized TaskList after clarification question ({reason})")
|
|
50
|
+
return projected
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class ForcedPlanModePorts:
|
|
55
|
+
forced_choice_name: Callable[[dict[str, Any]], str]
|
|
56
|
+
should_defer: Callable[..., bool]
|
|
57
|
+
tool_names: Callable[[dict[str, Any]], set[str]]
|
|
58
|
+
plan_mode_active: Callable[[dict[str, Any]], bool]
|
|
59
|
+
synthetic_response: Callable[..., dict[str, Any]]
|
|
60
|
+
write_json: Callable[..., Any]
|
|
61
|
+
log: Callable[[str, str], None]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class ForcedPlanModeController:
|
|
66
|
+
ports: ForcedPlanModePorts
|
|
67
|
+
|
|
68
|
+
def handle(
|
|
69
|
+
self,
|
|
70
|
+
handler: Any,
|
|
71
|
+
provider: str,
|
|
72
|
+
provider_config: dict[str, Any],
|
|
73
|
+
body: dict[str, Any],
|
|
74
|
+
) -> bool:
|
|
75
|
+
if provider == "anthropic":
|
|
76
|
+
return False
|
|
77
|
+
name = self.ports.forced_choice_name(body)
|
|
78
|
+
if name != "EnterPlanMode":
|
|
79
|
+
return False
|
|
80
|
+
if self.ports.should_defer(provider, provider_config, body, name):
|
|
81
|
+
self.ports.log(
|
|
82
|
+
"INFO",
|
|
83
|
+
f"deferred forced {name} tool_choice to native Anthropic-compatible upstream because thinking is enabled",
|
|
84
|
+
)
|
|
85
|
+
return False
|
|
86
|
+
available = self.ports.tool_names(body)
|
|
87
|
+
if available and name not in available:
|
|
88
|
+
return False
|
|
89
|
+
if self.ports.plan_mode_active(body):
|
|
90
|
+
self.ports.log("WARN", f"ignored forced {name} tool_choice because plan mode is already active")
|
|
91
|
+
return False
|
|
92
|
+
self.ports.log("INFO", f"synthesized {name} tool_use for {provider} forced tool_choice")
|
|
93
|
+
self.ports.write_json(
|
|
94
|
+
handler,
|
|
95
|
+
self.ports.synthetic_response(str(body.get("model") or ""), name, {}),
|
|
96
|
+
)
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
"ForcedPlanModeController",
|
|
102
|
+
"ForcedPlanModePorts",
|
|
103
|
+
"SyntheticTasklistPolicy",
|
|
104
|
+
"SyntheticTasklistPorts",
|
|
105
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Terminal sizing, POSIX PTY sizing, and input-mode reset policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Callable, Mapping
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
TERMINAL_INPUT_MODE_RESET = (
|
|
15
|
+
"\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1004l"
|
|
16
|
+
"\x1b[?1005l\x1b[?1006l\x1b[?1015l"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def read_clipboard_text() -> str:
|
|
21
|
+
if os.name == "nt":
|
|
22
|
+
commands = [["powershell", "-NoProfile", "-Command", "Get-Clipboard -Raw"]]
|
|
23
|
+
elif sys.platform == "darwin":
|
|
24
|
+
commands = [["pbpaste"]]
|
|
25
|
+
else:
|
|
26
|
+
commands = [
|
|
27
|
+
["xclip", "-selection", "clipboard", "-o"],
|
|
28
|
+
["xsel", "--clipboard", "--output"],
|
|
29
|
+
]
|
|
30
|
+
for command in commands:
|
|
31
|
+
try:
|
|
32
|
+
process = subprocess.run(
|
|
33
|
+
command,
|
|
34
|
+
text=True,
|
|
35
|
+
stdout=subprocess.PIPE,
|
|
36
|
+
stderr=subprocess.DEVNULL,
|
|
37
|
+
timeout=5,
|
|
38
|
+
)
|
|
39
|
+
if process.returncode == 0 and process.stdout.strip():
|
|
40
|
+
return process.stdout.strip()
|
|
41
|
+
except Exception:
|
|
42
|
+
continue
|
|
43
|
+
return ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def terminal_winsize_from_fd(fd: int) -> tuple[int, int]:
|
|
47
|
+
"""Return terminal size as (rows, columns), never 0x0."""
|
|
48
|
+
try:
|
|
49
|
+
size = os.get_terminal_size(fd)
|
|
50
|
+
rows = int(size.lines)
|
|
51
|
+
columns = int(size.columns)
|
|
52
|
+
except Exception:
|
|
53
|
+
rows = 0
|
|
54
|
+
columns = 0
|
|
55
|
+
if rows > 0 and columns > 0:
|
|
56
|
+
return rows, columns
|
|
57
|
+
fallback = shutil.get_terminal_size((80, 24))
|
|
58
|
+
rows = int(getattr(fallback, "lines", 0) or 0)
|
|
59
|
+
columns = int(getattr(fallback, "columns", 0) or 0)
|
|
60
|
+
if rows <= 0:
|
|
61
|
+
rows = 24
|
|
62
|
+
if columns <= 0:
|
|
63
|
+
columns = 80
|
|
64
|
+
return rows, columns
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def apply_pty_winsize(
|
|
68
|
+
pty_fd: int,
|
|
69
|
+
rows: int,
|
|
70
|
+
columns: int,
|
|
71
|
+
*,
|
|
72
|
+
platform_name: str | None = None,
|
|
73
|
+
) -> bool:
|
|
74
|
+
if (platform_name or os.name) != "posix" or rows <= 0 or columns <= 0:
|
|
75
|
+
return False
|
|
76
|
+
try:
|
|
77
|
+
import fcntl
|
|
78
|
+
import struct
|
|
79
|
+
import termios
|
|
80
|
+
|
|
81
|
+
fcntl.ioctl(
|
|
82
|
+
pty_fd,
|
|
83
|
+
termios.TIOCSWINSZ,
|
|
84
|
+
struct.pack("HHHH", rows, columns, 0, 0),
|
|
85
|
+
)
|
|
86
|
+
return True
|
|
87
|
+
except Exception:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True, slots=True)
|
|
92
|
+
class TerminalInputModeResetPolicy:
|
|
93
|
+
platform_name: str
|
|
94
|
+
environment: Mapping[str, str]
|
|
95
|
+
parse_bool: Callable[..., bool]
|
|
96
|
+
default_stream: Callable[[], Any]
|
|
97
|
+
|
|
98
|
+
def enabled(self) -> bool:
|
|
99
|
+
configured = self.environment.get(
|
|
100
|
+
"CIEL_RUNTIME_TERMINAL_INPUT_MODE_RESET"
|
|
101
|
+
)
|
|
102
|
+
if self.platform_name == "nt" and configured is None:
|
|
103
|
+
return False
|
|
104
|
+
return self.parse_bool(configured, True)
|
|
105
|
+
|
|
106
|
+
def interval_seconds(self, default: float = 2.0) -> float:
|
|
107
|
+
raw = self.environment.get(
|
|
108
|
+
"CIEL_RUNTIME_TERMINAL_INPUT_MODE_RESET_INTERVAL_SECONDS"
|
|
109
|
+
)
|
|
110
|
+
if raw is None:
|
|
111
|
+
return default
|
|
112
|
+
try:
|
|
113
|
+
return max(0.25, min(60.0, float(raw)))
|
|
114
|
+
except Exception:
|
|
115
|
+
return default
|
|
116
|
+
|
|
117
|
+
def write(self, stream: Any | None = None) -> None:
|
|
118
|
+
if not self.enabled():
|
|
119
|
+
return
|
|
120
|
+
target = stream if stream is not None else self.default_stream()
|
|
121
|
+
try:
|
|
122
|
+
if hasattr(target, "isatty") and not target.isatty():
|
|
123
|
+
return
|
|
124
|
+
target.write(TERMINAL_INPUT_MODE_RESET)
|
|
125
|
+
target.flush()
|
|
126
|
+
except Exception:
|
|
127
|
+
return
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""User-selectable timeout profile projection and mutation service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
TimeoutPreset = tuple[int, str, str]
|
|
11
|
+
LocalizedText = tuple[str, str]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class TimeoutProfileSettings:
|
|
16
|
+
default_timeout_ms: int
|
|
17
|
+
profiles: Mapping[str, TimeoutPreset]
|
|
18
|
+
localized_profiles: Mapping[str, Mapping[str, LocalizedText]]
|
|
19
|
+
llm_preset_timeouts: Mapping[str, int]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class TimeoutProfilePorts:
|
|
24
|
+
positive_int: Callable[[Any], int | None]
|
|
25
|
+
pad_cells: Callable[[str, int], str]
|
|
26
|
+
ui_text: Callable[[str, str], str]
|
|
27
|
+
format_minutes: Callable[[int, str], str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TimeoutProfileService:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
settings: TimeoutProfileSettings,
|
|
34
|
+
ports: TimeoutProfilePorts,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.settings = settings
|
|
37
|
+
self.ports = ports
|
|
38
|
+
|
|
39
|
+
def llm_preset_timeout(self, preset_id: str) -> int:
|
|
40
|
+
return self.settings.llm_preset_timeouts.get(
|
|
41
|
+
preset_id,
|
|
42
|
+
self.settings.default_timeout_ms,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def active_llm_preset_timeout(self, config: dict[str, Any]) -> int | None:
|
|
46
|
+
preset_id = str(config.get("llm_preset") or "").strip()
|
|
47
|
+
if not preset_id:
|
|
48
|
+
return None
|
|
49
|
+
return self.ports.positive_int(
|
|
50
|
+
self.settings.llm_preset_timeouts.get(preset_id)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def profile_id(self, milliseconds: int | None) -> str | None:
|
|
54
|
+
if not milliseconds:
|
|
55
|
+
return None
|
|
56
|
+
for profile_id, (profile_ms, _label, _description) in self.settings.profiles.items():
|
|
57
|
+
if milliseconds == profile_ms:
|
|
58
|
+
return profile_id
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def text(self, profile_id: str, language: str) -> LocalizedText:
|
|
62
|
+
if profile_id == "__custom__":
|
|
63
|
+
return {
|
|
64
|
+
"ko": ("사용자 지정", "직접 입력한 timeout 값"),
|
|
65
|
+
"ja": ("カスタム", "直接入力した timeout 値"),
|
|
66
|
+
"zh": ("自定义", "手动输入的 timeout 值"),
|
|
67
|
+
}.get(language, ("Custom", "manually entered timeout value"))
|
|
68
|
+
fallback = self.settings.profiles[profile_id]
|
|
69
|
+
return self.settings.localized_profiles.get(language, {}).get(
|
|
70
|
+
profile_id,
|
|
71
|
+
(fallback[1], fallback[2]),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def status(self, config: dict[str, Any], language: str) -> str:
|
|
75
|
+
milliseconds = (
|
|
76
|
+
self.ports.positive_int(config.get("request_timeout_ms"))
|
|
77
|
+
or self.settings.default_timeout_ms
|
|
78
|
+
)
|
|
79
|
+
profile_id = self.profile_id(milliseconds)
|
|
80
|
+
label = self.text(profile_id or "__custom__", language)[0]
|
|
81
|
+
idle = self.ports.positive_int(config.get("stream_idle_timeout_ms"))
|
|
82
|
+
idle_text = f"; idle {idle}ms" if idle and idle != milliseconds else ""
|
|
83
|
+
return f"{label}; {milliseconds}ms{idle_text}"
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def idle_timeout(milliseconds: int) -> int:
|
|
87
|
+
return min(milliseconds, 300000)
|
|
88
|
+
|
|
89
|
+
def panel_rows(
|
|
90
|
+
self,
|
|
91
|
+
config: dict[str, Any],
|
|
92
|
+
language: str,
|
|
93
|
+
) -> tuple[list[str], list[str]]:
|
|
94
|
+
current_ms = (
|
|
95
|
+
self.ports.positive_int(config.get("request_timeout_ms"))
|
|
96
|
+
or self.settings.default_timeout_ms
|
|
97
|
+
)
|
|
98
|
+
rows = [
|
|
99
|
+
f"Current timeout: {current_ms} ms = "
|
|
100
|
+
f"{self.ports.format_minutes(current_ms, language)}"
|
|
101
|
+
]
|
|
102
|
+
values = ["__info__"]
|
|
103
|
+
current_profile = self.profile_id(current_ms)
|
|
104
|
+
for profile_id, (milliseconds, _label, _description) in self.settings.profiles.items():
|
|
105
|
+
label, description = self.text(profile_id, language)
|
|
106
|
+
mark = "*" if profile_id == current_profile else " "
|
|
107
|
+
rows.append(
|
|
108
|
+
f"{mark} {self.ports.pad_cells(label, 22)} "
|
|
109
|
+
f"{milliseconds:>7} ms {description}"
|
|
110
|
+
)
|
|
111
|
+
values.append(profile_id)
|
|
112
|
+
rows.append(self.ports.ui_text("back", language))
|
|
113
|
+
values.append("back")
|
|
114
|
+
return rows, values
|
|
115
|
+
|
|
116
|
+
def apply(
|
|
117
|
+
self,
|
|
118
|
+
config: dict[str, Any],
|
|
119
|
+
profile_id: str,
|
|
120
|
+
language: str,
|
|
121
|
+
) -> list[str]:
|
|
122
|
+
if profile_id not in self.settings.profiles:
|
|
123
|
+
raise SystemExit(f"Unknown timeout preset: {profile_id}")
|
|
124
|
+
milliseconds, _label, _description = self.settings.profiles[profile_id]
|
|
125
|
+
idle_ms = self.idle_timeout(milliseconds)
|
|
126
|
+
config["request_timeout_ms"] = milliseconds
|
|
127
|
+
config["stream_idle_timeout_ms"] = idle_ms
|
|
128
|
+
label = self.text(profile_id, language)[0]
|
|
129
|
+
return [
|
|
130
|
+
f"Timeout preset: {label}",
|
|
131
|
+
f"request_timeout_ms: {milliseconds}",
|
|
132
|
+
f"stream_idle_timeout_ms: {idle_ms}",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
def with_llm_preset_timeout(
|
|
136
|
+
self,
|
|
137
|
+
tokens: list[str],
|
|
138
|
+
preset_id: str,
|
|
139
|
+
) -> list[str]:
|
|
140
|
+
filtered = [
|
|
141
|
+
token
|
|
142
|
+
for token in tokens
|
|
143
|
+
if not token.startswith(
|
|
144
|
+
(
|
|
145
|
+
"timeout=",
|
|
146
|
+
"timeout_ms=",
|
|
147
|
+
"request_timeout=",
|
|
148
|
+
"request_timeout_ms=",
|
|
149
|
+
"stream_idle_timeout=",
|
|
150
|
+
"stream_idle_timeout_ms=",
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
]
|
|
154
|
+
timeout_ms = self.llm_preset_timeout(preset_id)
|
|
155
|
+
idle_ms = self.idle_timeout(timeout_ms)
|
|
156
|
+
filtered.append(f"timeout={timeout_ms}")
|
|
157
|
+
filtered.append(f"stream_idle_timeout_ms={idle_ms}")
|
|
158
|
+
return filtered
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True, slots=True)
|
|
162
|
+
class TimeoutProfileApi:
|
|
163
|
+
"""Explicit compatibility API with an injected default-language source."""
|
|
164
|
+
|
|
165
|
+
service_factory: Callable[[], TimeoutProfileService]
|
|
166
|
+
default_language: Callable[[], str]
|
|
167
|
+
|
|
168
|
+
def _language(self, language: str | None) -> str:
|
|
169
|
+
return language or self.default_language()
|
|
170
|
+
|
|
171
|
+
def llm_preset_timeout_ms(self, preset_id: str) -> int:
|
|
172
|
+
return self.service_factory().llm_preset_timeout(preset_id)
|
|
173
|
+
|
|
174
|
+
def active_llm_preset_timeout_ms(self, pcfg: dict[str, Any]) -> int | None:
|
|
175
|
+
return self.service_factory().active_llm_preset_timeout(pcfg)
|
|
176
|
+
|
|
177
|
+
def timeout_profile_id_for_ms(self, ms: int | None) -> str | None:
|
|
178
|
+
return self.service_factory().profile_id(ms)
|
|
179
|
+
|
|
180
|
+
def timeout_profile_text(self, profile_id: str, lang: str | None = None) -> LocalizedText:
|
|
181
|
+
return self.service_factory().text(profile_id, self._language(lang))
|
|
182
|
+
|
|
183
|
+
def timeout_profile_status(self, pcfg: dict[str, Any], lang: str | None = None) -> str:
|
|
184
|
+
return self.service_factory().status(pcfg, self._language(lang))
|
|
185
|
+
|
|
186
|
+
def timeout_profile_idle_ms(self, request_timeout_ms: int) -> int:
|
|
187
|
+
return self.service_factory().idle_timeout(request_timeout_ms)
|
|
188
|
+
|
|
189
|
+
def timeout_profile_panel_rows(self, pcfg: dict[str, Any], lang: str | None = None) -> tuple[list[str], list[str]]:
|
|
190
|
+
return self.service_factory().panel_rows(pcfg, self._language(lang))
|
|
191
|
+
|
|
192
|
+
def apply_timeout_profile_to_provider(self, pcfg: dict[str, Any], profile_id: str, lang: str | None = None) -> list[str]:
|
|
193
|
+
return self.service_factory().apply(pcfg, profile_id, self._language(lang))
|
|
194
|
+
|
|
195
|
+
def with_preset_timeout_tokens(self, tokens: list[str], preset_id: str) -> list[str]:
|
|
196
|
+
return self.service_factory().with_llm_preset_timeout(tokens, preset_id)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Runtime-specific tool-name dialects and their registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .architecture import ToolDialect
|
|
10
|
+
from .registry import AdapterRegistry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def mcp_server_normalized_key(name: str) -> tuple[str, str] | None:
|
|
14
|
+
match = re.fullmatch(r"mcp__(.+)__(.+)", str(name or ""))
|
|
15
|
+
if not match:
|
|
16
|
+
return None
|
|
17
|
+
server, tool = match.groups()
|
|
18
|
+
return re.sub(r"[-_]", "", server).lower(), tool.lower()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def match_available_tool_name(name: str, available: set[str]) -> str | None:
|
|
22
|
+
if not available:
|
|
23
|
+
return None
|
|
24
|
+
if name in available:
|
|
25
|
+
return name
|
|
26
|
+
lowered = str(name or "").lower()
|
|
27
|
+
case_matches = [candidate for candidate in available if candidate.lower() == lowered]
|
|
28
|
+
if len(case_matches) == 1:
|
|
29
|
+
return case_matches[0]
|
|
30
|
+
if not name.startswith("mcp__"):
|
|
31
|
+
normalized = re.sub(r"[^a-z0-9]+", "", lowered)
|
|
32
|
+
matches = [
|
|
33
|
+
candidate
|
|
34
|
+
for candidate in available
|
|
35
|
+
if not candidate.startswith("mcp__")
|
|
36
|
+
and re.sub(r"[^a-z0-9]+", "", candidate.lower()) == normalized
|
|
37
|
+
]
|
|
38
|
+
if len(matches) == 1:
|
|
39
|
+
return matches[0]
|
|
40
|
+
requested_mcp = mcp_server_normalized_key(name)
|
|
41
|
+
if requested_mcp is not None:
|
|
42
|
+
matches = [candidate for candidate in available if mcp_server_normalized_key(candidate) == requested_mcp]
|
|
43
|
+
if len(matches) == 1:
|
|
44
|
+
return matches[0]
|
|
45
|
+
substring_matches = [
|
|
46
|
+
candidate for candidate in available if lowered and (lowered in candidate.lower() or candidate.lower() in lowered)
|
|
47
|
+
]
|
|
48
|
+
return sorted(substring_matches)[0] if substring_matches else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ClaudeToolDialect(ToolDialect):
|
|
52
|
+
name = "claude"
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
available_tools: set[str] | None = None,
|
|
58
|
+
repair: Callable[[str, dict[str, Any]], Mapping[str, Any]] | None = None,
|
|
59
|
+
blocked: set[str] | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
self._available_tools = available_tools or set()
|
|
62
|
+
self._repair = repair
|
|
63
|
+
self._blocked = frozenset(blocked or ())
|
|
64
|
+
|
|
65
|
+
def normalize_tool_name(self, name: str) -> str:
|
|
66
|
+
return match_available_tool_name(name, self._available_tools) or str(name)
|
|
67
|
+
|
|
68
|
+
def repair_tool_input(self, tool_name: str, value: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
69
|
+
payload = dict(value)
|
|
70
|
+
return self._repair(tool_name, payload) if self._repair else payload
|
|
71
|
+
|
|
72
|
+
def blocked_tools(self) -> frozenset[str]:
|
|
73
|
+
return self._blocked
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
TOOL_DIALECTS: AdapterRegistry[ToolDialect] = AdapterRegistry()
|
|
77
|
+
TOOL_DIALECTS.register("claude", lambda **kwargs: ClaudeToolDialect(**kwargs), aliases=("claude-code",))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"TOOL_DIALECTS",
|
|
82
|
+
"ClaudeToolDialect",
|
|
83
|
+
"match_available_tool_name",
|
|
84
|
+
"mcp_server_normalized_key",
|
|
85
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Policy for removing tools that must not be exposed to an upstream provider."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class ToolExposurePorts:
|
|
10
|
+
blocked_tools: Callable[[str, dict[str, Any]], set[str]]
|
|
11
|
+
ultracode_workflow_preferred: Callable[[dict[str, Any]], bool]
|
|
12
|
+
plan_mode_active: Callable[[dict[str, Any]], bool]
|
|
13
|
+
log: Callable[[str, str], None]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class ToolExposurePolicy:
|
|
18
|
+
ports: ToolExposurePorts
|
|
19
|
+
|
|
20
|
+
def filter(self, provider: str, provider_config: dict[str, Any], body: dict[str, Any]) -> dict[str, Any]:
|
|
21
|
+
blocked = set(self.ports.blocked_tools(provider, provider_config))
|
|
22
|
+
dynamic_blocked: set[str] = set()
|
|
23
|
+
if provider != "anthropic" and self.ports.ultracode_workflow_preferred(body) and not self.ports.plan_mode_active(body):
|
|
24
|
+
dynamic_blocked.add("EnterPlanMode")
|
|
25
|
+
blocked.update(dynamic_blocked)
|
|
26
|
+
if not blocked:
|
|
27
|
+
return body
|
|
28
|
+
tools = body.get("tools")
|
|
29
|
+
tool_choice = body.get("tool_choice") if isinstance(body.get("tool_choice"), dict) else None
|
|
30
|
+
tool_choice_name = tool_choice.get("name") if tool_choice else None
|
|
31
|
+
drop_choice = isinstance(tool_choice_name, str) and tool_choice_name in blocked
|
|
32
|
+
if not isinstance(tools, list) or not tools:
|
|
33
|
+
return self._without_blocked_choice(body, provider, tool_choice_name) if drop_choice else body
|
|
34
|
+
kept: list[Any] = []
|
|
35
|
+
dropped: list[str] = []
|
|
36
|
+
for tool in tools:
|
|
37
|
+
name = tool.get("name") if isinstance(tool, dict) else None
|
|
38
|
+
if isinstance(name, str) and name in blocked:
|
|
39
|
+
dropped.append(name)
|
|
40
|
+
else:
|
|
41
|
+
kept.append(tool)
|
|
42
|
+
if not dropped:
|
|
43
|
+
return self._without_blocked_choice(body, provider, tool_choice_name) if drop_choice else body
|
|
44
|
+
reason = " ultracode_workflow_preferred=true" if dynamic_blocked.intersection(dropped) else ""
|
|
45
|
+
self.ports.log("INFO", f"filtered upstream tools for {provider}: {', '.join(sorted(set(dropped)))}{reason}")
|
|
46
|
+
filtered = dict(body)
|
|
47
|
+
filtered["tools"] = kept
|
|
48
|
+
if drop_choice:
|
|
49
|
+
filtered.pop("tool_choice", None)
|
|
50
|
+
self._log_removed_choice(provider, tool_choice_name)
|
|
51
|
+
return filtered
|
|
52
|
+
|
|
53
|
+
def _without_blocked_choice(self, body: dict[str, Any], provider: str, tool_choice_name: Any) -> dict[str, Any]:
|
|
54
|
+
filtered = dict(body)
|
|
55
|
+
filtered.pop("tool_choice", None)
|
|
56
|
+
self._log_removed_choice(provider, tool_choice_name)
|
|
57
|
+
return filtered
|
|
58
|
+
|
|
59
|
+
def _log_removed_choice(self, provider: str, tool_choice_name: Any) -> None:
|
|
60
|
+
self.ports.log("WARN", f"removed blocked tool_choice for {provider}: {tool_choice_name}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__all__ = ["ToolExposurePolicy", "ToolExposurePorts"]
|