"""Cloud Control Plane's own AI Planner provider configuration. Analogous to ``runtime/planner_config.py``, but loaded from the Cloud API's own process environment rather than a Host Agent's. There is no ``enabled`` flag here: the planner-decision endpoint always exists once the Cloud API is running, and simply fails a given request if the configured provider call fails (see ``cloud.internal_api.api``). Provider API keys (``ANTHROPIC_API_KEY``/``OPENAI_API_KEY``) are not modeled as fields here -- like the Host Agent's direct-transport path, they are read implicitly by the ``anthropic``/``openai`` SDK clients from the process environment. """ from __future__ import annotations import os from collections.abc import Mapping from dataclasses import dataclass from runtime.tool_calling_client import ( AnthropicToolCallingClient, OpenAIToolCallingClient, ToolCallingClient, ) DEFAULT_PROVIDER = "anthropic" DEFAULT_MODEL_BY_PROVIDER = { "anthropic": "claude-sonnet-5", "openai": "gpt-5.6", } DEFAULT_TIMEOUT_SECONDS = 30.0 PROVIDER_ENV = "AI_PLANNER_PROVIDER" MODEL_ENV = "AI_PLANNER_MODEL" TIMEOUT_ENV = "AI_PLANNER_TIMEOUT_SECONDS" SUPPORTED_PROVIDERS = frozenset(DEFAULT_MODEL_BY_PROVIDER) @dataclass(frozen=True) class CloudPlannerConfig: provider: str = DEFAULT_PROVIDER model: str = "" timeout: float = DEFAULT_TIMEOUT_SECONDS def resolved_model(self) -> str: return self.model or DEFAULT_MODEL_BY_PROVIDER[self.provider] def load_cloud_planner_config( env: Mapping[str, str] | None = None, ) -> CloudPlannerConfig: values = env or os.environ return CloudPlannerConfig( provider=_parse_provider(values.get(PROVIDER_ENV)), model=values.get(MODEL_ENV) or "", timeout=_parse_timeout(values.get(TIMEOUT_ENV)), ) def build_cloud_planner_client(config: CloudPlannerConfig) -> ToolCallingClient: """Construct the same provider client the Host Agent's direct transport uses. Reuses ``runtime.tool_calling_client``'s Anthropic/OpenAI wire-format translation (see design decision D1) instead of a second implementation. """ model = config.resolved_model() if config.provider == "openai": return OpenAIToolCallingClient(model=model) return AnthropicToolCallingClient(model=model) def _parse_provider(value: str | None) -> str: if value is None: return DEFAULT_PROVIDER provider = value.strip().lower() return provider if provider in SUPPORTED_PROVIDERS else DEFAULT_PROVIDER def _parse_timeout(value: str | None) -> float: if value is None: return DEFAULT_TIMEOUT_SECONDS try: timeout = float(value) except ValueError: return DEFAULT_TIMEOUT_SECONDS return timeout if timeout > 0 else DEFAULT_TIMEOUT_SECONDS