llm-api-adapter 0.2.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.
- llm_api_adapter-0.2.0/MANIFEST.in +9 -0
- llm_api_adapter-0.2.0/PKG-INFO +254 -0
- llm_api_adapter-0.2.0/README.md +238 -0
- llm_api_adapter-0.2.0/pyproject.toml +29 -0
- llm_api_adapter-0.2.0/setup.cfg +4 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/__init__.py +0 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/adapters/__init__.py +0 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/adapters/anthropic_adapter.py +63 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/adapters/base_adapter.py +50 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/adapters/google_adapter.py +72 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/adapters/openai_adapter.py +61 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/errors/__init__.py +0 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/errors/llm_api_error.py +82 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/llms/__init__.py +0 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/llms/anthropic/__init__.py +0 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/llms/anthropic/sync_client.py +80 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/llms/google/__init__.py +0 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/llms/google/sync_client.py +79 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/llms/openai/__init__.py +0 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/llms/openai/sync_client.py +81 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/models/__init__.py +0 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/models/messages/__init__.py +0 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/models/messages/chat_message.py +21 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/models/responses/__init__.py +0 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/models/responses/chat_response.py +45 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter/universal_adapter.py +55 -0
- llm_api_adapter-0.2.0/src/llm_api_adapter.egg-info/SOURCES.txt +25 -0
- llm_api_adapter-0.2.0/tests/tests_runner.py +19 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llm-api-adapter
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Lightweight, pluggable adapter for multiple LLM APIs (OpenAI, Anthropic, Google)
|
|
5
|
+
Author: Sergey Inozemtsev
|
|
6
|
+
Project-URL: Repository, https://github.com/Inozem/llm_api_adapter/
|
|
7
|
+
Keywords: llm,adapter,lightweight,api
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Typing :: Typed
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: requests>=2.32
|
|
16
|
+
|
|
17
|
+
# LLM API Adapter SDK for Python
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
This SDK for Python allows you to use LLM APIs from various companies and models through a unified interface. Currently, the project supports API integration for the following companies: OpenAI, Anthropic, and Google. At this stage, only the chat functionality is implemented.
|
|
22
|
+
|
|
23
|
+
### Version
|
|
24
|
+
|
|
25
|
+
Current version: 0.2.0
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- **Unified Interface**: Work seamlessly with different LLM providers using a single, consistent API.
|
|
30
|
+
- **Multiple Provider Support**: Currently supports OpenAI, Anthropic, and Google APIs, allowing easy switching between them.
|
|
31
|
+
- **Chat Functionality**: Provides an easy way to interact with chat-based LLMs.
|
|
32
|
+
- **Extensible Design**: Built to easily extend support for additional providers and new functionalities in the future.
|
|
33
|
+
- **Error Handling**: Standardized error messages across all supported LLMs, simplifying integration and debugging.
|
|
34
|
+
- **Flexible Configuration**: Manage request parameters like temperature, max tokens, and other settings for fine-tuned control.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
To install the SDK, you can use pip:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install llm_api_adapter
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Note:** You will need to obtain API keys from each LLM provider you wish to use (OpenAI, Anthropic, Google). Refer to their respective documentation for instructions on obtaining API keys.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Getting Started
|
|
48
|
+
|
|
49
|
+
### Importing and Setting Up the Adapter
|
|
50
|
+
|
|
51
|
+
To start using the adapter, you need to import the necessary components:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from llm_api_adapter.models.messages.chat_message import (
|
|
55
|
+
AIMessage, Prompt, UserMessage
|
|
56
|
+
)
|
|
57
|
+
from llm_api_adapter.universal_adapter import UniversalLLMAPIAdapter
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Sending a Simple Request
|
|
61
|
+
|
|
62
|
+
The SDK supports three types of messages for interacting with the LLM:
|
|
63
|
+
|
|
64
|
+
- **Prompt**: Use `Prompt` to set the context or initial prompt for the model.
|
|
65
|
+
- **UserMessage**: Use `UserMessage` to send messages from the user during a conversation.
|
|
66
|
+
- **AIMessage**: Use `AIMessage` to simulate responses from the assistant during a conversation.
|
|
67
|
+
|
|
68
|
+
Here is an example of how to send a simple request to the adapter:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
messages = [
|
|
72
|
+
UserMessage("Hi! Can you explain how artificial intelligence works?")
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
adapter = UniversalLLMAPIAdapter(
|
|
76
|
+
organization="openai",
|
|
77
|
+
model="gpt-3.5-turbo",
|
|
78
|
+
api_key=openai_api_key
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
response = adapter.generate_chat_answer(
|
|
82
|
+
messages=messages,
|
|
83
|
+
max_tokens=max_tokens,
|
|
84
|
+
temperature=temperature,
|
|
85
|
+
top_p=top_p
|
|
86
|
+
)
|
|
87
|
+
print(response.content)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Parameters
|
|
91
|
+
|
|
92
|
+
- **max\_tokens**: The maximum number of tokens to generate in the response. This limits the length of the output. Default value: `256`.
|
|
93
|
+
|
|
94
|
+
- **temperature**: Controls the randomness of the response. Higher values (e.g., 0.8) make the output more random, while lower values (e.g., 0.2) make it more focused and deterministic. Default value: `1.0` (range: 0 to 2).
|
|
95
|
+
|
|
96
|
+
- **top\_p**: Limits the response to a certain cumulative probability. This is used to create more focused and coherent responses by considering only the highest probability options. Default value: `1.0` (range: 0 to 1).
|
|
97
|
+
|
|
98
|
+
## Handling Errors
|
|
99
|
+
|
|
100
|
+
### Common Errors
|
|
101
|
+
|
|
102
|
+
The SDK provides a set of standardized errors for easier debugging and integration:
|
|
103
|
+
|
|
104
|
+
- **LLMAPIError**: Base class for all API-related errors. This error is also used for any unexpected LLM API errors.
|
|
105
|
+
|
|
106
|
+
- **LLMAPIAuthorizationError**: Raised when authentication or authorization fails.
|
|
107
|
+
|
|
108
|
+
- **LLMAPIRateLimitError**: Raised when rate limits are exceeded.
|
|
109
|
+
|
|
110
|
+
- **LLMAPITokenLimitError**: Raised when token limits are exceeded.
|
|
111
|
+
|
|
112
|
+
- **LLMAPIClientError**: Raised when the client makes an invalid request.
|
|
113
|
+
|
|
114
|
+
- **LLMAPIServerError**: Raised when the server encounters an error.
|
|
115
|
+
|
|
116
|
+
- **LLMAPITimeoutError**: Raised when a request times out.
|
|
117
|
+
|
|
118
|
+
- **LLMAPIUsageLimitError**: Raised when usage limits are exceeded.
|
|
119
|
+
|
|
120
|
+
## Configuration and Management
|
|
121
|
+
|
|
122
|
+
### Using Different Providers and Models
|
|
123
|
+
|
|
124
|
+
The SDK allows you to easily switch between LLM providers and specify the model you want to use. Currently supported providers are OpenAI, Anthropic, and Google.
|
|
125
|
+
|
|
126
|
+
- **OpenAI**: You can use models like `gpt-4o-mini`, `gpt-4o`, `gpt-4-turbo`, `gpt-4`, `gpt-4-turbo-preview`, `gpt-3.5-turbo`. Set the `organization` parameter to `openai` and specify the `model` name.
|
|
127
|
+
|
|
128
|
+
- **Anthropic**: Available models include `claude-3-5-sonnet-20241022`, `claude-3-opus-20240229`, `claude-3-sonnet-20240229`, `claude-3-haiku-20240307`. Set the `organization` parameter to `anthropic` and specify the desired `model`.
|
|
129
|
+
|
|
130
|
+
- **Google**: Models such as `gemini-1.5-flash`, `gemini-1.5-flash-8b`, `gemini-1.5-pro` can be used. Set the `organization` parameter to `google` and specify the `model`.
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
adapter = UniversalLLMAPIAdapter(
|
|
136
|
+
organization="openai",
|
|
137
|
+
model="gpt-3.5-turbo",
|
|
138
|
+
api_key=openai_api_key
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
To switch to another provider, simply change the `organization` and `model` parameters.
|
|
143
|
+
|
|
144
|
+
### Switching Providers
|
|
145
|
+
|
|
146
|
+
Here is an example of how to switch between different LLM providers using the SDK:
|
|
147
|
+
|
|
148
|
+
**Note**: Each instance of `UniversalLLMAPIAdapter` is tied to a specific provider and model. You cannot change the `organization` parameter for an existing adapter object. To use a different provider, you must create a new instance.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
gpt = UniversalLLMAPIAdapter(
|
|
152
|
+
organization="openai",
|
|
153
|
+
model="gpt-3.5-turbo",
|
|
154
|
+
api_key=openai_api_key
|
|
155
|
+
)
|
|
156
|
+
gpt_response = gpt.generate_chat_answer(messages=messages)
|
|
157
|
+
print(gpt_response.content)
|
|
158
|
+
|
|
159
|
+
claude = UniversalLLMAPIAdapter(
|
|
160
|
+
organization="anthropic",
|
|
161
|
+
model="claude-3-haiku-20240307",
|
|
162
|
+
api_key=anthropic_api_key
|
|
163
|
+
)
|
|
164
|
+
claude_response = claude.generate_chat_answer(messages=messages)
|
|
165
|
+
print(claude_response.content)
|
|
166
|
+
|
|
167
|
+
google = UniversalLLMAPIAdapter(
|
|
168
|
+
organization="google",
|
|
169
|
+
model="gemini-1.5-flash",
|
|
170
|
+
api_key=google_api_key
|
|
171
|
+
)
|
|
172
|
+
google_response = google.generate_chat_answer(messages=messages)
|
|
173
|
+
print(google_response.content)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Example Use Case
|
|
177
|
+
|
|
178
|
+
Here is a comprehensive example that showcases all possible message types and interactions:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from llm_api_adapter.models.messages.chat_message import (
|
|
182
|
+
AIMessage, Prompt, UserMessage
|
|
183
|
+
)
|
|
184
|
+
from llm_api_adapter.universal_adapter import UniversalLLMAPIAdapter
|
|
185
|
+
|
|
186
|
+
messages = [
|
|
187
|
+
Prompt(
|
|
188
|
+
"You are a friendly assistant who explains complex concepts "
|
|
189
|
+
"in simple terms."
|
|
190
|
+
),
|
|
191
|
+
UserMessage("Hi! Can you explain how artificial intelligence works?"),
|
|
192
|
+
AIMessage(
|
|
193
|
+
"Sure! Artificial intelligence (AI) is a system that can perform "
|
|
194
|
+
"tasks requiring human-like intelligence, such as recognizing images "
|
|
195
|
+
"or understanding language. It learns by analyzing large amounts of "
|
|
196
|
+
"data, finding patterns, and making predictions."
|
|
197
|
+
),
|
|
198
|
+
UserMessage("How does AI learn?"),
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
adapter = UniversalLLMAPIAdapter(
|
|
202
|
+
organization="openai",
|
|
203
|
+
model="gpt-3.5-turbo",
|
|
204
|
+
api_key=openai_api_key
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
response = adapter.generate_chat_answer(
|
|
208
|
+
messages=messages,
|
|
209
|
+
max_tokens=256,
|
|
210
|
+
temperature=1.0,
|
|
211
|
+
top_p=1.0
|
|
212
|
+
)
|
|
213
|
+
print(response.content)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The `ChatResponse` object returned by `generate_chat_answer` includes several attributes that provide additional details about the response. These attributes will contain data only if they are included in the response from the LLM:
|
|
217
|
+
|
|
218
|
+
- **model**: The model that generated the response.
|
|
219
|
+
- **response_id**: A unique identifier for the response.
|
|
220
|
+
- **timestamp**: The time at which the response was generated.
|
|
221
|
+
- **tokens_used**: The number of tokens used for the response.
|
|
222
|
+
- **content**: The actual text content generated by the model.
|
|
223
|
+
- **finish_reason**: The reason why the generation was finished (e.g., "stop" or "length").
|
|
224
|
+
|
|
225
|
+
## Testing
|
|
226
|
+
|
|
227
|
+
This project uses `pytest` for testing. Tests are located in the `tests/` directory.
|
|
228
|
+
|
|
229
|
+
### Running Tests
|
|
230
|
+
|
|
231
|
+
To run all tests, use the following command:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
pytest
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Alternatively, you can run the tests using the `tests_runner.py` script:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
python tests/tests_runner.py
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Dependencies
|
|
244
|
+
|
|
245
|
+
Ensure you have the required dependencies installed. You can install them using:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
pip install -r requirements-test.txt
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Test Structure
|
|
252
|
+
|
|
253
|
+
* `unit/`: Contains unit tests for individual components.
|
|
254
|
+
* `integration/`: Contains integration tests to verify the interaction between different parts of the system.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# LLM API Adapter SDK for Python
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This SDK for Python allows you to use LLM APIs from various companies and models through a unified interface. Currently, the project supports API integration for the following companies: OpenAI, Anthropic, and Google. At this stage, only the chat functionality is implemented.
|
|
6
|
+
|
|
7
|
+
### Version
|
|
8
|
+
|
|
9
|
+
Current version: 0.2.0
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Unified Interface**: Work seamlessly with different LLM providers using a single, consistent API.
|
|
14
|
+
- **Multiple Provider Support**: Currently supports OpenAI, Anthropic, and Google APIs, allowing easy switching between them.
|
|
15
|
+
- **Chat Functionality**: Provides an easy way to interact with chat-based LLMs.
|
|
16
|
+
- **Extensible Design**: Built to easily extend support for additional providers and new functionalities in the future.
|
|
17
|
+
- **Error Handling**: Standardized error messages across all supported LLMs, simplifying integration and debugging.
|
|
18
|
+
- **Flexible Configuration**: Manage request parameters like temperature, max tokens, and other settings for fine-tuned control.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
To install the SDK, you can use pip:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install llm_api_adapter
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Note:** You will need to obtain API keys from each LLM provider you wish to use (OpenAI, Anthropic, Google). Refer to their respective documentation for instructions on obtaining API keys.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Getting Started
|
|
32
|
+
|
|
33
|
+
### Importing and Setting Up the Adapter
|
|
34
|
+
|
|
35
|
+
To start using the adapter, you need to import the necessary components:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from llm_api_adapter.models.messages.chat_message import (
|
|
39
|
+
AIMessage, Prompt, UserMessage
|
|
40
|
+
)
|
|
41
|
+
from llm_api_adapter.universal_adapter import UniversalLLMAPIAdapter
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Sending a Simple Request
|
|
45
|
+
|
|
46
|
+
The SDK supports three types of messages for interacting with the LLM:
|
|
47
|
+
|
|
48
|
+
- **Prompt**: Use `Prompt` to set the context or initial prompt for the model.
|
|
49
|
+
- **UserMessage**: Use `UserMessage` to send messages from the user during a conversation.
|
|
50
|
+
- **AIMessage**: Use `AIMessage` to simulate responses from the assistant during a conversation.
|
|
51
|
+
|
|
52
|
+
Here is an example of how to send a simple request to the adapter:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
messages = [
|
|
56
|
+
UserMessage("Hi! Can you explain how artificial intelligence works?")
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
adapter = UniversalLLMAPIAdapter(
|
|
60
|
+
organization="openai",
|
|
61
|
+
model="gpt-3.5-turbo",
|
|
62
|
+
api_key=openai_api_key
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
response = adapter.generate_chat_answer(
|
|
66
|
+
messages=messages,
|
|
67
|
+
max_tokens=max_tokens,
|
|
68
|
+
temperature=temperature,
|
|
69
|
+
top_p=top_p
|
|
70
|
+
)
|
|
71
|
+
print(response.content)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Parameters
|
|
75
|
+
|
|
76
|
+
- **max\_tokens**: The maximum number of tokens to generate in the response. This limits the length of the output. Default value: `256`.
|
|
77
|
+
|
|
78
|
+
- **temperature**: Controls the randomness of the response. Higher values (e.g., 0.8) make the output more random, while lower values (e.g., 0.2) make it more focused and deterministic. Default value: `1.0` (range: 0 to 2).
|
|
79
|
+
|
|
80
|
+
- **top\_p**: Limits the response to a certain cumulative probability. This is used to create more focused and coherent responses by considering only the highest probability options. Default value: `1.0` (range: 0 to 1).
|
|
81
|
+
|
|
82
|
+
## Handling Errors
|
|
83
|
+
|
|
84
|
+
### Common Errors
|
|
85
|
+
|
|
86
|
+
The SDK provides a set of standardized errors for easier debugging and integration:
|
|
87
|
+
|
|
88
|
+
- **LLMAPIError**: Base class for all API-related errors. This error is also used for any unexpected LLM API errors.
|
|
89
|
+
|
|
90
|
+
- **LLMAPIAuthorizationError**: Raised when authentication or authorization fails.
|
|
91
|
+
|
|
92
|
+
- **LLMAPIRateLimitError**: Raised when rate limits are exceeded.
|
|
93
|
+
|
|
94
|
+
- **LLMAPITokenLimitError**: Raised when token limits are exceeded.
|
|
95
|
+
|
|
96
|
+
- **LLMAPIClientError**: Raised when the client makes an invalid request.
|
|
97
|
+
|
|
98
|
+
- **LLMAPIServerError**: Raised when the server encounters an error.
|
|
99
|
+
|
|
100
|
+
- **LLMAPITimeoutError**: Raised when a request times out.
|
|
101
|
+
|
|
102
|
+
- **LLMAPIUsageLimitError**: Raised when usage limits are exceeded.
|
|
103
|
+
|
|
104
|
+
## Configuration and Management
|
|
105
|
+
|
|
106
|
+
### Using Different Providers and Models
|
|
107
|
+
|
|
108
|
+
The SDK allows you to easily switch between LLM providers and specify the model you want to use. Currently supported providers are OpenAI, Anthropic, and Google.
|
|
109
|
+
|
|
110
|
+
- **OpenAI**: You can use models like `gpt-4o-mini`, `gpt-4o`, `gpt-4-turbo`, `gpt-4`, `gpt-4-turbo-preview`, `gpt-3.5-turbo`. Set the `organization` parameter to `openai` and specify the `model` name.
|
|
111
|
+
|
|
112
|
+
- **Anthropic**: Available models include `claude-3-5-sonnet-20241022`, `claude-3-opus-20240229`, `claude-3-sonnet-20240229`, `claude-3-haiku-20240307`. Set the `organization` parameter to `anthropic` and specify the desired `model`.
|
|
113
|
+
|
|
114
|
+
- **Google**: Models such as `gemini-1.5-flash`, `gemini-1.5-flash-8b`, `gemini-1.5-pro` can be used. Set the `organization` parameter to `google` and specify the `model`.
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
adapter = UniversalLLMAPIAdapter(
|
|
120
|
+
organization="openai",
|
|
121
|
+
model="gpt-3.5-turbo",
|
|
122
|
+
api_key=openai_api_key
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
To switch to another provider, simply change the `organization` and `model` parameters.
|
|
127
|
+
|
|
128
|
+
### Switching Providers
|
|
129
|
+
|
|
130
|
+
Here is an example of how to switch between different LLM providers using the SDK:
|
|
131
|
+
|
|
132
|
+
**Note**: Each instance of `UniversalLLMAPIAdapter` is tied to a specific provider and model. You cannot change the `organization` parameter for an existing adapter object. To use a different provider, you must create a new instance.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
gpt = UniversalLLMAPIAdapter(
|
|
136
|
+
organization="openai",
|
|
137
|
+
model="gpt-3.5-turbo",
|
|
138
|
+
api_key=openai_api_key
|
|
139
|
+
)
|
|
140
|
+
gpt_response = gpt.generate_chat_answer(messages=messages)
|
|
141
|
+
print(gpt_response.content)
|
|
142
|
+
|
|
143
|
+
claude = UniversalLLMAPIAdapter(
|
|
144
|
+
organization="anthropic",
|
|
145
|
+
model="claude-3-haiku-20240307",
|
|
146
|
+
api_key=anthropic_api_key
|
|
147
|
+
)
|
|
148
|
+
claude_response = claude.generate_chat_answer(messages=messages)
|
|
149
|
+
print(claude_response.content)
|
|
150
|
+
|
|
151
|
+
google = UniversalLLMAPIAdapter(
|
|
152
|
+
organization="google",
|
|
153
|
+
model="gemini-1.5-flash",
|
|
154
|
+
api_key=google_api_key
|
|
155
|
+
)
|
|
156
|
+
google_response = google.generate_chat_answer(messages=messages)
|
|
157
|
+
print(google_response.content)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Example Use Case
|
|
161
|
+
|
|
162
|
+
Here is a comprehensive example that showcases all possible message types and interactions:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from llm_api_adapter.models.messages.chat_message import (
|
|
166
|
+
AIMessage, Prompt, UserMessage
|
|
167
|
+
)
|
|
168
|
+
from llm_api_adapter.universal_adapter import UniversalLLMAPIAdapter
|
|
169
|
+
|
|
170
|
+
messages = [
|
|
171
|
+
Prompt(
|
|
172
|
+
"You are a friendly assistant who explains complex concepts "
|
|
173
|
+
"in simple terms."
|
|
174
|
+
),
|
|
175
|
+
UserMessage("Hi! Can you explain how artificial intelligence works?"),
|
|
176
|
+
AIMessage(
|
|
177
|
+
"Sure! Artificial intelligence (AI) is a system that can perform "
|
|
178
|
+
"tasks requiring human-like intelligence, such as recognizing images "
|
|
179
|
+
"or understanding language. It learns by analyzing large amounts of "
|
|
180
|
+
"data, finding patterns, and making predictions."
|
|
181
|
+
),
|
|
182
|
+
UserMessage("How does AI learn?"),
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
adapter = UniversalLLMAPIAdapter(
|
|
186
|
+
organization="openai",
|
|
187
|
+
model="gpt-3.5-turbo",
|
|
188
|
+
api_key=openai_api_key
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
response = adapter.generate_chat_answer(
|
|
192
|
+
messages=messages,
|
|
193
|
+
max_tokens=256,
|
|
194
|
+
temperature=1.0,
|
|
195
|
+
top_p=1.0
|
|
196
|
+
)
|
|
197
|
+
print(response.content)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The `ChatResponse` object returned by `generate_chat_answer` includes several attributes that provide additional details about the response. These attributes will contain data only if they are included in the response from the LLM:
|
|
201
|
+
|
|
202
|
+
- **model**: The model that generated the response.
|
|
203
|
+
- **response_id**: A unique identifier for the response.
|
|
204
|
+
- **timestamp**: The time at which the response was generated.
|
|
205
|
+
- **tokens_used**: The number of tokens used for the response.
|
|
206
|
+
- **content**: The actual text content generated by the model.
|
|
207
|
+
- **finish_reason**: The reason why the generation was finished (e.g., "stop" or "length").
|
|
208
|
+
|
|
209
|
+
## Testing
|
|
210
|
+
|
|
211
|
+
This project uses `pytest` for testing. Tests are located in the `tests/` directory.
|
|
212
|
+
|
|
213
|
+
### Running Tests
|
|
214
|
+
|
|
215
|
+
To run all tests, use the following command:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
pytest
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Alternatively, you can run the tests using the `tests_runner.py` script:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
python tests/tests_runner.py
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Dependencies
|
|
228
|
+
|
|
229
|
+
Ensure you have the required dependencies installed. You can install them using:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
pip install -r requirements-test.txt
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Test Structure
|
|
236
|
+
|
|
237
|
+
* `unit/`: Contains unit tests for individual components.
|
|
238
|
+
* `integration/`: Contains integration tests to verify the interaction between different parts of the system.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "llm-api-adapter"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
keywords = ["llm", "adapter", "lightweight", "api"]
|
|
9
|
+
description = "Lightweight, pluggable adapter for multiple LLM APIs (OpenAI, Anthropic, Google)"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
license = {file = "LICENSE"}
|
|
13
|
+
authors = [{name = "Sergey Inozemtsev"}]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.9",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
dependencies = ["requests>=2.32"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/Inozem/llm_api_adapter/"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
|
28
|
+
include = ["llm_api_adapter*"]
|
|
29
|
+
exclude = ["tests*", "*/tests*"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import logging
|
|
3
|
+
from typing import FrozenSet, List, Optional
|
|
4
|
+
|
|
5
|
+
from ..adapters.base_adapter import LLMAdapterBase
|
|
6
|
+
from ..errors.llm_api_error import LLMAPIError
|
|
7
|
+
from ..llms.anthropic.sync_client import ClaudeSyncClient
|
|
8
|
+
from ..models.messages.chat_message import Message, Prompt
|
|
9
|
+
from ..models.responses.chat_response import ChatResponse
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AnthropicAdapter(LLMAdapterBase):
|
|
16
|
+
company: str = "anthropic"
|
|
17
|
+
verified_models: FrozenSet[str] = frozenset([
|
|
18
|
+
"claude-opus-4-20250514",
|
|
19
|
+
"claude-sonnet-4-20250514",
|
|
20
|
+
"claude-3-7-sonnet-latest",
|
|
21
|
+
"claude-3-5-haiku-latest",
|
|
22
|
+
"claude-3-5-sonnet-latest",
|
|
23
|
+
"claude-3-haiku-20240307"
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
def generate_chat_answer(
|
|
27
|
+
self,
|
|
28
|
+
messages: List[Message],
|
|
29
|
+
max_tokens: Optional[int] = 256,
|
|
30
|
+
temperature: float = 1.0,
|
|
31
|
+
top_p: float = 1.0
|
|
32
|
+
) -> ChatResponse:
|
|
33
|
+
temperature = self._validate_parameter(
|
|
34
|
+
name="temperature", value=temperature, min_value=0, max_value=2
|
|
35
|
+
)
|
|
36
|
+
top_p = self._validate_parameter(
|
|
37
|
+
name="top_p", value=top_p, min_value=0, max_value=1
|
|
38
|
+
)
|
|
39
|
+
try:
|
|
40
|
+
system_prompt = ""
|
|
41
|
+
transformed_messages = []
|
|
42
|
+
for msg in messages:
|
|
43
|
+
if isinstance(msg, Prompt):
|
|
44
|
+
system_prompt = msg.content
|
|
45
|
+
else:
|
|
46
|
+
transformed_messages.append({
|
|
47
|
+
"role": msg.role,
|
|
48
|
+
"content": msg.content
|
|
49
|
+
})
|
|
50
|
+
client = ClaudeSyncClient(api_key=self.api_key)
|
|
51
|
+
response = client.chat_completion(
|
|
52
|
+
model=self.model,
|
|
53
|
+
messages=transformed_messages,
|
|
54
|
+
max_tokens=max_tokens,
|
|
55
|
+
temperature=temperature,
|
|
56
|
+
top_p=top_p,
|
|
57
|
+
system=system_prompt,
|
|
58
|
+
)
|
|
59
|
+
return ChatResponse.from_anthropic_response(response)
|
|
60
|
+
except LLMAPIError as e:
|
|
61
|
+
self.handle_error(e, self.company)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
self.handle_error(e, self.company)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import FrozenSet
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
from ..models.responses.chat_response import ChatResponse
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class LLMAdapterBase(ABC):
|
|
13
|
+
model: str
|
|
14
|
+
api_key: str
|
|
15
|
+
verified_models: FrozenSet[str] = field(default_factory=frozenset)
|
|
16
|
+
|
|
17
|
+
def __post_init__(self):
|
|
18
|
+
if len(self.api_key) < 1:
|
|
19
|
+
erroe_message = "api_key must be a non-empty string"
|
|
20
|
+
logger.error(erroe_message)
|
|
21
|
+
raise ValueError(erroe_message)
|
|
22
|
+
if self.model not in self.verified_models:
|
|
23
|
+
warnings.warn(
|
|
24
|
+
(f"Model '{self.model}' is not verified for this adapter. "
|
|
25
|
+
"Continuing with the selected adapter."),
|
|
26
|
+
UserWarning
|
|
27
|
+
)
|
|
28
|
+
logger.warning(f"Unverified model used: {self.model}")
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def generate_chat_answer(self, **kwargs) -> ChatResponse:
|
|
32
|
+
"""
|
|
33
|
+
Generates a response based on the provided conversation.
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def _validate_parameter(
|
|
38
|
+
self, name: str, value: float, min_value: float, max_value: float
|
|
39
|
+
) -> float:
|
|
40
|
+
if not (min_value <= value <= max_value):
|
|
41
|
+
error_message = (f"{name} must be between {min_value} and "
|
|
42
|
+
f"{max_value}, got {value}")
|
|
43
|
+
logger.error(error_message)
|
|
44
|
+
raise ValueError(error_message)
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def handle_error(cls, error: Exception, company: str):
|
|
49
|
+
logger.error(f"Error in company {company}: {error}")
|
|
50
|
+
raise error
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import logging
|
|
3
|
+
from typing import FrozenSet, List, Optional
|
|
4
|
+
|
|
5
|
+
from ..adapters.base_adapter import LLMAdapterBase
|
|
6
|
+
from ..errors.llm_api_error import LLMAPIError
|
|
7
|
+
from ..llms.google.sync_client import GeminiSyncClient
|
|
8
|
+
from ..models.messages.chat_message import Message
|
|
9
|
+
from ..models.responses.chat_response import ChatResponse
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class GoogleAdapter(LLMAdapterBase):
|
|
16
|
+
company: str = "google"
|
|
17
|
+
verified_models: FrozenSet[str] = frozenset([
|
|
18
|
+
"gemini-2.5-pro",
|
|
19
|
+
"gemini-2.5-flash",
|
|
20
|
+
"gemini-2.5-flash-lite",
|
|
21
|
+
"gemini-2.0-flash",
|
|
22
|
+
"gemini-2.0-flash-lite"
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
def generate_chat_answer(
|
|
26
|
+
self,
|
|
27
|
+
messages: List[Message],
|
|
28
|
+
max_tokens: Optional[int] = 256,
|
|
29
|
+
temperature: float = 1.0,
|
|
30
|
+
top_p: float = 1.0
|
|
31
|
+
) -> ChatResponse:
|
|
32
|
+
temperature = self._validate_parameter(
|
|
33
|
+
name="temperature", value=temperature, min_value=0, max_value=2
|
|
34
|
+
)
|
|
35
|
+
top_p = self._validate_parameter(
|
|
36
|
+
name="top_p", value=top_p, min_value=0, max_value=1
|
|
37
|
+
)
|
|
38
|
+
try:
|
|
39
|
+
transformed_messages = []
|
|
40
|
+
prompt = None
|
|
41
|
+
for msg in messages:
|
|
42
|
+
if hasattr(msg, "role") and hasattr(msg, "content"):
|
|
43
|
+
if msg.role == "system" or isinstance(msg, ()):
|
|
44
|
+
pass
|
|
45
|
+
if isinstance(msg, type) and msg == "Prompt":
|
|
46
|
+
prompt = {
|
|
47
|
+
"parts": [{"text": msg.content}]
|
|
48
|
+
}
|
|
49
|
+
else:
|
|
50
|
+
transformed_messages.append({
|
|
51
|
+
"role": "user" if msg.role == "user" else "model",
|
|
52
|
+
"parts": [{"text": msg.content}]
|
|
53
|
+
})
|
|
54
|
+
payload = {
|
|
55
|
+
"contents": transformed_messages,
|
|
56
|
+
**({"system_instruction": prompt} if prompt else {}),
|
|
57
|
+
"generationConfig": {
|
|
58
|
+
"maxOutputTokens": max_tokens,
|
|
59
|
+
"temperature": temperature,
|
|
60
|
+
"topP": top_p,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
client = GeminiSyncClient(self.api_key)
|
|
64
|
+
response_json = client.chat_completion(
|
|
65
|
+
model=self.model,
|
|
66
|
+
**payload
|
|
67
|
+
)
|
|
68
|
+
return ChatResponse.from_google_response(response_json)
|
|
69
|
+
except LLMAPIError as e:
|
|
70
|
+
self.handle_error(e, self.company)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
self.handle_error(e, self.company)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import logging
|
|
3
|
+
from typing import FrozenSet, List, Optional
|
|
4
|
+
|
|
5
|
+
from ..adapters.base_adapter import LLMAdapterBase
|
|
6
|
+
from ..errors.llm_api_error import LLMAPIError
|
|
7
|
+
from ..llms.openai.sync_client import OpenAISyncClient
|
|
8
|
+
from ..models.messages.chat_message import Message
|
|
9
|
+
from ..models.responses.chat_response import ChatResponse
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class OpenAIAdapter(LLMAdapterBase):
|
|
16
|
+
company: str = "openai"
|
|
17
|
+
verified_models: FrozenSet[str] = frozenset([
|
|
18
|
+
"gpt-4.1",
|
|
19
|
+
"gpt-4.1-mini",
|
|
20
|
+
"gpt-4.1-nano",
|
|
21
|
+
"gpt-4.5-preview",
|
|
22
|
+
"gpt-4o",
|
|
23
|
+
"gpt-4o-mini",
|
|
24
|
+
"gpt-4-turbo",
|
|
25
|
+
"gpt-4",
|
|
26
|
+
"gpt-4-turbo-preview",
|
|
27
|
+
"gpt-3.5-turbo",
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
def generate_chat_answer(
|
|
31
|
+
self,
|
|
32
|
+
messages: List[Message],
|
|
33
|
+
max_tokens: Optional[int] = 256,
|
|
34
|
+
temperature: float = 1.0,
|
|
35
|
+
top_p: float = 1.0
|
|
36
|
+
) -> ChatResponse:
|
|
37
|
+
temperature = self._validate_parameter(
|
|
38
|
+
name="temperature", value=temperature, min_value=0, max_value=2
|
|
39
|
+
)
|
|
40
|
+
top_p = self._validate_parameter(
|
|
41
|
+
name="top_p", value=top_p, min_value=0, max_value=1
|
|
42
|
+
)
|
|
43
|
+
try:
|
|
44
|
+
transformed_messages = []
|
|
45
|
+
for msg in messages:
|
|
46
|
+
transformed_messages.append(
|
|
47
|
+
{"role": msg.role, "content": msg.content}
|
|
48
|
+
)
|
|
49
|
+
client = OpenAISyncClient(api_key=self.api_key)
|
|
50
|
+
response = client.chat_completion(
|
|
51
|
+
model=self.model,
|
|
52
|
+
messages=transformed_messages,
|
|
53
|
+
max_tokens=max_tokens,
|
|
54
|
+
temperature=temperature,
|
|
55
|
+
top_p=top_p,
|
|
56
|
+
)
|
|
57
|
+
return ChatResponse.from_openai_response(response)
|
|
58
|
+
except LLMAPIError as e:
|
|
59
|
+
self.handle_error(e, self.company)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
self.handle_error(e, self.company)
|
|
File without changes
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class LLMAPIError(Exception):
|
|
7
|
+
"""Base class for API-related errors."""
|
|
8
|
+
message: str = "An API error occurred."
|
|
9
|
+
detail: Optional[str] = None
|
|
10
|
+
|
|
11
|
+
def __post_init__(self):
|
|
12
|
+
full_message = self.message
|
|
13
|
+
if self.detail:
|
|
14
|
+
full_message = f"{self.message} Detail: {self.detail}"
|
|
15
|
+
super().__init__(full_message)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class LLMAPIAuthorizationError(LLMAPIError):
|
|
20
|
+
"""Raised when authentication or authorization fails."""
|
|
21
|
+
message: str = "Authentication or authorization failed."
|
|
22
|
+
openai_api_errors = ["InvalidAuthenticationError", "AuthenticationError"]
|
|
23
|
+
google_api_errors = ["INVALID_ARGUMENT", "PERMISSION_DENIED"]
|
|
24
|
+
anthropic_api_errors = ["AuthenticationError", "PermissionError"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class LLMAPIRateLimitError(LLMAPIError):
|
|
29
|
+
"""Raised when rate limits are exceeded."""
|
|
30
|
+
message: str = "Rate limit exceeded."
|
|
31
|
+
openai_api_errors = ["RateLimitError"]
|
|
32
|
+
google_api_errors = ["RESOURCE_EXHAUSTED"]
|
|
33
|
+
anthropic_api_errors = ["RateLimitError"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class LLMAPITokenLimitError(LLMAPIError):
|
|
38
|
+
"""Raised when token limits are exceeded."""
|
|
39
|
+
message: str = "Token limit exceeded."
|
|
40
|
+
openai_api_errors = ["MaxTokensExceededError", "TokenLimitError"]
|
|
41
|
+
google_api_errors = []
|
|
42
|
+
anthropic_api_errors = []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class LLMAPIClientError(LLMAPIError):
|
|
47
|
+
"""Raised when the client makes an invalid request."""
|
|
48
|
+
message: str = "Client error occurred."
|
|
49
|
+
openai_api_errors = ["InvalidRequestError", "BadRequestError"]
|
|
50
|
+
google_api_errors = [
|
|
51
|
+
"INVALID_ARGUMENT", "FAILED_PRECONDITION", "NOT_FOUND"
|
|
52
|
+
]
|
|
53
|
+
anthropic_api_errors = [
|
|
54
|
+
"InvalidRequestError", "RequestTooLargeError", "NotFoundError"
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class LLMAPIServerError(LLMAPIError):
|
|
60
|
+
"""Raised when the server encounters an error."""
|
|
61
|
+
message: str = "Server error occurred."
|
|
62
|
+
openai_api_errors = ["InternalServerError", "ServiceUnavailableError"]
|
|
63
|
+
google_api_errors = ["INTERNAL", "UNAVAILABLE"]
|
|
64
|
+
anthropic_api_errors = ["APIError", "OverloadedError"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class LLMAPITimeoutError(LLMAPIError):
|
|
69
|
+
"""Raised when a request times out."""
|
|
70
|
+
message: str = "Request timed out."
|
|
71
|
+
openai_api_errors = ["TimeoutError"]
|
|
72
|
+
google_api_errors = ["DEADLINE_EXCEEDED"]
|
|
73
|
+
anthropic_api_errors = []
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class LLMAPIUsageLimitError(LLMAPIError):
|
|
78
|
+
"""Raised when usage limits are exceeded."""
|
|
79
|
+
message: str = "Usage limit exceeded."
|
|
80
|
+
openai_api_errors = ["UsageLimitError", "QuotaExceededError"]
|
|
81
|
+
google_api_errors = []
|
|
82
|
+
anthropic_api_errors = []
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from ...errors.llm_api_error import (
|
|
7
|
+
LLMAPIAuthorizationError,
|
|
8
|
+
LLMAPIRateLimitError,
|
|
9
|
+
LLMAPITokenLimitError,
|
|
10
|
+
LLMAPIClientError,
|
|
11
|
+
LLMAPIServerError,
|
|
12
|
+
LLMAPITimeoutError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ClaudeSyncClient:
|
|
19
|
+
api_key: str
|
|
20
|
+
endpoint: str = "https://api.anthropic.com/v1"
|
|
21
|
+
api_version: str = "2023-06-01"
|
|
22
|
+
|
|
23
|
+
def _headers(self):
|
|
24
|
+
return {
|
|
25
|
+
"x-api-key": self.api_key,
|
|
26
|
+
"anthropic-version": self.api_version,
|
|
27
|
+
"Content-Type": "application/json"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def chat_completion(self, model: str, **kwargs):
|
|
31
|
+
url = f"{self.endpoint}/messages"
|
|
32
|
+
payload = {"model": model, **kwargs}
|
|
33
|
+
response = self._send_request(url, payload)
|
|
34
|
+
return response.json()
|
|
35
|
+
|
|
36
|
+
def _send_request(self, url, payload):
|
|
37
|
+
try:
|
|
38
|
+
response = requests.post(
|
|
39
|
+
url, headers=self._headers(), json=payload
|
|
40
|
+
)
|
|
41
|
+
response.raise_for_status()
|
|
42
|
+
except requests.exceptions.Timeout as e:
|
|
43
|
+
logger.error(f"Request timed out: {e}")
|
|
44
|
+
raise LLMAPITimeoutError(detail=str(e))
|
|
45
|
+
except requests.exceptions.HTTPError as http_err:
|
|
46
|
+
logger.error(f"HTTP error occurred: {http_err}")
|
|
47
|
+
self._handle_http_error(http_err)
|
|
48
|
+
except requests.exceptions.RequestException as e:
|
|
49
|
+
logger.error(f"Request exception: {e}")
|
|
50
|
+
raise LLMAPIClientError(detail=str(e))
|
|
51
|
+
return response
|
|
52
|
+
|
|
53
|
+
def _handle_http_error(self, http_err):
|
|
54
|
+
status_code = http_err.response.status_code
|
|
55
|
+
try:
|
|
56
|
+
error_json = http_err.response.json()
|
|
57
|
+
error_type = error_json.get("type")
|
|
58
|
+
error_message = error_json.get("message")
|
|
59
|
+
except Exception:
|
|
60
|
+
error_type = None
|
|
61
|
+
error_message = None
|
|
62
|
+
detail = error_message or str(http_err)
|
|
63
|
+
error_map = {
|
|
64
|
+
401: LLMAPIAuthorizationError,
|
|
65
|
+
429: LLMAPIRateLimitError,
|
|
66
|
+
}
|
|
67
|
+
if status_code in error_map:
|
|
68
|
+
raise error_map[status_code](detail=detail)
|
|
69
|
+
elif error_type in LLMAPIAuthorizationError.anthropic_api_errors:
|
|
70
|
+
raise LLMAPIAuthorizationError(detail=detail)
|
|
71
|
+
elif error_type in LLMAPIRateLimitError.anthropic_api_errors:
|
|
72
|
+
raise LLMAPIRateLimitError(detail=detail)
|
|
73
|
+
elif error_type in LLMAPITokenLimitError.anthropic_api_errors:
|
|
74
|
+
raise LLMAPITokenLimitError(detail=detail)
|
|
75
|
+
elif 400 <= status_code < 500:
|
|
76
|
+
raise LLMAPIClientError(detail=detail)
|
|
77
|
+
elif 500 <= status_code < 600:
|
|
78
|
+
raise LLMAPIServerError(detail=detail)
|
|
79
|
+
else:
|
|
80
|
+
raise LLMAPIClientError(detail=detail)
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from ...errors.llm_api_error import (
|
|
7
|
+
LLMAPIAuthorizationError,
|
|
8
|
+
LLMAPIRateLimitError,
|
|
9
|
+
LLMAPIClientError,
|
|
10
|
+
LLMAPIServerError,
|
|
11
|
+
LLMAPITimeoutError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class GeminiSyncClient:
|
|
18
|
+
api_key: str
|
|
19
|
+
endpoint: str = "https://generativelanguage.googleapis.com/v1beta"
|
|
20
|
+
|
|
21
|
+
def _headers(self):
|
|
22
|
+
return {
|
|
23
|
+
"x-goog-api-key": self.api_key,
|
|
24
|
+
"Content-Type": "application/json"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def chat_completion(self, model: str, **kwargs):
|
|
28
|
+
url = f"{self.endpoint}/models/{model}:generateContent"
|
|
29
|
+
payload = {"model": model, **kwargs}
|
|
30
|
+
response = self._send_request(url, payload)
|
|
31
|
+
return response.json()
|
|
32
|
+
|
|
33
|
+
def _send_request(self, url, payload):
|
|
34
|
+
try:
|
|
35
|
+
response = requests.post(
|
|
36
|
+
url, headers=self._headers(), json=payload, timeout=30
|
|
37
|
+
)
|
|
38
|
+
response.raise_for_status()
|
|
39
|
+
except requests.exceptions.Timeout as e:
|
|
40
|
+
logger.error(f"Request timeout: {e}")
|
|
41
|
+
raise LLMAPITimeoutError(detail=str(e))
|
|
42
|
+
except requests.exceptions.HTTPError as http_err:
|
|
43
|
+
logger.error(f"HTTP error occurred: {http_err}")
|
|
44
|
+
self._handle_http_error(http_err)
|
|
45
|
+
except requests.exceptions.RequestException as e:
|
|
46
|
+
logger.error(f"Request exception: {e}")
|
|
47
|
+
raise LLMAPIClientError(detail=str(e))
|
|
48
|
+
return response
|
|
49
|
+
|
|
50
|
+
def _handle_http_error(self, http_err):
|
|
51
|
+
status_code = http_err.response.status_code
|
|
52
|
+
try:
|
|
53
|
+
error_json = http_err.response.json()
|
|
54
|
+
error_status = error_json.get("error", {}).get("status", "")
|
|
55
|
+
error_message = error_json.get("error", {}).get("message")
|
|
56
|
+
except Exception:
|
|
57
|
+
logger.warning("Failed to parse error response JSON", exc_info=True)
|
|
58
|
+
error_status = ""
|
|
59
|
+
error_message = None
|
|
60
|
+
detail = error_message or str(http_err)
|
|
61
|
+
error_map = {
|
|
62
|
+
401: LLMAPIAuthorizationError,
|
|
63
|
+
429: LLMAPIRateLimitError,
|
|
64
|
+
}
|
|
65
|
+
if status_code in error_map:
|
|
66
|
+
raise error_map[status_code](detail=detail)
|
|
67
|
+
elif error_status in LLMAPIAuthorizationError.google_api_errors:
|
|
68
|
+
raise LLMAPIAuthorizationError(detail=detail)
|
|
69
|
+
elif error_status in LLMAPIRateLimitError.google_api_errors:
|
|
70
|
+
raise LLMAPIRateLimitError(detail=detail)
|
|
71
|
+
elif 400 <= status_code < 500:
|
|
72
|
+
raise LLMAPIClientError(detail=detail)
|
|
73
|
+
elif (
|
|
74
|
+
500 <= status_code < 600
|
|
75
|
+
or error_status in LLMAPIServerError.google_api_errors
|
|
76
|
+
):
|
|
77
|
+
raise LLMAPIServerError(detail=detail)
|
|
78
|
+
else:
|
|
79
|
+
raise LLMAPIClientError(detail=detail)
|
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from ...errors.llm_api_error import (
|
|
7
|
+
LLMAPIAuthorizationError,
|
|
8
|
+
LLMAPIRateLimitError,
|
|
9
|
+
LLMAPITokenLimitError,
|
|
10
|
+
LLMAPIClientError,
|
|
11
|
+
LLMAPIServerError,
|
|
12
|
+
LLMAPITimeoutError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class OpenAISyncClient:
|
|
20
|
+
api_key: str
|
|
21
|
+
endpoint: str = "https://api.openai.com/v1"
|
|
22
|
+
|
|
23
|
+
def _headers(self):
|
|
24
|
+
return {
|
|
25
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
26
|
+
"Content-Type": "application/json"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def chat_completion(self, model: str, **kwargs):
|
|
30
|
+
url = f"{self.endpoint}/chat/completions"
|
|
31
|
+
payload = {"model": model, **kwargs}
|
|
32
|
+
response = self._send_request(url, payload)
|
|
33
|
+
return response.json()
|
|
34
|
+
|
|
35
|
+
def _send_request(self, url, payload):
|
|
36
|
+
try:
|
|
37
|
+
response = requests.post(
|
|
38
|
+
url, headers=self._headers(), json=payload
|
|
39
|
+
)
|
|
40
|
+
response.raise_for_status()
|
|
41
|
+
except requests.exceptions.Timeout as e:
|
|
42
|
+
logger.error("Timeout error: %s", e)
|
|
43
|
+
raise LLMAPITimeoutError(detail=str(e))
|
|
44
|
+
except requests.exceptions.HTTPError as http_err:
|
|
45
|
+
logger.error("HTTP error: %s", http_err)
|
|
46
|
+
self._handle_http_error(http_err)
|
|
47
|
+
except requests.exceptions.RequestException as e:
|
|
48
|
+
logger.error("Request exception: %s", e)
|
|
49
|
+
raise LLMAPIClientError(detail=str(e))
|
|
50
|
+
return response
|
|
51
|
+
|
|
52
|
+
def _handle_http_error(self, http_err):
|
|
53
|
+
status_code = http_err.response.status_code
|
|
54
|
+
try:
|
|
55
|
+
error_json = http_err.response.json()
|
|
56
|
+
error = error_json.get("error", {})
|
|
57
|
+
error_type = error.get("type") or error.get("code")
|
|
58
|
+
error_message = error.get("message")
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.warning("Failed to parse error response: %s", e)
|
|
61
|
+
error_type = None
|
|
62
|
+
error_message = None
|
|
63
|
+
detail = error_message or str(http_err)
|
|
64
|
+
error_map = {
|
|
65
|
+
401: LLMAPIAuthorizationError,
|
|
66
|
+
429: LLMAPIRateLimitError,
|
|
67
|
+
}
|
|
68
|
+
if status_code in error_map:
|
|
69
|
+
raise error_map[status_code](detail=detail)
|
|
70
|
+
elif error_type in LLMAPIAuthorizationError.openai_api_errors:
|
|
71
|
+
raise LLMAPIAuthorizationError(detail=detail)
|
|
72
|
+
elif error_type in LLMAPIRateLimitError.openai_api_errors:
|
|
73
|
+
raise LLMAPIRateLimitError(detail=detail)
|
|
74
|
+
elif error_type in LLMAPITokenLimitError.openai_api_errors:
|
|
75
|
+
raise LLMAPITokenLimitError(detail=detail)
|
|
76
|
+
elif 400 <= status_code < 500:
|
|
77
|
+
raise LLMAPIClientError(detail=detail)
|
|
78
|
+
elif 500 <= status_code < 600:
|
|
79
|
+
raise LLMAPIServerError(detail=detail)
|
|
80
|
+
else:
|
|
81
|
+
raise LLMAPIClientError(detail=detail)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Message:
|
|
6
|
+
content: str
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Prompt(Message):
|
|
11
|
+
role: str = field(default="system", init=False)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class UserMessage(Message):
|
|
16
|
+
role: str = field(default="user", init=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class AIMessage(Message):
|
|
21
|
+
role: str = field(default="assistant", init=False)
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class ChatResponse:
|
|
7
|
+
model: Optional[str] = None
|
|
8
|
+
response_id: Optional[str] = None
|
|
9
|
+
timestamp: Optional[int] = None
|
|
10
|
+
tokens_used: Optional[int] = None
|
|
11
|
+
content: str = field(default_factory=str)
|
|
12
|
+
finish_reason: Optional[str] = None
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def from_openai_response(cls, api_response: dict) -> "ChatResponse":
|
|
16
|
+
return cls(
|
|
17
|
+
model=api_response.get("model"),
|
|
18
|
+
response_id=api_response.get("id"),
|
|
19
|
+
timestamp=api_response.get("created"),
|
|
20
|
+
tokens_used=api_response.get("usage", {}).get("total_tokens"),
|
|
21
|
+
content=api_response["choices"][0]["message"]["content"],
|
|
22
|
+
finish_reason=api_response["choices"][0].get("finish_reason")
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_anthropic_response(cls, api_response: dict) -> "ChatResponse":
|
|
27
|
+
print(f"API response {api_response}")
|
|
28
|
+
usage = api_response.get("usage")
|
|
29
|
+
tokens_used = usage.get("input_tokens") + usage.get("output_tokens")
|
|
30
|
+
return cls(
|
|
31
|
+
model=api_response.get("model"),
|
|
32
|
+
response_id=api_response.get("id"),
|
|
33
|
+
tokens_used=tokens_used,
|
|
34
|
+
content=api_response.get("content")[0].get("text"),
|
|
35
|
+
finish_reason=api_response.get("stop_reason")
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_google_response(cls, api_response: dict) -> "ChatResponse":
|
|
40
|
+
first_candidate = api_response["candidates"][0]
|
|
41
|
+
return cls(
|
|
42
|
+
tokens_used=api_response["usageMetadata"]["totalTokenCount"],
|
|
43
|
+
content=first_candidate["content"]["parts"][0]["text"],
|
|
44
|
+
finish_reason=str(first_candidate["finishReason"])
|
|
45
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from dataclasses import dataclass, fields
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .adapters.base_adapter import LLMAdapterBase
|
|
6
|
+
from .adapters.anthropic_adapter import AnthropicAdapter
|
|
7
|
+
from .adapters.openai_adapter import OpenAIAdapter
|
|
8
|
+
from .adapters.google_adapter import GoogleAdapter
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class UniversalLLMAPIAdapter:
|
|
15
|
+
organization: str
|
|
16
|
+
model: str
|
|
17
|
+
api_key: str
|
|
18
|
+
|
|
19
|
+
def __post_init__(self) -> None:
|
|
20
|
+
if not self.organization or not isinstance(self.organization, str):
|
|
21
|
+
raise ValueError("Invalid organization")
|
|
22
|
+
if not self.model or not isinstance(self.model, str):
|
|
23
|
+
raise ValueError("Invalid model")
|
|
24
|
+
if not self.api_key or not isinstance(self.api_key, str):
|
|
25
|
+
raise ValueError("Invalid API key")
|
|
26
|
+
self.adapter = self._select_adapter(self.organization, self.model,
|
|
27
|
+
self.api_key)
|
|
28
|
+
|
|
29
|
+
def _select_adapter(
|
|
30
|
+
self, organization: str, model: str, api_key: str
|
|
31
|
+
) -> LLMAdapterBase:
|
|
32
|
+
"""
|
|
33
|
+
Selects the adapter based on the company.
|
|
34
|
+
"""
|
|
35
|
+
for adapter_class in LLMAdapterBase.__subclasses__():
|
|
36
|
+
company_field = None
|
|
37
|
+
for field in fields(adapter_class):
|
|
38
|
+
if field.name == 'company':
|
|
39
|
+
company_field = field
|
|
40
|
+
break
|
|
41
|
+
if company_field and company_field.default == organization:
|
|
42
|
+
return adapter_class(model=model, api_key=api_key)
|
|
43
|
+
error_message = f"Unsupported organization: {organization}"
|
|
44
|
+
logger.error(error_message)
|
|
45
|
+
raise ValueError(error_message)
|
|
46
|
+
|
|
47
|
+
def __getattr__(self, name: str) -> Any:
|
|
48
|
+
"""
|
|
49
|
+
Redirects method calls to the selected adapter.
|
|
50
|
+
"""
|
|
51
|
+
if hasattr(self.adapter, name):
|
|
52
|
+
return getattr(self.adapter, name)
|
|
53
|
+
raise AttributeError(
|
|
54
|
+
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
|
55
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/llm_api_adapter/__init__.py
|
|
5
|
+
src/llm_api_adapter/universal_adapter.py
|
|
6
|
+
src/llm_api_adapter/adapters/__init__.py
|
|
7
|
+
src/llm_api_adapter/adapters/anthropic_adapter.py
|
|
8
|
+
src/llm_api_adapter/adapters/base_adapter.py
|
|
9
|
+
src/llm_api_adapter/adapters/google_adapter.py
|
|
10
|
+
src/llm_api_adapter/adapters/openai_adapter.py
|
|
11
|
+
src/llm_api_adapter/errors/__init__.py
|
|
12
|
+
src/llm_api_adapter/errors/llm_api_error.py
|
|
13
|
+
src/llm_api_adapter/llms/__init__.py
|
|
14
|
+
src/llm_api_adapter/llms/anthropic/__init__.py
|
|
15
|
+
src/llm_api_adapter/llms/anthropic/sync_client.py
|
|
16
|
+
src/llm_api_adapter/llms/google/__init__.py
|
|
17
|
+
src/llm_api_adapter/llms/google/sync_client.py
|
|
18
|
+
src/llm_api_adapter/llms/openai/__init__.py
|
|
19
|
+
src/llm_api_adapter/llms/openai/sync_client.py
|
|
20
|
+
src/llm_api_adapter/models/__init__.py
|
|
21
|
+
src/llm_api_adapter/models/messages/__init__.py
|
|
22
|
+
src/llm_api_adapter/models/messages/chat_message.py
|
|
23
|
+
src/llm_api_adapter/models/responses/__init__.py
|
|
24
|
+
src/llm_api_adapter/models/responses/chat_response.py
|
|
25
|
+
tests/tests_runner.py
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
if __name__ == "__main__":
|
|
4
|
+
unit_result = pytest.main([
|
|
5
|
+
"-v",
|
|
6
|
+
"--rootdir=.",
|
|
7
|
+
"--pyargs",
|
|
8
|
+
"unit"
|
|
9
|
+
])
|
|
10
|
+
if unit_result == 0:
|
|
11
|
+
integ_result = pytest.main([
|
|
12
|
+
"-v",
|
|
13
|
+
"--rootdir=.",
|
|
14
|
+
"--pyargs",
|
|
15
|
+
"integration"
|
|
16
|
+
])
|
|
17
|
+
exit(integ_result)
|
|
18
|
+
else:
|
|
19
|
+
exit(unit_result)
|