diff --git a/engine/core/session.py b/engine/core/session.py index 4f19eba..1cfaeac 100644 --- a/engine/core/session.py +++ b/engine/core/session.py @@ -71,6 +71,32 @@ class Session: "configVersionId", "config_version_id", } + _CLIENT_SERVICE_OVERRIDES = { + "llm": { + "provider", + "model", + "apiKey", + "baseUrl", + }, + "asr": { + "provider", + "model", + "apiKey", + "baseUrl", + "interimIntervalMs", + "minAudioMs", + }, + "tts": { + "enabled", + "provider", + "model", + "apiKey", + "baseUrl", + "voice", + "speed", + "mode", + }, + } def __init__( self, @@ -853,13 +879,52 @@ class Session: def _sanitize_client_metadata(self, metadata: Dict[str, Any]) -> Dict[str, Any]: """Apply client metadata whitelist and remove forbidden secrets.""" sanitized = self._sanitize_untrusted_runtime_metadata(metadata) - if isinstance(metadata.get("services"), dict): - logger.warning( - "Session {} provided metadata.services from client; client-side service config is ignored", - self.id, - ) + services = metadata.get("services") + if isinstance(services, dict): + if self._is_debug_metadata_source(metadata): + sanitized_services = self._sanitize_client_services(services) + if sanitized_services: + sanitized["services"] = sanitized_services + else: + logger.warning( + "Session {} provided metadata.services from client; client-side service config is ignored", + self.id, + ) return sanitized + @staticmethod + def _is_debug_metadata_source(metadata: Dict[str, Any]) -> bool: + source = str(metadata.get("source") or "").strip().lower() + if source == "debug": + return True + + history = metadata.get("history") + if isinstance(history, dict): + history_source = str(history.get("source") or "").strip().lower() + if history_source == "debug": + return True + return False + + @classmethod + def _sanitize_client_services(cls, services: Dict[str, Any]) -> Dict[str, Any]: + if not isinstance(services, dict): + return {} + + sanitized_services: Dict[str, Any] = {} + for service_name, allowed_keys in cls._CLIENT_SERVICE_OVERRIDES.items(): + payload = services.get(service_name) + if not isinstance(payload, dict): + continue + + sanitized_payload = { + key: payload[key] + for key in allowed_keys + if key in payload + } + if sanitized_payload: + sanitized_services[service_name] = sanitized_payload + return sanitized_services + def _build_config_resolved(self, metadata: Dict[str, Any]) -> Dict[str, Any]: """Build public resolved config payload (secrets removed).""" system_prompt = str(metadata.get("systemPrompt") or self.pipeline.conversation.system_prompt or "") diff --git a/web/pages/Assistants.tsx b/web/pages/Assistants.tsx index 1da3915..0aaf909 100644 --- a/web/pages/Assistants.tsx +++ b/web/pages/Assistants.tsx @@ -2296,10 +2296,10 @@ export const DebugDrawer: React.FC<{ useEffect(() => { if (!isOpen) return; if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return; - // If core TTS-related settings changed while drawer stays open, + // If core runtime settings changed while drawer stays open, // reset the active WS session so the next launch uses new metadata. closeWs(); - }, [isOpen, assistant.id, assistant.voice, assistant.speed]); + }, [isOpen, assistant.id, assistant.voice, assistant.speed, assistant.asrModelId]); useEffect(() => { if (!textTtsEnabled) {