"""Inference client factory with protocol alias resolution. Replaces the legacy VLLMClient/OllamaClient fallback pattern with a capability-aware routing layer. Unknown protocols ALWAYS fail closed — they never silently fall back to Ollama. Requirements: 2.2, 2.6 Design: Inference Gateway — profiles and capability probes """ from __future__ import annotations import warnings from services.shared.inference.clients.ollama_native import OllamaNativeClient from services.shared.inference.clients.openai_compatible import OpenAICompatibleClient from services.shared.inference.errors import InferenceError, InferenceErrorCategory from services.shared.inference.models import InferenceTarget # Backward-compatible protocol aliases. # "vllm" is retained as a deprecated alias for "openai_chat". # "ollama" is retained as an alias for "ollama_native". PROTOCOL_ALIASES: dict[str, str] = { "vllm": "openai_chat", "ollama": "ollama_native", } # Canonical protocol names that the factory can instantiate. KNOWN_PROTOCOLS: frozenset[str] = frozenset({ "openai_chat", "ollama_native", "specialist_http", }) def resolve_protocol(provider_name: str) -> str: """Resolve a protocol name or alias to a canonical protocol. Raises InferenceError(CAPABILITY_UNAVAILABLE) for unknown protocols. Emits a deprecation warning for the deprecated "vllm" alias. Args: provider_name: Raw protocol/provider string (e.g. "vllm", "ollama_native"). Returns: Canonical protocol string. Raises: InferenceError: If the protocol is unknown and cannot be resolved. """ normalized = provider_name.strip().lower() # Check if it's already a known canonical protocol if normalized in KNOWN_PROTOCOLS: return normalized # Check aliases if normalized in PROTOCOL_ALIASES: canonical = PROTOCOL_ALIASES[normalized] if normalized == "vllm": warnings.warn( "Provider 'vllm' is deprecated. Use protocol 'openai_chat' instead. " "The 'vllm' alias will be removed in a future version.", DeprecationWarning, stacklevel=2, ) return canonical # Unknown protocol — fail closed, NEVER fall back to Ollama raise InferenceError( InferenceErrorCategory.CAPABILITY_UNAVAILABLE, f"Unknown protocol: {normalized!r}. " f"Supported protocols: {sorted(KNOWN_PROTOCOLS)}. " f"Supported aliases: {sorted(PROTOCOL_ALIASES.keys())}.", ) def create_client( target: InferenceTarget, ) -> OpenAICompatibleClient | OllamaNativeClient: """Create the appropriate inference client for the given target. Routes based on the target's protocol field. If the protocol is an alias (e.g. "vllm"), it is resolved first. Unknown protocols raise a typed configuration error — they NEVER silently fall back to Ollama. Args: target: Fully resolved inference target with protocol, URL, etc. Returns: An OpenAICompatibleClient or OllamaNativeClient instance. Raises: InferenceError: If the protocol is unknown or unsupported. """ protocol = resolve_protocol(target.protocol) if protocol == "openai_chat": return OpenAICompatibleClient(target) if protocol == "ollama_native": return OllamaNativeClient(target) # specialist_http is a valid protocol but has no client implementation yet # (handled by a separate specialist service layer). Fail closed here. raise InferenceError( InferenceErrorCategory.CAPABILITY_UNAVAILABLE, f"Protocol {protocol!r} is recognized but has no client implementation in this factory. " f"Use the specialist HTTP service layer directly.", )