llmswitch-sdk 0.1.0__tar.gz
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_sdk-0.1.0/LICENSE +21 -0
- llmswitch_sdk-0.1.0/PKG-INFO +214 -0
- llmswitch_sdk-0.1.0/README.md +176 -0
- llmswitch_sdk-0.1.0/pyproject.toml +74 -0
- llmswitch_sdk-0.1.0/setup.cfg +4 -0
- llmswitch_sdk-0.1.0/src/llmswitch/__init__.py +47 -0
- llmswitch_sdk-0.1.0/src/llmswitch/base.py +39 -0
- llmswitch_sdk-0.1.0/src/llmswitch/config.py +115 -0
- llmswitch_sdk-0.1.0/src/llmswitch/constants.py +1 -0
- llmswitch_sdk-0.1.0/src/llmswitch/core/__init__.py +7 -0
- llmswitch_sdk-0.1.0/src/llmswitch/core/async_client.py +132 -0
- llmswitch_sdk-0.1.0/src/llmswitch/core/common.py +122 -0
- llmswitch_sdk-0.1.0/src/llmswitch/core/registry.py +66 -0
- llmswitch_sdk-0.1.0/src/llmswitch/core/sync_client.py +131 -0
- llmswitch_sdk-0.1.0/src/llmswitch/errors.py +333 -0
- llmswitch_sdk-0.1.0/src/llmswitch/models.py +343 -0
- llmswitch_sdk-0.1.0/src/llmswitch/providers/_shared.py +208 -0
- llmswitch_sdk-0.1.0/src/llmswitch/providers/anthropic_provider.py +1000 -0
- llmswitch_sdk-0.1.0/src/llmswitch/providers/azurefoundry_provider.py +749 -0
- llmswitch_sdk-0.1.0/src/llmswitch/providers/azureopenai_provider.py +599 -0
- llmswitch_sdk-0.1.0/src/llmswitch/providers/openai_provider.py +589 -0
- llmswitch_sdk-0.1.0/src/llmswitch/providers/vertexai_provider.py +817 -0
- llmswitch_sdk-0.1.0/src/llmswitch/py.typed +0 -0
- llmswitch_sdk-0.1.0/src/llmswitch/structured.py +246 -0
- llmswitch_sdk-0.1.0/src/llmswitch_sdk.egg-info/PKG-INFO +214 -0
- llmswitch_sdk-0.1.0/src/llmswitch_sdk.egg-info/SOURCES.txt +32 -0
- llmswitch_sdk-0.1.0/src/llmswitch_sdk.egg-info/dependency_links.txt +1 -0
- llmswitch_sdk-0.1.0/src/llmswitch_sdk.egg-info/requires.txt +22 -0
- llmswitch_sdk-0.1.0/src/llmswitch_sdk.egg-info/top_level.txt +1 -0
- llmswitch_sdk-0.1.0/tests/test_client_async.py +99 -0
- llmswitch_sdk-0.1.0/tests/test_client_options.py +67 -0
- llmswitch_sdk-0.1.0/tests/test_client_sync.py +203 -0
- llmswitch_sdk-0.1.0/tests/test_config.py +66 -0
- llmswitch_sdk-0.1.0/tests/test_providers.py +199 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rituraj Raman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llmswitch-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One Python client for chat completions and embeddings across OpenAI, Azure OpenAI, Azure AI Foundry, Anthropic and Vertex AI.
|
|
5
|
+
Author: Rituraj Raman
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/RITURAJRAMAN/llmswitch
|
|
8
|
+
Project-URL: Repository, https://github.com/RITURAJRAMAN/llmswitch
|
|
9
|
+
Project-URL: Issues, https://github.com/RITURAJRAMAN/llmswitch/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/RITURAJRAMAN/llmswitch/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: llm,openai,azure,anthropic,vertex-ai,embeddings,chat-completions
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: pydantic<3,>=2.14.1
|
|
21
|
+
Requires-Dist: openai<4.0.0,>=3.36.1
|
|
22
|
+
Provides-Extra: anthropic
|
|
23
|
+
Requires-Dist: anthropic<3,>=2.1.0; extra == "anthropic"
|
|
24
|
+
Provides-Extra: azure-foundry
|
|
25
|
+
Requires-Dist: azure-ai-inference==1.0.0b9; extra == "azure-foundry"
|
|
26
|
+
Requires-Dist: aiohttp<4.0.0,>=3.14.5; extra == "azure-foundry"
|
|
27
|
+
Provides-Extra: vertexai
|
|
28
|
+
Requires-Dist: google-genai<3,>=2.32.0; extra == "vertexai"
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: llmswitch-sdk[anthropic,azure-foundry,vertexai]; extra == "all"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build>=1.6.1; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest>=9.1.1; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=1.4.0; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.17.1; extra == "dev"
|
|
36
|
+
Requires-Dist: twine>=7.0.0; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# llmswitch
|
|
40
|
+
|
|
41
|
+
One Python client for chat completions and embeddings across OpenAI, Azure OpenAI,
|
|
42
|
+
Azure AI Foundry, Anthropic and Vertex AI. Switch providers by changing the constructor
|
|
43
|
+
arguments; the call sites stay the same.
|
|
44
|
+
|
|
45
|
+
Supported in 0.1.0: synchronous and asynchronous chat completions (including streaming and
|
|
46
|
+
structured output) and embeddings.
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install llmswitch-sdk # core: OpenAI and Azure OpenAI
|
|
52
|
+
pip install "llmswitch-sdk[anthropic]" # adds Anthropic
|
|
53
|
+
pip install "llmswitch-sdk[azure-foundry]" # adds Azure AI Foundry
|
|
54
|
+
pip install "llmswitch-sdk[vertexai]" # adds Vertex AI
|
|
55
|
+
pip install "llmswitch-sdk[all]" # every provider
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Requires Python 3.11 or later.
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from llmswitch import LLMSwitch
|
|
64
|
+
|
|
65
|
+
client = LLMSwitch(provider="openai", api_key="sk-...", model="gpt-4o")
|
|
66
|
+
|
|
67
|
+
reply = client.chat.completions.create(
|
|
68
|
+
messages=[{"role": "user", "content": "Say hello in French."}],
|
|
69
|
+
)
|
|
70
|
+
print(reply.choices[0].message.content)
|
|
71
|
+
|
|
72
|
+
vectors = client.embeddings.create(["first text", "second text"], model="text-embedding-3-small")
|
|
73
|
+
print(len(vectors.data), len(vectors.data[0].embedding))
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Async:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import asyncio
|
|
80
|
+
from llmswitch import AsyncLLMSwitch
|
|
81
|
+
|
|
82
|
+
async def main():
|
|
83
|
+
async with AsyncLLMSwitch(provider="anthropic", api_key="sk-ant-...", model="claude-opus-4-5") as client:
|
|
84
|
+
reply = await client.chat.completions.create(
|
|
85
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
86
|
+
)
|
|
87
|
+
print(reply.choices[0].message.content)
|
|
88
|
+
|
|
89
|
+
asyncio.run(main())
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Streaming returns an iterator of chunks:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
for chunk in client.chat.completions.create(messages=messages, stream=True):
|
|
96
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
97
|
+
print(chunk.choices[0].delta.content, end="")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
With `AsyncLLMSwitch`, await the call first, then iterate with `async for`:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
stream = await client.chat.completions.create(messages=messages, stream=True)
|
|
104
|
+
async for chunk in stream:
|
|
105
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
106
|
+
print(chunk.choices[0].delta.content, end="")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Structured output takes a Pydantic model class:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from pydantic import BaseModel
|
|
113
|
+
|
|
114
|
+
class City(BaseModel):
|
|
115
|
+
name: str
|
|
116
|
+
country: str
|
|
117
|
+
|
|
118
|
+
parsed = client.chat.completions.parse(
|
|
119
|
+
messages=[{"role": "user", "content": "Largest city in Japan?"}],
|
|
120
|
+
response_format=City,
|
|
121
|
+
)
|
|
122
|
+
print(parsed.choices[0].message.parsed)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Providers
|
|
126
|
+
|
|
127
|
+
| `provider` | Required settings | Chat (sync / async) | Embeddings |
|
|
128
|
+
|---|---|---|---|
|
|
129
|
+
| `openai` | `api_key` | yes / yes | yes |
|
|
130
|
+
| `azure_openai` | `api_key`, `endpoint`, `api_version`, `deployment_name` | yes / yes | yes |
|
|
131
|
+
| `azure_foundry` | `api_key`, `endpoint`, `api_version`, `deployment_name` | yes / yes | yes |
|
|
132
|
+
| `anthropic` | `api_key` | yes / yes | no |
|
|
133
|
+
| `vertexai` | `location`, `credentials_path` | yes / no | no |
|
|
134
|
+
|
|
135
|
+
Optional settings: `timeout` (seconds) for every provider; `endpoint`, `organization` and
|
|
136
|
+
`project` for `openai`; `endpoint` and `enable_prompt_caching` (default `True`) for `anthropic`;
|
|
137
|
+
`scopes` for `vertexai`.
|
|
138
|
+
|
|
139
|
+
Azure OpenAI example:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
client = LLMSwitch(
|
|
143
|
+
provider="azure_openai",
|
|
144
|
+
api_key="...",
|
|
145
|
+
endpoint="https://your-resource.openai.azure.com/",
|
|
146
|
+
api_version="2024-10-21",
|
|
147
|
+
deployment_name="your-gpt4o-deployment",
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
When `deployment_name` is set it is always used as the model. A per-call `model` argument
|
|
152
|
+
only overrides the configured model when no deployment is configured.
|
|
153
|
+
|
|
154
|
+
Vertex AI example:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
client = LLMSwitch(
|
|
158
|
+
provider="vertexai",
|
|
159
|
+
location="us-central1",
|
|
160
|
+
credentials_path="/path/to/service_account.json",
|
|
161
|
+
model="gemini-2.5-pro",
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Credentials are never read from the environment or from `.env` files by the library. Load
|
|
166
|
+
them in your application and pass them to the constructor.
|
|
167
|
+
|
|
168
|
+
## Native SDK options
|
|
169
|
+
|
|
170
|
+
Pass extra arguments for the provider's native client with `client_options`. They are
|
|
171
|
+
given to the SDK client constructor as keyword arguments:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
client = LLMSwitch(
|
|
175
|
+
provider="openai",
|
|
176
|
+
api_key="sk-...",
|
|
177
|
+
model="gpt-4o",
|
|
178
|
+
client_options={"max_retries": 3, "default_headers": {"X-Team": "search"}},
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Options that the unified constructor sets itself are rejected with a `ConfigurationError`:
|
|
183
|
+
`api_key`, `api_version`, `azure_endpoint`, `azure_deployment`, `base_url`, `endpoint`,
|
|
184
|
+
`organization`, `project`, `timeout`, `credential`, `credentials`, `location`, `vertexai`
|
|
185
|
+
and `http_options`. Use the unified argument for those instead.
|
|
186
|
+
|
|
187
|
+
Per-call keyword arguments are passed to the provider request unchanged, so provider-specific
|
|
188
|
+
parameters such as `seed`, `top_k` or `extra_body` can be given on `create()`.
|
|
189
|
+
|
|
190
|
+
## Errors
|
|
191
|
+
|
|
192
|
+
All errors derive from `LLMSwitchError`:
|
|
193
|
+
|
|
194
|
+
- `ConfigurationError`: invalid or missing constructor settings.
|
|
195
|
+
- `ChatCompletionError`, `EmbeddingGenerationError`: provider failures. `status_code` carries the HTTP status when known.
|
|
196
|
+
- `AuthenticationError`, `APIConnectionError`, `RateLimitExceededError`, `TimeOutError`, `ContentFilterError`, `ModelNotFoundError`, `InvalidResponseError`.
|
|
197
|
+
|
|
198
|
+
Calling an operation a provider does not support (for example embeddings on `anthropic`)
|
|
199
|
+
raises `LLMSwitchError` before any request is sent.
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
python -m venv .venv && source .venv/bin/activate
|
|
205
|
+
pip install -e ".[all,dev]"
|
|
206
|
+
pytest
|
|
207
|
+
ruff check src tests
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The tests use mocks and do not contact any provider. Live tests are not part of 0.1.0.
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# llmswitch
|
|
2
|
+
|
|
3
|
+
One Python client for chat completions and embeddings across OpenAI, Azure OpenAI,
|
|
4
|
+
Azure AI Foundry, Anthropic and Vertex AI. Switch providers by changing the constructor
|
|
5
|
+
arguments; the call sites stay the same.
|
|
6
|
+
|
|
7
|
+
Supported in 0.1.0: synchronous and asynchronous chat completions (including streaming and
|
|
8
|
+
structured output) and embeddings.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install llmswitch-sdk # core: OpenAI and Azure OpenAI
|
|
14
|
+
pip install "llmswitch-sdk[anthropic]" # adds Anthropic
|
|
15
|
+
pip install "llmswitch-sdk[azure-foundry]" # adds Azure AI Foundry
|
|
16
|
+
pip install "llmswitch-sdk[vertexai]" # adds Vertex AI
|
|
17
|
+
pip install "llmswitch-sdk[all]" # every provider
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Python 3.11 or later.
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from llmswitch import LLMSwitch
|
|
26
|
+
|
|
27
|
+
client = LLMSwitch(provider="openai", api_key="sk-...", model="gpt-4o")
|
|
28
|
+
|
|
29
|
+
reply = client.chat.completions.create(
|
|
30
|
+
messages=[{"role": "user", "content": "Say hello in French."}],
|
|
31
|
+
)
|
|
32
|
+
print(reply.choices[0].message.content)
|
|
33
|
+
|
|
34
|
+
vectors = client.embeddings.create(["first text", "second text"], model="text-embedding-3-small")
|
|
35
|
+
print(len(vectors.data), len(vectors.data[0].embedding))
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Async:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
from llmswitch import AsyncLLMSwitch
|
|
43
|
+
|
|
44
|
+
async def main():
|
|
45
|
+
async with AsyncLLMSwitch(provider="anthropic", api_key="sk-ant-...", model="claude-opus-4-5") as client:
|
|
46
|
+
reply = await client.chat.completions.create(
|
|
47
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
48
|
+
)
|
|
49
|
+
print(reply.choices[0].message.content)
|
|
50
|
+
|
|
51
|
+
asyncio.run(main())
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Streaming returns an iterator of chunks:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
for chunk in client.chat.completions.create(messages=messages, stream=True):
|
|
58
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
59
|
+
print(chunk.choices[0].delta.content, end="")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
With `AsyncLLMSwitch`, await the call first, then iterate with `async for`:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
stream = await client.chat.completions.create(messages=messages, stream=True)
|
|
66
|
+
async for chunk in stream:
|
|
67
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
68
|
+
print(chunk.choices[0].delta.content, end="")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Structured output takes a Pydantic model class:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pydantic import BaseModel
|
|
75
|
+
|
|
76
|
+
class City(BaseModel):
|
|
77
|
+
name: str
|
|
78
|
+
country: str
|
|
79
|
+
|
|
80
|
+
parsed = client.chat.completions.parse(
|
|
81
|
+
messages=[{"role": "user", "content": "Largest city in Japan?"}],
|
|
82
|
+
response_format=City,
|
|
83
|
+
)
|
|
84
|
+
print(parsed.choices[0].message.parsed)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Providers
|
|
88
|
+
|
|
89
|
+
| `provider` | Required settings | Chat (sync / async) | Embeddings |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| `openai` | `api_key` | yes / yes | yes |
|
|
92
|
+
| `azure_openai` | `api_key`, `endpoint`, `api_version`, `deployment_name` | yes / yes | yes |
|
|
93
|
+
| `azure_foundry` | `api_key`, `endpoint`, `api_version`, `deployment_name` | yes / yes | yes |
|
|
94
|
+
| `anthropic` | `api_key` | yes / yes | no |
|
|
95
|
+
| `vertexai` | `location`, `credentials_path` | yes / no | no |
|
|
96
|
+
|
|
97
|
+
Optional settings: `timeout` (seconds) for every provider; `endpoint`, `organization` and
|
|
98
|
+
`project` for `openai`; `endpoint` and `enable_prompt_caching` (default `True`) for `anthropic`;
|
|
99
|
+
`scopes` for `vertexai`.
|
|
100
|
+
|
|
101
|
+
Azure OpenAI example:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
client = LLMSwitch(
|
|
105
|
+
provider="azure_openai",
|
|
106
|
+
api_key="...",
|
|
107
|
+
endpoint="https://your-resource.openai.azure.com/",
|
|
108
|
+
api_version="2024-10-21",
|
|
109
|
+
deployment_name="your-gpt4o-deployment",
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
When `deployment_name` is set it is always used as the model. A per-call `model` argument
|
|
114
|
+
only overrides the configured model when no deployment is configured.
|
|
115
|
+
|
|
116
|
+
Vertex AI example:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
client = LLMSwitch(
|
|
120
|
+
provider="vertexai",
|
|
121
|
+
location="us-central1",
|
|
122
|
+
credentials_path="/path/to/service_account.json",
|
|
123
|
+
model="gemini-2.5-pro",
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Credentials are never read from the environment or from `.env` files by the library. Load
|
|
128
|
+
them in your application and pass them to the constructor.
|
|
129
|
+
|
|
130
|
+
## Native SDK options
|
|
131
|
+
|
|
132
|
+
Pass extra arguments for the provider's native client with `client_options`. They are
|
|
133
|
+
given to the SDK client constructor as keyword arguments:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
client = LLMSwitch(
|
|
137
|
+
provider="openai",
|
|
138
|
+
api_key="sk-...",
|
|
139
|
+
model="gpt-4o",
|
|
140
|
+
client_options={"max_retries": 3, "default_headers": {"X-Team": "search"}},
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Options that the unified constructor sets itself are rejected with a `ConfigurationError`:
|
|
145
|
+
`api_key`, `api_version`, `azure_endpoint`, `azure_deployment`, `base_url`, `endpoint`,
|
|
146
|
+
`organization`, `project`, `timeout`, `credential`, `credentials`, `location`, `vertexai`
|
|
147
|
+
and `http_options`. Use the unified argument for those instead.
|
|
148
|
+
|
|
149
|
+
Per-call keyword arguments are passed to the provider request unchanged, so provider-specific
|
|
150
|
+
parameters such as `seed`, `top_k` or `extra_body` can be given on `create()`.
|
|
151
|
+
|
|
152
|
+
## Errors
|
|
153
|
+
|
|
154
|
+
All errors derive from `LLMSwitchError`:
|
|
155
|
+
|
|
156
|
+
- `ConfigurationError`: invalid or missing constructor settings.
|
|
157
|
+
- `ChatCompletionError`, `EmbeddingGenerationError`: provider failures. `status_code` carries the HTTP status when known.
|
|
158
|
+
- `AuthenticationError`, `APIConnectionError`, `RateLimitExceededError`, `TimeOutError`, `ContentFilterError`, `ModelNotFoundError`, `InvalidResponseError`.
|
|
159
|
+
|
|
160
|
+
Calling an operation a provider does not support (for example embeddings on `anthropic`)
|
|
161
|
+
raises `LLMSwitchError` before any request is sent.
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
python -m venv .venv && source .venv/bin/activate
|
|
167
|
+
pip install -e ".[all,dev]"
|
|
168
|
+
pytest
|
|
169
|
+
ruff check src tests
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The tests use mocks and do not contact any provider. Live tests are not part of 0.1.0.
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=84.2.1"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "llmswitch-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "One Python client for chat completions and embeddings across OpenAI, Azure OpenAI, Azure AI Foundry, Anthropic and Vertex AI."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
authors = [{ name = "Rituraj Raman" }]
|
|
14
|
+
keywords = ["llm", "openai", "azure", "anthropic", "vertex-ai", "embeddings", "chat-completions"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
dependencies = [
|
|
24
|
+
"pydantic>=2.14.1,<3",
|
|
25
|
+
"openai>=3.36.1,<4.0.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/RITURAJRAMAN/llmswitch"
|
|
30
|
+
Repository = "https://github.com/RITURAJRAMAN/llmswitch"
|
|
31
|
+
Issues = "https://github.com/RITURAJRAMAN/llmswitch/issues"
|
|
32
|
+
Changelog = "https://github.com/RITURAJRAMAN/llmswitch/blob/main/CHANGELOG.md"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
anthropic = [
|
|
36
|
+
"anthropic>=2.1.0,<3",
|
|
37
|
+
]
|
|
38
|
+
azure-foundry = [
|
|
39
|
+
"azure-ai-inference==1.0.0b9",
|
|
40
|
+
"aiohttp>=3.14.5,<4.0.0",
|
|
41
|
+
]
|
|
42
|
+
vertexai = [
|
|
43
|
+
"google-genai>=2.32.0,<3",
|
|
44
|
+
]
|
|
45
|
+
all = [
|
|
46
|
+
"llmswitch-sdk[anthropic,azure-foundry,vertexai]",
|
|
47
|
+
]
|
|
48
|
+
dev = [
|
|
49
|
+
"build>=1.6.1",
|
|
50
|
+
"pytest>=9.1.1",
|
|
51
|
+
"pytest-asyncio>=1.4.0",
|
|
52
|
+
"ruff>=0.17.1",
|
|
53
|
+
"twine>=7.0.0",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.packages.find]
|
|
57
|
+
where = ["src"]
|
|
58
|
+
include = ["llmswitch*"]
|
|
59
|
+
|
|
60
|
+
[tool.setuptools.package-data]
|
|
61
|
+
llmswitch = ["py.typed"]
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
testpaths = ["tests"]
|
|
65
|
+
asyncio_mode = "auto"
|
|
66
|
+
|
|
67
|
+
[tool.ruff]
|
|
68
|
+
line-length = 120
|
|
69
|
+
target-version = "py311"
|
|
70
|
+
src = ["src"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint]
|
|
73
|
+
select = ["E", "F", "W", "I", "B"]
|
|
74
|
+
ignore = ["E501"]
|
|
@@ -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
|
+
]
|
|
@@ -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
|
+
"""
|
|
@@ -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
|
+
}
|