agenthub-python 0.4.4__tar.gz → 0.4.5__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.
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/PKG-INFO +2 -1
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/__init__.py +8 -1
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/ant_messages/client.py +26 -4
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/auto_client.py +69 -18
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/base_client.py +9 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/claude5/client.py +34 -5
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/deepseek_v4/client.py +17 -2
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/errors.py +14 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/gemini3_7/client.py +28 -6
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/glm5_3/client.py +17 -2
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/gpt5_6/client.py +24 -3
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/integration/playground.py +249 -45
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/kimi_k3/client.py +17 -2
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/minimax_m3/client.py +33 -20
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/openai_chat/client.py +17 -2
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/openai_embedding/client.py +17 -2
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/openai_responses/client.py +26 -17
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/registry.py +50 -23
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/utils.py +16 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/pyproject.toml +2 -1
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/pyproject.toml.orig +2 -2
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/README.md +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/abort_signal.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/ant_messages/__init__.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/claude5/__init__.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/deepseek_v4/__init__.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/gemini3_7/__init__.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/glm5_3/__init__.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/gpt5_6/__init__.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/integration/__init__.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/integration/tracer.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/kimi_k3/__init__.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/minimax_m3/__init__.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/openai_chat/__init__.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/openai_embedding/__init__.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/openai_responses/__init__.py +0 -0
- {agenthub_python-0.4.4 → agenthub_python-0.4.5}/agenthub/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agenthub-python
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: AgentHub is the LLM API Hub for the Agent era, built for high-precision autonomous agents.
|
|
5
5
|
Keywords: agent,llm,gemini,claude,gpt
|
|
6
6
|
Author: PrismShadow
|
|
@@ -14,6 +14,7 @@ Requires-Dist: httpx[socks] ; extra == 'dev'
|
|
|
14
14
|
Requires-Dist: pytest>=8.4.2 ; extra == 'dev'
|
|
15
15
|
Requires-Dist: pytest-asyncio>=0.23.0 ; extra == 'dev'
|
|
16
16
|
Requires-Dist: pytest-xdist>=3.6.0 ; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest-rerunfailures>=15.0 ; extra == 'dev'
|
|
17
18
|
Requires-Dist: ruff>=0.14.3 ; extra == 'dev'
|
|
18
19
|
Requires-Dist: pillow>=10.0.0 ; extra == 'dev'
|
|
19
20
|
Requires-Python: >=3.11
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
from .auto_client import AutoLLMClient
|
|
16
|
-
from .errors import
|
|
16
|
+
from .errors import (
|
|
17
|
+
AgentHubError,
|
|
18
|
+
EmptyResponseError,
|
|
19
|
+
ToolCallArgumentParseError,
|
|
20
|
+
UnsupportedOperationError,
|
|
21
|
+
UnsupportedParameterError,
|
|
22
|
+
)
|
|
17
23
|
from .registry import Currency, Modality, ModelPricing, SupportedModel, list_supported_models
|
|
18
24
|
from .types import PromptCaching, ThinkingLevel
|
|
19
25
|
|
|
@@ -29,6 +35,7 @@ __all__ = [
|
|
|
29
35
|
"SupportedModel",
|
|
30
36
|
"ThinkingLevel",
|
|
31
37
|
"ToolCallArgumentParseError",
|
|
38
|
+
"UnsupportedOperationError",
|
|
32
39
|
"UnsupportedParameterError",
|
|
33
40
|
"list_supported_models",
|
|
34
41
|
]
|
|
@@ -33,7 +33,7 @@ from ..types import (
|
|
|
33
33
|
UniMessage,
|
|
34
34
|
UsageMetadata,
|
|
35
35
|
)
|
|
36
|
-
from ..utils import fix_openrouter_usage_metadata
|
|
36
|
+
from ..utils import fix_openrouter_usage_metadata, is_debug_enabled
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
REDACTED_THINKING = "_REDACTED_THINKING"
|
|
@@ -42,14 +42,22 @@ REDACTED_THINKING = "_REDACTED_THINKING"
|
|
|
42
42
|
class AntMessagesClient(LLMClient):
|
|
43
43
|
"""Anthropic Messages-compatible client implementation."""
|
|
44
44
|
|
|
45
|
-
def __init__(
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
model: str,
|
|
48
|
+
api_key: str | None = None,
|
|
49
|
+
base_url: str | None = None,
|
|
50
|
+
default_headers: dict[str, str] | None = None,
|
|
51
|
+
):
|
|
46
52
|
"""Initialize Anthropic Messages-compatible client with model, API key, and base URL."""
|
|
47
53
|
self._model = model
|
|
48
54
|
api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
49
55
|
base_url = base_url or os.getenv("ANTHROPIC_BASE_URL")
|
|
50
56
|
# send the credential through both header conventions: Anthropic and DeepSeek read
|
|
51
57
|
# x-api-key while gateways such as OpenRouter and Z.AI read Authorization: Bearer
|
|
52
|
-
self._client = AsyncAnthropic(
|
|
58
|
+
self._client = AsyncAnthropic(
|
|
59
|
+
api_key=api_key, auth_token=api_key, base_url=base_url, default_headers=default_headers
|
|
60
|
+
)
|
|
53
61
|
self._history: list[UniMessage] = []
|
|
54
62
|
|
|
55
63
|
def _convert_image_url_to_source(self, url: str) -> dict[str, Any]:
|
|
@@ -306,9 +314,14 @@ class AntMessagesClient(LLMClient):
|
|
|
306
314
|
# gateways that relabel it onto another event
|
|
307
315
|
event_type = "unused"
|
|
308
316
|
|
|
309
|
-
|
|
317
|
+
elif is_debug_enabled():
|
|
310
318
|
raise ValueError(f"Unknown output: {model_output}")
|
|
311
319
|
|
|
320
|
+
else:
|
|
321
|
+
# a gateway injects its own events (heartbeats, cost tickers) into the stream, and
|
|
322
|
+
# killing a long generation over one costs more than dropping it
|
|
323
|
+
event_type = "unused"
|
|
324
|
+
|
|
312
325
|
return {
|
|
313
326
|
"role": "assistant",
|
|
314
327
|
"event_type": event_type,
|
|
@@ -412,3 +425,12 @@ class AntMessagesClient(LLMClient):
|
|
|
412
425
|
"finish_reason": event["finish_reason"],
|
|
413
426
|
}
|
|
414
427
|
partial_usage = {}
|
|
428
|
+
|
|
429
|
+
async def list_models(self) -> list[str]:
|
|
430
|
+
"""
|
|
431
|
+
List the model ids the configured endpoint serves.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
list[str]: The model ids, in the order the endpoint returned them.
|
|
435
|
+
"""
|
|
436
|
+
return [model.id async for model in self._client.models.list()]
|
|
@@ -20,6 +20,10 @@ from .base_client import LLMClient
|
|
|
20
20
|
from .types import UniConfig, UniEvent, UniMessage
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
# The generic protocol clients are named explicitly rather than deduced from a model id.
|
|
24
|
+
_PROTOCOL_CLIENT_TYPES = ("openai-chat", "openai-responses", "ant-messages", "openai-embedding")
|
|
25
|
+
|
|
26
|
+
|
|
23
27
|
class AutoLLMClient(LLMClient):
|
|
24
28
|
"""
|
|
25
29
|
Auto-routing LLM client that dispatches to appropriate model-specific client.
|
|
@@ -29,7 +33,12 @@ class AutoLLMClient(LLMClient):
|
|
|
29
33
|
"""
|
|
30
34
|
|
|
31
35
|
def __init__(
|
|
32
|
-
self,
|
|
36
|
+
self,
|
|
37
|
+
model: str,
|
|
38
|
+
api_key: str | None = None,
|
|
39
|
+
base_url: str | None = None,
|
|
40
|
+
client_type: str | None = None,
|
|
41
|
+
default_headers: dict[str, str] | None = None,
|
|
33
42
|
):
|
|
34
43
|
"""
|
|
35
44
|
Initialize AutoLLMClient with a specific model.
|
|
@@ -39,14 +48,22 @@ class AutoLLMClient(LLMClient):
|
|
|
39
48
|
api_key: Optional API key
|
|
40
49
|
base_url: Optional base URL for API requests
|
|
41
50
|
client_type: Optional client type override
|
|
51
|
+
default_headers: Optional headers sent with every request, for endpoints that demand their own
|
|
42
52
|
"""
|
|
43
|
-
self.
|
|
53
|
+
self._client_type = (client_type or os.getenv("CLIENT_TYPE") or model).lower()
|
|
54
|
+
self._client = self._create_client_for_model(model, api_key, base_url, self._client_type, default_headers)
|
|
44
55
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _client_class_for_model(client_type: str) -> type[LLMClient] | None:
|
|
58
|
+
"""
|
|
59
|
+
Resolve which client a resolved (lowercased) client type routes to.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
client_type: The resolved client type, which is the model id when none was given.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
type[LLMClient] | None: The client class, or None when no client claims the type.
|
|
66
|
+
"""
|
|
50
67
|
# every Gemini generation shares the unified client ("gemini-3" also matches the
|
|
51
68
|
# gemini-3.7/gemini-3.6/gemini-3.5-flash-lite client types)
|
|
52
69
|
if any(
|
|
@@ -54,51 +71,64 @@ class AutoLLMClient(LLMClient):
|
|
|
54
71
|
): # e.g., gemini-3.7-flash, gemini-3-flash-preview, gemini-embedding-2
|
|
55
72
|
from .gemini3_7 import Gemini3_7Client
|
|
56
73
|
|
|
57
|
-
return Gemini3_7Client
|
|
74
|
+
return Gemini3_7Client
|
|
58
75
|
elif "claude" in client_type and (
|
|
59
76
|
"4-6" in client_type or "4-7" in client_type or "4-8" in client_type or "-5" in client_type
|
|
60
77
|
): # the whole Claude 4.6+ series shares the unified client, e.g., claude-sonnet-4-6
|
|
61
78
|
from .claude5 import Claude5Client
|
|
62
79
|
|
|
63
|
-
return Claude5Client
|
|
80
|
+
return Claude5Client
|
|
64
81
|
elif "gpt-5.4" in client_type or "gpt-5.5" in client_type or "gpt-5.6" in client_type: # e.g., gpt-5.6
|
|
65
82
|
from .gpt5_6 import GPT5_6Client
|
|
66
83
|
|
|
67
|
-
return GPT5_6Client
|
|
84
|
+
return GPT5_6Client
|
|
68
85
|
elif "glm-5" in client_type: # the whole GLM series shares the unified client
|
|
69
86
|
from .glm5_3 import GLM5_3Client
|
|
70
87
|
|
|
71
|
-
return GLM5_3Client
|
|
88
|
+
return GLM5_3Client
|
|
72
89
|
elif "kimi-k3" in client_type or "kimi-k2.5" in client_type or "kimi-k2.6" in client_type:
|
|
73
90
|
# the whole Kimi K2.5+ series shares the unified client
|
|
74
91
|
from .kimi_k3 import KimiK3Client
|
|
75
92
|
|
|
76
|
-
return KimiK3Client
|
|
93
|
+
return KimiK3Client
|
|
77
94
|
elif client_type == "minimax-m3":
|
|
78
95
|
from .minimax_m3 import MiniMaxM3Client
|
|
79
96
|
|
|
80
|
-
return MiniMaxM3Client
|
|
97
|
+
return MiniMaxM3Client
|
|
81
98
|
elif "deepseek-v4" in client_type:
|
|
82
99
|
from .deepseek_v4 import DeepSeekV4Client
|
|
83
100
|
|
|
84
|
-
return DeepSeekV4Client
|
|
101
|
+
return DeepSeekV4Client
|
|
85
102
|
elif "ant-messages" in client_type:
|
|
86
103
|
from .ant_messages import AntMessagesClient
|
|
87
104
|
|
|
88
|
-
return AntMessagesClient
|
|
105
|
+
return AntMessagesClient
|
|
89
106
|
elif "openai-responses" in client_type:
|
|
90
107
|
from .openai_responses import OpenaiResponsesClient
|
|
91
108
|
|
|
92
|
-
return OpenaiResponsesClient
|
|
109
|
+
return OpenaiResponsesClient
|
|
93
110
|
elif "openai" in client_type and "embedding" in client_type:
|
|
94
111
|
from .openai_embedding import OpenaiEmbeddingClient
|
|
95
112
|
|
|
96
|
-
return OpenaiEmbeddingClient
|
|
113
|
+
return OpenaiEmbeddingClient
|
|
97
114
|
elif "openai" in client_type and "embedding" not in client_type: # openai-chat, plus bare "openai" as alias
|
|
98
115
|
from .openai_chat import OpenaiChatClient
|
|
99
116
|
|
|
100
|
-
return OpenaiChatClient
|
|
117
|
+
return OpenaiChatClient
|
|
101
118
|
else:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def _create_client_for_model(
|
|
122
|
+
self,
|
|
123
|
+
model: str,
|
|
124
|
+
api_key: str | None = None,
|
|
125
|
+
base_url: str | None = None,
|
|
126
|
+
client_type: str | None = None,
|
|
127
|
+
default_headers: dict[str, str] | None = None,
|
|
128
|
+
) -> LLMClient:
|
|
129
|
+
"""Create the appropriate client for the given model."""
|
|
130
|
+
client_class = self._client_class_for_model(client_type or model.lower())
|
|
131
|
+
if client_class is None:
|
|
102
132
|
raise ValueError(
|
|
103
133
|
f"{client_type} is not supported. "
|
|
104
134
|
"Supported client types: minimax-m3, gemini-3.7, gemini-3.6, gemini-3, "
|
|
@@ -107,6 +137,8 @@ class AutoLLMClient(LLMClient):
|
|
|
107
137
|
"openai-embedding, ant-messages, openai-responses, openai-chat."
|
|
108
138
|
)
|
|
109
139
|
|
|
140
|
+
return client_class(model=model, api_key=api_key, base_url=base_url, default_headers=default_headers)
|
|
141
|
+
|
|
110
142
|
def transform_uni_config_to_model_config(self, config: UniConfig) -> Any:
|
|
111
143
|
"""Delegate to underlying client's transform_uni_config_to_model_config."""
|
|
112
144
|
return self._client.transform_uni_config_to_model_config(config)
|
|
@@ -165,3 +197,22 @@ class AutoLLMClient(LLMClient):
|
|
|
165
197
|
def set_history(self, history: list[UniMessage]) -> None:
|
|
166
198
|
"""Set history in the underlying client."""
|
|
167
199
|
self._client.set_history(history)
|
|
200
|
+
|
|
201
|
+
async def list_models(self) -> list[str]:
|
|
202
|
+
"""
|
|
203
|
+
List the model ids the endpoint serves that the routed client can be used for.
|
|
204
|
+
|
|
205
|
+
A protocol client is chosen explicitly and speaks for whatever the endpoint serves, so its
|
|
206
|
+
listing is returned whole. A client deduced from a model id serves only the ids that deduce
|
|
207
|
+
back to it, so a gateway fronting many vendors is filtered down to that client's own models.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
list[str]: The model ids, in the order the endpoint returned them.
|
|
211
|
+
"""
|
|
212
|
+
model_ids = await self._client.list_models()
|
|
213
|
+
protocol_classes = {self._client_class_for_model(name) for name in _PROTOCOL_CLIENT_TYPES}
|
|
214
|
+
if type(self._client) in protocol_classes:
|
|
215
|
+
return model_ids
|
|
216
|
+
|
|
217
|
+
client_class = type(self._client)
|
|
218
|
+
return [model_id for model_id in model_ids if self._client_class_for_model(model_id.lower()) is client_class]
|
|
@@ -175,6 +175,15 @@ class LLMClient(ABC):
|
|
|
175
175
|
"""
|
|
176
176
|
pass
|
|
177
177
|
|
|
178
|
+
@abstractmethod
|
|
179
|
+
async def list_models(self) -> list[str]:
|
|
180
|
+
"""
|
|
181
|
+
List the model ids the configured endpoint serves.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
list[str]: The model ids, in the order the endpoint returned them.
|
|
185
|
+
"""
|
|
186
|
+
|
|
178
187
|
async def streaming_response(
|
|
179
188
|
self,
|
|
180
189
|
messages: list[UniMessage],
|
|
@@ -23,7 +23,7 @@ from anthropic import AsyncAnthropic, AsyncAnthropicBedrock
|
|
|
23
23
|
from anthropic.types.beta import BetaMessageParam, BetaRawMessageStreamEvent
|
|
24
24
|
|
|
25
25
|
from ..base_client import LLMClient
|
|
26
|
-
from ..errors import UnsupportedParameterError, parse_tool_call_arguments
|
|
26
|
+
from ..errors import UnsupportedOperationError, UnsupportedParameterError, parse_tool_call_arguments
|
|
27
27
|
from ..types import (
|
|
28
28
|
EventType,
|
|
29
29
|
FinishReason,
|
|
@@ -36,6 +36,7 @@ from ..types import (
|
|
|
36
36
|
UniMessage,
|
|
37
37
|
UsageMetadata,
|
|
38
38
|
)
|
|
39
|
+
from ..utils import is_debug_enabled
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
REDACTED_THINKING = "_REDACTED_THINKING"
|
|
@@ -44,7 +45,13 @@ REDACTED_THINKING = "_REDACTED_THINKING"
|
|
|
44
45
|
class Claude5Client(LLMClient):
|
|
45
46
|
"""Claude 5-specific LLM client implementation (also serves Claude 4.6 through 4.8)."""
|
|
46
47
|
|
|
47
|
-
def __init__(
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
model: str,
|
|
51
|
+
api_key: str | None = None,
|
|
52
|
+
base_url: str | None = None,
|
|
53
|
+
default_headers: dict[str, str] | None = None,
|
|
54
|
+
):
|
|
48
55
|
"""Initialize Claude 5 client with model and API key."""
|
|
49
56
|
self._model = model
|
|
50
57
|
api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
@@ -53,11 +60,14 @@ class Claude5Client(LLMClient):
|
|
|
53
60
|
region = base_url.replace("bedrock://", "")
|
|
54
61
|
access_key, secret_key = api_key.split(",")
|
|
55
62
|
self._client = AsyncAnthropicBedrock(
|
|
56
|
-
aws_secret_key=secret_key,
|
|
63
|
+
aws_secret_key=secret_key,
|
|
64
|
+
aws_access_key=access_key,
|
|
65
|
+
aws_region=region,
|
|
66
|
+
default_headers=default_headers,
|
|
57
67
|
)
|
|
58
68
|
self._use_bedrock = True
|
|
59
69
|
else:
|
|
60
|
-
self._client = AsyncAnthropic(api_key=api_key, base_url=base_url)
|
|
70
|
+
self._client = AsyncAnthropic(api_key=api_key, base_url=base_url, default_headers=default_headers)
|
|
61
71
|
self._use_bedrock = False
|
|
62
72
|
|
|
63
73
|
self._history: list[UniMessage] = []
|
|
@@ -356,9 +366,14 @@ class Claude5Client(LLMClient):
|
|
|
356
366
|
# gateways that relabel it onto another event
|
|
357
367
|
event_type = "unused"
|
|
358
368
|
|
|
359
|
-
|
|
369
|
+
elif is_debug_enabled():
|
|
360
370
|
raise ValueError(f"Unknown output: {model_output}")
|
|
361
371
|
|
|
372
|
+
else:
|
|
373
|
+
# a gateway injects its own events (heartbeats, cost tickers) into the stream, and
|
|
374
|
+
# killing a long generation over one costs more than dropping it
|
|
375
|
+
event_type = "unused"
|
|
376
|
+
|
|
362
377
|
return {
|
|
363
378
|
"role": "assistant",
|
|
364
379
|
"event_type": event_type,
|
|
@@ -467,3 +482,17 @@ class Claude5Client(LLMClient):
|
|
|
467
482
|
"finish_reason": event["finish_reason"],
|
|
468
483
|
}
|
|
469
484
|
partial_usage = {}
|
|
485
|
+
|
|
486
|
+
async def list_models(self) -> list[str]:
|
|
487
|
+
"""
|
|
488
|
+
List the model ids the configured endpoint serves.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
list[str]: The model ids, in the order the endpoint returned them.
|
|
492
|
+
"""
|
|
493
|
+
if self._use_bedrock:
|
|
494
|
+
raise UnsupportedOperationError(
|
|
495
|
+
self.__class__.__name__, "list_models", "Bedrock does not support listing models."
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
return [model.id async for model in self._client.models.list()]
|
|
@@ -38,12 +38,18 @@ from ..types import (
|
|
|
38
38
|
class DeepSeekV4Client(LLMClient):
|
|
39
39
|
"""DeepSeek V4-specific LLM client implementation using OpenAI-compatible Chat Completions."""
|
|
40
40
|
|
|
41
|
-
def __init__(
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
model: str,
|
|
44
|
+
api_key: str | None = None,
|
|
45
|
+
base_url: str | None = None,
|
|
46
|
+
default_headers: dict[str, str] | None = None,
|
|
47
|
+
):
|
|
42
48
|
"""Initialize DeepSeek client with model, API key, and base URL."""
|
|
43
49
|
self._model = model
|
|
44
50
|
api_key = api_key or os.getenv("DEEPSEEK_API_KEY")
|
|
45
51
|
base_url = base_url or os.getenv("DEEPSEEK_BASE_URL") or "https://api.deepseek.com"
|
|
46
|
-
self._client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
|
52
|
+
self._client = AsyncOpenAI(api_key=api_key, base_url=base_url, default_headers=default_headers)
|
|
47
53
|
self._history: list[UniMessage] = []
|
|
48
54
|
|
|
49
55
|
def _convert_thinking_level_to_config(self, thinking_level: ThinkingLevel) -> dict[str, str]:
|
|
@@ -374,3 +380,12 @@ class DeepSeekV4Client(LLMClient):
|
|
|
374
380
|
"finish_reason": partial_usage["finish_reason"],
|
|
375
381
|
}
|
|
376
382
|
partial_usage = {}
|
|
383
|
+
|
|
384
|
+
async def list_models(self) -> list[str]:
|
|
385
|
+
"""
|
|
386
|
+
List the model ids the configured endpoint serves.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
list[str]: The model ids, in the order the endpoint returned them.
|
|
390
|
+
"""
|
|
391
|
+
return [model.id async for model in self._client.models.list()]
|
|
@@ -43,6 +43,20 @@ class UnsupportedParameterError(AgentHubError):
|
|
|
43
43
|
super().__init__(message)
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
class UnsupportedOperationError(AgentHubError):
|
|
47
|
+
"""Raised when a client cannot perform an operation at all, whatever it is passed.
|
|
48
|
+
|
|
49
|
+
Distinct from UnsupportedParameterError, which rejects a UniConfig parameter value:
|
|
50
|
+
this one reports a capability the routed client does not have, such as listing models
|
|
51
|
+
through an SDK client that carries no models endpoint.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, client: str, operation: str, message: str) -> None:
|
|
55
|
+
self.client = client
|
|
56
|
+
self.operation = operation
|
|
57
|
+
super().__init__(message)
|
|
58
|
+
|
|
59
|
+
|
|
46
60
|
class EmptyResponseError(AgentHubError):
|
|
47
61
|
"""Raised when a completed response carries no non-thinking content and no tool calls.
|
|
48
62
|
|
|
@@ -17,7 +17,7 @@ import json
|
|
|
17
17
|
import mimetypes
|
|
18
18
|
import os
|
|
19
19
|
import re
|
|
20
|
-
from typing import AsyncIterator
|
|
20
|
+
from typing import Any, AsyncIterator
|
|
21
21
|
|
|
22
22
|
import httpx
|
|
23
23
|
from google import genai
|
|
@@ -40,6 +40,7 @@ from ..types import (
|
|
|
40
40
|
UniMessage,
|
|
41
41
|
UsageMetadata,
|
|
42
42
|
)
|
|
43
|
+
from ..utils import is_debug_enabled
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
class Gemini3_7Client(LLMClient):
|
|
@@ -52,12 +53,23 @@ class Gemini3_7Client(LLMClient):
|
|
|
52
53
|
applies that contract to the whole family: temperature is rejected everywhere.
|
|
53
54
|
"""
|
|
54
55
|
|
|
55
|
-
def __init__(
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
model: str,
|
|
59
|
+
api_key: str | None = None,
|
|
60
|
+
base_url: str | None = None,
|
|
61
|
+
default_headers: dict[str, str] | None = None,
|
|
62
|
+
):
|
|
56
63
|
"""Initialize Gemini 3.7 client with model and API key."""
|
|
57
64
|
self._model = model
|
|
58
65
|
api_key = api_key or os.getenv("GEMINI_API_KEY")
|
|
59
66
|
base_url = base_url or os.getenv("GEMINI_BASE_URL")
|
|
60
|
-
|
|
67
|
+
# the Gemini SDK carries connection headers inside http_options rather than its own argument
|
|
68
|
+
http_options: dict[str, Any] = {}
|
|
69
|
+
if base_url:
|
|
70
|
+
http_options["base_url"] = base_url
|
|
71
|
+
if default_headers:
|
|
72
|
+
http_options["headers"] = default_headers
|
|
61
73
|
if api_key and api_key.startswith("{"):
|
|
62
74
|
service_account_info = json.loads(api_key)
|
|
63
75
|
credentials = service_account.Credentials.from_service_account_info(
|
|
@@ -68,10 +80,10 @@ class Gemini3_7Client(LLMClient):
|
|
|
68
80
|
credentials=credentials,
|
|
69
81
|
project=service_account_info["project_id"],
|
|
70
82
|
location="global",
|
|
71
|
-
http_options=http_options,
|
|
83
|
+
http_options=http_options or None,
|
|
72
84
|
)
|
|
73
85
|
else:
|
|
74
|
-
self._client = genai.Client(api_key=api_key, http_options=http_options)
|
|
86
|
+
self._client = genai.Client(api_key=api_key, http_options=http_options or None)
|
|
75
87
|
|
|
76
88
|
self._history: list[UniMessage] = []
|
|
77
89
|
|
|
@@ -417,7 +429,7 @@ class Gemini3_7Client(LLMClient):
|
|
|
417
429
|
)
|
|
418
430
|
elif part.text is not None:
|
|
419
431
|
content_items.append({"type": "text", "text": part.text, **self._part_fidelity(part)})
|
|
420
|
-
|
|
432
|
+
elif is_debug_enabled():
|
|
421
433
|
raise ValueError(f"Unknown output: {part}")
|
|
422
434
|
|
|
423
435
|
if candidate.finish_reason:
|
|
@@ -543,3 +555,13 @@ class Gemini3_7Client(LLMClient):
|
|
|
543
555
|
}
|
|
544
556
|
|
|
545
557
|
yield event
|
|
558
|
+
|
|
559
|
+
async def list_models(self) -> list[str]:
|
|
560
|
+
"""
|
|
561
|
+
List the model ids the configured endpoint serves.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
list[str]: The model ids, in the order the endpoint returned them.
|
|
565
|
+
"""
|
|
566
|
+
# the API returns path-qualified names: models/gemini-3.7-flash, publishers/google/models/...
|
|
567
|
+
return [model.name.split("/")[-1] async for model in await self._client.aio.models.list() if model.name]
|
|
@@ -43,12 +43,18 @@ class GLM5_3Client(LLMClient):
|
|
|
43
43
|
contract differs per generation, handled model-by-model below.
|
|
44
44
|
"""
|
|
45
45
|
|
|
46
|
-
def __init__(
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
model: str,
|
|
49
|
+
api_key: str | None = None,
|
|
50
|
+
base_url: str | None = None,
|
|
51
|
+
default_headers: dict[str, str] | None = None,
|
|
52
|
+
):
|
|
47
53
|
"""Initialize GLM client with model and API key."""
|
|
48
54
|
self._model = model
|
|
49
55
|
api_key = api_key or os.getenv("ZAI_API_KEY")
|
|
50
56
|
base_url = base_url or os.getenv("ZAI_BASE_URL") or "https://api.z.ai/api/paas/v4/"
|
|
51
|
-
self._client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
|
57
|
+
self._client = AsyncOpenAI(api_key=api_key, base_url=base_url, default_headers=default_headers)
|
|
52
58
|
self._history: list[UniMessage] = []
|
|
53
59
|
|
|
54
60
|
def _convert_thinking_level_to_config(self, thinking_level: ThinkingLevel) -> dict[str, str | bool]:
|
|
@@ -437,3 +443,12 @@ class GLM5_3Client(LLMClient):
|
|
|
437
443
|
"finish_reason": partial_usage["finish_reason"],
|
|
438
444
|
}
|
|
439
445
|
partial_usage = {}
|
|
446
|
+
|
|
447
|
+
async def list_models(self) -> list[str]:
|
|
448
|
+
"""
|
|
449
|
+
List the model ids the configured endpoint serves.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
list[str]: The model ids, in the order the endpoint returned them.
|
|
453
|
+
"""
|
|
454
|
+
return [model.id async for model in self._client.models.list()]
|
|
@@ -33,17 +33,24 @@ from ..types import (
|
|
|
33
33
|
UniMessage,
|
|
34
34
|
UsageMetadata,
|
|
35
35
|
)
|
|
36
|
+
from ..utils import is_debug_enabled
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
class GPT5_6Client(LLMClient):
|
|
39
40
|
"""GPT-5.6-specific LLM client implementation (also serves GPT-5.4 and GPT-5.5)."""
|
|
40
41
|
|
|
41
|
-
def __init__(
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
model: str,
|
|
45
|
+
api_key: str | None = None,
|
|
46
|
+
base_url: str | None = None,
|
|
47
|
+
default_headers: dict[str, str] | None = None,
|
|
48
|
+
):
|
|
42
49
|
"""Initialize GPT-5.6 client with model and API key."""
|
|
43
50
|
self._model = model
|
|
44
51
|
api_key = api_key or os.getenv("OPENAI_API_KEY")
|
|
45
52
|
base_url = base_url or os.getenv("OPENAI_BASE_URL")
|
|
46
|
-
self._client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
|
53
|
+
self._client = AsyncOpenAI(api_key=api_key, base_url=base_url, default_headers=default_headers)
|
|
47
54
|
self._history: list[UniMessage] = []
|
|
48
55
|
|
|
49
56
|
def _convert_thinking_level_to_effort(self, thinking_level: ThinkingLevel) -> str:
|
|
@@ -306,9 +313,14 @@ class GPT5_6Client(LLMClient):
|
|
|
306
313
|
]:
|
|
307
314
|
event_type = "unused"
|
|
308
315
|
|
|
309
|
-
|
|
316
|
+
elif is_debug_enabled():
|
|
310
317
|
raise ValueError(f"Unknown output: {model_output}")
|
|
311
318
|
|
|
319
|
+
else:
|
|
320
|
+
# a gateway injects its own events (heartbeats, cost tickers) into the stream, and
|
|
321
|
+
# killing a long generation over one costs more than dropping it
|
|
322
|
+
event_type = "unused"
|
|
323
|
+
|
|
312
324
|
return {
|
|
313
325
|
"role": "assistant",
|
|
314
326
|
"event_type": event_type,
|
|
@@ -377,3 +389,12 @@ class GPT5_6Client(LLMClient):
|
|
|
377
389
|
|
|
378
390
|
if event["finish_reason"] or event["usage_metadata"]:
|
|
379
391
|
yield event
|
|
392
|
+
|
|
393
|
+
async def list_models(self) -> list[str]:
|
|
394
|
+
"""
|
|
395
|
+
List the model ids the configured endpoint serves.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
list[str]: The model ids, in the order the endpoint returned them.
|
|
399
|
+
"""
|
|
400
|
+
return [model.id async for model in self._client.models.list()]
|