agenthub-python 0.4.13__tar.gz → 0.4.15__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 (39) hide show
  1. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/PKG-INFO +1 -1
  2. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/deepseek_v4/client.py +63 -29
  3. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/gpt6/client.py +63 -29
  4. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/minimax_m3/client.py +51 -24
  5. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/openai_responses/client.py +53 -26
  6. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/types.py +3 -0
  7. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/pyproject.toml +1 -1
  8. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/pyproject.toml.orig +1 -1
  9. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/README.md +0 -0
  10. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/__init__.py +0 -0
  11. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/abort_signal.py +0 -0
  12. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/ant_messages/__init__.py +0 -0
  13. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/ant_messages/client.py +0 -0
  14. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/auto_client.py +0 -0
  15. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/base_client.py +0 -0
  16. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/claude5/__init__.py +0 -0
  17. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/claude5/client.py +0 -0
  18. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/deepseek_v4/__init__.py +0 -0
  19. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/errors.py +0 -0
  20. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/gemini3_8/__init__.py +0 -0
  21. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/gemini3_8/client.py +0 -0
  22. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/glm5_3/__init__.py +0 -0
  23. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/glm5_3/client.py +0 -0
  24. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/gpt6/__init__.py +0 -0
  25. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/integration/__init__.py +0 -0
  26. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/integration/playground.py +0 -0
  27. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/integration/tracer.py +0 -0
  28. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/kimi_k3/__init__.py +0 -0
  29. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/kimi_k3/client.py +0 -0
  30. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/minimax_m3/__init__.py +0 -0
  31. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/openai_chat/__init__.py +0 -0
  32. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/openai_chat/client.py +0 -0
  33. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/openai_chat_vllm_adapter/__init__.py +0 -0
  34. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/openai_chat_vllm_adapter/client.py +0 -0
  35. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/openai_embedding/__init__.py +0 -0
  36. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/openai_embedding/client.py +0 -0
  37. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/openai_responses/__init__.py +0 -0
  38. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/registry.py +0 -0
  39. {agenthub_python-0.4.13 → agenthub_python-0.4.15}/agenthub/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agenthub-python
3
- Version: 0.4.13
3
+ Version: 0.4.15
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
@@ -201,16 +201,23 @@ class DeepSeekV4Client(LLMClient):
201
201
  raise ValueError("tool_call_id is required for tool result.")
202
202
 
203
203
  # NOTE: tool results are input items
204
- tool_result = [{"type": "input_text", "text": item["text"]}]
204
+ image_parts = []
205
205
  if "images" in item:
206
206
  if not supports_image:
207
207
  raise ValueError(f"DeepSeek {self._model} does not support images in tool results.")
208
208
 
209
209
  for image_url in item["images"]:
210
- tool_result.append({"type": "input_image", "image_url": image_url})
210
+ image_parts.append({"type": "input_image", "image_url": image_url})
211
+
212
+ # a plain string is the form the Responses API documents for a text result and the
213
+ # one every endpoint that fronts this model accepts; the content-part list is
214
+ # reserved for results carrying images
215
+ output = (
216
+ [{"type": "input_text", "text": item["text"]}, *image_parts] if image_parts else item["text"]
217
+ )
211
218
 
212
219
  input_list.append(
213
- {"type": "function_call_output", "call_id": item["tool_call_id"], "output": tool_result}
220
+ {"type": "function_call_output", "call_id": item["tool_call_id"], "output": output}
214
221
  )
215
222
  else:
216
223
  raise ValueError(f"Unknown item: {item}")
@@ -253,6 +260,7 @@ class DeepSeekV4Client(LLMClient):
253
260
  "name": model_output.item.name,
254
261
  "arguments": "",
255
262
  "tool_call_id": model_output.item.call_id,
263
+ "item_id": model_output.item.id,
256
264
  }
257
265
  )
258
266
  else:
@@ -261,11 +269,27 @@ class DeepSeekV4Client(LLMClient):
261
269
  elif deepseek_event_type == "response.function_call_arguments.delta":
262
270
  event_type = "delta"
263
271
  content_items.append(
264
- {"type": "partial_tool_call", "name": "", "arguments": model_output.delta, "tool_call_id": ""}
272
+ {
273
+ "type": "partial_tool_call",
274
+ "name": "",
275
+ "arguments": model_output.delta,
276
+ "tool_call_id": "",
277
+ "item_id": model_output.item_id,
278
+ }
265
279
  )
266
280
 
267
281
  elif deepseek_event_type == "response.function_call_arguments.done":
282
+ # a stop naming the item closes that call
268
283
  event_type = "stop"
284
+ content_items.append(
285
+ {
286
+ "type": "partial_tool_call",
287
+ "name": "",
288
+ "arguments": "",
289
+ "tool_call_id": "",
290
+ "item_id": model_output.item_id,
291
+ }
292
+ )
269
293
 
270
294
  elif deepseek_event_type in ("response.completed", "response.incomplete"):
271
295
  event_type = "stop"
@@ -327,51 +351,61 @@ class DeepSeekV4Client(LLMClient):
327
351
  # Use unified message conversion
328
352
  input_list = self.transform_uni_message_to_model_input(messages)
329
353
 
354
+ # Calls still streaming, keyed by the item id their fragments carry (the call id when a
355
+ # server sends none): a gateway may open several before closing any of them.
356
+ open_tool_calls: dict[str, dict] = {}
357
+ last_opened = ""
358
+
359
+ def key_of(item_id: str | None) -> str:
360
+ return item_id if item_id in open_tool_calls else last_opened
361
+
330
362
  # Stream generate
331
- partial_tool_call = {}
332
363
  stream = await self._client.responses.create(**deepseek_config, input=input_list, stream=True)
333
- async for event in stream:
334
- event = self.transform_model_output_to_uni_event(event)
364
+ async for model_event in stream:
365
+ event = self.transform_model_output_to_uni_event(model_event)
366
+ fragments = [item for item in event["content_items"] if item["type"] == "partial_tool_call"]
335
367
  if event["event_type"] == "start":
336
- for item in event["content_items"]:
337
- if item["type"] == "partial_tool_call":
338
- # initialize partial_tool_call
339
- partial_tool_call = {
340
- "name": item["name"],
341
- "arguments": "",
342
- "tool_call_id": item["tool_call_id"],
343
- }
344
- yield event
368
+ for item in fragments:
369
+ last_opened = item.get("item_id") or item["tool_call_id"]
370
+ open_tool_calls[last_opened] = {
371
+ "name": item["name"],
372
+ "tool_call_id": item["tool_call_id"],
373
+ "arguments": "",
374
+ }
375
+ yield event
345
376
  elif event["event_type"] == "delta":
346
- for item in event["content_items"]:
347
- if item["type"] == "partial_tool_call":
348
- # update partial_tool_call
349
- partial_tool_call["arguments"] += item["arguments"]
350
-
377
+ for item in fragments:
378
+ tool_call = open_tool_calls.get(key_of(item.get("item_id")))
379
+ if tool_call is not None:
380
+ tool_call["arguments"] += item["arguments"]
351
381
  yield event
352
382
  elif event["event_type"] == "stop":
353
- if "name" in partial_tool_call and "arguments" in partial_tool_call:
354
- # finish partial_tool_call
383
+ # a stop that names calls closes them; the end of the response closes whatever
384
+ # a gateway never closed on its own
385
+ closing = [key_of(item.get("item_id")) for item in fragments] or list(open_tool_calls)
386
+ for key in closing:
387
+ tool_call = open_tool_calls.pop(key, None)
388
+ if tool_call is None:
389
+ continue
355
390
  yield {
356
391
  "role": "assistant",
357
392
  "event_type": "delta",
358
393
  "content_items": [
359
394
  {
360
395
  "type": "tool_call",
361
- "name": partial_tool_call["name"],
396
+ "name": tool_call["name"],
362
397
  "arguments": parse_tool_call_arguments(
363
- partial_tool_call["arguments"],
398
+ tool_call["arguments"],
364
399
  self.__class__.__name__,
365
- partial_tool_call["name"],
366
- partial_tool_call["tool_call_id"],
400
+ tool_call["name"],
401
+ tool_call["tool_call_id"],
367
402
  ),
368
- "tool_call_id": partial_tool_call["tool_call_id"],
403
+ "tool_call_id": tool_call["tool_call_id"],
369
404
  }
370
405
  ],
371
406
  "usage_metadata": None,
372
407
  "finish_reason": None,
373
408
  }
374
- partial_tool_call = {}
375
409
 
376
410
  if event["finish_reason"] or event["usage_metadata"]:
377
411
  yield event
@@ -212,13 +212,20 @@ class GPT6Client(LLMClient):
212
212
  raise ValueError("tool_call_id is required for tool result.")
213
213
 
214
214
  # NOTE: tool results are input items
215
- tool_result = [{"type": "input_text", "text": item["text"]}]
215
+ image_parts = []
216
216
  if "images" in item:
217
217
  for image_url in item["images"]:
218
- tool_result.append(self._convert_image_url(image_url))
218
+ image_parts.append(self._convert_image_url(image_url))
219
+
220
+ # a plain string is the form the Responses API documents for a text result and the
221
+ # one every endpoint that fronts this model accepts; the content-part list is
222
+ # reserved for results carrying images
223
+ output = (
224
+ [{"type": "input_text", "text": item["text"]}, *image_parts] if image_parts else item["text"]
225
+ )
219
226
 
220
227
  input_list.append(
221
- {"type": "function_call_output", "call_id": item["tool_call_id"], "output": tool_result}
228
+ {"type": "function_call_output", "call_id": item["tool_call_id"], "output": output}
222
229
  )
223
230
  else:
224
231
  raise ValueError(f"Unknown item: {item}")
@@ -265,6 +272,7 @@ class GPT6Client(LLMClient):
265
272
  "name": model_output.item.name,
266
273
  "arguments": "",
267
274
  "tool_call_id": model_output.item.call_id,
275
+ "item_id": model_output.item.id,
268
276
  }
269
277
  )
270
278
  elif model_output.item.type == "message":
@@ -281,11 +289,27 @@ class GPT6Client(LLMClient):
281
289
  elif openai_event_type == "response.function_call_arguments.delta":
282
290
  event_type = "delta"
283
291
  content_items.append(
284
- {"type": "partial_tool_call", "name": "", "arguments": model_output.delta, "tool_call_id": ""}
292
+ {
293
+ "type": "partial_tool_call",
294
+ "name": "",
295
+ "arguments": model_output.delta,
296
+ "tool_call_id": "",
297
+ "item_id": model_output.item_id,
298
+ }
285
299
  )
286
300
 
287
301
  elif openai_event_type == "response.function_call_arguments.done":
302
+ # a stop naming the item closes that call
288
303
  event_type = "stop"
304
+ content_items.append(
305
+ {
306
+ "type": "partial_tool_call",
307
+ "name": "",
308
+ "arguments": "",
309
+ "tool_call_id": "",
310
+ "item_id": model_output.item_id,
311
+ }
312
+ )
289
313
 
290
314
  elif openai_event_type in ("response.completed", "response.incomplete"):
291
315
  event_type = "stop"
@@ -375,51 +399,61 @@ class GPT6Client(LLMClient):
375
399
  # Use unified message conversion
376
400
  input_list = self.transform_uni_message_to_model_input(messages)
377
401
 
402
+ # Calls still streaming, keyed by the item id their fragments carry (the call id when a
403
+ # server sends none): a gateway may open several before closing any of them.
404
+ open_tool_calls: dict[str, dict] = {}
405
+ last_opened = ""
406
+
407
+ def key_of(item_id: str | None) -> str:
408
+ return item_id if item_id in open_tool_calls else last_opened
409
+
378
410
  # Stream generate
379
- partial_tool_call = {}
380
411
  stream = await self._client.responses.create(**openai_config, input=input_list, stream=True)
381
- async for event in stream:
382
- event = self.transform_model_output_to_uni_event(event)
412
+ async for model_event in stream:
413
+ event = self.transform_model_output_to_uni_event(model_event)
414
+ fragments = [item for item in event["content_items"] if item["type"] == "partial_tool_call"]
383
415
  if event["event_type"] == "start":
384
- for item in event["content_items"]:
385
- if item["type"] == "partial_tool_call":
386
- # initialize partial_tool_call
387
- partial_tool_call = {
388
- "name": item["name"],
389
- "arguments": "",
390
- "tool_call_id": item["tool_call_id"],
391
- }
392
- yield event
416
+ for item in fragments:
417
+ last_opened = item.get("item_id") or item["tool_call_id"]
418
+ open_tool_calls[last_opened] = {
419
+ "name": item["name"],
420
+ "tool_call_id": item["tool_call_id"],
421
+ "arguments": "",
422
+ }
423
+ yield event
393
424
  elif event["event_type"] == "delta":
394
- for item in event["content_items"]:
395
- if item["type"] == "partial_tool_call":
396
- # update partial_tool_call
397
- partial_tool_call["arguments"] += item["arguments"]
398
-
425
+ for item in fragments:
426
+ tool_call = open_tool_calls.get(key_of(item.get("item_id")))
427
+ if tool_call is not None:
428
+ tool_call["arguments"] += item["arguments"]
399
429
  yield event
400
430
  elif event["event_type"] == "stop":
401
- if "name" in partial_tool_call and "arguments" in partial_tool_call:
402
- # finish partial_tool_call
431
+ # a stop that names calls closes them; the end of the response closes whatever
432
+ # a gateway never closed on its own
433
+ closing = [key_of(item.get("item_id")) for item in fragments] or list(open_tool_calls)
434
+ for key in closing:
435
+ tool_call = open_tool_calls.pop(key, None)
436
+ if tool_call is None:
437
+ continue
403
438
  yield {
404
439
  "role": "assistant",
405
440
  "event_type": "delta",
406
441
  "content_items": [
407
442
  {
408
443
  "type": "tool_call",
409
- "name": partial_tool_call["name"],
444
+ "name": tool_call["name"],
410
445
  "arguments": parse_tool_call_arguments(
411
- partial_tool_call["arguments"],
446
+ tool_call["arguments"],
412
447
  self.__class__.__name__,
413
- partial_tool_call["name"],
414
- partial_tool_call["tool_call_id"],
448
+ tool_call["name"],
449
+ tool_call["tool_call_id"],
415
450
  ),
416
- "tool_call_id": partial_tool_call["tool_call_id"],
451
+ "tool_call_id": tool_call["tool_call_id"],
417
452
  }
418
453
  ],
419
454
  "usage_metadata": None,
420
455
  "finish_reason": None,
421
456
  }
422
- partial_tool_call = {}
423
457
 
424
458
  if event["finish_reason"] or event["usage_metadata"]:
425
459
  yield event
@@ -217,17 +217,34 @@ class MiniMaxM3Client(LLMClient):
217
217
  "name": model_output.item.name,
218
218
  "arguments": "",
219
219
  "tool_call_id": model_output.item.call_id,
220
+ "item_id": model_output.item.id,
220
221
  }
221
222
  )
222
223
 
223
224
  elif minimax_event_type == "response.function_call_arguments.delta":
224
225
  event_type = "delta"
225
226
  content_items.append(
226
- {"type": "partial_tool_call", "name": "", "arguments": model_output.delta, "tool_call_id": ""}
227
+ {
228
+ "type": "partial_tool_call",
229
+ "name": "",
230
+ "arguments": model_output.delta,
231
+ "tool_call_id": "",
232
+ "item_id": model_output.item_id,
233
+ }
227
234
  )
228
235
 
229
236
  elif minimax_event_type == "response.function_call_arguments.done":
237
+ # a stop naming the item closes that call
230
238
  event_type = "stop"
239
+ content_items.append(
240
+ {
241
+ "type": "partial_tool_call",
242
+ "name": "",
243
+ "arguments": "",
244
+ "tool_call_id": "",
245
+ "item_id": model_output.item_id,
246
+ }
247
+ )
231
248
 
232
249
  elif minimax_event_type in ("response.completed", "response.incomplete"):
233
250
  event_type = "stop"
@@ -279,50 +296,60 @@ class MiniMaxM3Client(LLMClient):
279
296
  minimax_config = self.transform_uni_config_to_model_config(config)
280
297
  input_list = self.transform_uni_message_to_model_input(messages)
281
298
 
282
- partial_tool_call = {}
299
+ # Calls still streaming, keyed by the item id their fragments carry (the call id when a
300
+ # server sends none): a gateway may open several before closing any of them.
301
+ open_tool_calls: dict[str, dict] = {}
302
+ last_opened = ""
303
+
304
+ def key_of(item_id: str | None) -> str:
305
+ return item_id if item_id in open_tool_calls else last_opened
306
+
283
307
  stream = await self._client.responses.create(**minimax_config, input=input_list, stream=True)
284
308
  async for model_event in stream:
285
309
  event = self.transform_model_output_to_uni_event(model_event)
310
+ fragments = [item for item in event["content_items"] if item["type"] == "partial_tool_call"]
286
311
  if event["event_type"] == "start":
287
- for item in event["content_items"]:
288
- if item["type"] == "partial_tool_call":
289
- # initialize partial_tool_call
290
- partial_tool_call = {
291
- "name": item["name"],
292
- "arguments": "",
293
- "tool_call_id": item["tool_call_id"],
294
- }
295
- yield event
312
+ for item in fragments:
313
+ last_opened = item.get("item_id") or item["tool_call_id"]
314
+ open_tool_calls[last_opened] = {
315
+ "name": item["name"],
316
+ "tool_call_id": item["tool_call_id"],
317
+ "arguments": "",
318
+ }
319
+ yield event
296
320
  elif event["event_type"] == "delta":
297
- for item in event["content_items"]:
298
- if item["type"] == "partial_tool_call":
299
- # update partial_tool_call
300
- partial_tool_call["arguments"] += item["arguments"]
301
-
321
+ for item in fragments:
322
+ tool_call = open_tool_calls.get(key_of(item.get("item_id")))
323
+ if tool_call is not None:
324
+ tool_call["arguments"] += item["arguments"]
302
325
  yield event
303
326
  elif event["event_type"] == "stop":
304
- if "name" in partial_tool_call and "arguments" in partial_tool_call:
305
- # finish partial_tool_call
327
+ # a stop that names calls closes them; the end of the response closes whatever
328
+ # a gateway never closed on its own
329
+ closing = [key_of(item.get("item_id")) for item in fragments] or list(open_tool_calls)
330
+ for key in closing:
331
+ tool_call = open_tool_calls.pop(key, None)
332
+ if tool_call is None:
333
+ continue
306
334
  yield {
307
335
  "role": "assistant",
308
336
  "event_type": "delta",
309
337
  "content_items": [
310
338
  {
311
339
  "type": "tool_call",
312
- "name": partial_tool_call["name"],
340
+ "name": tool_call["name"],
313
341
  "arguments": parse_tool_call_arguments(
314
- partial_tool_call["arguments"],
342
+ tool_call["arguments"],
315
343
  self.__class__.__name__,
316
- partial_tool_call["name"],
317
- partial_tool_call["tool_call_id"],
344
+ tool_call["name"],
345
+ tool_call["tool_call_id"],
318
346
  ),
319
- "tool_call_id": partial_tool_call["tool_call_id"],
347
+ "tool_call_id": tool_call["tool_call_id"],
320
348
  }
321
349
  ],
322
350
  "usage_metadata": None,
323
351
  "finish_reason": None,
324
352
  }
325
- partial_tool_call = {}
326
353
 
327
354
  if event["finish_reason"] or event["usage_metadata"]:
328
355
  yield event
@@ -268,6 +268,7 @@ class OpenaiResponsesClient(LLMClient):
268
268
  "name": model_output.item.name,
269
269
  "arguments": "",
270
270
  "tool_call_id": model_output.item.call_id,
271
+ "item_id": model_output.item.id,
271
272
  }
272
273
  )
273
274
  elif model_output.item.type == "message" and getattr(model_output.item, "phase", None):
@@ -295,11 +296,27 @@ class OpenaiResponsesClient(LLMClient):
295
296
  elif openai_event_type == "response.function_call_arguments.delta":
296
297
  event_type = "delta"
297
298
  content_items.append(
298
- {"type": "partial_tool_call", "name": "", "arguments": model_output.delta, "tool_call_id": ""}
299
+ {
300
+ "type": "partial_tool_call",
301
+ "name": "",
302
+ "arguments": model_output.delta,
303
+ "tool_call_id": "",
304
+ "item_id": model_output.item_id,
305
+ }
299
306
  )
300
307
 
301
308
  elif openai_event_type == "response.function_call_arguments.done":
309
+ # a stop naming the item closes that call
302
310
  event_type = "stop"
311
+ content_items.append(
312
+ {
313
+ "type": "partial_tool_call",
314
+ "name": "",
315
+ "arguments": "",
316
+ "tool_call_id": "",
317
+ "item_id": model_output.item_id,
318
+ }
319
+ )
303
320
 
304
321
  elif openai_event_type in ("response.completed", "response.incomplete"):
305
322
  event_type = "stop"
@@ -364,51 +381,61 @@ class OpenaiResponsesClient(LLMClient):
364
381
  # Use unified message conversion
365
382
  input_list = self.transform_uni_message_to_model_input(messages)
366
383
 
384
+ # Calls still streaming, keyed by the item id their fragments carry (the call id when a
385
+ # server sends none): a gateway may open several before closing any of them.
386
+ open_tool_calls: dict[str, dict] = {}
387
+ last_opened = ""
388
+
389
+ def key_of(item_id: str | None) -> str:
390
+ return item_id if item_id in open_tool_calls else last_opened
391
+
367
392
  # Stream generate
368
- partial_tool_call = {}
369
393
  stream = await self._client.responses.create(**openai_config, input=input_list, stream=True)
370
- async for event in stream:
371
- event = self.transform_model_output_to_uni_event(event)
394
+ async for model_event in stream:
395
+ event = self.transform_model_output_to_uni_event(model_event)
396
+ fragments = [item for item in event["content_items"] if item["type"] == "partial_tool_call"]
372
397
  if event["event_type"] == "start":
373
- for item in event["content_items"]:
374
- if item["type"] == "partial_tool_call":
375
- # initialize partial_tool_call
376
- partial_tool_call = {
377
- "name": item["name"],
378
- "arguments": "",
379
- "tool_call_id": item["tool_call_id"],
380
- }
381
- yield event
398
+ for item in fragments:
399
+ last_opened = item.get("item_id") or item["tool_call_id"]
400
+ open_tool_calls[last_opened] = {
401
+ "name": item["name"],
402
+ "tool_call_id": item["tool_call_id"],
403
+ "arguments": "",
404
+ }
405
+ yield event
382
406
  elif event["event_type"] == "delta":
383
- for item in event["content_items"]:
384
- if item["type"] == "partial_tool_call":
385
- # update partial_tool_call
386
- partial_tool_call["arguments"] += item["arguments"]
387
-
407
+ for item in fragments:
408
+ tool_call = open_tool_calls.get(key_of(item.get("item_id")))
409
+ if tool_call is not None:
410
+ tool_call["arguments"] += item["arguments"]
388
411
  yield event
389
412
  elif event["event_type"] == "stop":
390
- if "name" in partial_tool_call and "arguments" in partial_tool_call:
391
- # finish partial_tool_call
413
+ # a stop that names calls closes them; the end of the response closes whatever
414
+ # a gateway never closed on its own
415
+ closing = [key_of(item.get("item_id")) for item in fragments] or list(open_tool_calls)
416
+ for key in closing:
417
+ tool_call = open_tool_calls.pop(key, None)
418
+ if tool_call is None:
419
+ continue
392
420
  yield {
393
421
  "role": "assistant",
394
422
  "event_type": "delta",
395
423
  "content_items": [
396
424
  {
397
425
  "type": "tool_call",
398
- "name": partial_tool_call["name"],
426
+ "name": tool_call["name"],
399
427
  "arguments": parse_tool_call_arguments(
400
- partial_tool_call["arguments"],
428
+ tool_call["arguments"],
401
429
  self.__class__.__name__,
402
- partial_tool_call["name"],
403
- partial_tool_call["tool_call_id"],
430
+ tool_call["name"],
431
+ tool_call["tool_call_id"],
404
432
  ),
405
- "tool_call_id": partial_tool_call["tool_call_id"],
433
+ "tool_call_id": tool_call["tool_call_id"],
406
434
  }
407
435
  ],
408
436
  "usage_metadata": None,
409
437
  "finish_reason": None,
410
438
  }
411
- partial_tool_call = {}
412
439
 
413
440
  if event["finish_reason"] or event["usage_metadata"]:
414
441
  yield event
@@ -96,6 +96,9 @@ class PartialToolCallContentItem(TypedDict):
96
96
  name: str
97
97
  arguments: str
98
98
  tool_call_id: str
99
+ # id of the streamed item this fragment belongs to, when the provider sends one (the
100
+ # Responses output item id); ties fragments to their call when several calls stream at once
101
+ item_id: NotRequired[str | None]
99
102
  fidelity: NotRequired[Fidelity]
100
103
 
101
104
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agenthub-python"
3
- version = "0.4.13"
3
+ version = "0.4.15"
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",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agenthub-python"
3
- version = "0.4.13"
3
+ version = "0.4.15"
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"