llmswitch-sdk 0.1.0__py3-none-any.whl

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.
llmswitch/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ """
2
+ llmswitch
3
+
4
+ One client interface for chat completions and embeddings across OpenAI, Azure
5
+ OpenAI, Azure AI Foundry, Anthropic and Vertex AI.
6
+
7
+ from llmswitch import LLMSwitch
8
+ """
9
+ import logging
10
+
11
+ __version__ = "0.1.0"
12
+
13
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
14
+
15
+ from .core import AsyncLLMSwitch, LLMSwitch # noqa: E402
16
+ from .errors import ( # noqa: E402
17
+ APIConnectionError,
18
+ AuthenticationError,
19
+ ChatCompletionError,
20
+ ConfigurationError,
21
+ ContentFilterError,
22
+ EmbeddingGenerationError,
23
+ InvalidResponseError,
24
+ LLMSwitchError,
25
+ ModelNotFoundError,
26
+ RateLimitExceededError,
27
+ TimeOutError,
28
+ )
29
+ from .models import ProviderExtensions # noqa: E402
30
+
31
+ __all__ = [
32
+ "__version__",
33
+ "LLMSwitch",
34
+ "AsyncLLMSwitch",
35
+ "LLMSwitchError",
36
+ "ConfigurationError",
37
+ "ModelNotFoundError",
38
+ "APIConnectionError",
39
+ "AuthenticationError",
40
+ "RateLimitExceededError",
41
+ "ChatCompletionError",
42
+ "EmbeddingGenerationError",
43
+ "InvalidResponseError",
44
+ "ContentFilterError",
45
+ "TimeOutError",
46
+ "ProviderExtensions",
47
+ ]
llmswitch/base.py ADDED
@@ -0,0 +1,39 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class BaseChatProvider(ABC):
5
+ """
6
+ Abstract base class for chat providers.
7
+ """
8
+
9
+ @abstractmethod
10
+ def create(self, messages, **kwargs):
11
+ """
12
+ Create a chat completion.
13
+
14
+ Args:
15
+ messages: List of message dicts or objects.
16
+ **kwargs: Additional parameters for provider.
17
+
18
+ Returns:
19
+ ChatCompletion or generator of ChatCompletionChunk.
20
+ """
21
+
22
+
23
+ class BaseEmbeddingProvider(ABC):
24
+ """
25
+ Abstract base class for embedding providers.
26
+ """
27
+
28
+ @abstractmethod
29
+ def create(self, inputs, **kwargs):
30
+ """
31
+ Create embeddings for the given inputs.
32
+
33
+ Args:
34
+ inputs: List of input strings or objects.
35
+ **kwargs: Additional parameters for provider.
36
+
37
+ Returns:
38
+ Embedding response object.
39
+ """
llmswitch/config.py ADDED
@@ -0,0 +1,115 @@
1
+ """
2
+ config.py
3
+
4
+ Pydantic configuration models, one per supported provider. ``CONFIG_REGISTRY``
5
+ maps each provider name to its configuration class.
6
+ """
7
+
8
+ from typing import Any, Optional
9
+
10
+ from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator
11
+
12
+ # Native-SDK client arguments that the unified constructor sets itself. They cannot
13
+ # be overridden through ``client_options``; pass them as the unified arguments instead.
14
+ RESERVED_CLIENT_OPTIONS = frozenset({
15
+ "api_key", "api_version", "azure_endpoint", "azure_deployment", "base_url", "endpoint",
16
+ "organization", "project", "timeout", "credential", "credentials", "location",
17
+ "vertexai", "http_options",
18
+ })
19
+
20
+
21
+ class BaseLLMConfig(BaseModel):
22
+ """Fields shared by every provider. Provider configs inherit from this class."""
23
+
24
+ # The unified constructor accepts the union of provider-specific fields;
25
+ # each provider reads what it needs and ignores the rest.
26
+ model_config = ConfigDict(extra="allow", str_strip_whitespace=True)
27
+
28
+ provider: str = Field(..., min_length=1, description="Provider name (openai, azure_openai, ...)")
29
+ model: Optional[str] = Field(default=None, description="Model name or ID")
30
+ deployment_name: Optional[str] = Field(
31
+ default=None,
32
+ description="Deployment or model alias; when set it takes precedence over the per-call model",
33
+ )
34
+ timeout: Optional[float] = Field(default=None, gt=0, allow_inf_nan=False,
35
+ description="Request timeout in seconds")
36
+ client_options: Optional[dict[str, Any]] = Field(
37
+ default=None,
38
+ description="Extra keyword arguments passed to the native SDK client constructor, "
39
+ "for example default_headers or max_retries",
40
+ )
41
+
42
+ @field_validator("client_options")
43
+ @classmethod
44
+ def reject_reserved_client_options(cls, value: Optional[dict[str, Any]]) -> Optional[dict[str, Any]]:
45
+ if value is None:
46
+ return None
47
+ reserved = sorted(RESERVED_CLIENT_OPTIONS.intersection(value))
48
+ if reserved:
49
+ raise ValueError(
50
+ f"client_options cannot set {', '.join(reserved)}; use the unified constructor arguments instead"
51
+ )
52
+ return value
53
+
54
+ @field_validator("provider", mode="before")
55
+ @classmethod
56
+ def required_string_fields(cls, v, info: ValidationInfo):
57
+ if v is None or (isinstance(v, str) and not v.strip()):
58
+ raise ValueError(f"{info.field_name} is required and cannot be empty")
59
+ return v
60
+
61
+
62
+ class AzureOpenAIConfig(BaseLLMConfig):
63
+ """Azure OpenAI: API key, endpoint, API version and deployment name are required."""
64
+
65
+ api_key: str = Field(..., min_length=1, description="API key for authentication")
66
+ endpoint: str = Field(..., min_length=1, description="Azure OpenAI resource endpoint")
67
+ api_version: str = Field(..., min_length=1, description="Azure OpenAI API version")
68
+ deployment_name: str = Field(..., min_length=1, description="Azure OpenAI deployment name")
69
+
70
+
71
+ class AzureFoundryConfig(BaseLLMConfig):
72
+ """Azure AI Foundry: API key, endpoint, API version and deployment name are required."""
73
+
74
+ api_key: str = Field(..., min_length=1, description="API key for authentication")
75
+ endpoint: str = Field(..., min_length=1, description="Azure AI Foundry endpoint")
76
+ deployment_name: str = Field(..., min_length=1, description="Deployment or model name")
77
+ api_version: str = Field(..., min_length=1, description="Azure AI Foundry API version")
78
+
79
+
80
+ class VertexAIConfig(BaseLLMConfig):
81
+ """Vertex AI: location and service-account credentials are required."""
82
+
83
+ location: str = Field(..., min_length=1, description="GCP location (e.g. us-central1)")
84
+ credentials_path: str = Field(..., min_length=1, description="Path to GCP credentials JSON file")
85
+ scopes: list[str] = Field(default_factory=lambda: ["https://www.googleapis.com/auth/cloud-platform"],
86
+ description="OAuth scopes for GCP authentication")
87
+
88
+
89
+ class OpenAIConfig(BaseLLMConfig):
90
+ """Native OpenAI: API key is required; endpoint, organization and project are optional."""
91
+
92
+ api_key: str = Field(..., min_length=1, description="OpenAI API key")
93
+ endpoint: Optional[str] = Field(default=None, description="Custom base URL for compatible APIs")
94
+ organization: Optional[str] = Field(default=None, description="OpenAI organization ID")
95
+ project: Optional[str] = Field(default=None, description="OpenAI project ID")
96
+
97
+
98
+ class AnthropicConfig(BaseLLMConfig):
99
+ """Anthropic (Claude): API key is required. Prompt caching is on by default."""
100
+
101
+ api_key: str = Field(..., min_length=1, description="Anthropic API key")
102
+ endpoint: Optional[str] = Field(default=None, description="Custom base URL for an Anthropic-compatible gateway")
103
+ enable_prompt_caching: bool = Field(
104
+ default=True,
105
+ description="Inject cache_control on system messages and tool definitions",
106
+ )
107
+
108
+
109
+ CONFIG_REGISTRY = {
110
+ "azure_openai": AzureOpenAIConfig,
111
+ "azure_foundry": AzureFoundryConfig,
112
+ "vertexai": VertexAIConfig,
113
+ "openai": OpenAIConfig,
114
+ "anthropic": AnthropicConfig,
115
+ }
llmswitch/constants.py ADDED
@@ -0,0 +1 @@
1
+ DEFAULT_ANTHROPIC_MODEL = "claude-opus-4-5"
@@ -0,0 +1,7 @@
1
+ """Public client classes."""
2
+ from __future__ import annotations
3
+
4
+ from .async_client import AsyncLLMSwitch
5
+ from .sync_client import LLMSwitch
6
+
7
+ __all__ = ["LLMSwitch", "AsyncLLMSwitch"]
@@ -0,0 +1,132 @@
1
+ """
2
+ core.async_client
3
+
4
+ Asynchronous ``AsyncLLMSwitch`` client. Chat completions use each provider's
5
+ native async client. Embedding providers are synchronous, so they run in a
6
+ worker thread to keep the event loop free.
7
+
8
+ Native async chat is available for azure_openai, azure_foundry, openai and
9
+ anthropic. vertexai is sync-only in this release.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import logging
15
+ from typing import Any
16
+
17
+ from ..errors import LLMSwitchError
18
+ from .common import (
19
+ build_config,
20
+ map_chat_error,
21
+ map_embedding_error,
22
+ normalize_embedding_input,
23
+ provider_kwargs,
24
+ resolve_model,
25
+ validate_messages,
26
+ )
27
+ from .registry import ASYNC_CHAT_PROVIDER_REGISTRY, EMBEDDING_PROVIDER_REGISTRY
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class _AsyncChatCompletions:
33
+ def __init__(self, client: "AsyncLLMSwitch") -> None:
34
+ self._client = client
35
+
36
+ async def create(self, messages: list[dict[str, Any]], stream: bool = False, **kwargs: Any) -> Any:
37
+ """Async counterpart of ``LLMSwitch.chat.completions.create``.
38
+
39
+ With ``stream=True`` the awaited result is an async generator of chunks.
40
+ """
41
+ config = self._client.config
42
+ validate_messages(messages)
43
+ provider_cls = ASYNC_CHAT_PROVIDER_REGISTRY.get(config.provider)
44
+ if provider_cls is None:
45
+ raise LLMSwitchError(f"Unsupported provider for async chat completion: {config.provider}",
46
+ provider=config.provider)
47
+ if stream:
48
+ kwargs.setdefault("stream_options", {"include_usage": True})
49
+ resolve_model(config, kwargs)
50
+ provider = self._client._provider(provider_cls)
51
+ try:
52
+ return await provider.create(messages=messages, stream=stream, **kwargs)
53
+ except Exception as exc:
54
+ raise map_chat_error(exc, config) from exc
55
+
56
+ async def parse(self, messages: list[dict[str, Any]], response_format: Any, **kwargs: Any) -> Any:
57
+ """Async structured output. See ``LLMSwitch.chat.completions.parse``."""
58
+ return await self.create(messages, stream=False, response_format=response_format, **kwargs)
59
+
60
+
61
+ class _AsyncEmbeddings:
62
+ def __init__(self, client: "AsyncLLMSwitch") -> None:
63
+ self._client = client
64
+
65
+ async def create(self, input: str | list[str], **kwargs: Any) -> Any: # noqa: A002
66
+ """Create embeddings without blocking the event loop."""
67
+ config = self._client.config
68
+ inputs = normalize_embedding_input(input)
69
+ provider_cls = EMBEDDING_PROVIDER_REGISTRY.get(config.provider)
70
+ if provider_cls is None:
71
+ raise LLMSwitchError(f"Unsupported provider for embeddings: {config.provider}",
72
+ provider=config.provider)
73
+ resolve_model(config, kwargs)
74
+ provider = self._client._provider(provider_cls)
75
+ try:
76
+ return await asyncio.to_thread(provider.create, inputs, **kwargs)
77
+ except Exception as exc:
78
+ raise map_embedding_error(exc, config) from exc
79
+
80
+
81
+ class AsyncLLMSwitch:
82
+ """
83
+ Unified asynchronous client, mirroring ``LLMSwitch``.
84
+
85
+ Usage::
86
+
87
+ client = AsyncLLMSwitch(provider="openai", api_key="...", model="gpt-4o")
88
+ reply = await client.chat.completions.create(messages=[{"role": "user", "content": "Hi"}])
89
+ vectors = await client.embeddings.create(["hello", "world"])
90
+ await client.aclose()
91
+ """
92
+
93
+ def __init__(self, **kwargs: Any) -> None:
94
+ self.config = build_config(kwargs)
95
+ self._providers: dict[type, Any] = {}
96
+ self.chat = _AsyncChat(self)
97
+ self.embeddings = _AsyncEmbeddings(self)
98
+
99
+ def __repr__(self) -> str:
100
+ return f"AsyncLLMSwitch(provider={self.config.provider!r}, model={self.config.model!r})"
101
+
102
+ async def __aenter__(self) -> "AsyncLLMSwitch":
103
+ return self
104
+
105
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
106
+ await self.aclose()
107
+
108
+ def _provider(self, provider_cls: type) -> Any:
109
+ """Return the cached provider client for ``provider_cls``, building it on first use."""
110
+ provider = self._providers.get(provider_cls)
111
+ if provider is None:
112
+ provider = provider_cls(**provider_kwargs(self.config))
113
+ self._providers[provider_cls] = provider
114
+ return provider
115
+
116
+ async def aclose(self) -> None:
117
+ """Close the provider clients this client created, awaiting async close hooks."""
118
+ for provider in self._providers.values():
119
+ close_fn = getattr(provider, "aclose", None) or getattr(provider, "close", None)
120
+ if close_fn is None:
121
+ close_fn = getattr(getattr(provider, "client", None), "aclose", None) or getattr(
122
+ getattr(provider, "client", None), "close", None)
123
+ if callable(close_fn):
124
+ result = close_fn()
125
+ if asyncio.iscoroutine(result):
126
+ await result
127
+ self._providers.clear()
128
+
129
+
130
+ class _AsyncChat:
131
+ def __init__(self, client: AsyncLLMSwitch) -> None:
132
+ self.completions = _AsyncChatCompletions(client)
@@ -0,0 +1,122 @@
1
+ """
2
+ core.common
3
+
4
+ Helpers shared by the synchronous and asynchronous clients: configuration
5
+ loading, model resolution, input validation, provider construction and error
6
+ translation.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any, Optional
12
+
13
+ from ..config import CONFIG_REGISTRY
14
+ from ..constants import DEFAULT_ANTHROPIC_MODEL
15
+ from ..errors import (
16
+ APIConnectionError,
17
+ AuthenticationError,
18
+ ChatCompletionError,
19
+ ConfigurationError,
20
+ EmbeddingGenerationError,
21
+ LLMSwitchError,
22
+ )
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def build_config(kwargs: dict[str, Any]) -> Any:
28
+ """Validate constructor kwargs against the provider's configuration model."""
29
+ provider = kwargs.get("provider")
30
+ if not isinstance(provider, str):
31
+ raise LLMSwitchError(f"Unsupported provider config: {provider}")
32
+ config_cls = CONFIG_REGISTRY.get(provider)
33
+ if config_cls is None:
34
+ raise LLMSwitchError(f"Unsupported provider config: {provider}")
35
+ try:
36
+ return config_cls(**kwargs)
37
+ except ValueError as exc:
38
+ if hasattr(exc, "errors"):
39
+ errors = exc.errors()
40
+ reasons = "; ".join(str(error["msg"]).removeprefix("Value error, ") for error in errors)
41
+ raise ConfigurationError(f"Invalid switch configuration: {reasons}", details=errors) from exc
42
+ raise ConfigurationError(f"Invalid switch configuration: {exc}", details=str(exc)) from exc
43
+
44
+
45
+ def resolve_model(config: Any, kwargs: dict[str, Any]) -> Optional[str]:
46
+ """Pick the effective model and write it into ``kwargs``.
47
+
48
+ A configured deployment wins over everything. Otherwise a per-call ``model``
49
+ overrides the configured model. Anthropic falls back to a default model.
50
+ """
51
+ deployment_name = getattr(config, "deployment_name", None)
52
+ model = deployment_name or kwargs.get("model") or getattr(config, "model", None)
53
+ if model is None and config.provider == "anthropic":
54
+ model = DEFAULT_ANTHROPIC_MODEL
55
+ if model is not None:
56
+ kwargs["model"] = model
57
+ return model
58
+
59
+
60
+ def validate_messages(messages: Any) -> None:
61
+ if not isinstance(messages, list) or not messages:
62
+ raise ValueError("messages must be a non-empty list of message dicts")
63
+ if not all(isinstance(message, dict) for message in messages):
64
+ raise ValueError("every message must be a dict")
65
+
66
+
67
+ def normalize_embedding_input(input: Any) -> list[str]: # noqa: A002
68
+ inputs = [input] if isinstance(input, str) else list(input or [])
69
+ if not inputs:
70
+ raise ValueError("Embedding input must contain at least one string")
71
+ if not all(isinstance(text, str) and text for text in inputs):
72
+ raise ValueError("Embedding input must be a non-empty string or a list of non-empty strings")
73
+ return inputs
74
+
75
+
76
+ def provider_kwargs(config: Any) -> dict[str, Any]:
77
+ """Union of constructor kwargs; each provider reads the ones it understands."""
78
+ endpoint = getattr(config, "endpoint", None)
79
+ deployment_name = getattr(config, "deployment_name", None)
80
+ return {
81
+ "api_key": getattr(config, "api_key", None),
82
+ "endpoint": endpoint,
83
+ "azure_endpoint": endpoint,
84
+ "api_version": getattr(config, "api_version", None),
85
+ "deployment_name": deployment_name,
86
+ "azure_deployment": deployment_name,
87
+ "model": getattr(config, "model", None),
88
+ "location": getattr(config, "location", None),
89
+ "credentials_path": getattr(config, "credentials_path", None),
90
+ "scopes": getattr(config, "scopes", None),
91
+ "organization": getattr(config, "organization", None),
92
+ "project": getattr(config, "project", None),
93
+ "timeout": getattr(config, "timeout", None),
94
+ "enable_prompt_caching": getattr(config, "enable_prompt_caching", None),
95
+ "client_options": getattr(config, "client_options", None),
96
+ }
97
+
98
+
99
+ def map_chat_error(exc: Exception, config: Any) -> LLMSwitchError:
100
+ """Translate a provider exception into the public error hierarchy."""
101
+ if isinstance(exc, LLMSwitchError):
102
+ return exc
103
+ if isinstance(exc, ValueError):
104
+ return ChatCompletionError(str(exc), status_code=400, provider=config.provider)
105
+ if isinstance(exc, ConnectionError):
106
+ return APIConnectionError(config.endpoint, provider=config.provider)
107
+ if isinstance(exc, PermissionError):
108
+ return AuthenticationError(provider=config.provider)
109
+ return ChatCompletionError(str(exc), status_code=getattr(exc, "status_code", 500), provider=config.provider)
110
+
111
+
112
+ def map_embedding_error(exc: Exception, config: Any) -> LLMSwitchError:
113
+ if isinstance(exc, LLMSwitchError):
114
+ return exc
115
+ if isinstance(exc, ValueError):
116
+ return EmbeddingGenerationError(str(exc), status_code=400, provider=config.provider)
117
+ if isinstance(exc, ConnectionError):
118
+ return APIConnectionError(config.endpoint, provider=config.provider)
119
+ if isinstance(exc, PermissionError):
120
+ return AuthenticationError(provider=config.provider)
121
+ return EmbeddingGenerationError(str(exc), status_code=getattr(exc, "status_code", 500),
122
+ provider=config.provider)
@@ -0,0 +1,66 @@
1
+ """
2
+ core.registry
3
+
4
+ Maps provider names to their chat, async chat and embedding provider classes.
5
+ Each provider SDK is an optional extra, so every import is guarded: a missing
6
+ SDK leaves that provider out of the registry instead of breaking the package.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ CHAT_PROVIDER_REGISTRY: dict[str, type] = {}
11
+ ASYNC_CHAT_PROVIDER_REGISTRY: dict[str, type] = {}
12
+ EMBEDDING_PROVIDER_REGISTRY: dict[str, type] = {}
13
+
14
+ try:
15
+ from ..providers.openai_provider import (
16
+ AsyncOpenAIChatProvider,
17
+ OpenAIChatProvider,
18
+ OpenAIEmbeddingProvider,
19
+ )
20
+
21
+ CHAT_PROVIDER_REGISTRY["openai"] = OpenAIChatProvider
22
+ ASYNC_CHAT_PROVIDER_REGISTRY["openai"] = AsyncOpenAIChatProvider
23
+ EMBEDDING_PROVIDER_REGISTRY["openai"] = OpenAIEmbeddingProvider
24
+ except ImportError:
25
+ pass
26
+
27
+ try:
28
+ from ..providers.azureopenai_provider import (
29
+ AsyncAzureOpenAIChatProvider,
30
+ AzureOpenAIChatProvider,
31
+ AzureOpenAIEmbeddingProvider,
32
+ )
33
+
34
+ CHAT_PROVIDER_REGISTRY["azure_openai"] = AzureOpenAIChatProvider
35
+ ASYNC_CHAT_PROVIDER_REGISTRY["azure_openai"] = AsyncAzureOpenAIChatProvider
36
+ EMBEDDING_PROVIDER_REGISTRY["azure_openai"] = AzureOpenAIEmbeddingProvider
37
+ except ImportError:
38
+ pass
39
+
40
+ try:
41
+ from ..providers.azurefoundry_provider import (
42
+ AsyncAzureFoundryChatProvider,
43
+ AzureFoundryChatProvider,
44
+ AzureFoundryEmbeddingProvider,
45
+ )
46
+
47
+ CHAT_PROVIDER_REGISTRY["azure_foundry"] = AzureFoundryChatProvider
48
+ ASYNC_CHAT_PROVIDER_REGISTRY["azure_foundry"] = AsyncAzureFoundryChatProvider
49
+ EMBEDDING_PROVIDER_REGISTRY["azure_foundry"] = AzureFoundryEmbeddingProvider
50
+ except ImportError:
51
+ pass
52
+
53
+ try:
54
+ from ..providers.anthropic_provider import AnthropicChatProvider, AsyncAnthropicChatProvider
55
+
56
+ CHAT_PROVIDER_REGISTRY["anthropic"] = AnthropicChatProvider
57
+ ASYNC_CHAT_PROVIDER_REGISTRY["anthropic"] = AsyncAnthropicChatProvider
58
+ except ImportError:
59
+ pass
60
+
61
+ try:
62
+ from ..providers.vertexai_provider import VertexAIChatProvider
63
+
64
+ CHAT_PROVIDER_REGISTRY["vertexai"] = VertexAIChatProvider
65
+ except ImportError:
66
+ pass
@@ -0,0 +1,131 @@
1
+ """
2
+ core.sync_client
3
+
4
+ Synchronous ``LLMSwitch`` client with an OpenAI-style ``chat.completions`` and
5
+ ``embeddings`` surface.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import Any
11
+
12
+ from ..errors import LLMSwitchError
13
+ from .common import (
14
+ build_config,
15
+ map_chat_error,
16
+ map_embedding_error,
17
+ normalize_embedding_input,
18
+ provider_kwargs,
19
+ resolve_model,
20
+ validate_messages,
21
+ )
22
+ from .registry import CHAT_PROVIDER_REGISTRY, EMBEDDING_PROVIDER_REGISTRY
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class _ChatCompletions:
28
+ def __init__(self, client: "LLMSwitch") -> None:
29
+ self._client = client
30
+
31
+ def create(self, messages: list[dict[str, Any]], stream: bool = False, **kwargs: Any) -> Any:
32
+ """
33
+ Create a chat completion.
34
+
35
+ Args:
36
+ messages: Message dicts. ``content`` may be a string or a list of content parts.
37
+ stream: Return a generator of ``ChatCompletionChunk`` instead of a ``ChatCompletion``.
38
+ **kwargs: Pass-through provider parameters (``response_format``, ``tools``,
39
+ ``temperature``, ``max_tokens``, ...). ``model`` overrides the configured model
40
+ unless a deployment name is configured.
41
+ """
42
+ config = self._client.config
43
+ validate_messages(messages)
44
+ provider_cls = CHAT_PROVIDER_REGISTRY.get(config.provider)
45
+ if provider_cls is None:
46
+ raise LLMSwitchError(f"Unsupported provider for chat completion: {config.provider}",
47
+ provider=config.provider)
48
+ if stream:
49
+ kwargs.setdefault("stream_options", {"include_usage": True})
50
+ resolve_model(config, kwargs)
51
+ provider = self._client._provider(provider_cls)
52
+ try:
53
+ return provider.create(messages=messages, stream=stream, **kwargs)
54
+ except Exception as exc:
55
+ raise map_chat_error(exc, config) from exc
56
+
57
+ def parse(self, messages: list[dict[str, Any]], response_format: Any, **kwargs: Any) -> Any:
58
+ """
59
+ Structured output. Pass a Pydantic model class as ``response_format``; the parsed
60
+ instance is available on ``choice.message.parsed``. Always non-streaming.
61
+ """
62
+ return self.create(messages, stream=False, response_format=response_format, **kwargs)
63
+
64
+
65
+ class _Embeddings:
66
+ def __init__(self, client: "LLMSwitch") -> None:
67
+ self._client = client
68
+
69
+ def create(self, input: str | list[str], **kwargs: Any) -> Any: # noqa: A002
70
+ """Create embeddings for one string or a list of strings."""
71
+ config = self._client.config
72
+ inputs = normalize_embedding_input(input)
73
+ provider_cls = EMBEDDING_PROVIDER_REGISTRY.get(config.provider)
74
+ if provider_cls is None:
75
+ raise LLMSwitchError(f"Unsupported provider for embeddings: {config.provider}",
76
+ provider=config.provider)
77
+ resolve_model(config, kwargs)
78
+ provider = self._client._provider(provider_cls)
79
+ try:
80
+ return provider.create(inputs, **kwargs)
81
+ except Exception as exc:
82
+ raise map_embedding_error(exc, config) from exc
83
+
84
+
85
+ class LLMSwitch:
86
+ """
87
+ Unified synchronous client. Construct it with a ``provider`` name plus that
88
+ provider's credentials and settings; the same call shape works across providers.
89
+
90
+ Usage::
91
+
92
+ client = LLMSwitch(provider="openai", api_key="...", model="gpt-4o")
93
+ reply = client.chat.completions.create(messages=[{"role": "user", "content": "Hi"}])
94
+ vectors = client.embeddings.create(["hello", "world"])
95
+ """
96
+
97
+ def __init__(self, **kwargs: Any) -> None:
98
+ self.config = build_config(kwargs)
99
+ self._providers: dict[type, Any] = {}
100
+ self.chat = _Chat(self)
101
+ self.embeddings = _Embeddings(self)
102
+
103
+ def __repr__(self) -> str:
104
+ return f"LLMSwitch(provider={self.config.provider!r}, model={self.config.model!r})"
105
+
106
+ def __enter__(self) -> "LLMSwitch":
107
+ return self
108
+
109
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
110
+ self.close()
111
+
112
+ def _provider(self, provider_cls: type) -> Any:
113
+ """Return the cached provider client for ``provider_cls``, building it on first use."""
114
+ provider = self._providers.get(provider_cls)
115
+ if provider is None:
116
+ provider = provider_cls(**provider_kwargs(self.config))
117
+ self._providers[provider_cls] = provider
118
+ return provider
119
+
120
+ def close(self) -> None:
121
+ """Close the provider clients this client created."""
122
+ for provider in self._providers.values():
123
+ close_fn = getattr(provider, "close", None) or getattr(getattr(provider, "client", None), "close", None)
124
+ if callable(close_fn):
125
+ close_fn()
126
+ self._providers.clear()
127
+
128
+
129
+ class _Chat:
130
+ def __init__(self, client: LLMSwitch) -> None:
131
+ self.completions = _ChatCompletions(client)