ccs-llmconnector 1.1.2__py3-none-any.whl → 1.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccs-llmconnector
3
- Version: 1.1.2
3
+ Version: 1.1.4
4
4
  Summary: Lightweight wrapper around different LLM provider Python SDK Responses APIs.
5
5
  Author: CCS
6
6
  License: MIT
@@ -0,0 +1,16 @@
1
+ ccs_llmconnector-1.1.4.dist-info/licenses/LICENSE,sha256=rPcz2YmBB9VUWZTLJcRO_B4jKDpqmGRYi2eSI-unysg,1083
2
+ llmconnector/__init__.py,sha256=A6dO9r2E5uVFEgCPPK8F8aRqWoVhUf_v6-T0ro2KHyE,1399
3
+ llmconnector/anthropic_client.py,sha256=nR7gZJ5fa_cJ334SkeNsBuwTkEAF0pF4C0ew-VuhSRY,12995
4
+ llmconnector/client.py,sha256=3rNnHnIoYl2R9kiaZkHHqyf2gODvqwsPSFkhzLOOlTM,23141
5
+ llmconnector/client_cli.py,sha256=8-C275ah4VrYW1noiPr78p8BB-rf7utiMFYMbFLuUVc,11421
6
+ llmconnector/gemini_client.py,sha256=XPjeCnXLkmAPrhqw0GOsK9qGX7j6JYJOCiIQNxa8FJY,31484
7
+ llmconnector/grok_client.py,sha256=oHqY6ooeuCSAGhkPpUprAzbXxmdLlnI-Gk8XDCvWW_0,10986
8
+ llmconnector/openai_client.py,sha256=wIx-yUmbjg60jRHbJTjid9qr6O-d-L9ghPKKTOOHIv0,16266
9
+ llmconnector/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
10
+ llmconnector/types.py,sha256=7YvUHFY-pYAKL2YlwVJh0_Qe_SQNHDdV3TlaLzJe9oY,1644
11
+ llmconnector/utils.py,sha256=iUzjnSINn5cX2Jq3E0CIjMUR_yBLblHCvCcB8NuXUiM,1970
12
+ ccs_llmconnector-1.1.4.dist-info/METADATA,sha256=6ZzmSaWS4CmnsvjmA-ZKF704NDRlpMsF_7gniHIk6tc,17001
13
+ ccs_llmconnector-1.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
+ ccs_llmconnector-1.1.4.dist-info/entry_points.txt,sha256=eFvLY3nHAG_QhaKlemhhK7echfezW0KiMdSNMZOStLc,60
15
+ ccs_llmconnector-1.1.4.dist-info/top_level.txt,sha256=Doer7TAUsN8UXQfPHPNsuBXVNCz2uV-Q0v4t4fwv_MM,13
16
+ ccs_llmconnector-1.1.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
llmconnector/__init__.py CHANGED
@@ -2,27 +2,29 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any
6
-
7
- from .client import LLMClient
8
- from .types import ImageInput, Message, MessageSequence
9
-
10
- if TYPE_CHECKING:
11
- from .anthropic_client import AnthropicClient
12
- from .gemini_client import GeminiClient
13
- from .grok_client import GrokClient
14
- from .openai_client import OpenAIResponsesClient
15
-
16
- __all__ = [
17
- "LLMClient",
18
- "OpenAIResponsesClient",
19
- "GeminiClient",
20
- "AnthropicClient",
21
- "GrokClient",
22
- "ImageInput",
23
- "Message",
24
- "MessageSequence",
25
- ]
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from .client import LLMClient
8
+ from .types import ImageInput, LLMResponse, Message, MessageSequence, TokenUsage
9
+
10
+ if TYPE_CHECKING:
11
+ from .anthropic_client import AnthropicClient
12
+ from .gemini_client import GeminiClient
13
+ from .grok_client import GrokClient
14
+ from .openai_client import OpenAIResponsesClient
15
+
16
+ __all__ = [
17
+ "LLMClient",
18
+ "OpenAIResponsesClient",
19
+ "GeminiClient",
20
+ "AnthropicClient",
21
+ "GrokClient",
22
+ "LLMResponse",
23
+ "ImageInput",
24
+ "Message",
25
+ "MessageSequence",
26
+ "TokenUsage",
27
+ ]
26
28
 
27
29
 
28
30
  def __getattr__(name: str) -> Any:
@@ -2,53 +2,53 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import base64
6
- import mimetypes
7
- from pathlib import Path
8
- import logging
9
- from typing import Optional, Sequence
10
- from urllib.request import urlopen
11
-
12
- from anthropic import APIError, Anthropic
13
-
14
- from .types import ImageInput, MessageSequence, normalize_messages
15
- from .utils import clamp_retries, run_sync_in_thread, run_with_retries
16
-
17
- logger = logging.getLogger(__name__)
5
+ import base64
6
+ import mimetypes
7
+ from pathlib import Path
8
+ import logging
9
+ from typing import Optional, Sequence
10
+ from urllib.request import urlopen
11
+
12
+ from anthropic import APIError, Anthropic
13
+
14
+ from .types import ImageInput, MessageSequence, normalize_messages
15
+ from .utils import clamp_retries, run_sync_in_thread, run_with_retries
16
+
17
+ logger = logging.getLogger(__name__)
18
18
 
19
19
 
20
20
  class AnthropicClient:
21
21
  """Convenience wrapper around the Anthropic Messages API."""
22
22
 
23
- def generate_response(
24
- self,
25
- *,
26
- api_key: str,
27
- prompt: Optional[str] = None,
28
- model: str,
29
- max_tokens: int = 32000,
30
- reasoning_effort: Optional[str] = None,
31
- images: Optional[Sequence[ImageInput]] = None,
32
- messages: Optional[MessageSequence] = None,
33
- request_id: Optional[str] = None,
34
- timeout_s: Optional[float] = None,
35
- max_retries: Optional[int] = None,
36
- retry_backoff_s: float = 0.5,
37
- ) -> str:
23
+ def generate_response(
24
+ self,
25
+ *,
26
+ api_key: str,
27
+ prompt: Optional[str] = None,
28
+ model: str,
29
+ max_tokens: int = 32000,
30
+ reasoning_effort: Optional[str] = None,
31
+ images: Optional[Sequence[ImageInput]] = None,
32
+ messages: Optional[MessageSequence] = None,
33
+ request_id: Optional[str] = None,
34
+ timeout_s: Optional[float] = None,
35
+ max_retries: Optional[int] = None,
36
+ retry_backoff_s: float = 0.5,
37
+ ) -> str:
38
38
  """Generate a response from the specified Anthropic model.
39
39
 
40
40
  Args:
41
41
  api_key: API key used to authenticate with Anthropic.
42
- prompt: Natural-language instruction or query for the model.
43
- model: Identifier of the Anthropic model to target (for example, ``"claude-3-5-sonnet-20241022"``).
44
- max_tokens: Cap for tokens across the entire exchange, defaults to 32000.
45
- reasoning_effort: Included for API parity; currently unused by the Anthropic SDK.
46
- images: Optional collection of image references (local paths, URLs, or data URLs).
47
- messages: Optional list of chat-style messages (role/content).
48
- request_id: Optional request identifier for tracing/logging.
49
- timeout_s: Optional request timeout in seconds.
50
- max_retries: Optional retry count for transient failures.
51
- retry_backoff_s: Base delay (seconds) for exponential backoff between retries.
42
+ prompt: Natural-language instruction or query for the model.
43
+ model: Identifier of the Anthropic model to target (for example, ``"claude-3-5-sonnet-20241022"``).
44
+ max_tokens: Cap for tokens across the entire exchange, defaults to 32000.
45
+ reasoning_effort: Included for API parity; currently unused by the Anthropic SDK.
46
+ images: Optional collection of image references (local paths, URLs, or data URLs).
47
+ messages: Optional list of chat-style messages (role/content).
48
+ request_id: Optional request identifier for tracing/logging.
49
+ timeout_s: Optional request timeout in seconds.
50
+ max_retries: Optional retry count for transient failures.
51
+ retry_backoff_s: Base delay (seconds) for exponential backoff between retries.
52
52
 
53
53
  Returns:
54
54
  The text output produced by the model.
@@ -57,240 +57,240 @@ class AnthropicClient:
57
57
  ValueError: If required arguments are missing or the request payload is empty.
58
58
  URLError: If an image URL cannot be retrieved.
59
59
  APIError: If the underlying Anthropic request fails.
60
- """
61
- if not api_key:
62
- raise ValueError("api_key must be provided.")
63
- if not prompt and not messages and not images:
64
- raise ValueError("At least one of prompt, messages, or images must be provided.")
65
- if not model:
66
- raise ValueError("model must be provided.")
67
-
68
- normalized_messages = normalize_messages(prompt=prompt, messages=messages)
69
- message_payloads: list[dict] = []
70
- for message in normalized_messages:
71
- blocks: list[dict] = []
72
- if message["content"]:
73
- blocks.append({"type": "text", "text": message["content"]})
74
- message_payloads.append({"role": message["role"], "content": blocks})
75
-
76
- if images:
77
- image_blocks = [self._to_image_block(image) for image in images]
78
- target_index = next(
79
- (
80
- index
81
- for index in range(len(message_payloads) - 1, -1, -1)
82
- if message_payloads[index]["role"] == "user"
83
- ),
84
- None,
85
- )
86
- if target_index is None:
87
- message_payloads.append({"role": "user", "content": image_blocks})
88
- else:
89
- message_payloads[target_index]["content"].extend(image_blocks)
90
-
91
- if not message_payloads or not any(msg["content"] for msg in message_payloads):
92
- raise ValueError("No content provided for response generation.")
93
-
94
- retry_count = clamp_retries(max_retries)
95
-
96
- def _run_request() -> str:
97
- client_kwargs = {"api_key": api_key}
98
- if timeout_s is not None:
99
- client_kwargs["timeout"] = timeout_s
100
- client = Anthropic(**client_kwargs)
101
-
102
- try:
103
- response = client.messages.create(
104
- model=model,
105
- max_tokens=max_tokens,
106
- messages=message_payloads,
107
- )
108
- except Exception as exc:
109
- logger.exception(
110
- "Anthropic messages.create failed: %s request_id=%s",
111
- exc,
112
- request_id,
113
- )
114
- raise
115
-
116
- text_blocks: list[str] = []
117
- for block in getattr(response, "content", []) or []:
118
- if getattr(block, "type", None) == "text":
119
- text = getattr(block, "text", None)
120
- if text:
121
- text_blocks.append(text)
122
-
123
- if text_blocks:
124
- result_text = "".join(text_blocks)
125
- logger.info(
126
- "Anthropic messages.create succeeded: model=%s images=%d text_len=%d request_id=%s",
127
- model,
128
- len(images or []),
129
- len(result_text or ""),
130
- request_id,
131
- )
132
- return result_text
133
-
134
- # Treat successful calls without textual content as a successful, empty response
135
- # rather than raising. This aligns with callers that handle empty outputs gracefully.
136
- logger.info(
137
- "Anthropic messages.create succeeded with no text: model=%s images=%d request_id=%s",
138
- model,
139
- len(images or []),
140
- request_id,
141
- )
142
- return ""
143
-
144
- return run_with_retries(
145
- func=_run_request,
146
- max_retries=retry_count,
147
- retry_backoff_s=retry_backoff_s,
148
- request_id=request_id,
149
- )
150
-
151
- async def async_generate_response(
152
- self,
153
- *,
154
- api_key: str,
155
- prompt: Optional[str] = None,
156
- model: str,
157
- max_tokens: int = 32000,
158
- reasoning_effort: Optional[str] = None,
159
- images: Optional[Sequence[ImageInput]] = None,
160
- messages: Optional[MessageSequence] = None,
161
- request_id: Optional[str] = None,
162
- timeout_s: Optional[float] = None,
163
- max_retries: Optional[int] = None,
164
- retry_backoff_s: float = 0.5,
165
- ) -> str:
166
- return await run_sync_in_thread(
167
- lambda: self.generate_response(
168
- api_key=api_key,
169
- prompt=prompt,
170
- model=model,
171
- max_tokens=max_tokens,
172
- reasoning_effort=reasoning_effort,
173
- images=images,
174
- messages=messages,
175
- request_id=request_id,
176
- timeout_s=timeout_s,
177
- max_retries=max_retries,
178
- retry_backoff_s=retry_backoff_s,
179
- )
180
- )
181
-
182
- def generate_image(
183
- self,
184
- *,
185
- api_key: str,
186
- prompt: str,
60
+ """
61
+ if not api_key:
62
+ raise ValueError("api_key must be provided.")
63
+ if not prompt and not messages and not images:
64
+ raise ValueError("At least one of prompt, messages, or images must be provided.")
65
+ if not model:
66
+ raise ValueError("model must be provided.")
67
+
68
+ normalized_messages = normalize_messages(prompt=prompt, messages=messages)
69
+ message_payloads: list[dict] = []
70
+ for message in normalized_messages:
71
+ blocks: list[dict] = []
72
+ if message["content"]:
73
+ blocks.append({"type": "text", "text": message["content"]})
74
+ message_payloads.append({"role": message["role"], "content": blocks})
75
+
76
+ if images:
77
+ image_blocks = [self._to_image_block(image) for image in images]
78
+ target_index = next(
79
+ (
80
+ index
81
+ for index in range(len(message_payloads) - 1, -1, -1)
82
+ if message_payloads[index]["role"] == "user"
83
+ ),
84
+ None,
85
+ )
86
+ if target_index is None:
87
+ message_payloads.append({"role": "user", "content": image_blocks})
88
+ else:
89
+ message_payloads[target_index]["content"].extend(image_blocks)
90
+
91
+ if not message_payloads or not any(msg["content"] for msg in message_payloads):
92
+ raise ValueError("No content provided for response generation.")
93
+
94
+ retry_count = clamp_retries(max_retries)
95
+
96
+ def _run_request() -> str:
97
+ client_kwargs = {"api_key": api_key}
98
+ if timeout_s is not None:
99
+ client_kwargs["timeout"] = timeout_s
100
+ client = Anthropic(**client_kwargs)
101
+
102
+ try:
103
+ response = client.messages.create(
104
+ model=model,
105
+ max_tokens=max_tokens,
106
+ messages=message_payloads,
107
+ )
108
+ except Exception as exc:
109
+ logger.exception(
110
+ "Anthropic messages.create failed: %s request_id=%s",
111
+ exc,
112
+ request_id,
113
+ )
114
+ raise
115
+
116
+ text_blocks: list[str] = []
117
+ for block in getattr(response, "content", []) or []:
118
+ if getattr(block, "type", None) == "text":
119
+ text = getattr(block, "text", None)
120
+ if text:
121
+ text_blocks.append(text)
122
+
123
+ if text_blocks:
124
+ result_text = "".join(text_blocks)
125
+ logger.info(
126
+ "Anthropic messages.create succeeded: model=%s images=%d text_len=%d request_id=%s",
127
+ model,
128
+ len(images or []),
129
+ len(result_text or ""),
130
+ request_id,
131
+ )
132
+ return result_text
133
+
134
+ # Treat successful calls without textual content as a successful, empty response
135
+ # rather than raising. This aligns with callers that handle empty outputs gracefully.
136
+ logger.info(
137
+ "Anthropic messages.create succeeded with no text: model=%s images=%d request_id=%s",
138
+ model,
139
+ len(images or []),
140
+ request_id,
141
+ )
142
+ return ""
143
+
144
+ return run_with_retries(
145
+ func=_run_request,
146
+ max_retries=retry_count,
147
+ retry_backoff_s=retry_backoff_s,
148
+ request_id=request_id,
149
+ )
150
+
151
+ async def async_generate_response(
152
+ self,
153
+ *,
154
+ api_key: str,
155
+ prompt: Optional[str] = None,
156
+ model: str,
157
+ max_tokens: int = 32000,
158
+ reasoning_effort: Optional[str] = None,
159
+ images: Optional[Sequence[ImageInput]] = None,
160
+ messages: Optional[MessageSequence] = None,
161
+ request_id: Optional[str] = None,
162
+ timeout_s: Optional[float] = None,
163
+ max_retries: Optional[int] = None,
164
+ retry_backoff_s: float = 0.5,
165
+ ) -> str:
166
+ return await run_sync_in_thread(
167
+ lambda: self.generate_response(
168
+ api_key=api_key,
169
+ prompt=prompt,
170
+ model=model,
171
+ max_tokens=max_tokens,
172
+ reasoning_effort=reasoning_effort,
173
+ images=images,
174
+ messages=messages,
175
+ request_id=request_id,
176
+ timeout_s=timeout_s,
177
+ max_retries=max_retries,
178
+ retry_backoff_s=retry_backoff_s,
179
+ )
180
+ )
181
+
182
+ def generate_image(
183
+ self,
184
+ *,
185
+ api_key: str,
186
+ prompt: str,
187
187
  model: str,
188
188
  image_size: str = "2K",
189
189
  image: Optional[ImageInput] = None,
190
190
  ) -> bytes:
191
191
  """Generate an image using the Anthropic API.
192
192
 
193
- Raises:
194
- NotImplementedError: This method is not yet implemented for Anthropic.
195
- """
196
- raise NotImplementedError("Image generation is not implemented for Anthropic.")
197
-
198
- async def async_generate_image(
199
- self,
200
- *,
201
- api_key: str,
202
- prompt: str,
203
- model: str,
204
- image_size: str = "2K",
205
- image: Optional[ImageInput] = None,
206
- ) -> bytes:
207
- return await run_sync_in_thread(
208
- lambda: self.generate_image(
209
- api_key=api_key,
210
- prompt=prompt,
211
- model=model,
212
- image_size=image_size,
213
- image=image,
214
- )
215
- )
216
-
217
- def list_models(
218
- self,
219
- *,
220
- api_key: str,
221
- request_id: Optional[str] = None,
222
- timeout_s: Optional[float] = None,
223
- max_retries: Optional[int] = None,
224
- retry_backoff_s: float = 0.5,
225
- ) -> list[dict[str, Optional[str]]]:
226
- """Return the models available to the authenticated Anthropic account."""
227
- if not api_key:
228
- raise ValueError("api_key must be provided.")
229
-
230
- retry_count = clamp_retries(max_retries)
231
-
232
- def _run_request() -> list[dict[str, Optional[str]]]:
233
- client_kwargs = {"api_key": api_key}
234
- if timeout_s is not None:
235
- client_kwargs["timeout"] = timeout_s
236
- client = Anthropic(**client_kwargs)
237
- models: list[dict[str, Optional[str]]] = []
238
-
239
- try:
240
- iterator = client.models.list()
241
- except Exception as exc:
242
- logger.exception(
243
- "Anthropic list models failed: %s request_id=%s",
244
- exc,
245
- request_id,
246
- )
247
- raise
248
-
249
- for model in iterator:
250
- model_id = getattr(model, "id", None)
251
- if model_id is None and isinstance(model, dict):
252
- model_id = model.get("id")
253
- if not model_id:
254
- continue
255
-
256
- display_name = getattr(model, "display_name", None)
257
- if display_name is None and isinstance(model, dict):
258
- display_name = model.get("display_name")
259
-
260
- models.append({"id": model_id, "display_name": display_name})
261
-
262
- logger.info(
263
- "Anthropic list_models succeeded: count=%d request_id=%s",
264
- len(models),
265
- request_id,
266
- )
267
- return models
268
-
269
- return run_with_retries(
270
- func=_run_request,
271
- max_retries=retry_count,
272
- retry_backoff_s=retry_backoff_s,
273
- request_id=request_id,
274
- )
275
-
276
- async def async_list_models(
277
- self,
278
- *,
279
- api_key: str,
280
- request_id: Optional[str] = None,
281
- timeout_s: Optional[float] = None,
282
- max_retries: Optional[int] = None,
283
- retry_backoff_s: float = 0.5,
284
- ) -> list[dict[str, Optional[str]]]:
285
- return await run_sync_in_thread(
286
- lambda: self.list_models(
287
- api_key=api_key,
288
- request_id=request_id,
289
- timeout_s=timeout_s,
290
- max_retries=max_retries,
291
- retry_backoff_s=retry_backoff_s,
292
- )
293
- )
193
+ Raises:
194
+ NotImplementedError: This method is not yet implemented for Anthropic.
195
+ """
196
+ raise NotImplementedError("Image generation is not implemented for Anthropic.")
197
+
198
+ async def async_generate_image(
199
+ self,
200
+ *,
201
+ api_key: str,
202
+ prompt: str,
203
+ model: str,
204
+ image_size: str = "2K",
205
+ image: Optional[ImageInput] = None,
206
+ ) -> bytes:
207
+ return await run_sync_in_thread(
208
+ lambda: self.generate_image(
209
+ api_key=api_key,
210
+ prompt=prompt,
211
+ model=model,
212
+ image_size=image_size,
213
+ image=image,
214
+ )
215
+ )
216
+
217
+ def list_models(
218
+ self,
219
+ *,
220
+ api_key: str,
221
+ request_id: Optional[str] = None,
222
+ timeout_s: Optional[float] = None,
223
+ max_retries: Optional[int] = None,
224
+ retry_backoff_s: float = 0.5,
225
+ ) -> list[dict[str, Optional[str]]]:
226
+ """Return the models available to the authenticated Anthropic account."""
227
+ if not api_key:
228
+ raise ValueError("api_key must be provided.")
229
+
230
+ retry_count = clamp_retries(max_retries)
231
+
232
+ def _run_request() -> list[dict[str, Optional[str]]]:
233
+ client_kwargs = {"api_key": api_key}
234
+ if timeout_s is not None:
235
+ client_kwargs["timeout"] = timeout_s
236
+ client = Anthropic(**client_kwargs)
237
+ models: list[dict[str, Optional[str]]] = []
238
+
239
+ try:
240
+ iterator = client.models.list()
241
+ except Exception as exc:
242
+ logger.exception(
243
+ "Anthropic list models failed: %s request_id=%s",
244
+ exc,
245
+ request_id,
246
+ )
247
+ raise
248
+
249
+ for model in iterator:
250
+ model_id = getattr(model, "id", None)
251
+ if model_id is None and isinstance(model, dict):
252
+ model_id = model.get("id")
253
+ if not model_id:
254
+ continue
255
+
256
+ display_name = getattr(model, "display_name", None)
257
+ if display_name is None and isinstance(model, dict):
258
+ display_name = model.get("display_name")
259
+
260
+ models.append({"id": model_id, "display_name": display_name})
261
+
262
+ logger.info(
263
+ "Anthropic list_models succeeded: count=%d request_id=%s",
264
+ len(models),
265
+ request_id,
266
+ )
267
+ return models
268
+
269
+ return run_with_retries(
270
+ func=_run_request,
271
+ max_retries=retry_count,
272
+ retry_backoff_s=retry_backoff_s,
273
+ request_id=request_id,
274
+ )
275
+
276
+ async def async_list_models(
277
+ self,
278
+ *,
279
+ api_key: str,
280
+ request_id: Optional[str] = None,
281
+ timeout_s: Optional[float] = None,
282
+ max_retries: Optional[int] = None,
283
+ retry_backoff_s: float = 0.5,
284
+ ) -> list[dict[str, Optional[str]]]:
285
+ return await run_sync_in_thread(
286
+ lambda: self.list_models(
287
+ api_key=api_key,
288
+ request_id=request_id,
289
+ timeout_s=timeout_s,
290
+ max_retries=max_retries,
291
+ retry_backoff_s=retry_backoff_s,
292
+ )
293
+ )
294
294
 
295
295
  @staticmethod
296
296
  def _to_image_block(image: ImageInput) -> dict: