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.
Files changed (28) hide show
  1. llm_api_adapter-0.2.0/MANIFEST.in +9 -0
  2. llm_api_adapter-0.2.0/PKG-INFO +254 -0
  3. llm_api_adapter-0.2.0/README.md +238 -0
  4. llm_api_adapter-0.2.0/pyproject.toml +29 -0
  5. llm_api_adapter-0.2.0/setup.cfg +4 -0
  6. llm_api_adapter-0.2.0/src/llm_api_adapter/__init__.py +0 -0
  7. llm_api_adapter-0.2.0/src/llm_api_adapter/adapters/__init__.py +0 -0
  8. llm_api_adapter-0.2.0/src/llm_api_adapter/adapters/anthropic_adapter.py +63 -0
  9. llm_api_adapter-0.2.0/src/llm_api_adapter/adapters/base_adapter.py +50 -0
  10. llm_api_adapter-0.2.0/src/llm_api_adapter/adapters/google_adapter.py +72 -0
  11. llm_api_adapter-0.2.0/src/llm_api_adapter/adapters/openai_adapter.py +61 -0
  12. llm_api_adapter-0.2.0/src/llm_api_adapter/errors/__init__.py +0 -0
  13. llm_api_adapter-0.2.0/src/llm_api_adapter/errors/llm_api_error.py +82 -0
  14. llm_api_adapter-0.2.0/src/llm_api_adapter/llms/__init__.py +0 -0
  15. llm_api_adapter-0.2.0/src/llm_api_adapter/llms/anthropic/__init__.py +0 -0
  16. llm_api_adapter-0.2.0/src/llm_api_adapter/llms/anthropic/sync_client.py +80 -0
  17. llm_api_adapter-0.2.0/src/llm_api_adapter/llms/google/__init__.py +0 -0
  18. llm_api_adapter-0.2.0/src/llm_api_adapter/llms/google/sync_client.py +79 -0
  19. llm_api_adapter-0.2.0/src/llm_api_adapter/llms/openai/__init__.py +0 -0
  20. llm_api_adapter-0.2.0/src/llm_api_adapter/llms/openai/sync_client.py +81 -0
  21. llm_api_adapter-0.2.0/src/llm_api_adapter/models/__init__.py +0 -0
  22. llm_api_adapter-0.2.0/src/llm_api_adapter/models/messages/__init__.py +0 -0
  23. llm_api_adapter-0.2.0/src/llm_api_adapter/models/messages/chat_message.py +21 -0
  24. llm_api_adapter-0.2.0/src/llm_api_adapter/models/responses/__init__.py +0 -0
  25. llm_api_adapter-0.2.0/src/llm_api_adapter/models/responses/chat_response.py +45 -0
  26. llm_api_adapter-0.2.0/src/llm_api_adapter/universal_adapter.py +55 -0
  27. llm_api_adapter-0.2.0/src/llm_api_adapter.egg-info/SOURCES.txt +25 -0
  28. llm_api_adapter-0.2.0/tests/tests_runner.py +19 -0
@@ -0,0 +1,9 @@
1
+ # Exclude compiled python files and caches
2
+ global-exclude *.py[cod]
3
+ prune **/__pycache__
4
+ prune **/.pytest_cache
5
+
6
+ # Exclude macOS metadata and other common artifacts
7
+ global-exclude .DS_Store
8
+ global-exclude *.egg-info
9
+ prune **/*.egg-info
@@ -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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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)
@@ -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 = []
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)