@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,150 @@
|
|
|
1
|
+
"""Runtime routing and provider-native compatibility policies."""
|
|
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
|
+
from ciel_runtime_support.providers.constants import OPENCODE_PROVIDER_NAMES
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class RuntimeModePolicy:
|
|
14
|
+
parse_bool: Callable[..., bool]
|
|
15
|
+
runtime_providers: Mapping[str, str]
|
|
16
|
+
|
|
17
|
+
def native_enabled(self, runtime: str, provider: str) -> bool:
|
|
18
|
+
return provider == self.runtime_providers.get(runtime)
|
|
19
|
+
|
|
20
|
+
def routed_enabled(
|
|
21
|
+
self, runtime: str, provider: str, config: dict[str, Any]
|
|
22
|
+
) -> bool:
|
|
23
|
+
return self.native_enabled(runtime, provider) and self.parse_bool(
|
|
24
|
+
config.get("route_through_router"), default=False
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def direct_enabled(
|
|
28
|
+
self, runtime: str, provider: str, config: dict[str, Any]
|
|
29
|
+
) -> bool:
|
|
30
|
+
return self.native_enabled(runtime, provider) and not self.routed_enabled(
|
|
31
|
+
runtime, provider, config
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def label(self, provider: str, config: dict[str, Any]) -> str:
|
|
35
|
+
for runtime in ("anthropic", "agy", "codex"):
|
|
36
|
+
if self.direct_enabled(runtime, provider, config):
|
|
37
|
+
return f"{runtime}-native"
|
|
38
|
+
if self.routed_enabled(runtime, provider, config):
|
|
39
|
+
return f"{runtime}-routed"
|
|
40
|
+
return "ciel-runtime-router"
|
|
41
|
+
|
|
42
|
+
def native_anthropic(self, provider: str) -> bool:
|
|
43
|
+
return self.native_enabled("anthropic", provider)
|
|
44
|
+
|
|
45
|
+
def anthropic_routed(
|
|
46
|
+
self, provider: str, config: dict[str, Any]
|
|
47
|
+
) -> bool:
|
|
48
|
+
return self.routed_enabled("anthropic", provider, config)
|
|
49
|
+
|
|
50
|
+
def direct_anthropic(
|
|
51
|
+
self, provider: str, config: dict[str, Any]
|
|
52
|
+
) -> bool:
|
|
53
|
+
return self.direct_enabled("anthropic", provider, config)
|
|
54
|
+
|
|
55
|
+
def native_agy(self, provider: str) -> bool:
|
|
56
|
+
return self.native_enabled("agy", provider)
|
|
57
|
+
|
|
58
|
+
def agy_routed(self, provider: str, config: dict[str, Any]) -> bool:
|
|
59
|
+
return self.routed_enabled("agy", provider, config)
|
|
60
|
+
|
|
61
|
+
def direct_agy(self, provider: str, config: dict[str, Any]) -> bool:
|
|
62
|
+
return self.direct_enabled("agy", provider, config)
|
|
63
|
+
|
|
64
|
+
def native_codex(self, provider: str) -> bool:
|
|
65
|
+
return self.native_enabled("codex", provider)
|
|
66
|
+
|
|
67
|
+
def codex_routed(self, provider: str, config: dict[str, Any]) -> bool:
|
|
68
|
+
return self.routed_enabled("codex", provider, config)
|
|
69
|
+
|
|
70
|
+
def direct_codex(self, provider: str, config: dict[str, Any]) -> bool:
|
|
71
|
+
return self.direct_enabled("codex", provider, config)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_default_runtime_mode_policy(
|
|
75
|
+
parse_bool: Callable[..., bool],
|
|
76
|
+
) -> RuntimeModePolicy:
|
|
77
|
+
"""Build the standard native runtime-to-provider mapping."""
|
|
78
|
+
return RuntimeModePolicy(
|
|
79
|
+
parse_bool=parse_bool,
|
|
80
|
+
runtime_providers={
|
|
81
|
+
"anthropic": "anthropic",
|
|
82
|
+
"agy": "agy",
|
|
83
|
+
"codex": "codex",
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True, slots=True)
|
|
89
|
+
class ProviderNativeCompatibilityPolicy:
|
|
90
|
+
native_enabled: Callable[[str, dict[str, Any]], bool]
|
|
91
|
+
compatibility_groups: Mapping[str, frozenset[str]]
|
|
92
|
+
|
|
93
|
+
def group_enabled(
|
|
94
|
+
self, group: str, provider: str, config: dict[str, Any]
|
|
95
|
+
) -> bool:
|
|
96
|
+
return (
|
|
97
|
+
provider in self.compatibility_groups[group]
|
|
98
|
+
and self.native_enabled(provider, config)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def ollama(self, provider: str, config: dict[str, Any]) -> bool:
|
|
102
|
+
return self.group_enabled("ollama", provider, config)
|
|
103
|
+
|
|
104
|
+
def vllm(self, provider: str, config: dict[str, Any]) -> bool:
|
|
105
|
+
return self.group_enabled("vllm", provider, config)
|
|
106
|
+
|
|
107
|
+
def nim(self, provider: str, config: dict[str, Any]) -> bool:
|
|
108
|
+
return self.group_enabled("nim", provider, config)
|
|
109
|
+
|
|
110
|
+
def lm_studio(self, provider: str, config: dict[str, Any]) -> bool:
|
|
111
|
+
return self.group_enabled("lm_studio", provider, config)
|
|
112
|
+
|
|
113
|
+
def nvidia(self, provider: str, config: dict[str, Any]) -> bool:
|
|
114
|
+
return self.group_enabled("nvidia", provider, config)
|
|
115
|
+
|
|
116
|
+
def deepseek(self, provider: str, config: dict[str, Any]) -> bool:
|
|
117
|
+
return self.group_enabled("deepseek", provider, config)
|
|
118
|
+
|
|
119
|
+
def opencode(self, provider: str, config: dict[str, Any]) -> bool:
|
|
120
|
+
return self.group_enabled("opencode", provider, config)
|
|
121
|
+
|
|
122
|
+
def kimi(self, provider: str, config: dict[str, Any]) -> bool:
|
|
123
|
+
return self.group_enabled("kimi", provider, config)
|
|
124
|
+
|
|
125
|
+
def zai(self, provider: str, config: dict[str, Any]) -> bool:
|
|
126
|
+
return self.group_enabled("zai", provider, config)
|
|
127
|
+
|
|
128
|
+
def fireworks(self, provider: str, config: dict[str, Any]) -> bool:
|
|
129
|
+
return self.group_enabled("fireworks", provider, config)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def build_default_native_compatibility_policy(
|
|
133
|
+
native_enabled: Callable[[str, dict[str, Any]], bool],
|
|
134
|
+
) -> ProviderNativeCompatibilityPolicy:
|
|
135
|
+
"""Build the provider families supported by native compatibility mode."""
|
|
136
|
+
return ProviderNativeCompatibilityPolicy(
|
|
137
|
+
native_enabled=native_enabled,
|
|
138
|
+
compatibility_groups={
|
|
139
|
+
"ollama": frozenset({"ollama"}),
|
|
140
|
+
"vllm": frozenset({"vllm"}),
|
|
141
|
+
"nim": frozenset({"self-hosted-nim"}),
|
|
142
|
+
"lm_studio": frozenset({"lm-studio"}),
|
|
143
|
+
"nvidia": frozenset({"nvidia-hosted"}),
|
|
144
|
+
"deepseek": frozenset({"deepseek"}),
|
|
145
|
+
"opencode": frozenset(OPENCODE_PROVIDER_NAMES),
|
|
146
|
+
"kimi": frozenset({"kimi"}),
|
|
147
|
+
"zai": frozenset({"zai"}),
|
|
148
|
+
"fireworks": frozenset({"fireworks"}),
|
|
149
|
+
},
|
|
150
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Provider sampling option normalization and validation policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .config_value_codec import finite_float, positive_int
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProviderSamplingPolicy:
|
|
11
|
+
"""Canonicalize and validate provider-independent sampling options."""
|
|
12
|
+
|
|
13
|
+
_ALIASES = {
|
|
14
|
+
"temp": "temperature",
|
|
15
|
+
"temperature": "temperature",
|
|
16
|
+
"top": "top_p",
|
|
17
|
+
"top_p": "top_p",
|
|
18
|
+
"topp": "top_p",
|
|
19
|
+
"topk": "top_k",
|
|
20
|
+
"top_k": "top_k",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def option_key(self, key: str) -> str | None:
|
|
24
|
+
normalized = key.strip().lower().replace("-", "_")
|
|
25
|
+
return self._ALIASES.get(normalized)
|
|
26
|
+
|
|
27
|
+
def validate(self, key: str, value: Any) -> float | int:
|
|
28
|
+
if key == "temperature":
|
|
29
|
+
fixed = finite_float(value)
|
|
30
|
+
if fixed is None or fixed < 0 or fixed > 2:
|
|
31
|
+
raise SystemExit("temperature must be a number from 0 to 2")
|
|
32
|
+
return fixed
|
|
33
|
+
if key == "top_p":
|
|
34
|
+
fixed = finite_float(value)
|
|
35
|
+
if fixed is None or fixed <= 0 or fixed > 1:
|
|
36
|
+
raise SystemExit("top_p must be a number greater than 0 and up to 1")
|
|
37
|
+
return fixed
|
|
38
|
+
if key == "top_k":
|
|
39
|
+
fixed = positive_int(value)
|
|
40
|
+
if not fixed:
|
|
41
|
+
raise SystemExit("top_k must be a positive integer")
|
|
42
|
+
return fixed
|
|
43
|
+
raise SystemExit(f"Unknown provider option: {key}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = ["ProviderSamplingPolicy"]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Provider base URL status projection application service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.request
|
|
10
|
+
|
|
11
|
+
from .architecture import ProviderStatusPolicy
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class ProviderStatusRouting:
|
|
16
|
+
codex_routed: Callable[..., bool]
|
|
17
|
+
agy_routed: Callable[..., bool]
|
|
18
|
+
nvidia_native: Callable[..., bool]
|
|
19
|
+
native_anthropic_base: Callable[..., str]
|
|
20
|
+
router_up: Callable[[], bool]
|
|
21
|
+
router_base: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class ProviderStatusCatalog:
|
|
26
|
+
model_headers: Callable[..., dict[str, str]]
|
|
27
|
+
http_json: Callable[..., Any]
|
|
28
|
+
join_url: Callable[..., str]
|
|
29
|
+
management_base: Callable[..., str]
|
|
30
|
+
model_ids: Callable[..., list[str]]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class ProviderStatusGeneric:
|
|
35
|
+
primary_api_key: Callable[..., str]
|
|
36
|
+
meaningful_key: Callable[..., bool]
|
|
37
|
+
with_user_agent: Callable[..., dict[str, str]]
|
|
38
|
+
provider_urlopen: Callable[..., Any]
|
|
39
|
+
model_context_limit: Callable[..., int | None]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class ProviderStatusServices:
|
|
44
|
+
routing: ProviderStatusRouting
|
|
45
|
+
catalog: ProviderStatusCatalog
|
|
46
|
+
generic: ProviderStatusGeneric
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def base_url_status_line(
|
|
50
|
+
provider: str,
|
|
51
|
+
pcfg: dict[str, Any],
|
|
52
|
+
policy: ProviderStatusPolicy,
|
|
53
|
+
*,
|
|
54
|
+
services: ProviderStatusServices,
|
|
55
|
+
) -> str:
|
|
56
|
+
base = str(pcfg.get("base_url") or "").rstrip("/")
|
|
57
|
+
if not base:
|
|
58
|
+
return "Base URL: missing"
|
|
59
|
+
if "your-" in base:
|
|
60
|
+
return f"Base URL: placeholder ({base})"
|
|
61
|
+
routing = services.routing
|
|
62
|
+
if policy.kind == "native_codex":
|
|
63
|
+
if routing.codex_routed(provider, pcfg):
|
|
64
|
+
return f"Base URL: Codex routed through local router ({routing.router_base}/backend-api/codex)"
|
|
65
|
+
return "Base URL: native Codex config (ciel-runtime does not override it)"
|
|
66
|
+
if policy.kind == "native_agy":
|
|
67
|
+
if routing.agy_routed(provider, pcfg):
|
|
68
|
+
return "Base URL: AGY routed uses native Antigravity model upstream; Ciel routes channel/PTY wake only"
|
|
69
|
+
return "Base URL: native AGY config (ciel-runtime does not override it)"
|
|
70
|
+
if policy.kind == "nvidia":
|
|
71
|
+
if routing.nvidia_native(provider, pcfg):
|
|
72
|
+
return f"Base URL: NVIDIA hosted native ({routing.native_anthropic_base(provider, pcfg)}/v1/messages)"
|
|
73
|
+
state = "ready" if routing.router_up() else "starts on launch"
|
|
74
|
+
return f"Base URL: NVIDIA hosted ({base}); local router {routing.router_base} {state}"
|
|
75
|
+
if policy.kind == "configured":
|
|
76
|
+
return f"Base URL: {policy.configured_description} ({base})"
|
|
77
|
+
if policy.kind == "catalog":
|
|
78
|
+
return _catalog_status(provider, pcfg, base, policy, services.catalog)
|
|
79
|
+
return _generic_status(provider, pcfg, base, policy, services.generic, services.catalog.join_url)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _catalog_status(
|
|
83
|
+
provider: str,
|
|
84
|
+
pcfg: dict[str, Any],
|
|
85
|
+
base: str,
|
|
86
|
+
policy: ProviderStatusPolicy,
|
|
87
|
+
catalog: ProviderStatusCatalog,
|
|
88
|
+
) -> str:
|
|
89
|
+
probe_base = catalog.management_base(pcfg) if policy.catalog_scope == "fireworks_management" else base
|
|
90
|
+
scope = "model API" if policy.catalog_scope == "fireworks_management" else "model list"
|
|
91
|
+
try:
|
|
92
|
+
data = catalog.http_json(
|
|
93
|
+
catalog.join_url(probe_base, policy.catalog_path),
|
|
94
|
+
headers=catalog.model_headers(provider, pcfg),
|
|
95
|
+
timeout=2.5,
|
|
96
|
+
provider=provider,
|
|
97
|
+
pcfg=pcfg,
|
|
98
|
+
)
|
|
99
|
+
count = len(catalog.model_ids(data))
|
|
100
|
+
return f"Base URL: {policy.label} {scope} reachable ({policy.catalog_path}, {count} {policy.catalog_count_label})"
|
|
101
|
+
except urllib.error.HTTPError as exc:
|
|
102
|
+
if exc.code in (401, 403):
|
|
103
|
+
return f"Base URL: {policy.label} reachable, auth rejected ({exc.code})"
|
|
104
|
+
return f"Base URL: {policy.label} HTTP {exc.code}"
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
return f"Base URL: {policy.label} {scope} unreachable ({type(exc).__name__})"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _generic_status(
|
|
110
|
+
provider: str,
|
|
111
|
+
pcfg: dict[str, Any],
|
|
112
|
+
base: str,
|
|
113
|
+
policy: ProviderStatusPolicy,
|
|
114
|
+
generic: ProviderStatusGeneric,
|
|
115
|
+
join_url: Callable[..., str],
|
|
116
|
+
) -> str:
|
|
117
|
+
headers: dict[str, str] = {}
|
|
118
|
+
key = generic.primary_api_key(provider, pcfg)
|
|
119
|
+
if generic.meaningful_key(key):
|
|
120
|
+
headers = {"x-api-key": key, "authorization": f"Bearer {key}"}
|
|
121
|
+
headers = generic.with_user_agent(headers)
|
|
122
|
+
path = policy.catalog_path or "/v1/models"
|
|
123
|
+
try:
|
|
124
|
+
request = urllib.request.Request(join_url(base, path), headers=headers)
|
|
125
|
+
with generic.provider_urlopen(request, timeout=2.5, provider=provider, pcfg=pcfg) as response:
|
|
126
|
+
body = response.read(131072).decode("utf-8", errors="ignore")
|
|
127
|
+
count = ""
|
|
128
|
+
try:
|
|
129
|
+
data = json.loads(body)
|
|
130
|
+
except (TypeError, ValueError):
|
|
131
|
+
data = {}
|
|
132
|
+
models = data.get(policy.catalog_count_key) if isinstance(data, dict) else None
|
|
133
|
+
if isinstance(models, list):
|
|
134
|
+
count = f", {len(models)} models"
|
|
135
|
+
if policy.catalog_count_key == "data":
|
|
136
|
+
limit = generic.model_context_limit(provider, pcfg, timeout=1.0)
|
|
137
|
+
if limit:
|
|
138
|
+
count += f", max_model_len {limit}"
|
|
139
|
+
return f"Base URL: model list reachable ({path}{count})"
|
|
140
|
+
except urllib.error.HTTPError as exc:
|
|
141
|
+
if exc.code in (401, 403):
|
|
142
|
+
return f"Base URL: model list reachable, auth rejected ({exc.code})"
|
|
143
|
+
return f"Base URL: HTTP {exc.code}"
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
return f"Base URL: unreachable ({type(exc).__name__})"
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Context-aware provider request timeout calculation and application policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .architecture import ProviderContextPolicy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class ProviderTimeoutSettings:
|
|
15
|
+
default_ms: int
|
|
16
|
+
minimum_ms: int
|
|
17
|
+
maximum_ms: int
|
|
18
|
+
round_ms: int
|
|
19
|
+
idle_max_ms: int
|
|
20
|
+
preset_timeouts: Mapping[str, int]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class ProviderTimeoutPorts:
|
|
25
|
+
positive_int: Callable[[Any], int | None]
|
|
26
|
+
context_policy: Callable[[str, dict[str, Any]], ProviderContextPolicy]
|
|
27
|
+
context_capacity: Callable[[str, dict[str, Any]], int | None]
|
|
28
|
+
output_token_cap: Callable[[str, dict[str, Any], int | None], int | None]
|
|
29
|
+
ollama_options: Callable[[dict[str, Any]], dict[str, Any]]
|
|
30
|
+
catalog_timeout: Callable[[str], int | None]
|
|
31
|
+
model_preset: Callable[[str], Mapping[str, Any]]
|
|
32
|
+
timeout_for_context: Callable[[int | None], int]
|
|
33
|
+
format_context: Callable[[int | None], str]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ProviderTimeoutPolicy:
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
settings: ProviderTimeoutSettings,
|
|
40
|
+
ports: ProviderTimeoutPorts,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.settings = settings
|
|
43
|
+
self.ports = ports
|
|
44
|
+
|
|
45
|
+
def configured_context(
|
|
46
|
+
self,
|
|
47
|
+
provider: str,
|
|
48
|
+
config: dict[str, Any],
|
|
49
|
+
) -> int | None:
|
|
50
|
+
strategy = self.ports.context_policy(provider, config).settings_strategy
|
|
51
|
+
positive_int = self.ports.positive_int
|
|
52
|
+
if strategy == "ollama":
|
|
53
|
+
fixed = positive_int(config.get("num_ctx"))
|
|
54
|
+
if fixed:
|
|
55
|
+
return fixed
|
|
56
|
+
return self.ports.context_capacity(provider, config) or positive_int(
|
|
57
|
+
config.get("num_ctx_max")
|
|
58
|
+
)
|
|
59
|
+
if strategy == "standard":
|
|
60
|
+
return positive_int(
|
|
61
|
+
config.get("context_window")
|
|
62
|
+
) or self.ports.context_capacity(provider, config)
|
|
63
|
+
return self.ports.context_capacity(provider, config)
|
|
64
|
+
|
|
65
|
+
def configured_output(
|
|
66
|
+
self,
|
|
67
|
+
provider: str,
|
|
68
|
+
config: dict[str, Any],
|
|
69
|
+
) -> int | None:
|
|
70
|
+
positive_int = self.ports.positive_int
|
|
71
|
+
if self.ports.context_policy(provider, config).settings_strategy == "ollama":
|
|
72
|
+
options = self.ports.ollama_options(config)
|
|
73
|
+
configured = positive_int(options.get("num_predict")) or positive_int(
|
|
74
|
+
config.get("max_output_tokens")
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
configured = positive_int(
|
|
78
|
+
config.get("max_output_tokens")
|
|
79
|
+
) or positive_int(config.get("num_predict"))
|
|
80
|
+
return self.ports.output_token_cap(provider, config, configured)
|
|
81
|
+
|
|
82
|
+
def clamp(self, milliseconds: int | float | None) -> int:
|
|
83
|
+
value = self.ports.positive_int(milliseconds) or self.settings.default_ms
|
|
84
|
+
value = max(self.settings.minimum_ms, min(self.settings.maximum_ms, value))
|
|
85
|
+
return int(
|
|
86
|
+
math.ceil(value / self.settings.round_ms) * self.settings.round_ms
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def calculated(
|
|
90
|
+
self,
|
|
91
|
+
provider: str,
|
|
92
|
+
config: dict[str, Any],
|
|
93
|
+
timeout_candidates: list[int] | None = None,
|
|
94
|
+
) -> int:
|
|
95
|
+
context_policy = self.ports.context_policy(provider, config)
|
|
96
|
+
context_tokens = self.ports.positive_int(
|
|
97
|
+
self.configured_context(provider, config)
|
|
98
|
+
)
|
|
99
|
+
output_tokens = self.ports.positive_int(
|
|
100
|
+
self.configured_output(provider, config)
|
|
101
|
+
)
|
|
102
|
+
timeout_ms = self.settings.minimum_ms
|
|
103
|
+
if context_tokens:
|
|
104
|
+
context_score = max(
|
|
105
|
+
0.0,
|
|
106
|
+
min(
|
|
107
|
+
1.0,
|
|
108
|
+
math.log2(max(context_tokens, 65536) / 65536) / 4.0,
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
timeout_ms += int(240000 * context_score)
|
|
112
|
+
if output_tokens:
|
|
113
|
+
output_score = max(0.0, min(1.0, (output_tokens - 2048) / 6144))
|
|
114
|
+
timeout_ms += int(120000 * output_score)
|
|
115
|
+
if context_policy.hosted_timeout:
|
|
116
|
+
timeout_ms += 60000
|
|
117
|
+
for candidate in timeout_candidates or []:
|
|
118
|
+
fixed = self.ports.positive_int(candidate)
|
|
119
|
+
if fixed:
|
|
120
|
+
timeout_ms = max(timeout_ms, fixed)
|
|
121
|
+
timeout_ms *= context_policy.timeout_weight
|
|
122
|
+
return self.clamp(timeout_ms)
|
|
123
|
+
|
|
124
|
+
def recommended(
|
|
125
|
+
self,
|
|
126
|
+
provider: str,
|
|
127
|
+
config: dict[str, Any],
|
|
128
|
+
*,
|
|
129
|
+
use_context_fallback: bool = True,
|
|
130
|
+
) -> int:
|
|
131
|
+
model = str(config.get("current_model") or "")
|
|
132
|
+
candidates: list[int] = []
|
|
133
|
+
context_policy = self.ports.context_policy(provider, config)
|
|
134
|
+
preset_id = str(config.get("llm_preset") or "").strip()
|
|
135
|
+
preset_timeout = self.ports.positive_int(
|
|
136
|
+
self.settings.preset_timeouts.get(preset_id)
|
|
137
|
+
)
|
|
138
|
+
if preset_timeout:
|
|
139
|
+
candidates.append(preset_timeout)
|
|
140
|
+
if context_policy.uses_catalog_timeout:
|
|
141
|
+
catalog_timeout = self.ports.catalog_timeout(model)
|
|
142
|
+
if catalog_timeout:
|
|
143
|
+
candidates.append(catalog_timeout)
|
|
144
|
+
model_timeout = self.ports.positive_int(
|
|
145
|
+
self.ports.model_preset(model).get("recommended_timeout_ms")
|
|
146
|
+
)
|
|
147
|
+
if model_timeout:
|
|
148
|
+
candidates.append(model_timeout)
|
|
149
|
+
if candidates:
|
|
150
|
+
return self.calculated(provider, config, candidates)
|
|
151
|
+
if not use_context_fallback:
|
|
152
|
+
return self.settings.default_ms
|
|
153
|
+
context_timeout = self.ports.timeout_for_context(
|
|
154
|
+
self.configured_context(provider, config)
|
|
155
|
+
)
|
|
156
|
+
return self.calculated(provider, config, [context_timeout])
|
|
157
|
+
|
|
158
|
+
def apply(
|
|
159
|
+
self,
|
|
160
|
+
provider: str,
|
|
161
|
+
config: dict[str, Any],
|
|
162
|
+
*,
|
|
163
|
+
use_context_fallback: bool = True,
|
|
164
|
+
) -> list[str]:
|
|
165
|
+
timeout_ms = self.recommended(
|
|
166
|
+
provider,
|
|
167
|
+
config,
|
|
168
|
+
use_context_fallback=use_context_fallback,
|
|
169
|
+
)
|
|
170
|
+
idle_ms = min(timeout_ms, self.settings.idle_max_ms)
|
|
171
|
+
positive_int = self.ports.positive_int
|
|
172
|
+
changed = (
|
|
173
|
+
positive_int(config.get("request_timeout_ms")) != timeout_ms
|
|
174
|
+
or positive_int(config.get("stream_idle_timeout_ms")) != idle_ms
|
|
175
|
+
)
|
|
176
|
+
config["request_timeout_ms"] = timeout_ms
|
|
177
|
+
config["stream_idle_timeout_ms"] = idle_ms
|
|
178
|
+
if not changed:
|
|
179
|
+
return []
|
|
180
|
+
context = self.configured_context(provider, config)
|
|
181
|
+
return [
|
|
182
|
+
f"Auto timeout: {timeout_ms}ms for context {self.ports.format_context(context)}.",
|
|
183
|
+
f"stream_idle_timeout_ms: {idle_ms}",
|
|
184
|
+
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Provider-owned tool exposure and tool-choice application policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
from .protocols.anthropic_thinking_policy import (
|
|
9
|
+
ToolChoicePorts,
|
|
10
|
+
normalize_tool_choice,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
ProviderConfig = dict[str, Any]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class ProviderToolPolicy:
|
|
19
|
+
adapter_for: Callable[..., Any]
|
|
20
|
+
contract_for: Callable[..., Any]
|
|
21
|
+
current_model: Callable[[str, ProviderConfig], str]
|
|
22
|
+
strip_context_suffix: Callable[[str], str]
|
|
23
|
+
resolve_emitted_name: Callable[
|
|
24
|
+
[str, dict[str, Any] | None],
|
|
25
|
+
str,
|
|
26
|
+
]
|
|
27
|
+
default_blocked_tools: frozenset[str]
|
|
28
|
+
repair_tools: frozenset[str]
|
|
29
|
+
log: Callable[[str, str], None]
|
|
30
|
+
|
|
31
|
+
def blocked_tools(
|
|
32
|
+
self,
|
|
33
|
+
provider: str,
|
|
34
|
+
config: ProviderConfig,
|
|
35
|
+
) -> set[str]:
|
|
36
|
+
override = config.get("blocked_tools")
|
|
37
|
+
if override is False:
|
|
38
|
+
return set()
|
|
39
|
+
if isinstance(override, list):
|
|
40
|
+
return {
|
|
41
|
+
str(name).strip()
|
|
42
|
+
for name in override
|
|
43
|
+
if str(name).strip()
|
|
44
|
+
}
|
|
45
|
+
adapter, contract = self._adapter(provider, config)
|
|
46
|
+
if adapter.capabilities(contract).blocks_default_tools:
|
|
47
|
+
return set(self.default_blocked_tools)
|
|
48
|
+
return set()
|
|
49
|
+
|
|
50
|
+
def normalize_anthropic_stream_tool_use(
|
|
51
|
+
self,
|
|
52
|
+
provider: str,
|
|
53
|
+
config: ProviderConfig,
|
|
54
|
+
) -> bool:
|
|
55
|
+
adapter, contract = self._adapter(provider, config)
|
|
56
|
+
return adapter.normalizes_anthropic_tool_use(contract)
|
|
57
|
+
|
|
58
|
+
def supports_tool_choice(
|
|
59
|
+
self,
|
|
60
|
+
provider: str,
|
|
61
|
+
config: ProviderConfig,
|
|
62
|
+
body: dict[str, Any],
|
|
63
|
+
) -> bool:
|
|
64
|
+
raw_model = str(
|
|
65
|
+
body.get("model") or config.get("current_model") or ""
|
|
66
|
+
)
|
|
67
|
+
model = self.strip_context_suffix(raw_model).lower()
|
|
68
|
+
adapter, contract = self._adapter(provider, config)
|
|
69
|
+
return adapter.supports_tool_choice(contract, model)
|
|
70
|
+
|
|
71
|
+
def tool_choice_status(
|
|
72
|
+
self,
|
|
73
|
+
provider: str,
|
|
74
|
+
config: ProviderConfig,
|
|
75
|
+
) -> str:
|
|
76
|
+
configured = config.get("supports_tool_choice")
|
|
77
|
+
if configured is not None:
|
|
78
|
+
return "on" if bool(configured) else "off"
|
|
79
|
+
model = self.current_model(provider, config)
|
|
80
|
+
enabled = self.supports_tool_choice(
|
|
81
|
+
provider,
|
|
82
|
+
config,
|
|
83
|
+
{"model": model},
|
|
84
|
+
)
|
|
85
|
+
return f"auto ({'on' if enabled else 'off'})"
|
|
86
|
+
|
|
87
|
+
def normalize_tool_choice(
|
|
88
|
+
self,
|
|
89
|
+
provider: str,
|
|
90
|
+
config: ProviderConfig,
|
|
91
|
+
body: dict[str, Any],
|
|
92
|
+
) -> dict[str, Any]:
|
|
93
|
+
return normalize_tool_choice(
|
|
94
|
+
provider,
|
|
95
|
+
config,
|
|
96
|
+
body,
|
|
97
|
+
ToolChoicePorts(
|
|
98
|
+
normalize=self._normalize_choice,
|
|
99
|
+
supports=self.supports_tool_choice,
|
|
100
|
+
log=self.log,
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def should_repair_passthrough_input(
|
|
105
|
+
self,
|
|
106
|
+
provider: str,
|
|
107
|
+
config: ProviderConfig,
|
|
108
|
+
raw_name: str,
|
|
109
|
+
source_body: dict[str, Any] | None,
|
|
110
|
+
) -> bool:
|
|
111
|
+
adapter, contract = self._adapter(provider, config)
|
|
112
|
+
if not adapter.capabilities(contract).repairs_anthropic_tool_input:
|
|
113
|
+
return False
|
|
114
|
+
return self.resolve_emitted_name(
|
|
115
|
+
raw_name,
|
|
116
|
+
source_body,
|
|
117
|
+
) in self.repair_tools
|
|
118
|
+
|
|
119
|
+
def _adapter(
|
|
120
|
+
self,
|
|
121
|
+
provider: str,
|
|
122
|
+
config: ProviderConfig,
|
|
123
|
+
) -> tuple[Any, Any]:
|
|
124
|
+
return (
|
|
125
|
+
self.adapter_for(provider, config),
|
|
126
|
+
self.contract_for(provider, config),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def _normalize_choice(
|
|
130
|
+
self,
|
|
131
|
+
provider: str,
|
|
132
|
+
config: ProviderConfig,
|
|
133
|
+
request: dict[str, Any],
|
|
134
|
+
choice: Any,
|
|
135
|
+
) -> Any:
|
|
136
|
+
adapter, contract = self._adapter(provider, config)
|
|
137
|
+
model = str(
|
|
138
|
+
request.get("model")
|
|
139
|
+
or config.get("current_model")
|
|
140
|
+
or ""
|
|
141
|
+
)
|
|
142
|
+
return adapter.normalize_tool_choice(contract, model, choice)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
__all__ = ["ProviderToolPolicy"]
|