ccs-llmconnector 1.0.6__py3-none-any.whl → 1.1.1__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.
- {ccs_llmconnector-1.0.6.dist-info → ccs_llmconnector-1.1.1.dist-info}/METADATA +51 -8
- ccs_llmconnector-1.1.1.dist-info/RECORD +16 -0
- {ccs_llmconnector-1.0.6.dist-info → ccs_llmconnector-1.1.1.dist-info}/WHEEL +1 -1
- llmconnector/__init__.py +11 -1
- llmconnector/anthropic_client.py +209 -66
- llmconnector/client.py +225 -10
- llmconnector/client_cli.py +27 -0
- llmconnector/gemini_client.py +316 -119
- llmconnector/grok_client.py +208 -78
- llmconnector/openai_client.py +194 -62
- llmconnector/types.py +49 -0
- llmconnector/utils.py +78 -0
- ccs_llmconnector-1.0.6.dist-info/RECORD +0 -14
- {ccs_llmconnector-1.0.6.dist-info → ccs_llmconnector-1.1.1.dist-info}/entry_points.txt +0 -0
- {ccs_llmconnector-1.0.6.dist-info → ccs_llmconnector-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {ccs_llmconnector-1.0.6.dist-info → ccs_llmconnector-1.1.1.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ccs-llmconnector
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Lightweight wrapper around different LLM provider Python SDK Responses APIs.
|
|
5
5
|
Author: CCS
|
|
6
6
|
License: MIT
|
|
@@ -9,9 +9,16 @@ Requires-Python: >=3.8
|
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: openai>=1.0.0
|
|
12
|
-
|
|
13
|
-
Requires-Dist:
|
|
14
|
-
|
|
12
|
+
Provides-Extra: gemini
|
|
13
|
+
Requires-Dist: google-genai; extra == "gemini"
|
|
14
|
+
Provides-Extra: anthropic
|
|
15
|
+
Requires-Dist: anthropic; extra == "anthropic"
|
|
16
|
+
Provides-Extra: xai
|
|
17
|
+
Requires-Dist: xai-sdk; extra == "xai"
|
|
18
|
+
Provides-Extra: all
|
|
19
|
+
Requires-Dist: google-genai; extra == "all"
|
|
20
|
+
Requires-Dist: anthropic; extra == "all"
|
|
21
|
+
Requires-Dist: xai-sdk; extra == "all"
|
|
15
22
|
Dynamic: license-file
|
|
16
23
|
|
|
17
24
|
# ccs-llmconnector
|
|
@@ -29,16 +36,24 @@ the models available to your account with each provider.
|
|
|
29
36
|
# from PyPI (normalized project name)
|
|
30
37
|
pip install ccs-llmconnector
|
|
31
38
|
|
|
39
|
+
# install additional providers
|
|
40
|
+
pip install "ccs-llmconnector[gemini]"
|
|
41
|
+
pip install "ccs-llmconnector[anthropic]"
|
|
42
|
+
pip install "ccs-llmconnector[xai]"
|
|
43
|
+
pip install "ccs-llmconnector[all]"
|
|
44
|
+
|
|
32
45
|
# or from source (this repository)
|
|
33
46
|
pip install .
|
|
34
47
|
```
|
|
35
48
|
|
|
36
49
|
### Requirements
|
|
37
50
|
|
|
38
|
-
- `openai` (installed automatically with the package)
|
|
39
|
-
-
|
|
40
|
-
- `
|
|
41
|
-
- `
|
|
51
|
+
- `openai` (installed automatically with the base package)
|
|
52
|
+
- Optional extras:
|
|
53
|
+
- `ccs-llmconnector[gemini]` -> `google-genai`
|
|
54
|
+
- `ccs-llmconnector[anthropic]` -> `anthropic`
|
|
55
|
+
- `ccs-llmconnector[xai]` -> `xai-sdk` (Python 3.10+)
|
|
56
|
+
- `ccs-llmconnector[all]` -> all providers
|
|
42
57
|
|
|
43
58
|
## Components
|
|
44
59
|
|
|
@@ -48,6 +63,17 @@ pip install .
|
|
|
48
63
|
- `GrokClient` - wrapper around the xAI Grok chat API, usable when `xai-sdk` is installed. Includes a model discovery helper.
|
|
49
64
|
- `LLMClient` - provider router that delegates to registered clients (OpenAI included by default) so additional vendors can be added without changing call sites.
|
|
50
65
|
|
|
66
|
+
## Common Options
|
|
67
|
+
|
|
68
|
+
All clients expose the same optional controls:
|
|
69
|
+
|
|
70
|
+
- `messages`: list of `{role, content}` entries (e.g., `system`, `user`, `assistant`). If both `prompt` and `messages` are provided, `prompt` is appended as the last user message.
|
|
71
|
+
- `request_id`: free-form request identifier for tracing/logging.
|
|
72
|
+
- `timeout_s`: optional timeout in seconds (best-effort depending on provider).
|
|
73
|
+
- `max_retries` and `retry_backoff_s`: retry count and exponential backoff base delay.
|
|
74
|
+
|
|
75
|
+
Async counterparts are available as `async_generate_response`, `async_generate_image`, and `async_list_models`.
|
|
76
|
+
|
|
51
77
|
## GeminiClient
|
|
52
78
|
|
|
53
79
|
### Usage
|
|
@@ -326,6 +352,15 @@ response_via_router = llm_client.generate_response(
|
|
|
326
352
|
max_tokens=1500,
|
|
327
353
|
)
|
|
328
354
|
|
|
355
|
+
# async usage
|
|
356
|
+
# response_via_router = await llm_client.async_generate_response(
|
|
357
|
+
# provider="openai",
|
|
358
|
+
# api_key="sk-your-api-key",
|
|
359
|
+
# messages=[{"role": "system", "content": "You are concise."}],
|
|
360
|
+
# prompt="Summarize the plan.",
|
|
361
|
+
# model="gpt-4o-mini",
|
|
362
|
+
# )
|
|
363
|
+
|
|
329
364
|
gemini_response = llm_client.generate_response(
|
|
330
365
|
provider="gemini", # google-genai is installed with llmconnector
|
|
331
366
|
api_key="your-gemini-api-key",
|
|
@@ -373,10 +408,15 @@ for model in llm_client.list_models(provider="openai", api_key="sk-your-api-key"
|
|
|
373
408
|
| `provider` | `str` | Yes | Registered provider key (default registry includes `'openai'`, `'gemini'`, `'anthropic'`, `'grok'`/`'xai'`). |
|
|
374
409
|
| `api_key` | `str` | Yes | Provider-specific API key. |
|
|
375
410
|
| `prompt` | `Optional[str]` | Conditional | Plain-text prompt. Required unless `images` is supplied. |
|
|
411
|
+
| `messages` | `Optional[Sequence[dict]]` | No | Chat-style messages (`role`, `content`). |
|
|
376
412
|
| `model` | `str` | Yes | Provider-specific model identifier. |
|
|
377
413
|
| `max_tokens` | `int` | No | Defaults to `32000`. |
|
|
378
414
|
| `reasoning_effort` | `Optional[str]` | No | Reasoning hint forwarded when supported. |
|
|
379
415
|
| `images` | `Optional[Sequence[str \| Path]]` | No | Image references forwarded to the provider implementation. |
|
|
416
|
+
| `request_id` | `Optional[str]` | No | Request identifier for tracing/logging. |
|
|
417
|
+
| `timeout_s` | `Optional[float]` | No | Timeout in seconds (best-effort). |
|
|
418
|
+
| `max_retries` | `Optional[int]` | No | Retry count for transient failures. |
|
|
419
|
+
| `retry_backoff_s` | `Optional[float]` | No | Base delay (seconds) for exponential backoff. |
|
|
380
420
|
|
|
381
421
|
Use `LLMClient.register_provider(name, client)` to add additional providers that implement
|
|
382
422
|
`generate_response` with the same signature.
|
|
@@ -399,6 +439,9 @@ Examples:
|
|
|
399
439
|
# Generate a response
|
|
400
440
|
client_cli respond --provider openai --model gpt-4o --prompt "Hello!"
|
|
401
441
|
|
|
442
|
+
# Generate with retry/timeout controls
|
|
443
|
+
client_cli respond --provider openai --model gpt-4o --prompt "Hello!" --timeout-s 30 --max-retries 2
|
|
444
|
+
|
|
402
445
|
# List models for one provider (human-readable)
|
|
403
446
|
client_cli models --provider gemini
|
|
404
447
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
ccs_llmconnector-1.1.1.dist-info/licenses/LICENSE,sha256=rPcz2YmBB9VUWZTLJcRO_B4jKDpqmGRYi2eSI-unysg,1083
|
|
2
|
+
llmconnector/__init__.py,sha256=w68d7BCkhjjqMaOG9VDkPoYR4PNbv8NJqGmAV_Vyn6g,1358
|
|
3
|
+
llmconnector/anthropic_client.py,sha256=hGFZlUQ4yAs_H8StxWzHAYmqwgNT6lGI4yW8UmKhtm8,13260
|
|
4
|
+
llmconnector/client.py,sha256=2rhDpcBKKbQ1kGp7-Bk8ci_VVe9DEEvBf9nH4hKwg3w,13849
|
|
5
|
+
llmconnector/client_cli.py,sha256=ojLPNJ14lFycsLWtEMtdGpV033v2-23qPMuWqFlnySA,11463
|
|
6
|
+
llmconnector/gemini_client.py,sha256=_XPy6C5WQeuP_GY2eb3dy4DMFX9Oj8DRhb9u7XA5TfI,19155
|
|
7
|
+
llmconnector/grok_client.py,sha256=uicZI28-vw7go677zZB5MTkw5aqWp2UP_GaguNUU99o,11254
|
|
8
|
+
llmconnector/openai_client.py,sha256=wvrEFzltg8PctM2DcNZ_7qmRPpF-CB8WQek6iYoRvR4,11051
|
|
9
|
+
llmconnector/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
10
|
+
llmconnector/types.py,sha256=JhXKjKncdUB2yPd-bqjOUnl-4Wgs6GIleDMChmwPW8w,1287
|
|
11
|
+
llmconnector/utils.py,sha256=Tw6wQLh7Cjk6igLJ-MnYGSSPJBUGl5-7R1xB0Aj7o8Y,2047
|
|
12
|
+
ccs_llmconnector-1.1.1.dist-info/METADATA,sha256=k4esAN7SCw583miw_IqV9sehyt0GGcgGylpLLPAL35s,17001
|
|
13
|
+
ccs_llmconnector-1.1.1.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
14
|
+
ccs_llmconnector-1.1.1.dist-info/entry_points.txt,sha256=eFvLY3nHAG_QhaKlemhhK7echfezW0KiMdSNMZOStLc,60
|
|
15
|
+
ccs_llmconnector-1.1.1.dist-info/top_level.txt,sha256=Doer7TAUsN8UXQfPHPNsuBXVNCz2uV-Q0v4t4fwv_MM,13
|
|
16
|
+
ccs_llmconnector-1.1.1.dist-info/RECORD,,
|
llmconnector/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
7
|
from .client import LLMClient
|
|
8
|
+
from .types import ImageInput, Message, MessageSequence
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
10
11
|
from .anthropic_client import AnthropicClient
|
|
@@ -12,7 +13,16 @@ if TYPE_CHECKING:
|
|
|
12
13
|
from .grok_client import GrokClient
|
|
13
14
|
from .openai_client import OpenAIResponsesClient
|
|
14
15
|
|
|
15
|
-
__all__ = [
|
|
16
|
+
__all__ = [
|
|
17
|
+
"LLMClient",
|
|
18
|
+
"OpenAIResponsesClient",
|
|
19
|
+
"GeminiClient",
|
|
20
|
+
"AnthropicClient",
|
|
21
|
+
"GrokClient",
|
|
22
|
+
"ImageInput",
|
|
23
|
+
"Message",
|
|
24
|
+
"MessageSequence",
|
|
25
|
+
]
|
|
16
26
|
|
|
17
27
|
|
|
18
28
|
def __getattr__(name: str) -> Any:
|
llmconnector/anthropic_client.py
CHANGED
|
@@ -6,13 +6,14 @@ import base64
|
|
|
6
6
|
import mimetypes
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import logging
|
|
9
|
-
from typing import Optional, Sequence
|
|
10
|
-
from urllib.error import URLError
|
|
9
|
+
from typing import Optional, Sequence
|
|
11
10
|
from urllib.request import urlopen
|
|
12
11
|
|
|
13
12
|
from anthropic import APIError, Anthropic
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
from .types import ImageInput, MessageSequence, normalize_messages
|
|
15
|
+
from .utils import clamp_retries, run_sync_in_thread, run_with_retries
|
|
16
|
+
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
17
18
|
|
|
18
19
|
|
|
@@ -23,11 +24,16 @@ class AnthropicClient:
|
|
|
23
24
|
self,
|
|
24
25
|
*,
|
|
25
26
|
api_key: str,
|
|
26
|
-
prompt: str,
|
|
27
|
+
prompt: Optional[str] = None,
|
|
27
28
|
model: str,
|
|
28
29
|
max_tokens: int = 32000,
|
|
29
30
|
reasoning_effort: Optional[str] = None,
|
|
30
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,
|
|
31
37
|
) -> str:
|
|
32
38
|
"""Generate a response from the specified Anthropic model.
|
|
33
39
|
|
|
@@ -38,6 +44,11 @@ class AnthropicClient:
|
|
|
38
44
|
max_tokens: Cap for tokens across the entire exchange, defaults to 32000.
|
|
39
45
|
reasoning_effort: Included for API parity; currently unused by the Anthropic SDK.
|
|
40
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.
|
|
41
52
|
|
|
42
53
|
Returns:
|
|
43
54
|
The text output produced by the model.
|
|
@@ -49,59 +60,124 @@ class AnthropicClient:
|
|
|
49
60
|
"""
|
|
50
61
|
if not api_key:
|
|
51
62
|
raise ValueError("api_key must be provided.")
|
|
52
|
-
if not prompt and not images:
|
|
53
|
-
raise ValueError("At least one of prompt or images 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.")
|
|
54
65
|
if not model:
|
|
55
66
|
raise ValueError("model must be provided.")
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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})
|
|
60
75
|
|
|
61
76
|
if images:
|
|
62
|
-
for image in images
|
|
63
|
-
|
|
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)
|
|
64
90
|
|
|
65
|
-
if not
|
|
91
|
+
if not message_payloads or not any(msg["content"] for msg in message_payloads):
|
|
66
92
|
raise ValueError("No content provided for response generation.")
|
|
67
93
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
89
136
|
logger.info(
|
|
90
|
-
"Anthropic messages.create succeeded: model=%s images=%d
|
|
137
|
+
"Anthropic messages.create succeeded with no text: model=%s images=%d request_id=%s",
|
|
91
138
|
model,
|
|
92
139
|
len(images or []),
|
|
93
|
-
|
|
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,
|
|
94
179
|
)
|
|
95
|
-
return result_text
|
|
96
|
-
|
|
97
|
-
# Treat successful calls without textual content as a successful, empty response
|
|
98
|
-
# rather than raising. This aligns with callers that handle empty outputs gracefully.
|
|
99
|
-
logger.info(
|
|
100
|
-
"Anthropic messages.create succeeded with no text: model=%s images=%d",
|
|
101
|
-
model,
|
|
102
|
-
len(images or []),
|
|
103
180
|
)
|
|
104
|
-
return ""
|
|
105
181
|
|
|
106
182
|
def generate_image(
|
|
107
183
|
self,
|
|
@@ -119,35 +195,102 @@ class AnthropicClient:
|
|
|
119
195
|
"""
|
|
120
196
|
raise NotImplementedError("Image generation is not implemented for Anthropic.")
|
|
121
197
|
|
|
122
|
-
def
|
|
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]]]:
|
|
123
226
|
"""Return the models available to the authenticated Anthropic account."""
|
|
124
227
|
if not api_key:
|
|
125
228
|
raise ValueError("api_key must be provided.")
|
|
126
229
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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})
|
|
142
261
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
262
|
+
logger.info(
|
|
263
|
+
"Anthropic list_models succeeded: count=%d request_id=%s",
|
|
264
|
+
len(models),
|
|
265
|
+
request_id,
|
|
266
|
+
)
|
|
267
|
+
return models
|
|
146
268
|
|
|
147
|
-
|
|
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
|
+
)
|
|
148
275
|
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
)
|
|
151
294
|
|
|
152
295
|
@staticmethod
|
|
153
296
|
def _to_image_block(image: ImageInput) -> dict:
|