agenthub-python 0.4.2__tar.gz → 0.4.4__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.
Files changed (37) hide show
  1. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/PKG-INFO +2 -1
  2. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/ant_messages/client.py +4 -1
  3. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/auto_client.py +4 -4
  4. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/claude5/client.py +5 -1
  5. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/deepseek_v4/client.py +13 -4
  6. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/gemini3_7/client.py +12 -0
  7. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/glm5_3/client.py +5 -1
  8. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/gpt5_6/client.py +2 -0
  9. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/integration/playground.py +2 -1
  10. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/kimi_k3/client.py +5 -1
  11. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/minimax_m3/client.py +3 -0
  12. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/openai_chat/client.py +3 -1
  13. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/openai_responses/client.py +2 -0
  14. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/types.py +1 -0
  15. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/pyproject.toml +2 -1
  16. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/pyproject.toml.orig +2 -2
  17. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/README.md +0 -0
  18. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/__init__.py +0 -0
  19. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/abort_signal.py +0 -0
  20. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/ant_messages/__init__.py +0 -0
  21. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/base_client.py +0 -0
  22. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/claude5/__init__.py +0 -0
  23. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/deepseek_v4/__init__.py +0 -0
  24. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/errors.py +0 -0
  25. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/gemini3_7/__init__.py +0 -0
  26. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/glm5_3/__init__.py +0 -0
  27. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/gpt5_6/__init__.py +0 -0
  28. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/integration/__init__.py +0 -0
  29. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/integration/tracer.py +0 -0
  30. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/kimi_k3/__init__.py +0 -0
  31. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/minimax_m3/__init__.py +0 -0
  32. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/openai_chat/__init__.py +0 -0
  33. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/openai_embedding/__init__.py +0 -0
  34. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/openai_embedding/client.py +0 -0
  35. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/openai_responses/__init__.py +0 -0
  36. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/registry.py +0 -0
  37. {agenthub_python-0.4.2 → agenthub_python-0.4.4}/agenthub/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agenthub-python
3
- Version: 0.4.2
3
+ Version: 0.4.4
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
@@ -13,6 +13,7 @@ Requires-Dist: httpx>=0.27.0
13
13
  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
+ Requires-Dist: pytest-xdist>=3.6.0 ; extra == 'dev'
16
17
  Requires-Dist: ruff>=0.14.3 ; extra == 'dev'
17
18
  Requires-Dist: pillow>=10.0.0 ; extra == 'dev'
18
19
  Requires-Python: >=3.11
@@ -75,6 +75,7 @@ class AntMessagesClient(LLMClient):
75
75
  ThinkingLevel.MEDIUM: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "medium"}},
76
76
  ThinkingLevel.HIGH: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "high"}},
77
77
  ThinkingLevel.XHIGH: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "xhigh"}},
78
+ ThinkingLevel.MAX: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "max"}},
78
79
  }
79
80
  return mapping.get(thinking_level)
80
81
 
@@ -300,7 +301,9 @@ class AntMessagesClient(LLMClient):
300
301
  elif ant_event_type == "message_stop":
301
302
  event_type = "stop"
302
303
 
303
- elif ant_event_type in ["text", "thinking", "signature", "input_json"]:
304
+ elif ant_event_type in ["text", "thinking", "signature", "input_json", "ping"]:
305
+ # the SDK drops the "ping" heartbeat at the SSE layer; it reaches here only from
306
+ # gateways that relabel it onto another event
304
307
  event_type = "unused"
305
308
 
306
309
  else:
@@ -47,10 +47,6 @@ 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") 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
50
  # every Gemini generation shares the unified client ("gemini-3" also matches the
55
51
  # gemini-3.7/gemini-3.6/gemini-3.5-flash-lite client types)
56
52
  if any(
@@ -78,6 +74,10 @@ class AutoLLMClient(LLMClient):
78
74
  from .kimi_k3 import KimiK3Client
79
75
 
80
76
  return KimiK3Client(model=model, api_key=api_key, base_url=base_url)
77
+ elif client_type == "minimax-m3":
78
+ from .minimax_m3 import MiniMaxM3Client
79
+
80
+ return MiniMaxM3Client(model=model, api_key=api_key, base_url=base_url)
81
81
  elif "deepseek-v4" in client_type:
82
82
  from .deepseek_v4 import DeepSeekV4Client
83
83
 
@@ -115,6 +115,8 @@ class Claude5Client(LLMClient):
115
115
  "thinking": {"type": "adaptive"},
116
116
  "output_config": {"effort": "high" if "4-6" in self._model else "xhigh"},
117
117
  },
118
+ # every model this client serves is 4.6 or later, and max spans that whole range
119
+ ThinkingLevel.MAX: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "max"}},
118
120
  }
119
121
  return mapping.get(thinking_level)
120
122
 
@@ -349,7 +351,9 @@ class Claude5Client(LLMClient):
349
351
  elif claude_event_type == "message_stop":
350
352
  event_type = "stop"
351
353
 
352
- elif claude_event_type in ["text", "thinking", "signature", "input_json"]:
354
+ elif claude_event_type in ["text", "thinking", "signature", "input_json", "ping"]:
355
+ # the SDK drops the "ping" heartbeat at the SSE layer; it reaches here only from
356
+ # gateways that relabel it onto another event
353
357
  event_type = "unused"
354
358
 
355
359
  else:
@@ -54,17 +54,24 @@ class DeepSeekV4Client(LLMClient):
54
54
  ThinkingLevel.MEDIUM: {"type": "enabled"},
55
55
  ThinkingLevel.HIGH: {"type": "enabled"},
56
56
  ThinkingLevel.XHIGH: {"type": "enabled"},
57
+ ThinkingLevel.MAX: {"type": "enabled"},
57
58
  }
58
59
  return mapping[thinking_level]
59
60
 
60
61
  def _convert_reasoning_effort(self, thinking_level: ThinkingLevel) -> str | None:
61
- """Convert ThinkingLevel enum to DeepSeek's reasoning_effort."""
62
+ """Convert ThinkingLevel enum to DeepSeek's reasoning_effort.
63
+
64
+ DeepSeek accepts low/high/max and maps medium and xhigh onto high server-side
65
+ (llmsdk_docs/deepseek_v4/docs/thinking-mode.md), so this sends the value the
66
+ server would settle on anyway.
67
+ """
62
68
  mapping = {
63
69
  ThinkingLevel.NONE: None,
64
- ThinkingLevel.LOW: "high",
70
+ ThinkingLevel.LOW: "low",
65
71
  ThinkingLevel.MEDIUM: "high",
66
72
  ThinkingLevel.HIGH: "high",
67
- ThinkingLevel.XHIGH: "max",
73
+ ThinkingLevel.XHIGH: "high",
74
+ ThinkingLevel.MAX: "max",
68
75
  }
69
76
  return mapping[thinking_level]
70
77
 
@@ -206,7 +213,9 @@ class DeepSeekV4Client(LLMClient):
206
213
  usage_metadata: UsageMetadata | None = None
207
214
  finish_reason: FinishReason | None = None
208
215
 
209
- if len(model_output.choices) > 0:
216
+ # gateways inject content-free heartbeat chunks on long generations, whose choices
217
+ # the SDK leaves as None rather than an empty list
218
+ if model_output.choices:
210
219
  choice = model_output.choices[0]
211
220
  delta = choice.delta
212
221
 
@@ -142,6 +142,8 @@ class Gemini3_7Client(LLMClient):
142
142
  ThinkingLevel.MEDIUM: types.ThinkingLevel.MEDIUM,
143
143
  ThinkingLevel.HIGH: types.ThinkingLevel.HIGH,
144
144
  ThinkingLevel.XHIGH: types.ThinkingLevel.HIGH,
145
+ # Gemini stops at "high", so both top levels land there before per-model clamping
146
+ ThinkingLevel.MAX: types.ThinkingLevel.HIGH,
145
147
  }
146
148
  level = mapping.get(thinking_level)
147
149
  if level is None:
@@ -201,6 +203,8 @@ class Gemini3_7Client(LLMClient):
201
203
  "sampling parameters starting with the 3.6 generation.",
202
204
  )
203
205
 
206
+ # include_thoughts asks for thought summaries, but whether generateContent returns any
207
+ # is model-dependent (llmsdk_docs/gemini3_7/docs/thinking.md)
204
208
  thinking_summary = config.get("thinking_summary")
205
209
  thinking_level = config.get("thinking_level")
206
210
  if thinking_summary is not None or thinking_level is not None:
@@ -436,6 +440,11 @@ class Gemini3_7Client(LLMClient):
436
440
  "response_tokens": model_output.usage_metadata.candidates_token_count,
437
441
  }
438
442
 
443
+ if not content_items and usage_metadata is None and finish_reason is None:
444
+ # nothing was read out of the chunk, so there is nothing to emit: a gateway
445
+ # heartbeat looks like this, and so does any other chunk we take no value from
446
+ event_type = "unused"
447
+
439
448
  return {
440
449
  "role": "assistant",
441
450
  "event_type": event_type,
@@ -511,6 +520,9 @@ class Gemini3_7Client(LLMClient):
511
520
  )
512
521
  async for chunk in response_stream:
513
522
  event = self.transform_model_output_to_uni_event(chunk)
523
+ if event["event_type"] == "unused":
524
+ continue
525
+
514
526
  for item in event["content_items"]:
515
527
  if item["type"] == "tool_call":
516
528
  # gemini 3.7 does not support partial tool call, mock a partial tool call event
@@ -81,6 +81,7 @@ class GLM5_3Client(LLMClient):
81
81
  ThinkingLevel.MEDIUM: "high",
82
82
  ThinkingLevel.HIGH: "high",
83
83
  ThinkingLevel.XHIGH: "max",
84
+ ThinkingLevel.MAX: "max",
84
85
  }
85
86
  return mapping.get(thinking_level)
86
87
  if "glm-5.2" in model:
@@ -90,6 +91,7 @@ class GLM5_3Client(LLMClient):
90
91
  ThinkingLevel.MEDIUM: "medium",
91
92
  ThinkingLevel.HIGH: "high",
92
93
  ThinkingLevel.XHIGH: "xhigh",
94
+ ThinkingLevel.MAX: "max",
93
95
  }
94
96
  return mapping.get(thinking_level)
95
97
  return None
@@ -239,7 +241,9 @@ class GLM5_3Client(LLMClient):
239
241
  usage_metadata: UsageMetadata | None = None
240
242
  finish_reason: FinishReason | None = None
241
243
 
242
- if len(model_output.choices) > 0:
244
+ # gateways inject content-free heartbeat chunks on long generations, whose choices
245
+ # the SDK leaves as None rather than an empty list
246
+ if model_output.choices:
243
247
  choice = model_output.choices[0]
244
248
  delta = choice.delta
245
249
 
@@ -54,6 +54,7 @@ class GPT5_6Client(LLMClient):
54
54
  ThinkingLevel.MEDIUM: "medium",
55
55
  ThinkingLevel.HIGH: "high",
56
56
  ThinkingLevel.XHIGH: "xhigh",
57
+ ThinkingLevel.MAX: "max",
57
58
  }
58
59
  return mapping.get(thinking_level)
59
60
 
@@ -301,6 +302,7 @@ class GPT5_6Client(LLMClient):
301
302
  "response.reasoning_text.done",
302
303
  "response.content_part.added",
303
304
  "response.content_part.done",
305
+ "keepalive", # gateway heartbeat on long generations; carries no content
304
306
  ]:
305
307
  event_type = "unused"
306
308
 
@@ -286,7 +286,8 @@ def create_chat_app() -> Flask:
286
286
  <button type="button" role="option" aria-selected="false" class="w-full px-3 py-2 text-left hover:bg-gray-50 focus:bg-gray-50 focus:outline-none" data-combobox-option data-value="low" data-label="Low" data-description="Low thinking budget" onclick="selectComboboxOption('thinkingLevelCombobox', this)">Low</button>
287
287
  <button type="button" role="option" aria-selected="false" class="w-full px-3 py-2 text-left hover:bg-gray-50 focus:bg-gray-50 focus:outline-none" data-combobox-option data-value="medium" data-label="Medium" data-description="Medium thinking budget" onclick="selectComboboxOption('thinkingLevelCombobox', this)">Medium</button>
288
288
  <button type="button" role="option" aria-selected="false" class="w-full px-3 py-2 text-left hover:bg-gray-50 focus:bg-gray-50 focus:outline-none" data-combobox-option data-value="high" data-label="High" data-description="High thinking budget" onclick="selectComboboxOption('thinkingLevelCombobox', this)">High</button>
289
- <button type="button" role="option" aria-selected="false" class="w-full px-3 py-2 text-left hover:bg-gray-50 focus:bg-gray-50 focus:outline-none" data-combobox-option data-value="xhigh" data-label="XHigh" data-description="Maximum thinking budget" onclick="selectComboboxOption('thinkingLevelCombobox', this)">XHigh</button>
289
+ <button type="button" role="option" aria-selected="false" class="w-full px-3 py-2 text-left hover:bg-gray-50 focus:bg-gray-50 focus:outline-none" data-combobox-option data-value="xhigh" data-label="XHigh" data-description="Extended thinking budget" onclick="selectComboboxOption('thinkingLevelCombobox', this)">XHigh</button>
290
+ <button type="button" role="option" aria-selected="false" class="w-full px-3 py-2 text-left hover:bg-gray-50 focus:bg-gray-50 focus:outline-none" data-combobox-option data-value="max" data-label="Max" data-description="Maximum thinking budget" onclick="selectComboboxOption('thinkingLevelCombobox', this)">Max</button>
290
291
  </div>
291
292
  </div>
292
293
  </div>
@@ -78,6 +78,7 @@ class KimiK3Client(LLMClient):
78
78
  ThinkingLevel.MEDIUM: {"type": "enabled", "keep": "all"},
79
79
  ThinkingLevel.HIGH: {"type": "enabled", "keep": "all"},
80
80
  ThinkingLevel.XHIGH: {"type": "enabled", "keep": "all"},
81
+ ThinkingLevel.MAX: {"type": "enabled", "keep": "all"},
81
82
  }
82
83
  return mapping.get(thinking_level)
83
84
 
@@ -92,6 +93,7 @@ class KimiK3Client(LLMClient):
92
93
  ThinkingLevel.MEDIUM: "high",
93
94
  ThinkingLevel.HIGH: "high",
94
95
  ThinkingLevel.XHIGH: "max",
96
+ ThinkingLevel.MAX: "max",
95
97
  }
96
98
  return mapping.get(thinking_level)
97
99
 
@@ -267,7 +269,9 @@ class KimiK3Client(LLMClient):
267
269
  usage_metadata: UsageMetadata | None = None
268
270
  finish_reason: FinishReason | None = None
269
271
 
270
- if len(model_output.choices) > 0:
272
+ # gateways inject content-free heartbeat chunks on long generations, whose choices
273
+ # the SDK leaves as None rather than an empty list
274
+ if model_output.choices:
271
275
  choice = model_output.choices[0]
272
276
  delta = choice.delta
273
277
 
@@ -63,6 +63,8 @@ class MiniMaxM3Client(LLMClient):
63
63
  ThinkingLevel.MEDIUM: "medium",
64
64
  ThinkingLevel.HIGH: "high",
65
65
  ThinkingLevel.XHIGH: "high",
66
+ # MiniMax stops at "high"
67
+ ThinkingLevel.MAX: "high",
66
68
  }
67
69
  return mapping[thinking_level]
68
70
 
@@ -248,6 +250,7 @@ class MiniMaxM3Client(LLMClient):
248
250
  "response.output_item.done",
249
251
  "response.content_part.added",
250
252
  "response.content_part.done",
253
+ "keepalive", # gateway heartbeat on long generations; carries no content
251
254
  ):
252
255
  raise ValueError(f"Unknown output: {model_output}")
253
256
 
@@ -220,7 +220,9 @@ class OpenaiChatClient(LLMClient):
220
220
  usage_metadata: UsageMetadata | None = None
221
221
  finish_reason: FinishReason | None = None
222
222
 
223
- if len(model_output.choices) > 0:
223
+ # gateways inject content-free heartbeat chunks on long generations, whose choices
224
+ # the SDK leaves as None rather than an empty list
225
+ if model_output.choices:
224
226
  choice = model_output.choices[0]
225
227
  delta = choice.delta
226
228
 
@@ -54,6 +54,7 @@ class OpenaiResponsesClient(LLMClient):
54
54
  ThinkingLevel.MEDIUM: "medium",
55
55
  ThinkingLevel.HIGH: "high",
56
56
  ThinkingLevel.XHIGH: "xhigh",
57
+ ThinkingLevel.MAX: "max",
57
58
  }
58
59
  return mapping.get(thinking_level)
59
60
 
@@ -298,6 +299,7 @@ class OpenaiResponsesClient(LLMClient):
298
299
  "response.reasoning_summary_text.done",
299
300
  "response.content_part.added",
300
301
  "response.content_part.done",
302
+ "keepalive", # gateway heartbeat on long generations; carries no content
301
303
  ):
302
304
  event_type = "unused"
303
305
 
@@ -24,6 +24,7 @@ class ThinkingLevel(StrEnum):
24
24
  MEDIUM = "medium"
25
25
  HIGH = "high"
26
26
  XHIGH = "xhigh"
27
+ MAX = "max"
27
28
 
28
29
 
29
30
  class PromptCaching(StrEnum):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agenthub-python"
3
- version = "0.4.2"
3
+ version = "0.4.4"
4
4
  description = "AgentHub is the LLM API Hub for the Agent era, built for high-precision autonomous agents."
5
5
  keywords = [
6
6
  "agent",
@@ -33,6 +33,7 @@ dev = [
33
33
  "httpx[socks]",
34
34
  "pytest>=8.4.2",
35
35
  "pytest-asyncio>=0.23.0",
36
+ "pytest-xdist>=3.6.0",
36
37
  "ruff>=0.14.3",
37
38
  "pillow>=10.0.0",
38
39
  ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agenthub-python"
3
- version = "0.4.2"
3
+ version = "0.4.4"
4
4
  description = "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
  readme = "README.md"
@@ -17,7 +17,7 @@ Repository = "https://github.com/Prism-Shadow/agenthub"
17
17
  Issues = "https://github.com/Prism-Shadow/agenthub/issues"
18
18
 
19
19
  [project.optional-dependencies]
20
- dev = ["httpx[socks]", "pytest>=8.4.2", "pytest-asyncio>=0.23.0", "ruff>=0.14.3", "pillow>=10.0.0"]
20
+ dev = ["httpx[socks]", "pytest>=8.4.2", "pytest-asyncio>=0.23.0", "pytest-xdist>=3.6.0", "ruff>=0.14.3", "pillow>=10.0.0"]
21
21
 
22
22
  [build-system]
23
23
  requires = ["uv_build>=0.9.16,<0.10.0"]