agenthub-python 0.3.3__tar.gz → 0.4.1__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.3.3 → agenthub_python-0.4.1}/PKG-INFO +4 -1
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/__init__.py +16 -1
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/auto_client.py +22 -5
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/base_client.py +42 -9
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/claude4_6/client.py +20 -7
- agenthub_python-0.4.1/agenthub/claude5/__init__.py +18 -0
- {agenthub_python-0.3.3/agenthub/claude4_8 → agenthub_python-0.4.1/agenthub/claude5}/client.py +26 -11
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/deepseek_v4/client.py +31 -6
- agenthub_python-0.4.1/agenthub/errors.py +89 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/gemini3/client.py +42 -17
- {agenthub_python-0.3.3/agenthub/claude4_8 → agenthub_python-0.4.1/agenthub/gemini3_6}/__init__.py +2 -2
- agenthub_python-0.4.1/agenthub/gemini3_6/client.py +458 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/glm5_1/client.py +51 -13
- agenthub_python-0.4.1/agenthub/glm5_2/__init__.py +18 -0
- agenthub_python-0.4.1/agenthub/glm5_2/client.py +410 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/gpt5_5/client.py +25 -14
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/kimi_k2_6/client.py +54 -14
- agenthub_python-0.4.1/agenthub/kimi_k3/__init__.py +18 -0
- agenthub_python-0.4.1/agenthub/kimi_k3/client.py +436 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/openai/client.py +48 -12
- agenthub_python-0.4.1/agenthub/registry.py +564 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/types.py +11 -7
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/pyproject.toml +6 -1
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/README.md +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/abort_signal.py +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/claude4_6/__init__.py +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/deepseek_v4/__init__.py +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/gemini3/__init__.py +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/glm5_1/__init__.py +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/gpt5_5/__init__.py +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/integration/__init__.py +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/integration/playground.py +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/integration/tracer.py +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/kimi_k2_6/__init__.py +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/openai/__init__.py +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/openai_embedding/__init__.py +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/openai_embedding/client.py +0 -0
- {agenthub_python-0.3.3 → agenthub_python-0.4.1}/agenthub/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agenthub-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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
|
|
@@ -16,6 +16,9 @@ Requires-Dist: pytest-asyncio>=0.23.0 ; extra == 'dev'
|
|
|
16
16
|
Requires-Dist: ruff>=0.14.3 ; extra == 'dev'
|
|
17
17
|
Requires-Dist: pillow>=10.0.0 ; extra == 'dev'
|
|
18
18
|
Requires-Python: >=3.11
|
|
19
|
+
Project-URL: Homepage, https://github.com/Prism-Shadow/agenthub
|
|
20
|
+
Project-URL: Repository, https://github.com/Prism-Shadow/agenthub
|
|
21
|
+
Project-URL: Issues, https://github.com/Prism-Shadow/agenthub/issues
|
|
19
22
|
Provides-Extra: dev
|
|
20
23
|
Description-Content-Type: text/markdown
|
|
21
24
|
|
|
@@ -13,7 +13,22 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
from .auto_client import AutoLLMClient
|
|
16
|
+
from .errors import AgentHubError, EmptyResponseError, ToolCallArgumentParseError, UnsupportedParameterError
|
|
17
|
+
from .registry import Currency, Modality, ModelPricing, SupportedModel, list_supported_models
|
|
16
18
|
from .types import PromptCaching, ThinkingLevel
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
__all__ = [
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AgentHubError",
|
|
23
|
+
"AutoLLMClient",
|
|
24
|
+
"Currency",
|
|
25
|
+
"EmptyResponseError",
|
|
26
|
+
"Modality",
|
|
27
|
+
"ModelPricing",
|
|
28
|
+
"PromptCaching",
|
|
29
|
+
"SupportedModel",
|
|
30
|
+
"ThinkingLevel",
|
|
31
|
+
"ToolCallArgumentParseError",
|
|
32
|
+
"UnsupportedParameterError",
|
|
33
|
+
"list_supported_models",
|
|
34
|
+
]
|
|
@@ -47,16 +47,25 @@ class AutoLLMClient(LLMClient):
|
|
|
47
47
|
) -> LLMClient:
|
|
48
48
|
"""Create the appropriate client for the given model."""
|
|
49
49
|
client_type = (client_type or os.getenv("CLIENT_TYPE", model)).lower()
|
|
50
|
+
# gemini-3.6 must be matched before the broader gemini-3 prefix below
|
|
50
51
|
if any(
|
|
52
|
+
prefix in client_type for prefix in ("gemini-3.6", "gemini-3.5-flash-lite")
|
|
53
|
+
): # e.g., gemini-3.6-flash; gemini-3.5-flash-lite shares the sampling-parameter deprecation
|
|
54
|
+
from .gemini3_6 import Gemini3_6Client
|
|
55
|
+
|
|
56
|
+
return Gemini3_6Client(model=model, api_key=api_key, base_url=base_url)
|
|
57
|
+
elif any(
|
|
51
58
|
prefix in client_type for prefix in ("gemini-3", "gemini-embedding")
|
|
52
59
|
): # e.g., gemini-3-flash-preview, gemini-embedding-2
|
|
53
60
|
from .gemini3 import Gemini3Client
|
|
54
61
|
|
|
55
62
|
return Gemini3Client(model=model, api_key=api_key, base_url=base_url)
|
|
56
|
-
elif "claude" in client_type and (
|
|
57
|
-
|
|
63
|
+
elif "claude" in client_type and (
|
|
64
|
+
"4-7" in client_type or "4-8" in client_type or "-5" in client_type
|
|
65
|
+
): # e.g., claude-opus-4-7
|
|
66
|
+
from .claude5 import Claude5Client
|
|
58
67
|
|
|
59
|
-
return
|
|
68
|
+
return Claude5Client(model=model, api_key=api_key, base_url=base_url)
|
|
60
69
|
elif "claude" in client_type and "4-6" in client_type: # e.g., claude-sonnet-4-6
|
|
61
70
|
from .claude4_6 import Claude4_6Client
|
|
62
71
|
|
|
@@ -65,10 +74,18 @@ class AutoLLMClient(LLMClient):
|
|
|
65
74
|
from .gpt5_5 import GPT5_5Client
|
|
66
75
|
|
|
67
76
|
return GPT5_5Client(model=model, api_key=api_key, base_url=base_url)
|
|
77
|
+
elif "glm-5.2" in client_type:
|
|
78
|
+
from .glm5_2 import GLM5_2Client
|
|
79
|
+
|
|
80
|
+
return GLM5_2Client(model=model, api_key=api_key, base_url=base_url)
|
|
68
81
|
elif "glm-5" in client_type or "glm-5.1" in client_type:
|
|
69
82
|
from .glm5_1 import GLM5_1Client
|
|
70
83
|
|
|
71
84
|
return GLM5_1Client(model=model, api_key=api_key, base_url=base_url)
|
|
85
|
+
elif "kimi-k3" in client_type:
|
|
86
|
+
from .kimi_k3 import KimiK3Client
|
|
87
|
+
|
|
88
|
+
return KimiK3Client(model=model, api_key=api_key, base_url=base_url)
|
|
72
89
|
elif "kimi-k2.5" in client_type or "kimi-k2.6" in client_type:
|
|
73
90
|
from .kimi_k2_6 import KimiK2_6Client
|
|
74
91
|
|
|
@@ -81,14 +98,14 @@ class AutoLLMClient(LLMClient):
|
|
|
81
98
|
from .openai_embedding import OpenaiEmbeddingClient
|
|
82
99
|
|
|
83
100
|
return OpenaiEmbeddingClient(model=model, api_key=api_key, base_url=base_url)
|
|
84
|
-
elif "openai" in client_type:
|
|
101
|
+
elif "openai" in client_type and "embedding" not in client_type:
|
|
85
102
|
from .openai import OpenaiClient
|
|
86
103
|
|
|
87
104
|
return OpenaiClient(model=model, api_key=api_key, base_url=base_url)
|
|
88
105
|
else:
|
|
89
106
|
raise ValueError(
|
|
90
107
|
f"{client_type} is not supported. "
|
|
91
|
-
"Supported client types: gemini-3, claude-4-8, claude-4-7, claude-4-6, gpt-5.5, gpt-5.4, glm-5.1, kimi-k2.6, kimi-k2.5, deepseek-v4, openai-embedding, openai."
|
|
108
|
+
"Supported client types: gemini-3.6, gemini-3, claude-5, claude-4-8, claude-4-7, claude-4-6, gpt-5.5, gpt-5.4, glm-5.2, glm-5.1, kimi-k3, kimi-k2.6, kimi-k2.5, deepseek-v4, openai-embedding, openai."
|
|
92
109
|
)
|
|
93
110
|
|
|
94
111
|
def transform_uni_config_to_model_config(self, config: UniConfig) -> Any:
|
|
@@ -19,6 +19,7 @@ from contextlib import suppress
|
|
|
19
19
|
from typing import Any, AsyncIterator
|
|
20
20
|
|
|
21
21
|
from .abort_signal import AbortSignal
|
|
22
|
+
from .errors import EmptyResponseError
|
|
22
23
|
from .types import (
|
|
23
24
|
ContentItem,
|
|
24
25
|
FinishReason,
|
|
@@ -100,28 +101,40 @@ class LLMClient(ABC):
|
|
|
100
101
|
for event in events:
|
|
101
102
|
# Merge content_items from all events
|
|
102
103
|
for item in event["content_items"]:
|
|
104
|
+
last_fidelity = (content_items[-1].get("fidelity") or {}) if content_items else {}
|
|
105
|
+
item_fidelity = item.get("fidelity") or {}
|
|
103
106
|
if item["type"] == "text":
|
|
107
|
+
# a delta announcing a different phase starts a new item; same-phase and
|
|
108
|
+
# phaseless deltas merge until a signature finishes the item
|
|
104
109
|
if (
|
|
105
110
|
content_items
|
|
106
111
|
and content_items[-1]["type"] == "text"
|
|
107
|
-
and
|
|
108
|
-
and
|
|
112
|
+
and last_fidelity.get("signature") is None # not finished by a signature yet
|
|
113
|
+
and (
|
|
114
|
+
item_fidelity.get("phase") is None # phaseless deltas continue the item
|
|
115
|
+
or item_fidelity.get("phase") == last_fidelity.get("phase") # same phase merges
|
|
116
|
+
)
|
|
109
117
|
):
|
|
110
118
|
content_items[-1]["text"] += item["text"]
|
|
111
|
-
if
|
|
112
|
-
content_items[-1]["
|
|
113
|
-
elif item["text"] or
|
|
119
|
+
if item_fidelity: # a signature finishes the current item
|
|
120
|
+
content_items[-1]["fidelity"] = {**last_fidelity, **item_fidelity}
|
|
121
|
+
elif item["text"] or item_fidelity.get("phase") is not None: # text or new phase starts an item
|
|
114
122
|
content_items.append(item.copy())
|
|
115
123
|
elif item["type"] == "thinking":
|
|
124
|
+
# a new item starts only when the open item's fidelity is non-empty and
|
|
125
|
+
# differs from the incoming delta's; everything else merges into it
|
|
116
126
|
if (
|
|
117
127
|
content_items
|
|
118
128
|
and content_items[-1]["type"] == "thinking"
|
|
119
|
-
and
|
|
129
|
+
and (
|
|
130
|
+
not last_fidelity # not finished by fidelity yet
|
|
131
|
+
or last_fidelity == item_fidelity # a run of equal fidelity is one item
|
|
132
|
+
)
|
|
120
133
|
):
|
|
121
134
|
content_items[-1]["thinking"] += item["thinking"]
|
|
122
|
-
if
|
|
123
|
-
content_items[-1]["
|
|
124
|
-
elif item["thinking"] or
|
|
135
|
+
if item_fidelity: # fidelity finishes the current item
|
|
136
|
+
content_items[-1]["fidelity"] = item_fidelity
|
|
137
|
+
elif item["thinking"] or item_fidelity: # omit empty thinking items
|
|
125
138
|
content_items.append(item.copy())
|
|
126
139
|
elif item["type"] == "partial_tool_call":
|
|
127
140
|
# Skip partial_tool_call items - they should already be converted to tool_call
|
|
@@ -244,6 +257,7 @@ class LLMClient(ABC):
|
|
|
244
257
|
await stream.aclose()
|
|
245
258
|
|
|
246
259
|
self._validate_last_event(last_event)
|
|
260
|
+
self._validate_non_thinking_output(events)
|
|
247
261
|
|
|
248
262
|
# Save history to file if trace_id is specified
|
|
249
263
|
if config.get("trace_id") and events:
|
|
@@ -312,6 +326,25 @@ class LLMClient(ABC):
|
|
|
312
326
|
if last_event["finish_reason"] is None:
|
|
313
327
|
raise ValueError(f"Last event must carry finish_reason, got: {last_event}")
|
|
314
328
|
|
|
329
|
+
def _validate_non_thinking_output(self, events: list[UniEvent]) -> None:
|
|
330
|
+
"""Validate that the completed response carries content other than thinking.
|
|
331
|
+
|
|
332
|
+
Replaying a thinking-only assistant message on the next turn fails with a 400
|
|
333
|
+
error, so the response is rejected as soon as the stream completes.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
events: All events yielded by streaming_response
|
|
337
|
+
|
|
338
|
+
Raises:
|
|
339
|
+
EmptyResponseError: If every content item in the response is thinking
|
|
340
|
+
"""
|
|
341
|
+
thinking_only = all(
|
|
342
|
+
item["type"] in ("thinking", "inline_thinking") for event in events for item in event["content_items"]
|
|
343
|
+
)
|
|
344
|
+
if thinking_only:
|
|
345
|
+
finish_reason = events[-1]["finish_reason"] if events else None
|
|
346
|
+
raise EmptyResponseError(self.__class__.__name__, finish_reason)
|
|
347
|
+
|
|
315
348
|
def clear_history(self) -> None:
|
|
316
349
|
"""Clear the message history."""
|
|
317
350
|
self._history.clear()
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
import base64
|
|
16
|
-
import json
|
|
17
16
|
import mimetypes
|
|
18
17
|
import os
|
|
19
18
|
import re
|
|
@@ -24,6 +23,7 @@ from anthropic import AsyncAnthropic, AsyncAnthropicBedrock
|
|
|
24
23
|
from anthropic.types.beta import BetaMessageParam, BetaRawMessageStreamEvent
|
|
25
24
|
|
|
26
25
|
from ..base_client import LLMClient
|
|
26
|
+
from ..errors import UnsupportedParameterError, parse_tool_call_arguments
|
|
27
27
|
from ..types import (
|
|
28
28
|
EventType,
|
|
29
29
|
FinishReason,
|
|
@@ -118,7 +118,9 @@ class Claude4_6Client(LLMClient):
|
|
|
118
118
|
"""Convert ToolChoice to Claude's tool_choice format."""
|
|
119
119
|
if isinstance(tool_choice, list):
|
|
120
120
|
if len(tool_choice) > 1:
|
|
121
|
-
raise
|
|
121
|
+
raise UnsupportedParameterError(
|
|
122
|
+
self.__class__.__name__, "tool_choice", "Claude supports only one tool choice."
|
|
123
|
+
)
|
|
122
124
|
|
|
123
125
|
return {"type": "any", "name": tool_choice[0]}
|
|
124
126
|
elif tool_choice == "none":
|
|
@@ -204,10 +206,14 @@ class Claude4_6Client(LLMClient):
|
|
|
204
206
|
content_blocks.append(await self._convert_image_url_to_source(item["image_url"]))
|
|
205
207
|
elif item["type"] == "thinking":
|
|
206
208
|
if item["thinking"] == REDACTED_THINKING:
|
|
207
|
-
content_blocks.append({"type": "redacted_thinking", "data": item["signature"]})
|
|
209
|
+
content_blocks.append({"type": "redacted_thinking", "data": item["fidelity"]["signature"]})
|
|
208
210
|
else:
|
|
209
211
|
content_blocks.append(
|
|
210
|
-
{
|
|
212
|
+
{
|
|
213
|
+
"type": "thinking",
|
|
214
|
+
"thinking": item["thinking"],
|
|
215
|
+
"signature": item["fidelity"]["signature"],
|
|
216
|
+
}
|
|
211
217
|
)
|
|
212
218
|
elif item["type"] == "tool_call":
|
|
213
219
|
content_blocks.append(
|
|
@@ -263,7 +269,9 @@ class Claude4_6Client(LLMClient):
|
|
|
263
269
|
{"type": "partial_tool_call", "name": block.name, "arguments": "", "tool_call_id": block.id}
|
|
264
270
|
)
|
|
265
271
|
elif block.type == "redacted_thinking":
|
|
266
|
-
content_items.append(
|
|
272
|
+
content_items.append(
|
|
273
|
+
{"type": "thinking", "thinking": REDACTED_THINKING, "fidelity": {"signature": block.data}}
|
|
274
|
+
)
|
|
267
275
|
|
|
268
276
|
elif claude_event_type == "content_block_delta":
|
|
269
277
|
event_type = "delta"
|
|
@@ -277,7 +285,7 @@ class Claude4_6Client(LLMClient):
|
|
|
277
285
|
{"type": "partial_tool_call", "name": "", "arguments": delta.partial_json, "tool_call_id": ""}
|
|
278
286
|
)
|
|
279
287
|
elif delta.type == "signature_delta":
|
|
280
|
-
content_items.append({"type": "thinking", "thinking": "", "signature": delta.signature})
|
|
288
|
+
content_items.append({"type": "thinking", "thinking": "", "fidelity": {"signature": delta.signature}})
|
|
281
289
|
|
|
282
290
|
elif claude_event_type == "content_block_stop":
|
|
283
291
|
event_type = "stop"
|
|
@@ -403,7 +411,12 @@ class Claude4_6Client(LLMClient):
|
|
|
403
411
|
{
|
|
404
412
|
"type": "tool_call",
|
|
405
413
|
"name": partial_tool_call["name"],
|
|
406
|
-
"arguments":
|
|
414
|
+
"arguments": parse_tool_call_arguments(
|
|
415
|
+
partial_tool_call["arguments"],
|
|
416
|
+
self.__class__.__name__,
|
|
417
|
+
partial_tool_call["name"],
|
|
418
|
+
partial_tool_call["tool_call_id"],
|
|
419
|
+
),
|
|
407
420
|
"tool_call_id": partial_tool_call["tool_call_id"],
|
|
408
421
|
}
|
|
409
422
|
],
|
|
@@ -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 Claude5Client
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["Claude5Client"]
|
{agenthub_python-0.3.3/agenthub/claude4_8 → agenthub_python-0.4.1/agenthub/claude5}/client.py
RENAMED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
import base64
|
|
16
|
-
import json
|
|
17
16
|
import mimetypes
|
|
18
17
|
import os
|
|
19
18
|
import re
|
|
@@ -24,6 +23,7 @@ from anthropic import AsyncAnthropic, AsyncAnthropicBedrock
|
|
|
24
23
|
from anthropic.types.beta import BetaMessageParam, BetaRawMessageStreamEvent
|
|
25
24
|
|
|
26
25
|
from ..base_client import LLMClient
|
|
26
|
+
from ..errors import UnsupportedParameterError, parse_tool_call_arguments
|
|
27
27
|
from ..types import (
|
|
28
28
|
EventType,
|
|
29
29
|
FinishReason,
|
|
@@ -41,11 +41,11 @@ from ..types import (
|
|
|
41
41
|
REDACTED_THINKING = "_REDACTED_THINKING"
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
class
|
|
45
|
-
"""Claude
|
|
44
|
+
class Claude5Client(LLMClient):
|
|
45
|
+
"""Claude 5-specific LLM client implementation."""
|
|
46
46
|
|
|
47
47
|
def __init__(self, model: str, api_key: str | None = None, base_url: str | None = None):
|
|
48
|
-
"""Initialize Claude
|
|
48
|
+
"""Initialize Claude 5 client with model and API key."""
|
|
49
49
|
self._model = model
|
|
50
50
|
api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
51
51
|
base_url = base_url or os.getenv("ANTHROPIC_BASE_URL")
|
|
@@ -118,7 +118,9 @@ class Claude4_8Client(LLMClient):
|
|
|
118
118
|
"""Convert ToolChoice to Claude's tool_choice format."""
|
|
119
119
|
if isinstance(tool_choice, list):
|
|
120
120
|
if len(tool_choice) > 1:
|
|
121
|
-
raise
|
|
121
|
+
raise UnsupportedParameterError(
|
|
122
|
+
self.__class__.__name__, "tool_choice", "Claude supports only one tool choice."
|
|
123
|
+
)
|
|
122
124
|
|
|
123
125
|
return {"type": "any", "name": tool_choice[0]}
|
|
124
126
|
elif tool_choice == "none":
|
|
@@ -149,7 +151,9 @@ class Claude4_8Client(LLMClient):
|
|
|
149
151
|
claude_config["max_tokens"] = 64000 # Claude requires max_tokens to be specified
|
|
150
152
|
|
|
151
153
|
if config.get("temperature") is not None and config["temperature"] != 1.0:
|
|
152
|
-
raise
|
|
154
|
+
raise UnsupportedParameterError(
|
|
155
|
+
self.__class__.__name__, "temperature", "Claude 4.8 does not support setting temperature."
|
|
156
|
+
)
|
|
153
157
|
|
|
154
158
|
if config.get("thinking_level") is not None:
|
|
155
159
|
claude_config.update(self._convert_thinking_level_to_thinking_config(config["thinking_level"]))
|
|
@@ -204,10 +208,14 @@ class Claude4_8Client(LLMClient):
|
|
|
204
208
|
content_blocks.append(await self._convert_image_url_to_source(item["image_url"]))
|
|
205
209
|
elif item["type"] == "thinking":
|
|
206
210
|
if item["thinking"] == REDACTED_THINKING:
|
|
207
|
-
content_blocks.append({"type": "redacted_thinking", "data": item["signature"]})
|
|
211
|
+
content_blocks.append({"type": "redacted_thinking", "data": item["fidelity"]["signature"]})
|
|
208
212
|
else:
|
|
209
213
|
content_blocks.append(
|
|
210
|
-
{
|
|
214
|
+
{
|
|
215
|
+
"type": "thinking",
|
|
216
|
+
"thinking": item["thinking"],
|
|
217
|
+
"signature": item["fidelity"]["signature"],
|
|
218
|
+
}
|
|
211
219
|
)
|
|
212
220
|
elif item["type"] == "tool_call":
|
|
213
221
|
content_blocks.append(
|
|
@@ -263,7 +271,9 @@ class Claude4_8Client(LLMClient):
|
|
|
263
271
|
{"type": "partial_tool_call", "name": block.name, "arguments": "", "tool_call_id": block.id}
|
|
264
272
|
)
|
|
265
273
|
elif block.type == "redacted_thinking":
|
|
266
|
-
content_items.append(
|
|
274
|
+
content_items.append(
|
|
275
|
+
{"type": "thinking", "thinking": REDACTED_THINKING, "fidelity": {"signature": block.data}}
|
|
276
|
+
)
|
|
267
277
|
|
|
268
278
|
elif claude_event_type == "content_block_delta":
|
|
269
279
|
event_type = "delta"
|
|
@@ -277,7 +287,7 @@ class Claude4_8Client(LLMClient):
|
|
|
277
287
|
{"type": "partial_tool_call", "name": "", "arguments": delta.partial_json, "tool_call_id": ""}
|
|
278
288
|
)
|
|
279
289
|
elif delta.type == "signature_delta":
|
|
280
|
-
content_items.append({"type": "thinking", "thinking": "", "signature": delta.signature})
|
|
290
|
+
content_items.append({"type": "thinking", "thinking": "", "fidelity": {"signature": delta.signature}})
|
|
281
291
|
|
|
282
292
|
elif claude_event_type == "content_block_stop":
|
|
283
293
|
event_type = "stop"
|
|
@@ -403,7 +413,12 @@ class Claude4_8Client(LLMClient):
|
|
|
403
413
|
{
|
|
404
414
|
"type": "tool_call",
|
|
405
415
|
"name": partial_tool_call["name"],
|
|
406
|
-
"arguments":
|
|
416
|
+
"arguments": parse_tool_call_arguments(
|
|
417
|
+
partial_tool_call["arguments"],
|
|
418
|
+
self.__class__.__name__,
|
|
419
|
+
partial_tool_call["name"],
|
|
420
|
+
partial_tool_call["tool_call_id"],
|
|
421
|
+
),
|
|
407
422
|
"tool_call_id": partial_tool_call["tool_call_id"],
|
|
408
423
|
}
|
|
409
424
|
],
|
|
@@ -20,6 +20,7 @@ from openai import AsyncOpenAI
|
|
|
20
20
|
from openai.types.chat import ChatCompletionChunk, ChatCompletionMessageParam
|
|
21
21
|
|
|
22
22
|
from ..base_client import LLMClient
|
|
23
|
+
from ..errors import UnsupportedParameterError, parse_tool_call_arguments
|
|
23
24
|
from ..types import (
|
|
24
25
|
EventType,
|
|
25
26
|
FinishReason,
|
|
@@ -71,7 +72,9 @@ class DeepSeekV4Client(LLMClient):
|
|
|
71
72
|
"""Convert ToolChoice to DeepSeek's OpenAI-compatible tool_choice format."""
|
|
72
73
|
if tool_choice in ["auto", "none"]:
|
|
73
74
|
return tool_choice
|
|
74
|
-
raise
|
|
75
|
+
raise UnsupportedParameterError(
|
|
76
|
+
self.__class__.__name__, "tool_choice", "DeepSeek V4 only supports 'auto' and 'none' for tool_choice."
|
|
77
|
+
)
|
|
75
78
|
|
|
76
79
|
def transform_uni_config_to_model_config(self, config: UniConfig) -> dict[str, Any]:
|
|
77
80
|
"""
|
|
@@ -89,7 +92,9 @@ class DeepSeekV4Client(LLMClient):
|
|
|
89
92
|
deepseek_config["max_tokens"] = config["max_tokens"]
|
|
90
93
|
|
|
91
94
|
if config.get("temperature") is not None and config["temperature"] != 1.0:
|
|
92
|
-
raise
|
|
95
|
+
raise UnsupportedParameterError(
|
|
96
|
+
self.__class__.__name__, "temperature", "DeepSeek V4 does not support setting temperature."
|
|
97
|
+
)
|
|
93
98
|
|
|
94
99
|
thinking_level = config.get("thinking_level")
|
|
95
100
|
if thinking_level is not None:
|
|
@@ -105,7 +110,9 @@ class DeepSeekV4Client(LLMClient):
|
|
|
105
110
|
deepseek_config["tool_choice"] = self._convert_tool_choice(config["tool_choice"])
|
|
106
111
|
|
|
107
112
|
if config.get("prompt_caching") is not None and config["prompt_caching"] != PromptCaching.ENABLE:
|
|
108
|
-
raise
|
|
113
|
+
raise UnsupportedParameterError(
|
|
114
|
+
self.__class__.__name__, "prompt_caching", "prompt_caching must be ENABLE for DeepSeek."
|
|
115
|
+
)
|
|
109
116
|
|
|
110
117
|
return deepseek_config
|
|
111
118
|
|
|
@@ -200,7 +207,15 @@ class DeepSeekV4Client(LLMClient):
|
|
|
200
207
|
|
|
201
208
|
if getattr(delta, "reasoning_content", None):
|
|
202
209
|
event_type = "delta"
|
|
203
|
-
|
|
210
|
+
# record the wire field so a replay through another OpenAI-compatible
|
|
211
|
+
# client reproduces the exact field DeepSeek produced
|
|
212
|
+
content_items.append(
|
|
213
|
+
{
|
|
214
|
+
"type": "thinking",
|
|
215
|
+
"thinking": getattr(delta, "reasoning_content"),
|
|
216
|
+
"fidelity": {"reasoning_field": "reasoning_content"},
|
|
217
|
+
}
|
|
218
|
+
)
|
|
204
219
|
|
|
205
220
|
if delta.content:
|
|
206
221
|
event_type = "delta"
|
|
@@ -289,7 +304,12 @@ class DeepSeekV4Client(LLMClient):
|
|
|
289
304
|
{
|
|
290
305
|
"type": "tool_call",
|
|
291
306
|
"name": partial_tool_call["name"],
|
|
292
|
-
"arguments":
|
|
307
|
+
"arguments": parse_tool_call_arguments(
|
|
308
|
+
partial_tool_call["arguments"],
|
|
309
|
+
self.__class__.__name__,
|
|
310
|
+
partial_tool_call["name"],
|
|
311
|
+
partial_tool_call["tool_call_id"],
|
|
312
|
+
),
|
|
293
313
|
"tool_call_id": partial_tool_call["tool_call_id"],
|
|
294
314
|
}
|
|
295
315
|
],
|
|
@@ -317,7 +337,12 @@ class DeepSeekV4Client(LLMClient):
|
|
|
317
337
|
{
|
|
318
338
|
"type": "tool_call",
|
|
319
339
|
"name": partial_tool_call["name"],
|
|
320
|
-
"arguments":
|
|
340
|
+
"arguments": parse_tool_call_arguments(
|
|
341
|
+
partial_tool_call["arguments"],
|
|
342
|
+
self.__class__.__name__,
|
|
343
|
+
partial_tool_call["name"],
|
|
344
|
+
partial_tool_call["tool_call_id"],
|
|
345
|
+
),
|
|
321
346
|
"tool_call_id": partial_tool_call["tool_call_id"],
|
|
322
347
|
}
|
|
323
348
|
],
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
import json
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _preview_tool_call_arguments(raw: str) -> str:
|
|
20
|
+
max_length = 160
|
|
21
|
+
if len(raw) <= max_length:
|
|
22
|
+
return raw
|
|
23
|
+
|
|
24
|
+
edge_length = 72
|
|
25
|
+
return f"{raw[:edge_length]}...[truncated]...{raw[-edge_length:]}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AgentHubError(ValueError):
|
|
29
|
+
"""Base class for errors raised by AgentHub clients."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UnsupportedParameterError(AgentHubError):
|
|
33
|
+
"""Raised when a UniConfig parameter value is not supported by the target model client.
|
|
34
|
+
|
|
35
|
+
Thinking levels never raise this by design: every client maps each ThinkingLevel
|
|
36
|
+
onto the closest level the model supports. Parameters such as temperature and
|
|
37
|
+
tool_choice may reject unsupported values with this error.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, client: str, parameter: str, message: str) -> None:
|
|
41
|
+
self.client = client
|
|
42
|
+
self.parameter = parameter
|
|
43
|
+
super().__init__(message)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class EmptyResponseError(AgentHubError):
|
|
47
|
+
"""Raised when a completed response carries no non-thinking content and no tool calls.
|
|
48
|
+
|
|
49
|
+
Models occasionally finish a turn with thinking output only (reasoning models in
|
|
50
|
+
particular); replaying such an assistant message on the next turn fails with a 400
|
|
51
|
+
error, so the response is rejected as soon as the stream completes.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, client: str, finish_reason: str | None) -> None:
|
|
55
|
+
self.client = client
|
|
56
|
+
self.finish_reason = finish_reason
|
|
57
|
+
super().__init__(f"{client} returned no content other than thinking (finish_reason={finish_reason!r}).")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ToolCallArgumentParseError(AgentHubError):
|
|
61
|
+
def __init__(self, client: str, tool_name: str, tool_call_id: str, raw_arguments: str, reason: str) -> None:
|
|
62
|
+
self.client = client
|
|
63
|
+
self.tool_name = tool_name
|
|
64
|
+
self.tool_call_id = tool_call_id
|
|
65
|
+
self.raw_arguments_length = len(raw_arguments)
|
|
66
|
+
self.raw_arguments_preview = _preview_tool_call_arguments(raw_arguments)
|
|
67
|
+
super().__init__(
|
|
68
|
+
f'Invalid streamed tool call arguments from {client} for tool "{tool_name}" '
|
|
69
|
+
f'(tool_call_id="{tool_call_id}", length={self.raw_arguments_length}, '
|
|
70
|
+
f"preview={self.raw_arguments_preview!r}): {reason}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_tool_call_arguments(
|
|
75
|
+
raw_arguments: str | None,
|
|
76
|
+
client: str,
|
|
77
|
+
tool_name: str,
|
|
78
|
+
tool_call_id: str,
|
|
79
|
+
) -> dict[str, Any]:
|
|
80
|
+
raw = raw_arguments or "{}"
|
|
81
|
+
try:
|
|
82
|
+
parsed = json.loads(raw)
|
|
83
|
+
except (TypeError, ValueError) as exc:
|
|
84
|
+
raise ToolCallArgumentParseError(client, tool_name, tool_call_id, raw, str(exc)) from exc
|
|
85
|
+
|
|
86
|
+
if not isinstance(parsed, dict):
|
|
87
|
+
raise ToolCallArgumentParseError(client, tool_name, tool_call_id, raw, "Expected a JSON object.")
|
|
88
|
+
|
|
89
|
+
return parsed
|