agenthub-python 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agenthub_python-0.1.0 → agenthub_python-0.2.0}/PKG-INFO +3 -3
- {agenthub_python-0.1.0 → agenthub_python-0.2.0}/agenthub/__init__.py +2 -3
- {agenthub_python-0.1.0 → agenthub_python-0.2.0}/agenthub/auto_client.py +31 -10
- {agenthub_python-0.1.0 → agenthub_python-0.2.0}/agenthub/base_client.py +7 -3
- {agenthub_python-0.1.0 → agenthub_python-0.2.0}/agenthub/claude4_5/client.py +65 -33
- {agenthub_python-0.1.0 → agenthub_python-0.2.0}/agenthub/gemini3/client.py +47 -20
- agenthub_python-0.2.0/agenthub/glm4_7/__init__.py +18 -0
- agenthub_python-0.2.0/agenthub/glm4_7/client.py +302 -0
- agenthub_python-0.2.0/agenthub/gpt5_2/__init__.py +18 -0
- agenthub_python-0.2.0/agenthub/gpt5_2/client.py +320 -0
- agenthub_python-0.2.0/agenthub/integration/__init__.py +14 -0
- agenthub_python-0.2.0/agenthub/integration/playground.py +771 -0
- {agenthub_python-0.1.0/agenthub → agenthub_python-0.2.0/agenthub/integration}/tracer.py +42 -14
- agenthub_python-0.2.0/agenthub/qwen3/__init__.py +18 -0
- agenthub_python-0.2.0/agenthub/qwen3/client.py +330 -0
- {agenthub_python-0.1.0 → agenthub_python-0.2.0}/agenthub/types.py +19 -19
- {agenthub_python-0.1.0 → agenthub_python-0.2.0}/pyproject.toml +4 -4
- {agenthub_python-0.1.0 → agenthub_python-0.2.0}/agenthub/claude4_5/__init__.py +0 -0
- {agenthub_python-0.1.0 → agenthub_python-0.2.0}/agenthub/gemini3/__init__.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: agenthub-python
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: AgentHub
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: AgentHub is the only SDK you need to connect to state-of-the-art LLMs
|
|
5
5
|
Requires-Dist: google-genai>=1.5.0
|
|
6
|
-
Requires-Dist: httpx[socks]
|
|
7
6
|
Requires-Dist: anthropic>=0.40.0
|
|
8
7
|
Requires-Dist: flask>=3.0.0
|
|
8
|
+
Requires-Dist: openai>=1.0.0
|
|
9
9
|
Requires-Python: >=3.11
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
from .auto_client import AutoLLMClient
|
|
16
|
-
from .
|
|
17
|
-
from .types import ThinkingLevel
|
|
16
|
+
from .types import PromptCaching, ThinkingLevel
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
__all__ = ["AutoLLMClient", "
|
|
19
|
+
__all__ = ["AutoLLMClient", "PromptCaching", "ThinkingLevel"]
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
import os
|
|
15
16
|
from typing import Any, AsyncIterator
|
|
16
17
|
|
|
17
18
|
from .base_client import LLMClient
|
|
@@ -26,30 +27,50 @@ class AutoLLMClient(LLMClient):
|
|
|
26
27
|
conversation history for that specific model.
|
|
27
28
|
"""
|
|
28
29
|
|
|
29
|
-
def __init__(
|
|
30
|
+
def __init__(
|
|
31
|
+
self, model: str, api_key: str | None = None, base_url: str | None = None, client_type: str | None = None
|
|
32
|
+
):
|
|
30
33
|
"""
|
|
31
34
|
Initialize AutoLLMClient with a specific model.
|
|
32
35
|
|
|
33
36
|
Args:
|
|
34
37
|
model: Model identifier (determines which client to use)
|
|
35
38
|
api_key: Optional API key
|
|
39
|
+
base_url: Optional base URL for API requests
|
|
40
|
+
client_type: Optional client type override
|
|
36
41
|
"""
|
|
37
|
-
self._client = self._create_client_for_model(model, api_key)
|
|
42
|
+
self._client = self._create_client_for_model(model, api_key, base_url, client_type)
|
|
38
43
|
|
|
39
|
-
def _create_client_for_model(
|
|
44
|
+
def _create_client_for_model(
|
|
45
|
+
self, model: str, api_key: str | None = None, base_url: str | None = None, client_type: str | None = None
|
|
46
|
+
) -> LLMClient:
|
|
40
47
|
"""Create the appropriate client for the given model."""
|
|
41
|
-
|
|
48
|
+
client_type = client_type or os.getenv("CLIENT_TYPE", model.lower())
|
|
49
|
+
if "gemini-3" in client_type: # e.g., gemini-3-flash-preview
|
|
42
50
|
from .gemini3 import Gemini3Client
|
|
43
51
|
|
|
44
|
-
return Gemini3Client(model=model, api_key=api_key)
|
|
45
|
-
elif "claude" in
|
|
52
|
+
return Gemini3Client(model=model, api_key=api_key, base_url=base_url)
|
|
53
|
+
elif "claude" in client_type and "4-5" in client_type: # e.g., claude-sonnet-4-5
|
|
46
54
|
from .claude4_5 import Claude4_5Client
|
|
47
55
|
|
|
48
|
-
return Claude4_5Client(model=model, api_key=api_key)
|
|
49
|
-
elif "gpt-5.2" in
|
|
50
|
-
|
|
56
|
+
return Claude4_5Client(model=model, api_key=api_key, base_url=base_url)
|
|
57
|
+
elif "gpt-5.1" in client_type or "gpt-5.2" in client_type: # e.g., gpt-5.2
|
|
58
|
+
from .gpt5_2 import GPT5_2Client
|
|
59
|
+
|
|
60
|
+
return GPT5_2Client(model=model, api_key=api_key, base_url=base_url)
|
|
61
|
+
elif "glm-4.7" in client_type: # e.g., glm-4.7
|
|
62
|
+
from .glm4_7 import GLM4_7Client
|
|
63
|
+
|
|
64
|
+
return GLM4_7Client(model=model, api_key=api_key, base_url=base_url)
|
|
65
|
+
elif "qwen3" in client_type:
|
|
66
|
+
from .qwen3 import Qwen3Client
|
|
67
|
+
|
|
68
|
+
return Qwen3Client(model=model, api_key=api_key, base_url=base_url)
|
|
51
69
|
else:
|
|
52
|
-
raise ValueError(
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"{client_type} is not supported. "
|
|
72
|
+
"Supported client types: gemini-3, claude-4-5, gpt-5.2, glm-4.7, qwen3."
|
|
73
|
+
)
|
|
53
74
|
|
|
54
75
|
def transform_uni_config_to_model_config(self, config: UniConfig) -> Any:
|
|
55
76
|
"""Delegate to underlying client's transform_uni_config_to_model_config."""
|
|
@@ -26,6 +26,7 @@ class LLMClient(ABC):
|
|
|
26
26
|
the required abstract methods for complete SDK abstraction.
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
+
_model: str
|
|
29
30
|
_history: list[UniMessage] = []
|
|
30
31
|
|
|
31
32
|
@abstractmethod
|
|
@@ -99,8 +100,11 @@ class LLMClient(ABC):
|
|
|
99
100
|
content_items[-1]["thinking"] += item["thinking"]
|
|
100
101
|
if "signature" in item: # signature may appear at the last item
|
|
101
102
|
content_items[-1]["signature"] = item["signature"]
|
|
102
|
-
elif item["thinking"]: # omit empty thinking items
|
|
103
|
+
elif item["thinking"] or item.get("signature"): # omit empty thinking items
|
|
103
104
|
content_items.append(item.copy())
|
|
105
|
+
elif item["type"] == "partial_tool_call":
|
|
106
|
+
# Skip partial_tool_call items - they should already be converted to tool_call
|
|
107
|
+
pass
|
|
104
108
|
else:
|
|
105
109
|
content_items.append(item.copy())
|
|
106
110
|
|
|
@@ -171,10 +175,10 @@ class LLMClient(ABC):
|
|
|
171
175
|
|
|
172
176
|
# Save history to file if trace_id is specified
|
|
173
177
|
if config.get("trace_id"):
|
|
174
|
-
from .tracer import Tracer
|
|
178
|
+
from .integration.tracer import Tracer
|
|
175
179
|
|
|
176
180
|
tracer = Tracer()
|
|
177
|
-
tracer.save_history(self._history, config["trace_id"], config)
|
|
181
|
+
tracer.save_history(self._model, self._history, config["trace_id"], config)
|
|
178
182
|
|
|
179
183
|
def clear_history(self) -> None:
|
|
180
184
|
"""Clear the message history."""
|
|
@@ -21,9 +21,10 @@ from anthropic.types import MessageParam, MessageStreamEvent
|
|
|
21
21
|
|
|
22
22
|
from ..base_client import LLMClient
|
|
23
23
|
from ..types import (
|
|
24
|
+
EventType,
|
|
24
25
|
FinishReason,
|
|
25
26
|
PartialContentItem,
|
|
26
|
-
|
|
27
|
+
PromptCaching,
|
|
27
28
|
ThinkingLevel,
|
|
28
29
|
ToolChoice,
|
|
29
30
|
UniConfig,
|
|
@@ -36,14 +37,15 @@ from ..types import (
|
|
|
36
37
|
class Claude4_5Client(LLMClient):
|
|
37
38
|
"""Claude 4.5-specific LLM client implementation."""
|
|
38
39
|
|
|
39
|
-
def __init__(self, model: str, api_key: str | None = None):
|
|
40
|
+
def __init__(self, model: str, api_key: str | None = None, base_url: str | None = None):
|
|
40
41
|
"""Initialize Claude 4.5 client with model and API key."""
|
|
41
42
|
self._model = model
|
|
42
43
|
api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
43
|
-
|
|
44
|
+
base_url = base_url or os.getenv("ANTHROPIC_BASE_URL")
|
|
45
|
+
self._client = AsyncAnthropic(api_key=api_key, base_url=base_url)
|
|
44
46
|
self._history: list[UniMessage] = []
|
|
45
47
|
|
|
46
|
-
def _convert_thinking_level_to_budget(self, thinking_level: ThinkingLevel) -> dict:
|
|
48
|
+
def _convert_thinking_level_to_budget(self, thinking_level: ThinkingLevel) -> dict[str, Any]:
|
|
47
49
|
"""Convert ThinkingLevel enum to Claude's budget_tokens."""
|
|
48
50
|
|
|
49
51
|
mapping = {
|
|
@@ -54,7 +56,7 @@ class Claude4_5Client(LLMClient):
|
|
|
54
56
|
}
|
|
55
57
|
return mapping.get(thinking_level)
|
|
56
58
|
|
|
57
|
-
def _convert_tool_choice(self, tool_choice: ToolChoice) -> dict[str,
|
|
59
|
+
def _convert_tool_choice(self, tool_choice: ToolChoice) -> dict[str, str]:
|
|
58
60
|
"""Convert ToolChoice to Claude's tool_choice format."""
|
|
59
61
|
if isinstance(tool_choice, list):
|
|
60
62
|
if len(tool_choice) > 1:
|
|
@@ -80,21 +82,17 @@ class Claude4_5Client(LLMClient):
|
|
|
80
82
|
"""
|
|
81
83
|
claude_config = {"model": self._model}
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
if config.get("system_prompt") is not None:
|
|
86
|
+
claude_config["system"] = config["system_prompt"]
|
|
87
|
+
|
|
84
88
|
if config.get("max_tokens") is not None:
|
|
85
89
|
claude_config["max_tokens"] = config["max_tokens"]
|
|
86
90
|
else:
|
|
87
91
|
claude_config["max_tokens"] = 32768 # Claude requires max_tokens to be specified
|
|
88
92
|
|
|
89
|
-
# Add temperature
|
|
90
93
|
if config.get("temperature") is not None:
|
|
91
94
|
claude_config["temperature"] = config["temperature"]
|
|
92
95
|
|
|
93
|
-
# Add system prompt
|
|
94
|
-
if config.get("system_prompt") is not None:
|
|
95
|
-
claude_config["system"] = config["system_prompt"]
|
|
96
|
-
|
|
97
|
-
# Convert thinking configuration
|
|
98
96
|
# NOTE: Claude always provides thinking summary
|
|
99
97
|
if config.get("thinking_level") is not None:
|
|
100
98
|
claude_config["temperature"] = 1.0 # `temperature` may only be set to 1 when thinking is enabled
|
|
@@ -148,7 +146,7 @@ class Claude4_5Client(LLMClient):
|
|
|
148
146
|
"type": "tool_use",
|
|
149
147
|
"id": item["tool_call_id"],
|
|
150
148
|
"name": item["name"],
|
|
151
|
-
"input": item["
|
|
149
|
+
"input": item["arguments"],
|
|
152
150
|
}
|
|
153
151
|
)
|
|
154
152
|
elif item["type"] == "tool_result":
|
|
@@ -165,7 +163,7 @@ class Claude4_5Client(LLMClient):
|
|
|
165
163
|
|
|
166
164
|
return claude_messages
|
|
167
165
|
|
|
168
|
-
def transform_model_output_to_uni_event(self, model_output: MessageStreamEvent) ->
|
|
166
|
+
def transform_model_output_to_uni_event(self, model_output: MessageStreamEvent) -> UniEvent:
|
|
169
167
|
"""
|
|
170
168
|
Transform Claude model output to universal event format.
|
|
171
169
|
|
|
@@ -177,7 +175,7 @@ class Claude4_5Client(LLMClient):
|
|
|
177
175
|
Returns:
|
|
178
176
|
Universal event dictionary
|
|
179
177
|
"""
|
|
180
|
-
event_type = None
|
|
178
|
+
event_type: EventType | None = None
|
|
181
179
|
content_items: list[PartialContentItem] = []
|
|
182
180
|
usage_metadata: UsageMetadata | None = None
|
|
183
181
|
finish_reason: FinishReason | None = None
|
|
@@ -188,7 +186,7 @@ class Claude4_5Client(LLMClient):
|
|
|
188
186
|
block = model_output.content_block
|
|
189
187
|
if block.type == "tool_use":
|
|
190
188
|
content_items.append(
|
|
191
|
-
{"type": "partial_tool_call", "name": block.name, "
|
|
189
|
+
{"type": "partial_tool_call", "name": block.name, "arguments": "", "tool_call_id": block.id}
|
|
192
190
|
)
|
|
193
191
|
|
|
194
192
|
elif claude_event_type == "content_block_delta":
|
|
@@ -199,7 +197,9 @@ class Claude4_5Client(LLMClient):
|
|
|
199
197
|
elif delta.type == "text_delta":
|
|
200
198
|
content_items.append({"type": "text", "text": delta.text})
|
|
201
199
|
elif delta.type == "input_json_delta":
|
|
202
|
-
content_items.append(
|
|
200
|
+
content_items.append(
|
|
201
|
+
{"type": "partial_tool_call", "name": "", "arguments": delta.partial_json, "tool_call_id": ""}
|
|
202
|
+
)
|
|
203
203
|
elif delta.type == "signature_delta":
|
|
204
204
|
content_items.append({"type": "thinking", "thinking": "", "signature": delta.signature})
|
|
205
205
|
|
|
@@ -214,6 +214,7 @@ class Claude4_5Client(LLMClient):
|
|
|
214
214
|
"prompt_tokens": message.usage.input_tokens,
|
|
215
215
|
"thoughts_tokens": None,
|
|
216
216
|
"response_tokens": None,
|
|
217
|
+
"cached_tokens": message.usage.cache_read_input_tokens,
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
elif claude_event_type == "message_delta":
|
|
@@ -233,6 +234,7 @@ class Claude4_5Client(LLMClient):
|
|
|
233
234
|
"prompt_tokens": None,
|
|
234
235
|
"thoughts_tokens": None,
|
|
235
236
|
"response_tokens": model_output.usage.output_tokens,
|
|
237
|
+
"cached_tokens": None,
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
elif claude_event_type == "message_stop":
|
|
@@ -246,7 +248,7 @@ class Claude4_5Client(LLMClient):
|
|
|
246
248
|
|
|
247
249
|
return {
|
|
248
250
|
"role": "assistant",
|
|
249
|
-
"
|
|
251
|
+
"event_type": event_type,
|
|
250
252
|
"content_items": content_items,
|
|
251
253
|
"usage_metadata": usage_metadata,
|
|
252
254
|
"finish_reason": finish_reason,
|
|
@@ -264,51 +266,81 @@ class Claude4_5Client(LLMClient):
|
|
|
264
266
|
# Use unified message conversion
|
|
265
267
|
claude_messages = self.transform_uni_message_to_model_input(messages)
|
|
266
268
|
|
|
269
|
+
# Add cache_control to last user message's last item if enabled
|
|
270
|
+
prompt_caching = config.get("prompt_caching", PromptCaching.ENABLE)
|
|
271
|
+
if prompt_caching != PromptCaching.DISABLE and claude_messages:
|
|
272
|
+
try:
|
|
273
|
+
last_user_message = next(filter(lambda x: x["role"] == "user", claude_messages[::-1]))
|
|
274
|
+
last_content_item = last_user_message["content"][-1]
|
|
275
|
+
last_content_item["cache_control"] = {
|
|
276
|
+
"type": "ephemeral",
|
|
277
|
+
"ttl": "1h" if prompt_caching == PromptCaching.ENHANCE else "5m",
|
|
278
|
+
}
|
|
279
|
+
except StopIteration:
|
|
280
|
+
pass
|
|
281
|
+
|
|
267
282
|
# Stream generate
|
|
268
283
|
partial_tool_call = {}
|
|
269
284
|
partial_usage = {}
|
|
270
285
|
async with self._client.messages.stream(**claude_config, messages=claude_messages) as stream:
|
|
271
286
|
async for event in stream:
|
|
272
287
|
event = self.transform_model_output_to_uni_event(event)
|
|
273
|
-
if event["
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
288
|
+
if event["event_type"] == "start":
|
|
289
|
+
for item in event["content_items"]:
|
|
290
|
+
if item["type"] == "partial_tool_call":
|
|
291
|
+
# initialize partial_tool_call
|
|
292
|
+
partial_tool_call = {
|
|
293
|
+
"name": item["name"],
|
|
294
|
+
"arguments": "",
|
|
295
|
+
"tool_call_id": item["tool_call_id"],
|
|
296
|
+
}
|
|
297
|
+
yield event
|
|
278
298
|
|
|
279
299
|
if event["usage_metadata"] is not None:
|
|
280
|
-
|
|
300
|
+
# initialize partial_usage
|
|
301
|
+
partial_usage = {
|
|
302
|
+
"prompt_tokens": event["usage_metadata"]["prompt_tokens"],
|
|
303
|
+
"cached_tokens": event["usage_metadata"]["cached_tokens"],
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
elif event["event_type"] == "delta":
|
|
307
|
+
for item in event["content_items"]:
|
|
308
|
+
if item["type"] == "partial_tool_call":
|
|
309
|
+
# update partial_tool_call
|
|
310
|
+
partial_tool_call["arguments"] += item["arguments"]
|
|
281
311
|
|
|
282
|
-
|
|
283
|
-
if event["content_items"][0]["type"] == "partial_tool_call":
|
|
284
|
-
partial_tool_call["argument"] += event["content_items"][0]["argument"]
|
|
285
|
-
else:
|
|
286
|
-
event.pop("event")
|
|
287
|
-
yield event
|
|
312
|
+
yield event
|
|
288
313
|
|
|
289
|
-
elif event["
|
|
290
|
-
if "name" in partial_tool_call and "
|
|
314
|
+
elif event["event_type"] == "stop":
|
|
315
|
+
if "name" in partial_tool_call and "arguments" in partial_tool_call:
|
|
316
|
+
# finish partial_tool_call
|
|
291
317
|
yield {
|
|
292
318
|
"role": "assistant",
|
|
319
|
+
"event_type": "delta",
|
|
293
320
|
"content_items": [
|
|
294
321
|
{
|
|
295
322
|
"type": "tool_call",
|
|
296
323
|
"name": partial_tool_call["name"],
|
|
297
|
-
"
|
|
324
|
+
"arguments": json.loads(partial_tool_call["arguments"]),
|
|
298
325
|
"tool_call_id": partial_tool_call["tool_call_id"],
|
|
299
326
|
}
|
|
300
327
|
],
|
|
328
|
+
"usage_metadata": None,
|
|
329
|
+
"finish_reason": None,
|
|
301
330
|
}
|
|
302
331
|
partial_tool_call = {}
|
|
303
332
|
|
|
304
333
|
if "prompt_tokens" in partial_usage and event["usage_metadata"] is not None:
|
|
334
|
+
# finish partial_usage
|
|
305
335
|
yield {
|
|
306
336
|
"role": "assistant",
|
|
337
|
+
"event_type": "stop",
|
|
307
338
|
"content_items": [],
|
|
308
339
|
"usage_metadata": {
|
|
309
340
|
"prompt_tokens": partial_usage["prompt_tokens"],
|
|
310
341
|
"thoughts_tokens": None,
|
|
311
342
|
"response_tokens": event["usage_metadata"]["response_tokens"],
|
|
343
|
+
"cached_tokens": partial_usage["cached_tokens"],
|
|
312
344
|
},
|
|
313
345
|
"finish_reason": event["finish_reason"],
|
|
314
346
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
import json
|
|
15
16
|
import os
|
|
16
17
|
from typing import AsyncIterator
|
|
17
18
|
|
|
@@ -20,9 +21,10 @@ from google.genai import types
|
|
|
20
21
|
|
|
21
22
|
from ..base_client import LLMClient
|
|
22
23
|
from ..types import (
|
|
24
|
+
EventType,
|
|
23
25
|
FinishReason,
|
|
24
26
|
PartialContentItem,
|
|
25
|
-
|
|
27
|
+
PromptCaching,
|
|
26
28
|
ThinkingLevel,
|
|
27
29
|
ToolChoice,
|
|
28
30
|
UniConfig,
|
|
@@ -35,11 +37,14 @@ from ..types import (
|
|
|
35
37
|
class Gemini3Client(LLMClient):
|
|
36
38
|
"""Gemini 3-specific LLM client implementation."""
|
|
37
39
|
|
|
38
|
-
def __init__(self, model: str, api_key: str | None = None):
|
|
40
|
+
def __init__(self, model: str, api_key: str | None = None, base_url: str | None = None):
|
|
39
41
|
"""Initialize Gemini 3 client with model and API key."""
|
|
40
42
|
self._model = model
|
|
41
43
|
api_key = api_key or os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
|
|
42
|
-
|
|
44
|
+
base_url = base_url or os.getenv("GOOGLE_GEMINI_BASE_URL")
|
|
45
|
+
self._client = (
|
|
46
|
+
genai.Client(api_key=api_key, http_options={"base_url": base_url}) if api_key else genai.Client()
|
|
47
|
+
)
|
|
43
48
|
self._history: list[UniMessage] = []
|
|
44
49
|
|
|
45
50
|
def _detect_mime_type(self, url: str) -> str | None:
|
|
@@ -49,7 +54,7 @@ class Gemini3Client(LLMClient):
|
|
|
49
54
|
mime_type, _ = mimetypes.guess_type(url)
|
|
50
55
|
return mime_type
|
|
51
56
|
|
|
52
|
-
def _convert_thinking_level(self, thinking_level: ThinkingLevel) -> types.ThinkingLevel | None:
|
|
57
|
+
def _convert_thinking_level(self, thinking_level: ThinkingLevel | None) -> types.ThinkingLevel | None:
|
|
53
58
|
"""Convert ThinkingLevel enum to Gemini's ThinkingLevel."""
|
|
54
59
|
mapping = {
|
|
55
60
|
ThinkingLevel.NONE: types.ThinkingLevel.MINIMAL,
|
|
@@ -90,7 +95,6 @@ class Gemini3Client(LLMClient):
|
|
|
90
95
|
if config.get("temperature") is not None:
|
|
91
96
|
config_params["temperature"] = config["temperature"]
|
|
92
97
|
|
|
93
|
-
# Convert thinking level
|
|
94
98
|
thinking_summary = config.get("thinking_summary")
|
|
95
99
|
thinking_level = config.get("thinking_level")
|
|
96
100
|
if thinking_summary is not None or thinking_level is not None:
|
|
@@ -98,7 +102,6 @@ class Gemini3Client(LLMClient):
|
|
|
98
102
|
include_thoughts=thinking_summary, thinking_level=self._convert_thinking_level(thinking_level)
|
|
99
103
|
)
|
|
100
104
|
|
|
101
|
-
# Convert tools and tool choice
|
|
102
105
|
if config.get("tools") is not None:
|
|
103
106
|
config_params["tools"] = [types.Tool(function_declarations=config["tools"])]
|
|
104
107
|
tool_choice = config.get("tool_choice")
|
|
@@ -106,6 +109,9 @@ class Gemini3Client(LLMClient):
|
|
|
106
109
|
tool_config = self._convert_tool_choice(tool_choice)
|
|
107
110
|
config_params["tool_config"] = types.ToolConfig(function_calling_config=tool_config)
|
|
108
111
|
|
|
112
|
+
if config.get("prompt_caching") is not None and config["prompt_caching"] != PromptCaching.ENABLE:
|
|
113
|
+
raise ValueError("prompt_caching must be ENABLE for Gemini 3.")
|
|
114
|
+
|
|
109
115
|
return types.GenerateContentConfig(**config_params) if config_params else None
|
|
110
116
|
|
|
111
117
|
def transform_uni_message_to_model_input(self, messages: list[UniMessage]) -> list[types.Content]:
|
|
@@ -135,7 +141,7 @@ class Gemini3Client(LLMClient):
|
|
|
135
141
|
types.Part(text=item["thinking"], thought=True, thought_signature=item.get("signature"))
|
|
136
142
|
)
|
|
137
143
|
elif item["type"] == "tool_call":
|
|
138
|
-
function_call = types.FunctionCall(name=item["name"], args=item["
|
|
144
|
+
function_call = types.FunctionCall(name=item["name"], args=item["arguments"])
|
|
139
145
|
parts.append(types.Part(function_call=function_call, thought_signature=item.get("signature")))
|
|
140
146
|
elif item["type"] == "tool_result":
|
|
141
147
|
if "tool_call_id" not in item:
|
|
@@ -153,7 +159,7 @@ class Gemini3Client(LLMClient):
|
|
|
153
159
|
|
|
154
160
|
return contents
|
|
155
161
|
|
|
156
|
-
def transform_model_output_to_uni_event(self, model_output: types.GenerateContentResponse) ->
|
|
162
|
+
def transform_model_output_to_uni_event(self, model_output: types.GenerateContentResponse) -> UniEvent:
|
|
157
163
|
"""
|
|
158
164
|
Transform Gemini model output to universal event format.
|
|
159
165
|
|
|
@@ -163,6 +169,7 @@ class Gemini3Client(LLMClient):
|
|
|
163
169
|
Returns:
|
|
164
170
|
Universal event dictionary
|
|
165
171
|
"""
|
|
172
|
+
event_type: EventType = "delta"
|
|
166
173
|
content_items: list[PartialContentItem] = []
|
|
167
174
|
usage_metadata: UsageMetadata | None = None
|
|
168
175
|
finish_reason: FinishReason | None = None
|
|
@@ -174,7 +181,7 @@ class Gemini3Client(LLMClient):
|
|
|
174
181
|
{
|
|
175
182
|
"type": "tool_call",
|
|
176
183
|
"name": part.function_call.name,
|
|
177
|
-
"
|
|
184
|
+
"arguments": part.function_call.args,
|
|
178
185
|
"tool_call_id": part.function_call.name,
|
|
179
186
|
"signature": part.thought_signature,
|
|
180
187
|
}
|
|
@@ -186,23 +193,26 @@ class Gemini3Client(LLMClient):
|
|
|
186
193
|
else:
|
|
187
194
|
raise ValueError(f"Unknown output: {part}")
|
|
188
195
|
|
|
189
|
-
if model_output.usage_metadata:
|
|
190
|
-
usage_metadata = {
|
|
191
|
-
"prompt_tokens": model_output.usage_metadata.prompt_token_count,
|
|
192
|
-
"thoughts_tokens": model_output.usage_metadata.thoughts_token_count,
|
|
193
|
-
"response_tokens": model_output.usage_metadata.candidates_token_count,
|
|
194
|
-
}
|
|
195
|
-
|
|
196
196
|
if candidate.finish_reason:
|
|
197
|
+
event_type = "stop"
|
|
197
198
|
stop_reason_mapping = {
|
|
198
199
|
types.FinishReason.STOP: "stop",
|
|
199
200
|
types.FinishReason.MAX_TOKENS: "length",
|
|
200
201
|
}
|
|
201
202
|
finish_reason = stop_reason_mapping.get(candidate.finish_reason, "unknown")
|
|
202
203
|
|
|
204
|
+
if model_output.usage_metadata:
|
|
205
|
+
event_type = event_type or "delta" # deal with separate usage data
|
|
206
|
+
usage_metadata = {
|
|
207
|
+
"prompt_tokens": model_output.usage_metadata.prompt_token_count,
|
|
208
|
+
"thoughts_tokens": model_output.usage_metadata.thoughts_token_count,
|
|
209
|
+
"response_tokens": model_output.usage_metadata.candidates_token_count,
|
|
210
|
+
"cached_tokens": model_output.usage_metadata.cached_content_token_count,
|
|
211
|
+
}
|
|
212
|
+
|
|
203
213
|
return {
|
|
204
214
|
"role": "assistant",
|
|
205
|
-
"
|
|
215
|
+
"event_type": event_type,
|
|
206
216
|
"content_items": content_items,
|
|
207
217
|
"usage_metadata": usage_metadata,
|
|
208
218
|
"finish_reason": finish_reason,
|
|
@@ -226,6 +236,23 @@ class Gemini3Client(LLMClient):
|
|
|
226
236
|
)
|
|
227
237
|
async for chunk in response_stream:
|
|
228
238
|
event = self.transform_model_output_to_uni_event(chunk)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
239
|
+
for item in event["content_items"]:
|
|
240
|
+
if item["type"] == "tool_call":
|
|
241
|
+
# gemini 3 does not support partial tool call, mock a partial tool call event
|
|
242
|
+
yield {
|
|
243
|
+
"role": "assistant",
|
|
244
|
+
"event_type": "delta",
|
|
245
|
+
"content_items": [
|
|
246
|
+
{
|
|
247
|
+
"type": "partial_tool_call",
|
|
248
|
+
"name": item["name"],
|
|
249
|
+
"arguments": json.dumps(item["arguments"], ensure_ascii=False),
|
|
250
|
+
"tool_call_id": item["tool_call_id"],
|
|
251
|
+
"signature": item.get("signature"),
|
|
252
|
+
}
|
|
253
|
+
],
|
|
254
|
+
"usage_metadata": None,
|
|
255
|
+
"finish_reason": None,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
yield event
|
|
@@ -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 GLM4_7Client
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["GLM4_7Client"]
|