agenthub-python 0.4.0__py3-none-any.whl → 0.4.2__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.
- agenthub/__init__.py +8 -1
- agenthub/ant_messages/__init__.py +18 -0
- agenthub/{claude4_6 → ant_messages}/client.py +117 -146
- agenthub/auto_client.py +37 -23
- agenthub/claude5/client.py +30 -5
- agenthub/deepseek_v4/client.py +15 -4
- agenthub/errors.py +14 -0
- agenthub/{claude4_6 → gemini3_7}/__init__.py +2 -2
- agenthub/{gemini3 → gemini3_7}/client.py +103 -16
- agenthub/{openai → glm5_3}/__init__.py +2 -2
- agenthub/{glm5_1 → glm5_3}/client.py +61 -15
- agenthub/{glm5_1 → gpt5_6}/__init__.py +2 -2
- agenthub/{gpt5_5 → gpt5_6}/client.py +54 -38
- agenthub/{gpt5_5 → kimi_k3}/__init__.py +2 -2
- agenthub/{kimi_k2_6 → kimi_k3}/client.py +51 -14
- agenthub/{gemini3 → minimax_m3}/__init__.py +2 -2
- agenthub/minimax_m3/client.py +315 -0
- agenthub/openai_chat/__init__.py +18 -0
- agenthub/{openai → openai_chat}/client.py +8 -3
- agenthub/openai_embedding/client.py +6 -0
- agenthub/openai_responses/__init__.py +18 -0
- agenthub/openai_responses/client.py +374 -0
- agenthub/registry.py +635 -0
- agenthub/types.py +3 -0
- {agenthub_python-0.4.0.dist-info → agenthub_python-0.4.2.dist-info}/METADATA +30 -41
- agenthub_python-0.4.2.dist-info/RECORD +36 -0
- {agenthub_python-0.4.0.dist-info → agenthub_python-0.4.2.dist-info}/WHEEL +1 -1
- agenthub/kimi_k2_6/__init__.py +0 -18
- agenthub_python-0.4.0.dist-info/RECORD +0 -31
agenthub/__init__.py
CHANGED
|
@@ -13,15 +13,22 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
from .auto_client import AutoLLMClient
|
|
16
|
-
from .errors import AgentHubError, EmptyResponseError, ToolCallArgumentParseError
|
|
16
|
+
from .errors import AgentHubError, EmptyResponseError, ToolCallArgumentParseError, UnsupportedParameterError
|
|
17
|
+
from .registry import Currency, Modality, ModelPricing, SupportedModel, list_supported_models
|
|
17
18
|
from .types import PromptCaching, ThinkingLevel
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
21
22
|
"AgentHubError",
|
|
22
23
|
"AutoLLMClient",
|
|
24
|
+
"Currency",
|
|
23
25
|
"EmptyResponseError",
|
|
26
|
+
"Modality",
|
|
27
|
+
"ModelPricing",
|
|
24
28
|
"PromptCaching",
|
|
29
|
+
"SupportedModel",
|
|
25
30
|
"ThinkingLevel",
|
|
26
31
|
"ToolCallArgumentParseError",
|
|
32
|
+
"UnsupportedParameterError",
|
|
33
|
+
"list_supported_models",
|
|
27
34
|
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Copyright 2025 Prism Shadow. and/or its affiliates
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from .client import AntMessagesClient
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["AntMessagesClient"]
|
|
@@ -12,18 +12,15 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
import base64
|
|
16
|
-
import mimetypes
|
|
17
15
|
import os
|
|
18
16
|
import re
|
|
19
17
|
from typing import Any, AsyncIterator
|
|
20
18
|
|
|
21
|
-
import
|
|
22
|
-
from anthropic import AsyncAnthropic, AsyncAnthropicBedrock
|
|
19
|
+
from anthropic import AsyncAnthropic
|
|
23
20
|
from anthropic.types.beta import BetaMessageParam, BetaRawMessageStreamEvent
|
|
24
21
|
|
|
25
22
|
from ..base_client import LLMClient
|
|
26
|
-
from ..errors import parse_tool_call_arguments
|
|
23
|
+
from ..errors import UnsupportedParameterError, parse_tool_call_arguments
|
|
27
24
|
from ..types import (
|
|
28
25
|
EventType,
|
|
29
26
|
FinishReason,
|
|
@@ -36,91 +33,60 @@ from ..types import (
|
|
|
36
33
|
UniMessage,
|
|
37
34
|
UsageMetadata,
|
|
38
35
|
)
|
|
36
|
+
from ..utils import fix_openrouter_usage_metadata
|
|
39
37
|
|
|
40
38
|
|
|
41
39
|
REDACTED_THINKING = "_REDACTED_THINKING"
|
|
42
40
|
|
|
43
41
|
|
|
44
|
-
class
|
|
45
|
-
"""
|
|
42
|
+
class AntMessagesClient(LLMClient):
|
|
43
|
+
"""Anthropic Messages-compatible client implementation."""
|
|
46
44
|
|
|
47
45
|
def __init__(self, model: str, api_key: str | None = None, base_url: str | None = None):
|
|
48
|
-
"""Initialize
|
|
46
|
+
"""Initialize Anthropic Messages-compatible client with model, API key, and base URL."""
|
|
49
47
|
self._model = model
|
|
50
48
|
api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
51
49
|
base_url = base_url or os.getenv("ANTHROPIC_BASE_URL")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
self._client = AsyncAnthropicBedrock(
|
|
56
|
-
aws_secret_key=secret_key, aws_access_key=access_key, aws_region=region
|
|
57
|
-
)
|
|
58
|
-
self._use_bedrock = True
|
|
59
|
-
else:
|
|
60
|
-
self._client = AsyncAnthropic(api_key=api_key, base_url=base_url)
|
|
61
|
-
self._use_bedrock = False
|
|
62
|
-
|
|
50
|
+
# send the credential through both header conventions: Anthropic and DeepSeek read
|
|
51
|
+
# x-api-key while gateways such as OpenRouter and Z.AI read Authorization: Bearer
|
|
52
|
+
self._client = AsyncAnthropic(api_key=api_key, auth_token=api_key, base_url=base_url)
|
|
63
53
|
self._history: list[UniMessage] = []
|
|
64
54
|
|
|
65
|
-
|
|
66
|
-
"""Convert image URL to image source.
|
|
67
|
-
|
|
68
|
-
Bedrock does not support image url sources, so we need to fetch the image bytes and encode them.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
url: Image URL to convert
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
Image source
|
|
75
|
-
"""
|
|
55
|
+
def _convert_image_url_to_source(self, url: str) -> dict[str, Any]:
|
|
56
|
+
"""Convert image URL to an Anthropic image source block."""
|
|
76
57
|
if url.startswith("data:"):
|
|
77
58
|
match = re.match(r"data:([^;]+);base64,(.+)", url)
|
|
78
|
-
if match:
|
|
79
|
-
media_type = match.group(1)
|
|
80
|
-
base64_data = match.group(2)
|
|
81
|
-
source = {
|
|
82
|
-
"type": "image",
|
|
83
|
-
"source": {"type": "base64", "media_type": media_type, "data": base64_data},
|
|
84
|
-
}
|
|
85
|
-
else:
|
|
59
|
+
if not match:
|
|
86
60
|
raise ValueError(f"Invalid base64 image: {url}")
|
|
87
|
-
elif self._use_bedrock:
|
|
88
|
-
async with httpx.AsyncClient() as client:
|
|
89
|
-
response = await client.get(url)
|
|
90
|
-
response.raise_for_status()
|
|
91
|
-
image_bytes = response.content
|
|
92
|
-
mime_type = mimetypes.guess_type(url)[0] or "image/jpeg"
|
|
93
|
-
source = {
|
|
94
|
-
"type": "image",
|
|
95
|
-
"source": {
|
|
96
|
-
"type": "base64",
|
|
97
|
-
"media_type": mime_type,
|
|
98
|
-
"data": base64.b64encode(image_bytes).decode("utf-8"),
|
|
99
|
-
},
|
|
100
|
-
}
|
|
101
|
-
else:
|
|
102
|
-
source = {"type": "image", "source": {"type": "url", "url": url}}
|
|
103
61
|
|
|
104
|
-
|
|
62
|
+
return {
|
|
63
|
+
"type": "image",
|
|
64
|
+
"source": {"type": "base64", "media_type": match.group(1), "data": match.group(2)},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {"type": "image", "source": {"type": "url", "url": url}}
|
|
105
68
|
|
|
106
69
|
def _convert_thinking_level_to_thinking_config(self, thinking_level: ThinkingLevel) -> dict[str, Any]:
|
|
107
|
-
"""Convert ThinkingLevel enum to
|
|
70
|
+
"""Convert ThinkingLevel enum to the Messages API thinking config."""
|
|
71
|
+
# NONE is explicit rather than omitted because some servers (e.g. Z.AI) think by default
|
|
108
72
|
mapping = {
|
|
109
|
-
ThinkingLevel.NONE: {},
|
|
73
|
+
ThinkingLevel.NONE: {"thinking": {"type": "disabled"}},
|
|
110
74
|
ThinkingLevel.LOW: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "low"}},
|
|
111
75
|
ThinkingLevel.MEDIUM: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "medium"}},
|
|
112
76
|
ThinkingLevel.HIGH: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "high"}},
|
|
113
|
-
ThinkingLevel.XHIGH: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "
|
|
77
|
+
ThinkingLevel.XHIGH: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "xhigh"}},
|
|
114
78
|
}
|
|
115
79
|
return mapping.get(thinking_level)
|
|
116
80
|
|
|
117
81
|
def _convert_tool_choice(self, tool_choice: ToolChoice) -> dict[str, str]:
|
|
118
|
-
"""Convert ToolChoice to
|
|
82
|
+
"""Convert ToolChoice to the Messages API tool_choice format."""
|
|
119
83
|
if isinstance(tool_choice, list):
|
|
120
84
|
if len(tool_choice) > 1:
|
|
121
|
-
raise
|
|
85
|
+
raise UnsupportedParameterError(
|
|
86
|
+
self.__class__.__name__, "tool_choice", "The Messages API does not support multiple tool choices."
|
|
87
|
+
)
|
|
122
88
|
|
|
123
|
-
return {"type": "
|
|
89
|
+
return {"type": "tool", "name": tool_choice[0]}
|
|
124
90
|
elif tool_choice == "none":
|
|
125
91
|
return {"type": "none"}
|
|
126
92
|
elif tool_choice == "auto":
|
|
@@ -130,70 +96,70 @@ class Claude4_6Client(LLMClient):
|
|
|
130
96
|
|
|
131
97
|
def transform_uni_config_to_model_config(self, config: UniConfig) -> dict[str, Any]:
|
|
132
98
|
"""
|
|
133
|
-
Transform universal configuration to
|
|
99
|
+
Transform universal configuration to Anthropic Messages-compatible configuration.
|
|
134
100
|
|
|
135
101
|
Args:
|
|
136
102
|
config: Universal configuration dict
|
|
137
103
|
|
|
138
104
|
Returns:
|
|
139
|
-
|
|
105
|
+
Anthropic Messages API configuration dictionary
|
|
140
106
|
"""
|
|
141
|
-
|
|
107
|
+
ant_config = {"model": self._model, "stream": True}
|
|
142
108
|
|
|
143
109
|
if config.get("system_prompt") is not None:
|
|
144
|
-
|
|
110
|
+
ant_config["system"] = config["system_prompt"]
|
|
145
111
|
|
|
146
112
|
if config.get("max_tokens") is not None:
|
|
147
|
-
|
|
113
|
+
ant_config["max_tokens"] = config["max_tokens"]
|
|
148
114
|
else:
|
|
149
|
-
|
|
115
|
+
ant_config["max_tokens"] = 64000 # the Messages API requires max_tokens to be specified
|
|
150
116
|
|
|
151
117
|
if config.get("temperature") is not None:
|
|
152
|
-
|
|
118
|
+
ant_config["temperature"] = config["temperature"]
|
|
153
119
|
|
|
154
|
-
# NOTE: Claude always provides thinking summary
|
|
155
120
|
if config.get("thinking_level") is not None:
|
|
156
|
-
|
|
157
|
-
|
|
121
|
+
ant_config.update(self._convert_thinking_level_to_thinking_config(config["thinking_level"]))
|
|
122
|
+
if config.get("thinking_summary") and ant_config.get("thinking", {}).get("type") == "adaptive":
|
|
123
|
+
ant_config["thinking"]["display"] = "summarized"
|
|
158
124
|
|
|
159
|
-
# Convert tools to
|
|
125
|
+
# Convert tools to the Messages API tool schema
|
|
160
126
|
if config.get("tools") is not None:
|
|
161
|
-
|
|
127
|
+
ant_tools = []
|
|
162
128
|
for tool in config["tools"]:
|
|
163
|
-
|
|
129
|
+
ant_tool = {}
|
|
164
130
|
for key, value in tool.items():
|
|
165
|
-
|
|
131
|
+
ant_tool[key.replace("parameters", "input_schema")] = value
|
|
166
132
|
|
|
167
|
-
|
|
133
|
+
ant_tools.append(ant_tool)
|
|
168
134
|
|
|
169
|
-
|
|
135
|
+
ant_config["tools"] = ant_tools
|
|
170
136
|
|
|
171
137
|
# Convert tool_choice
|
|
172
138
|
if config.get("tool_choice") is not None:
|
|
173
|
-
|
|
139
|
+
ant_config["tool_choice"] = self._convert_tool_choice(config["tool_choice"])
|
|
140
|
+
|
|
141
|
+
if config.get("fast_mode"):
|
|
142
|
+
ant_config["speed"] = "fast"
|
|
143
|
+
ant_config["betas"] = ["fast-mode-2026-02-01"]
|
|
174
144
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if prompt_caching == PromptCaching.ENABLE:
|
|
180
|
-
claude_config["cache_control"] = {"type": "ephemeral"}
|
|
181
|
-
elif prompt_caching == PromptCaching.ENHANCE:
|
|
182
|
-
claude_config["cache_control"] = {"type": "ephemeral", "ttl": "1h"}
|
|
145
|
+
if config.get("prompt_caching") is not None and config["prompt_caching"] != PromptCaching.ENABLE:
|
|
146
|
+
raise UnsupportedParameterError(
|
|
147
|
+
self.__class__.__name__, "prompt_caching", "prompt_caching must be ENABLE for the Messages API."
|
|
148
|
+
)
|
|
183
149
|
|
|
184
|
-
return
|
|
150
|
+
return ant_config
|
|
185
151
|
|
|
186
|
-
|
|
152
|
+
def transform_uni_message_to_model_input(self, messages: list[UniMessage]) -> list[BetaMessageParam]:
|
|
187
153
|
"""
|
|
188
|
-
Transform universal message format to
|
|
154
|
+
Transform universal message format to the Messages API BetaMessageParam format.
|
|
189
155
|
|
|
190
156
|
Args:
|
|
191
157
|
messages: List of universal message dictionaries
|
|
192
158
|
|
|
193
159
|
Returns:
|
|
194
|
-
List of
|
|
160
|
+
List of Messages API BetaMessageParam objects
|
|
195
161
|
"""
|
|
196
|
-
|
|
162
|
+
ant_messages: list[BetaMessageParam] = []
|
|
197
163
|
|
|
198
164
|
for msg in messages:
|
|
199
165
|
content_blocks = []
|
|
@@ -201,18 +167,19 @@ class Claude4_6Client(LLMClient):
|
|
|
201
167
|
if item["type"] == "text":
|
|
202
168
|
content_blocks.append({"type": "text", "text": item["text"]})
|
|
203
169
|
elif item["type"] == "image_url":
|
|
204
|
-
content_blocks.append(
|
|
170
|
+
content_blocks.append(self._convert_image_url_to_source(item["image_url"]))
|
|
205
171
|
elif item["type"] == "thinking":
|
|
206
172
|
if item["thinking"] == REDACTED_THINKING:
|
|
207
173
|
content_blocks.append({"type": "redacted_thinking", "data": item["fidelity"]["signature"]})
|
|
208
174
|
else:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
175
|
+
# third-party servers accept thinking without a signature, but the
|
|
176
|
+
# official API requires the one it emitted
|
|
177
|
+
thinking_block = {"type": "thinking", "thinking": item["thinking"]}
|
|
178
|
+
signature = (item.get("fidelity") or {}).get("signature")
|
|
179
|
+
if signature is not None:
|
|
180
|
+
thinking_block["signature"] = signature
|
|
181
|
+
|
|
182
|
+
content_blocks.append(thinking_block)
|
|
216
183
|
elif item["type"] == "tool_call":
|
|
217
184
|
content_blocks.append(
|
|
218
185
|
{
|
|
@@ -229,7 +196,7 @@ class Claude4_6Client(LLMClient):
|
|
|
229
196
|
tool_result = [{"type": "text", "text": item["text"]}]
|
|
230
197
|
if "images" in item:
|
|
231
198
|
for image_url in item["images"]:
|
|
232
|
-
tool_result.append(
|
|
199
|
+
tool_result.append(self._convert_image_url_to_source(image_url))
|
|
233
200
|
|
|
234
201
|
content_blocks.append(
|
|
235
202
|
{"type": "tool_result", "content": tool_result, "tool_use_id": item["tool_call_id"]}
|
|
@@ -237,18 +204,18 @@ class Claude4_6Client(LLMClient):
|
|
|
237
204
|
else:
|
|
238
205
|
raise ValueError(f"Unknown item: {item}")
|
|
239
206
|
|
|
240
|
-
|
|
207
|
+
ant_messages.append({"role": msg["role"], "content": content_blocks})
|
|
241
208
|
|
|
242
|
-
return
|
|
209
|
+
return ant_messages
|
|
243
210
|
|
|
244
211
|
def transform_model_output_to_uni_event(self, model_output: BetaRawMessageStreamEvent) -> UniEvent:
|
|
245
212
|
"""
|
|
246
|
-
Transform
|
|
213
|
+
Transform a Messages API streaming event to universal event format.
|
|
247
214
|
|
|
248
|
-
NOTE:
|
|
215
|
+
NOTE: the Messages API always has only one content item per event.
|
|
249
216
|
|
|
250
217
|
Args:
|
|
251
|
-
model_output:
|
|
218
|
+
model_output: Messages API streaming event
|
|
252
219
|
|
|
253
220
|
Returns:
|
|
254
221
|
Universal event dictionary
|
|
@@ -258,8 +225,8 @@ class Claude4_6Client(LLMClient):
|
|
|
258
225
|
usage_metadata: UsageMetadata | None = None
|
|
259
226
|
finish_reason: FinishReason | None = None
|
|
260
227
|
|
|
261
|
-
|
|
262
|
-
if
|
|
228
|
+
ant_event_type = model_output.type
|
|
229
|
+
if ant_event_type == "content_block_start":
|
|
263
230
|
event_type = "start"
|
|
264
231
|
block = model_output.content_block
|
|
265
232
|
if block.type == "tool_use":
|
|
@@ -271,7 +238,7 @@ class Claude4_6Client(LLMClient):
|
|
|
271
238
|
{"type": "thinking", "thinking": REDACTED_THINKING, "fidelity": {"signature": block.data}}
|
|
272
239
|
)
|
|
273
240
|
|
|
274
|
-
elif
|
|
241
|
+
elif ant_event_type == "content_block_delta":
|
|
275
242
|
event_type = "delta"
|
|
276
243
|
delta = model_output.delta
|
|
277
244
|
if delta.type == "thinking_delta":
|
|
@@ -285,10 +252,10 @@ class Claude4_6Client(LLMClient):
|
|
|
285
252
|
elif delta.type == "signature_delta":
|
|
286
253
|
content_items.append({"type": "thinking", "thinking": "", "fidelity": {"signature": delta.signature}})
|
|
287
254
|
|
|
288
|
-
elif
|
|
255
|
+
elif ant_event_type == "content_block_stop":
|
|
289
256
|
event_type = "stop"
|
|
290
257
|
|
|
291
|
-
elif
|
|
258
|
+
elif ant_event_type == "message_start":
|
|
292
259
|
event_type = "start"
|
|
293
260
|
message = model_output.message
|
|
294
261
|
if getattr(message, "usage", None):
|
|
@@ -300,7 +267,7 @@ class Claude4_6Client(LLMClient):
|
|
|
300
267
|
"response_tokens": None,
|
|
301
268
|
}
|
|
302
269
|
|
|
303
|
-
elif
|
|
270
|
+
elif ant_event_type == "message_delta":
|
|
304
271
|
event_type = "stop"
|
|
305
272
|
delta = model_output.delta
|
|
306
273
|
if getattr(delta, "stop_reason", None):
|
|
@@ -312,19 +279,28 @@ class Claude4_6Client(LLMClient):
|
|
|
312
279
|
}
|
|
313
280
|
finish_reason = stop_reason_mapping.get(delta.stop_reason, "unknown")
|
|
314
281
|
|
|
315
|
-
|
|
316
|
-
|
|
282
|
+
usage = getattr(model_output, "usage", None)
|
|
283
|
+
if usage:
|
|
284
|
+
# gateways report zero usage in message_start and the full counts here, so the
|
|
285
|
+
# delta also carries the input-side fields (None on servers that omit them)
|
|
286
|
+
if usage.input_tokens is not None:
|
|
287
|
+
prompt_tokens = usage.input_tokens + (usage.cache_creation_input_tokens or 0)
|
|
288
|
+
else:
|
|
289
|
+
prompt_tokens = None
|
|
290
|
+
|
|
291
|
+
output_details = getattr(usage, "output_tokens_details", None)
|
|
292
|
+
thinking_tokens = getattr(output_details, "thinking_tokens", None) if output_details else None
|
|
317
293
|
usage_metadata = {
|
|
318
|
-
"cached_tokens":
|
|
319
|
-
"prompt_tokens":
|
|
320
|
-
"thoughts_tokens":
|
|
321
|
-
"response_tokens":
|
|
294
|
+
"cached_tokens": usage.cache_read_input_tokens,
|
|
295
|
+
"prompt_tokens": prompt_tokens,
|
|
296
|
+
"thoughts_tokens": thinking_tokens,
|
|
297
|
+
"response_tokens": usage.output_tokens - (thinking_tokens or 0),
|
|
322
298
|
}
|
|
323
299
|
|
|
324
|
-
elif
|
|
300
|
+
elif ant_event_type == "message_stop":
|
|
325
301
|
event_type = "stop"
|
|
326
302
|
|
|
327
|
-
elif
|
|
303
|
+
elif ant_event_type in ["text", "thinking", "signature", "input_json"]:
|
|
328
304
|
event_type = "unused"
|
|
329
305
|
|
|
330
306
|
else:
|
|
@@ -343,32 +319,17 @@ class Claude4_6Client(LLMClient):
|
|
|
343
319
|
messages: list[UniMessage],
|
|
344
320
|
config: UniConfig,
|
|
345
321
|
) -> AsyncIterator[UniEvent]:
|
|
346
|
-
"""Stream generate using
|
|
322
|
+
"""Stream generate using an Anthropic Messages-compatible API with unified conversion methods."""
|
|
347
323
|
# Use unified config conversion
|
|
348
|
-
|
|
324
|
+
ant_config = self.transform_uni_config_to_model_config(config)
|
|
349
325
|
|
|
350
326
|
# Use unified message conversion
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
# Add cache_control to last user message's last item if using bedrock and enabled prompt caching
|
|
354
|
-
# TODO: remove after bedrock supports cache_control in config
|
|
355
|
-
if self._use_bedrock:
|
|
356
|
-
prompt_caching = config.get("prompt_caching", PromptCaching.ENABLE)
|
|
357
|
-
if prompt_caching != PromptCaching.DISABLE and claude_messages:
|
|
358
|
-
try:
|
|
359
|
-
last_user_message = next(filter(lambda x: x["role"] == "user", claude_messages[::-1]))
|
|
360
|
-
last_content_item = last_user_message["content"][-1]
|
|
361
|
-
last_content_item["cache_control"] = {
|
|
362
|
-
"type": "ephemeral",
|
|
363
|
-
"ttl": "1h" if prompt_caching == PromptCaching.ENHANCE else "5m",
|
|
364
|
-
}
|
|
365
|
-
except StopIteration:
|
|
366
|
-
pass
|
|
327
|
+
ant_messages = self.transform_uni_message_to_model_input(messages)
|
|
367
328
|
|
|
368
329
|
# Stream generate
|
|
369
330
|
partial_tool_call = {}
|
|
370
331
|
partial_usage = {}
|
|
371
|
-
stream = await self._client.beta.messages.create(**
|
|
332
|
+
stream = await self._client.beta.messages.create(**ant_config, messages=ant_messages)
|
|
372
333
|
async for event in stream:
|
|
373
334
|
event = self.transform_model_output_to_uni_event(event)
|
|
374
335
|
if event["event_type"] == "start":
|
|
@@ -423,18 +384,28 @@ class Claude4_6Client(LLMClient):
|
|
|
423
384
|
}
|
|
424
385
|
partial_tool_call = {}
|
|
425
386
|
|
|
426
|
-
if
|
|
427
|
-
# finish partial_usage
|
|
387
|
+
if event["usage_metadata"] is not None:
|
|
388
|
+
# finish partial_usage: the message_delta counts win over message_start
|
|
389
|
+
delta_usage = event["usage_metadata"]
|
|
390
|
+
usage_metadata = {
|
|
391
|
+
"prompt_tokens": (
|
|
392
|
+
delta_usage["prompt_tokens"]
|
|
393
|
+
if delta_usage["prompt_tokens"] is not None
|
|
394
|
+
else partial_usage.get("prompt_tokens")
|
|
395
|
+
),
|
|
396
|
+
"cached_tokens": (
|
|
397
|
+
delta_usage["cached_tokens"]
|
|
398
|
+
if delta_usage["cached_tokens"] is not None
|
|
399
|
+
else partial_usage.get("cached_tokens")
|
|
400
|
+
),
|
|
401
|
+
"thoughts_tokens": delta_usage["thoughts_tokens"],
|
|
402
|
+
"response_tokens": delta_usage["response_tokens"],
|
|
403
|
+
}
|
|
428
404
|
yield {
|
|
429
405
|
"role": "assistant",
|
|
430
406
|
"event_type": "stop",
|
|
431
407
|
"content_items": [],
|
|
432
|
-
"usage_metadata":
|
|
433
|
-
"prompt_tokens": partial_usage["prompt_tokens"],
|
|
434
|
-
"thoughts_tokens": None,
|
|
435
|
-
"response_tokens": event["usage_metadata"]["response_tokens"],
|
|
436
|
-
"cached_tokens": partial_usage["cached_tokens"],
|
|
437
|
-
},
|
|
408
|
+
"usage_metadata": fix_openrouter_usage_metadata(usage_metadata, str(self._client.base_url)),
|
|
438
409
|
"finish_reason": event["finish_reason"],
|
|
439
410
|
}
|
|
440
411
|
partial_usage = {}
|
agenthub/auto_client.py
CHANGED
|
@@ -46,51 +46,65 @@ class AutoLLMClient(LLMClient):
|
|
|
46
46
|
self, model: str, api_key: str | None = None, base_url: str | None = None, client_type: str | None = None
|
|
47
47
|
) -> LLMClient:
|
|
48
48
|
"""Create the appropriate client for the given model."""
|
|
49
|
-
client_type = (client_type or os.getenv("CLIENT_TYPE"
|
|
49
|
+
client_type = (client_type or os.getenv("CLIENT_TYPE") or model).lower()
|
|
50
|
+
if client_type == "minimax-m3":
|
|
51
|
+
from .minimax_m3 import MiniMaxM3Client
|
|
52
|
+
|
|
53
|
+
return MiniMaxM3Client(model=model, api_key=api_key, base_url=base_url)
|
|
54
|
+
# every Gemini generation shares the unified client ("gemini-3" also matches the
|
|
55
|
+
# gemini-3.7/gemini-3.6/gemini-3.5-flash-lite client types)
|
|
50
56
|
if any(
|
|
51
57
|
prefix in client_type for prefix in ("gemini-3", "gemini-embedding")
|
|
52
|
-
): # e.g., gemini-3-flash-preview, gemini-embedding-2
|
|
53
|
-
from .
|
|
58
|
+
): # e.g., gemini-3.7-flash, gemini-3-flash-preview, gemini-embedding-2
|
|
59
|
+
from .gemini3_7 import Gemini3_7Client
|
|
54
60
|
|
|
55
|
-
return
|
|
61
|
+
return Gemini3_7Client(model=model, api_key=api_key, base_url=base_url)
|
|
56
62
|
elif "claude" in client_type and (
|
|
57
|
-
"4-7" in client_type or "4-8" in client_type or "-5" in client_type
|
|
58
|
-
): # e.g., claude-
|
|
63
|
+
"4-6" in client_type or "4-7" in client_type or "4-8" in client_type or "-5" in client_type
|
|
64
|
+
): # the whole Claude 4.6+ series shares the unified client, e.g., claude-sonnet-4-6
|
|
59
65
|
from .claude5 import Claude5Client
|
|
60
66
|
|
|
61
67
|
return Claude5Client(model=model, api_key=api_key, base_url=base_url)
|
|
62
|
-
elif "
|
|
63
|
-
from .
|
|
64
|
-
|
|
65
|
-
return Claude4_6Client(model=model, api_key=api_key, base_url=base_url)
|
|
66
|
-
elif "gpt-5.4" in client_type or "gpt-5.5" in client_type: # e.g., gpt-5.5
|
|
67
|
-
from .gpt5_5 import GPT5_5Client
|
|
68
|
+
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
|
|
69
|
+
from .gpt5_6 import GPT5_6Client
|
|
68
70
|
|
|
69
|
-
return
|
|
70
|
-
elif "glm-5" in client_type
|
|
71
|
-
from .
|
|
71
|
+
return GPT5_6Client(model=model, api_key=api_key, base_url=base_url)
|
|
72
|
+
elif "glm-5" in client_type: # the whole GLM series shares the unified client
|
|
73
|
+
from .glm5_3 import GLM5_3Client
|
|
72
74
|
|
|
73
|
-
return
|
|
74
|
-
elif "kimi-k2.5" in client_type or "kimi-k2.6" in client_type:
|
|
75
|
-
|
|
75
|
+
return GLM5_3Client(model=model, api_key=api_key, base_url=base_url)
|
|
76
|
+
elif "kimi-k3" in client_type or "kimi-k2.5" in client_type or "kimi-k2.6" in client_type:
|
|
77
|
+
# the whole Kimi K2.5+ series shares the unified client
|
|
78
|
+
from .kimi_k3 import KimiK3Client
|
|
76
79
|
|
|
77
|
-
return
|
|
80
|
+
return KimiK3Client(model=model, api_key=api_key, base_url=base_url)
|
|
78
81
|
elif "deepseek-v4" in client_type:
|
|
79
82
|
from .deepseek_v4 import DeepSeekV4Client
|
|
80
83
|
|
|
81
84
|
return DeepSeekV4Client(model=model, api_key=api_key, base_url=base_url)
|
|
85
|
+
elif "ant-messages" in client_type:
|
|
86
|
+
from .ant_messages import AntMessagesClient
|
|
87
|
+
|
|
88
|
+
return AntMessagesClient(model=model, api_key=api_key, base_url=base_url)
|
|
89
|
+
elif "openai-responses" in client_type:
|
|
90
|
+
from .openai_responses import OpenaiResponsesClient
|
|
91
|
+
|
|
92
|
+
return OpenaiResponsesClient(model=model, api_key=api_key, base_url=base_url)
|
|
82
93
|
elif "openai" in client_type and "embedding" in client_type:
|
|
83
94
|
from .openai_embedding import OpenaiEmbeddingClient
|
|
84
95
|
|
|
85
96
|
return OpenaiEmbeddingClient(model=model, api_key=api_key, base_url=base_url)
|
|
86
|
-
elif "openai" in client_type and "embedding" not in client_type:
|
|
87
|
-
from .
|
|
97
|
+
elif "openai" in client_type and "embedding" not in client_type: # openai-chat, plus bare "openai" as alias
|
|
98
|
+
from .openai_chat import OpenaiChatClient
|
|
88
99
|
|
|
89
|
-
return
|
|
100
|
+
return OpenaiChatClient(model=model, api_key=api_key, base_url=base_url)
|
|
90
101
|
else:
|
|
91
102
|
raise ValueError(
|
|
92
103
|
f"{client_type} is not supported. "
|
|
93
|
-
"Supported client types:
|
|
104
|
+
"Supported client types: minimax-m3, gemini-3.7, gemini-3.6, gemini-3, "
|
|
105
|
+
"claude-5, claude-4-8, claude-4-7, claude-4-6, gpt-5.6, gpt-5.5, gpt-5.4, "
|
|
106
|
+
"glm-5.3, glm-5.2, glm-5.1, kimi-k3, kimi-k2.6, kimi-k2.5, deepseek-v4, "
|
|
107
|
+
"openai-embedding, ant-messages, openai-responses, openai-chat."
|
|
94
108
|
)
|
|
95
109
|
|
|
96
110
|
def transform_uni_config_to_model_config(self, config: UniConfig) -> Any:
|