from functools import cache

from langchain_anthropic import ChatAnthropic
from langchain_core.language_models import BaseChatModel
from langchain_ollama import ChatOllama
from langchain_openai import ChatOpenAI
from loguru import logger

from ..llm import get_llm_from_registry, is_llm_registered
from ..utilities.search_utilities import remove_think_tags
from ..utilities.url_utils import normalize_url
from .constants import (
    DEFAULT_LLAMACPP_URL,
    DEFAULT_LMSTUDIO_URL,
    DEFAULT_OLLAMA_URL,
)

# Import providers module to trigger auto-discovery
try:
    from ..llm.providers import discover_providers  # noqa: F401
    # Auto-discovery happens on module import
except ImportError:
    logger.debug("Providers module not available yet")
from ..llm.providers.base import normalize_provider
from .thread_settings import (
    get_setting_from_snapshot,
    NoSettingsContextError,
)

# Valid provider options
VALID_PROVIDERS = [
    "ollama",
    "openai",
    "anthropic",
    "google",
    "openrouter",
    "openai_endpoint",
    "lmstudio",
    "llamacpp",
    "none",
]


def is_openai_available(settings_snapshot=None):
    """Check if OpenAI is available by delegating to the provider class."""
    try:
        from ..llm.providers.implementations.openai import OpenAIProvider

        return OpenAIProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        logger.debug("Error checking OpenAI availability", exc_info=True)
        return False


def is_anthropic_available(settings_snapshot=None):
    """Check if Anthropic is available by delegating to the provider class."""
    try:
        from ..llm.providers.implementations.anthropic import AnthropicProvider

        return AnthropicProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        logger.debug("Error checking Anthropic availability", exc_info=True)
        return False


def is_openai_endpoint_available(settings_snapshot=None):
    """Check if OpenAI endpoint is available by delegating to the provider class."""
    try:
        from ..llm.providers.implementations.custom_openai_endpoint import (
            CustomOpenAIEndpointProvider,
        )

        return CustomOpenAIEndpointProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        logger.debug(
            "Error checking OpenAI endpoint availability", exc_info=True
        )
        return False


def is_ollama_available(settings_snapshot=None):
    """Check if Ollama is running by delegating to the provider class."""
    try:
        from ..llm.providers.implementations.ollama import OllamaProvider

        return OllamaProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        logger.debug("Error checking Ollama availability", exc_info=True)
        return False


def is_lmstudio_available(settings_snapshot=None):
    """Check if LM Studio is available by delegating to the provider class."""
    try:
        from ..llm.providers.implementations.lmstudio import LMStudioProvider

        return LMStudioProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        logger.debug("Error checking LM Studio availability", exc_info=True)
        return False


def is_llamacpp_available(settings_snapshot=None):
    """Check if llama.cpp's HTTP server is reachable.

    Talks to `llama-server`'s OpenAI-compatible endpoint (no in-process
    `llama-cpp-python` binding required).
    """
    try:
        from ..llm.providers.implementations.llamacpp import LlamaCppProvider

        return LlamaCppProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        logger.debug("Error checking llama.cpp availability", exc_info=True)
        return False


def is_google_available(settings_snapshot=None):
    """Check if Google/Gemini is available"""
    try:
        from ..llm.providers.implementations.google import GoogleProvider

        return GoogleProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        logger.debug("Error checking Google availability", exc_info=True)
        return False


def is_openrouter_available(settings_snapshot=None):
    """Check if OpenRouter is available"""
    try:
        from ..llm.providers.implementations.openrouter import (
            OpenRouterProvider,
        )

        return OpenRouterProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        logger.debug("Error checking OpenRouter availability", exc_info=True)
        return False


@cache
def get_available_providers(settings_snapshot=None):
    """Return available model providers"""
    providers = {}

    if is_ollama_available(settings_snapshot):
        providers["ollama"] = "Ollama (local models)"

    if is_openai_available(settings_snapshot):
        providers["openai"] = "OpenAI API"

    if is_anthropic_available(settings_snapshot):
        providers["anthropic"] = "Anthropic API"

    if is_google_available(settings_snapshot):
        providers["google"] = "Google Gemini API"

    if is_openrouter_available(settings_snapshot):
        providers["openrouter"] = "OpenRouter API"

    if is_openai_endpoint_available(settings_snapshot):
        providers["openai_endpoint"] = "OpenAI-Compatible Endpoint"

    if is_lmstudio_available(settings_snapshot):
        providers["lmstudio"] = "LM Studio (local models)"

    if is_llamacpp_available(settings_snapshot):
        providers["llamacpp"] = "LlamaCpp (local models)"

    # Default fallback
    if not providers:
        providers["none"] = "No model providers available"

    return providers


def get_selected_llm_provider(settings_snapshot=None):
    return normalize_provider(
        get_setting_from_snapshot(
            "llm.provider", "ollama", settings_snapshot=settings_snapshot
        )
    )


def _get_context_window_for_provider(provider_type, settings_snapshot=None):
    """Get context window size from settings based on provider type.

    Local providers (ollama, llamacpp, lmstudio) use a smaller default to prevent
    memory issues. Cloud providers check if unrestricted mode is enabled.

    Returns:
        int or None: The context window size, or None for unrestricted cloud providers.
    """
    if provider_type in ["ollama", "llamacpp", "lmstudio"]:
        # Local providers: use smaller default to prevent memory issues
        window_size = get_setting_from_snapshot(
            "llm.local_context_window_size",
            8192,
            settings_snapshot=settings_snapshot,
        )
        # Ensure it's an integer
        return int(window_size) if window_size is not None else 8192
    # Cloud providers: check if unrestricted mode is enabled
    use_unrestricted = get_setting_from_snapshot(
        "llm.context_window_unrestricted",
        True,
        settings_snapshot=settings_snapshot,
    )
    if use_unrestricted:
        # Let cloud providers auto-handle context (return None or very large value)
        return None  # Will be handled per provider
    # Use user-specified limit
    window_size = get_setting_from_snapshot(
        "llm.context_window_size",
        128000,
        settings_snapshot=settings_snapshot,
    )
    return int(window_size) if window_size is not None else 128000


def get_llm(
    model_name=None,
    temperature=None,
    provider=None,
    openai_endpoint_url=None,
    research_id=None,
    research_context=None,
    settings_snapshot=None,
):
    """
    Get LLM instance based on model name and provider.

    Args:
        model_name: Name of the model to use (if None, uses database setting)
        temperature: Model temperature (if None, uses database setting)
        provider: Provider to use (if None, uses database setting)
        openai_endpoint_url: Custom endpoint URL to use (if None, uses database
            setting)
        research_id: Optional research ID for token tracking
        research_context: Optional research context for enhanced token tracking

    Returns:
        A LangChain LLM instance with automatic think-tag removal
    """

    # Use database values for parameters if not provided
    if model_name is None:
        model_name = get_setting_from_snapshot(
            "llm.model", "", settings_snapshot=settings_snapshot
        )
    if temperature is None:
        temperature = get_setting_from_snapshot(
            "llm.temperature", 0.7, settings_snapshot=settings_snapshot
        )
    if provider is None:
        provider = get_setting_from_snapshot(
            "llm.provider", "ollama", settings_snapshot=settings_snapshot
        )

    # Clean model name: remove quotes and extra whitespace
    if model_name:
        model_name = model_name.strip().strip("\"'").strip()

    # Clean provider: remove quotes and extra whitespace
    if provider:
        provider = provider.strip().strip("\"'").strip()

    # Normalize provider: convert to lowercase canonical form
    provider = normalize_provider(provider)

    # Check if this is a registered custom LLM first
    if provider and is_llm_registered(provider):
        logger.info(f"Using registered custom LLM: {provider}")
        custom_llm = get_llm_from_registry(provider)

        # Check if it's a callable (factory function) or a BaseChatModel instance
        if callable(custom_llm) and not isinstance(custom_llm, BaseChatModel):
            # It's a callable (factory function), call it with parameters
            try:
                llm_instance = custom_llm(
                    model_name=model_name,
                    temperature=temperature,
                    settings_snapshot=settings_snapshot,
                )
            except TypeError as e:
                # Re-raise TypeError with better message
                raise TypeError(
                    f"Registered LLM factory '{provider}' has invalid signature. "
                    f"Factory functions must accept 'model_name', 'temperature', and 'settings_snapshot' parameters. "
                    f"Error: {e}"
                )

            # Validate the result is a BaseChatModel
            if not isinstance(llm_instance, BaseChatModel):
                raise ValueError(
                    f"Factory function for {provider} must return a BaseChatModel instance, "
                    f"got {type(llm_instance).__name__}"
                )
        elif isinstance(custom_llm, BaseChatModel):
            # It's already a proper LLM instance, use it directly
            llm_instance = custom_llm
        else:
            raise ValueError(
                f"Registered LLM {provider} must be either a BaseChatModel instance "
                f"or a callable factory function. Got: {type(custom_llm).__name__}"
            )

        return wrap_llm_without_think_tags(
            llm_instance,
            research_id=research_id,
            provider=provider,
            research_context=research_context,
            settings_snapshot=settings_snapshot,
        )

    # Validate provider
    if provider not in VALID_PROVIDERS:
        logger.error(f"Invalid provider in settings: {provider}")
        raise ValueError(
            f"Invalid provider: {provider}. Must be one of: {VALID_PROVIDERS}"
        )

    # Require an explicit model for built-in providers. Mirrors the
    # API-key-not-configured pattern in openai_base.py and the URL-not-
    # configured pattern in providers/implementations/ollama.py: no silent
    # substitution to a hardcoded default model.
    if not model_name or not model_name.strip():
        logger.error("llm.model is not configured (empty/None after lookup)")
        raise ValueError(
            "LLM model not configured. Please open Settings, choose an LLM "
            "provider, and select a model name (e.g. 'gpt-4o-mini' for "
            "OpenAI, 'claude-3-5-sonnet-20241022' for Anthropic, "
            "'llama3.1:8b' for Ollama). The 'llm.model' setting is required."
        )
    logger.info(
        f"Getting LLM with model: {model_name}, temperature: {temperature}, provider: {provider}"
    )

    # Common parameters for all models
    common_params = {
        "temperature": temperature,
    }

    context_window_size = _get_context_window_for_provider(
        provider, settings_snapshot
    )

    # Add context limit to research context for overflow detection
    if research_context and context_window_size:
        research_context["context_limit"] = context_window_size
        logger.info(
            f"Set context_limit={context_window_size} in research_context"
        )
    else:
        logger.debug(
            f"Context limit not set: research_context={bool(research_context)}, context_window_size={context_window_size}"
        )

    max_tokens = None
    if get_setting_from_snapshot(
        "llm.supports_max_tokens", True, settings_snapshot=settings_snapshot
    ):
        # Use 80% of context window to leave room for prompts
        if context_window_size is not None:
            max_tokens = min(
                int(
                    get_setting_from_snapshot(
                        "llm.max_tokens",
                        100000,
                        settings_snapshot=settings_snapshot,
                    )
                ),
                int(context_window_size * 0.8),
            )
            common_params["max_tokens"] = max_tokens
        else:
            # Unrestricted context: use provider's default max_tokens
            max_tokens = int(
                get_setting_from_snapshot(
                    "llm.max_tokens",
                    100000,
                    settings_snapshot=settings_snapshot,
                )
            )
            common_params["max_tokens"] = max_tokens

    # Handle different providers
    if provider == "anthropic":
        api_key = get_setting_from_snapshot(
            "llm.anthropic.api_key", settings_snapshot=settings_snapshot
        )

        if not api_key:
            raise ValueError(
                "Anthropic API key not configured. Please set llm.anthropic.api_key in settings."
            )

        llm: BaseChatModel = ChatAnthropic(
            model=model_name,
            anthropic_api_key=api_key,
            **common_params,  # type: ignore[call-arg]
        )
        return wrap_llm_without_think_tags(
            llm,
            research_id=research_id,
            provider=provider,
            research_context=research_context,
            settings_snapshot=settings_snapshot,
        )

    if provider == "openai":
        api_key = get_setting_from_snapshot(
            "llm.openai.api_key", settings_snapshot=settings_snapshot
        )

        if not api_key:
            raise ValueError(
                "OpenAI API key not configured. Please set llm.openai.api_key in settings."
            )

        # Build OpenAI-specific parameters
        openai_params = {
            "model": model_name,
            "api_key": api_key,
            **common_params,
        }

        # Add optional parameters if they exist in settings
        try:
            api_base = get_setting_from_snapshot(
                "llm.openai.api_base",
                default=None,
                settings_snapshot=settings_snapshot,
            )
            if api_base:
                openai_params["openai_api_base"] = api_base
        except NoSettingsContextError:
            pass  # Optional parameter

        try:
            organization = get_setting_from_snapshot(
                "llm.openai.organization",
                default=None,
                settings_snapshot=settings_snapshot,
            )
            if organization:
                openai_params["openai_organization"] = organization
        except NoSettingsContextError:
            pass  # Optional parameter

        try:
            streaming = get_setting_from_snapshot(
                "llm.streaming",
                default=None,
                settings_snapshot=settings_snapshot,
            )
        except NoSettingsContextError:
            streaming = None  # Optional parameter
        if streaming is not None:
            openai_params["streaming"] = streaming

        try:
            max_retries = get_setting_from_snapshot(
                "llm.max_retries",
                default=None,
                settings_snapshot=settings_snapshot,
            )
            if max_retries is not None:
                openai_params["max_retries"] = max_retries
        except NoSettingsContextError:
            pass  # Optional parameter

        try:
            request_timeout = get_setting_from_snapshot(
                "llm.request_timeout",
                default=None,
                settings_snapshot=settings_snapshot,
            )
            if request_timeout is not None:
                openai_params["request_timeout"] = request_timeout
        except NoSettingsContextError:
            pass  # Optional parameter

        llm = ChatOpenAI(**openai_params)  # type: ignore[assignment]
        return wrap_llm_without_think_tags(
            llm,
            research_id=research_id,
            provider=provider,
            research_context=research_context,
            settings_snapshot=settings_snapshot,
        )

    if provider == "openai_endpoint":
        api_key = get_setting_from_snapshot(
            "llm.openai_endpoint.api_key", settings_snapshot=settings_snapshot
        )

        # Local servers (e.g. llama.cpp) don't require an API key.
        # Use a placeholder so ChatOpenAI doesn't reject the request.
        if not api_key:
            logger.info(
                "No API key configured for openai_endpoint provider. "
                "Using placeholder key. If you are connecting to a hosted "
                "service, set llm.openai_endpoint.api_key in settings."
            )
            api_key = "not-needed"  # noqa: S105 # gitleaks:allow

        # Get endpoint URL from settings
        if openai_endpoint_url is None:
            openai_endpoint_url = get_setting_from_snapshot(
                "llm.openai_endpoint.url",
                "https://openrouter.ai/api/v1",
                settings_snapshot=settings_snapshot,
            )
        openai_endpoint_url = normalize_url(openai_endpoint_url)

        llm = ChatOpenAI(  # type: ignore[assignment, call-arg]
            model=model_name,
            api_key=api_key,
            openai_api_base=openai_endpoint_url,
            **common_params,
        )
        return wrap_llm_without_think_tags(
            llm,
            research_id=research_id,
            provider=provider,
            research_context=research_context,
            settings_snapshot=settings_snapshot,
        )

    if provider == "ollama":
        try:
            # Use the configurable Ollama base URL
            raw_base_url = get_setting_from_snapshot(
                "llm.ollama.url",
                DEFAULT_OLLAMA_URL,
                settings_snapshot=settings_snapshot,
            )
            base_url = (
                normalize_url(raw_base_url)
                if raw_base_url
                else DEFAULT_OLLAMA_URL
            )

            logger.info(
                f"Creating ChatOllama with model={model_name}, base_url={base_url}"
            )
            try:
                # Add num_ctx parameter for Ollama context window size
                ollama_params = {**common_params}
                if context_window_size is not None:
                    ollama_params["num_ctx"] = context_window_size

                # Thinking/reasoning handling for models like deepseek-r1:
                # The 'reasoning' parameter controls both:
                # 1. Whether the model performs thinking (makes it smarter when True)
                # 2. Whether thinking is separated from the answer (always separated when True)
                #
                # When reasoning=True:
                # - Model performs thinking/reasoning
                # - Thinking goes to additional_kwargs["reasoning_content"] (discarded by LDR)
                # - Only the final answer appears in response.content
                #
                # When reasoning=False:
                # - Model does NOT perform thinking (faster but less smart)
                # - Gives direct answers

                enable_thinking = get_setting_from_snapshot(
                    "llm.ollama.enable_thinking",
                    True,  # Default: enable thinking (smarter responses)
                    settings_snapshot=settings_snapshot,
                )

                if enable_thinking is not None and isinstance(
                    enable_thinking, bool
                ):
                    ollama_params["reasoning"] = enable_thinking
                    logger.debug(
                        f"Ollama thinking enabled: {enable_thinking} "
                        f"(thinking will be {'shown internally but discarded' if enable_thinking else 'disabled'})"
                    )

                llm = ChatOllama(  # type: ignore[assignment]
                    model=model_name, base_url=base_url, **ollama_params
                )

                # Log the actual client configuration after creation
                logger.debug(
                    f"ChatOllama created - base_url attribute: {getattr(llm, 'base_url', 'not found')}"
                )
                if hasattr(llm, "_client"):
                    client = llm._client
                    logger.debug(f"ChatOllama _client type: {type(client)}")
                    if hasattr(client, "_client"):
                        inner_client = client._client
                        logger.debug(
                            f"ChatOllama inner client type: {type(inner_client)}"
                        )
                        if hasattr(inner_client, "base_url"):
                            logger.debug(
                                f"ChatOllama inner client base_url: {inner_client.base_url}"
                            )

                return wrap_llm_without_think_tags(
                    llm,
                    research_id=research_id,
                    provider=provider,
                    research_context=research_context,
                    settings_snapshot=settings_snapshot,
                )
            except Exception:
                logger.exception("Error creating or testing ChatOllama")
                raise
        except Exception:
            logger.exception("Error in Ollama provider section")
            raise

    elif provider == "lmstudio":
        # LM Studio supports OpenAI API format, so we can use ChatOpenAI directly
        lmstudio_url = get_setting_from_snapshot(
            "llm.lmstudio.url",
            DEFAULT_LMSTUDIO_URL,
            settings_snapshot=settings_snapshot,
        )
        # Use URL as-is (default already includes /v1)
        base_url = normalize_url(lmstudio_url)
        # Optional API key for LM Studio with auth enabled. Empty/whitespace
        # falls back to a placeholder ChatOpenAI accepts; a no-auth LM Studio
        # ignores it. Whitespace stripping mirrors LMStudioProvider.create_llm.
        lmstudio_auth_raw = get_setting_from_snapshot(  # gitleaks:allow
            "llm.lmstudio.api_key", "", settings_snapshot=settings_snapshot
        )
        lmstudio_auth = (
            str(lmstudio_auth_raw or "").strip()
            or "lm-studio"  # gitleaks:allow
        )

        llm = ChatOpenAI(  # type: ignore[assignment, call-arg, arg-type]
            model=model_name,
            api_key=lmstudio_auth,  # gitleaks:allow
            base_url=base_url,
            temperature=temperature,
            max_tokens=max_tokens,  # Use calculated max_tokens based on context size
        )
        return wrap_llm_without_think_tags(
            llm,
            research_id=research_id,
            provider=provider,
            research_context=research_context,
            settings_snapshot=settings_snapshot,
        )

    elif provider == "llamacpp":
        # llama.cpp's `llama-server` exposes an OpenAI-compatible API, so we
        # use ChatOpenAI directly (same pattern as lmstudio above).
        llamacpp_url = get_setting_from_snapshot(
            "llm.llamacpp.url",
            DEFAULT_LLAMACPP_URL,
            settings_snapshot=settings_snapshot,
        )
        base_url = normalize_url(llamacpp_url)
        # Optional API key for setups behind an auth proxy. Empty falls back
        # to a placeholder ChatOpenAI accepts; a no-auth llama-server ignores it.
        llamacpp_auth = (  # gitleaks:allow
            get_setting_from_snapshot(
                "llm.llamacpp.api_key", "", settings_snapshot=settings_snapshot
            )
            or "lm-studio"  # gitleaks:allow
        )

        llm = ChatOpenAI(  # type: ignore[assignment, call-arg, arg-type]
            model=model_name,
            api_key=llamacpp_auth,  # gitleaks:allow
            base_url=base_url,
            temperature=temperature,
            max_tokens=max_tokens,
        )
        return wrap_llm_without_think_tags(
            llm,
            research_id=research_id,
            provider=provider,
            research_context=research_context,
            settings_snapshot=settings_snapshot,
        )

    elif provider == "none":
        raise ValueError(
            "No LLM provider configured. Please set llm.provider in settings "
            "to a valid provider (e.g., 'ollama', 'openai', 'anthropic')."
        )

    else:
        # Provider validated above but not handled - this shouldn't happen
        # since VALID_PROVIDERS check above would catch unknown providers
        raise ValueError(
            f"Provider '{provider}' is valid but not implemented. "
            f"This is a bug - please report it."
        )


def wrap_llm_without_think_tags(
    llm,
    research_id=None,
    provider=None,
    research_context=None,
    settings_snapshot=None,
):
    """Create a wrapper class that processes LLM outputs with remove_think_tags and token counting"""

    # First apply rate limiting if enabled
    from ..web_search_engines.rate_limiting.llm import (
        create_rate_limited_llm_wrapper,
    )

    # Check if LLM rate limiting is enabled (independent of search rate limiting)
    # Use the thread-safe get_db_setting defined in this module
    if get_setting_from_snapshot(
        "rate_limiting.llm_enabled", False, settings_snapshot=settings_snapshot
    ):
        llm = create_rate_limited_llm_wrapper(llm, provider)

    # Set context_limit in research_context for overflow detection.
    # This is needed for providers that go through the registered provider path
    # (which returns before the code in get_llm that sets context_limit).
    if research_context is not None and provider is not None:
        if "context_limit" not in research_context:
            context_limit = _get_context_window_for_provider(
                provider, settings_snapshot
            )
            if context_limit is not None:
                research_context["context_limit"] = context_limit
                logger.info(
                    f"Set context_limit={context_limit} in wrap_llm for provider={provider}"
                )

    # Import token counting functionality if research_id is provided
    callbacks = []
    if research_id is not None:
        from ..metrics import TokenCounter

        token_counter = TokenCounter()
        token_callback = token_counter.create_callback(
            research_id, research_context
        )
        # Set provider and model info on the callback
        if provider:
            token_callback.preset_provider = provider
        # Try to extract model name from the LLM instance
        if hasattr(llm, "model_name"):
            token_callback.preset_model = llm.model_name
        elif hasattr(llm, "model"):
            token_callback.preset_model = llm.model
        callbacks.append(token_callback)

    # Add callbacks to the LLM if it supports them
    if callbacks and hasattr(llm, "callbacks"):
        if llm.callbacks is None:
            llm.callbacks = callbacks
        else:
            llm.callbacks.extend(callbacks)

    class ProcessingLLMWrapper:
        def __init__(self, base_llm):
            self.base_llm = base_llm

        def invoke(self, *args, **kwargs):
            # Removed verbose debug logging to reduce log clutter
            # Uncomment the lines below if you need to debug LLM requests
            try:
                response = self.base_llm.invoke(*args, **kwargs)
            except Exception as e:
                logger.exception("LLM Request - Failed with error")
                # Log any URL information from the error
                error_str = str(e)
                if "http://" in error_str or "https://" in error_str:
                    logger.exception(
                        f"LLM Request - Error contains URL info: {error_str}"
                    )
                raise

            # Process the response content if it has a content attribute
            if hasattr(response, "content"):
                response.content = remove_think_tags(response.content)
            elif isinstance(response, str):
                response = remove_think_tags(response)

            return response

        # Pass through any other attributes to the base LLM
        def __getattr__(self, name):
            return getattr(self.base_llm, name)

        def close(self):
            """Close underlying HTTP clients held by this LLM. Idempotent."""
            try:
                from ..utilities.llm_utils import _close_base_llm

                _close_base_llm(self.base_llm)
            except Exception:
                logger.debug(
                    "best-effort cleanup of HTTP clients on shutdown",
                    exc_info=True,
                )

    return ProcessingLLMWrapper(llm)
