agenthub-python 0.4.9__tar.gz → 0.4.11__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.9 → agenthub_python-0.4.11}/PKG-INFO +1 -1
  2. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/ant_messages/client.py +9 -2
  3. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/auto_client.py +25 -11
  4. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/claude5/client.py +8 -2
  5. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/deepseek_v4/client.py +20 -5
  6. {agenthub_python-0.4.9/agenthub/gemini3_7 → agenthub_python-0.4.11/agenthub/gemini3_8}/__init__.py +2 -2
  7. {agenthub_python-0.4.9/agenthub/gemini3_7 → agenthub_python-0.4.11/agenthub/gemini3_8}/client.py +18 -20
  8. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/glm5_3/client.py +14 -14
  9. {agenthub_python-0.4.9/agenthub/gpt5_6 → agenthub_python-0.4.11/agenthub/gpt6}/__init__.py +2 -2
  10. {agenthub_python-0.4.9/agenthub/gpt5_6 → agenthub_python-0.4.11/agenthub/gpt6}/client.py +24 -8
  11. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/openai_chat/client.py +7 -2
  12. agenthub_python-0.4.11/agenthub/openai_chat_vllm_adapter/__init__.py +4 -0
  13. agenthub_python-0.4.11/agenthub/openai_chat_vllm_adapter/client.py +122 -0
  14. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/openai_responses/client.py +21 -3
  15. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/registry.py +71 -17
  16. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/pyproject.toml +1 -1
  17. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/pyproject.toml.orig +1 -1
  18. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/README.md +0 -0
  19. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/__init__.py +0 -0
  20. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/abort_signal.py +0 -0
  21. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/ant_messages/__init__.py +0 -0
  22. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/base_client.py +0 -0
  23. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/claude5/__init__.py +0 -0
  24. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/deepseek_v4/__init__.py +0 -0
  25. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/errors.py +0 -0
  26. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/glm5_3/__init__.py +0 -0
  27. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/integration/__init__.py +0 -0
  28. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/integration/playground.py +0 -0
  29. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/integration/tracer.py +0 -0
  30. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/kimi_k3/__init__.py +0 -0
  31. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/kimi_k3/client.py +0 -0
  32. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/minimax_m3/__init__.py +0 -0
  33. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/minimax_m3/client.py +0 -0
  34. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/openai_chat/__init__.py +0 -0
  35. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/openai_embedding/__init__.py +0 -0
  36. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/openai_embedding/client.py +0 -0
  37. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/openai_responses/__init__.py +0 -0
  38. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/types.py +0 -0
  39. {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agenthub-python
3
- Version: 0.4.9
3
+ Version: 0.4.11
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
@@ -128,8 +128,15 @@ class AntMessagesClient(LLMClient):
128
128
 
129
129
  if config.get("thinking_level") is not None:
130
130
  ant_config.update(self._convert_thinking_level_to_thinking_config(config["thinking_level"]))
131
- if config.get("thinking_summary") and ant_config.get("thinking", {}).get("type") == "adaptive":
132
- ant_config["thinking"]["display"] = "summarized"
131
+
132
+ if config.get("thinking_summary") is not None:
133
+ # display lives on the thinking block, so a summary asked for on its own selects
134
+ # adaptive thinking. A disabled block is the one place it cannot ride along --
135
+ # "thinking.disabled.display: Extra inputs are not permitted" (400, verified live
136
+ # 2026-09-03) -- and thinking_level NONE disables thinking, leaving nothing to show.
137
+ thinking = ant_config.setdefault("thinking", {"type": "adaptive"})
138
+ if thinking["type"] != "disabled":
139
+ thinking["display"] = "summarized" if config["thinking_summary"] else "omitted"
133
140
 
134
141
  # Convert tools to the Messages API tool schema
135
142
  if config.get("tools") is not None:
@@ -21,7 +21,13 @@ from .types import UniConfig, UniEvent, UniMessage
21
21
 
22
22
 
23
23
  # The generic protocol clients are named explicitly rather than deduced from a model id.
24
- _PROTOCOL_CLIENT_TYPES = ("openai-chat", "openai-responses", "ant-messages", "openai-embedding")
24
+ _PROTOCOL_CLIENT_TYPES = (
25
+ "openai-chat",
26
+ "openai-chat-vllm-adapter",
27
+ "openai-responses",
28
+ "ant-messages",
29
+ "openai-embedding",
30
+ )
25
31
 
26
32
 
27
33
  class AutoLLMClient(LLMClient):
@@ -65,23 +71,25 @@ class AutoLLMClient(LLMClient):
65
71
  type[LLMClient] | None: The client class, or None when no client claims the type.
66
72
  """
67
73
  # every Gemini generation shares the unified client ("gemini-3" also matches the
68
- # gemini-3.7/gemini-3.6/gemini-3.5-flash-lite client types)
74
+ # gemini-3.8/gemini-3.7/gemini-3.6/gemini-3.5-flash-lite client types)
69
75
  if any(
70
76
  prefix in client_type for prefix in ("gemini-3", "gemini-embedding")
71
- ): # e.g., gemini-3.7-flash, gemini-3-flash-preview, gemini-embedding-2
72
- from .gemini3_7 import Gemini3_7Client
77
+ ): # e.g., gemini-3.8-flash, gemini-3-flash-preview, gemini-embedding-2
78
+ from .gemini3_8 import Gemini3_8Client
73
79
 
74
- return Gemini3_7Client
80
+ return Gemini3_8Client
75
81
  elif "claude" in client_type and (
76
82
  "4-6" in client_type or "4-7" in client_type or "4-8" in client_type or "-5" in client_type
77
83
  ): # the whole Claude 4.6+ series shares the unified client, e.g., claude-sonnet-4-6
78
84
  from .claude5 import Claude5Client
79
85
 
80
86
  return Claude5Client
81
- elif "gpt-5.4" in client_type or "gpt-5.5" in client_type or "gpt-5.6" in client_type: # e.g., gpt-5.6
82
- from .gpt5_6 import GPT5_6Client
87
+ elif (
88
+ "gpt-5.4" in client_type or "gpt-5.5" in client_type or "gpt-5.6" in client_type or "gpt-6" in client_type
89
+ ): # e.g., gpt-6-astra
90
+ from .gpt6 import GPT6Client
83
91
 
84
- return GPT5_6Client
92
+ return GPT6Client
85
93
  elif "glm-5" in client_type: # the whole GLM series shares the unified client
86
94
  from .glm5_3 import GLM5_3Client
87
95
 
@@ -99,6 +107,12 @@ class AutoLLMClient(LLMClient):
99
107
  from .deepseek_v4 import DeepSeekV4Client
100
108
 
101
109
  return DeepSeekV4Client
110
+ elif client_type == "openai-chat-vllm-adapter":
111
+ # exact match: "openai-chat-vllm-adapter" contains "openai", so the substring
112
+ # branches below would otherwise claim it
113
+ from .openai_chat_vllm_adapter import OpenaiChatVllmAdapterClient
114
+
115
+ return OpenaiChatVllmAdapterClient
102
116
  elif "ant-messages" in client_type:
103
117
  from .ant_messages import AntMessagesClient
104
118
 
@@ -131,10 +145,10 @@ class AutoLLMClient(LLMClient):
131
145
  if client_class is None:
132
146
  raise ValueError(
133
147
  f"{client_type} is not supported. "
134
- "Supported client types: minimax-m3, gemini-3.7, gemini-3.6, gemini-3, "
135
- "claude-5, claude-4-8, claude-4-7, claude-4-6, gpt-5.6, gpt-5.5, gpt-5.4, "
148
+ "Supported client types: minimax-m3, gemini-3.8, gemini-3.7, gemini-3.6, gemini-3, "
149
+ "claude-5, claude-4-8, claude-4-7, claude-4-6, gpt-6, gpt-5.6, gpt-5.5, gpt-5.4, "
136
150
  "glm-5.3, glm-5.2, glm-5.1, kimi-k3, kimi-k2.6, kimi-k2.5, deepseek-v4, "
137
- "openai-embedding, ant-messages, openai-responses, openai-chat."
151
+ "openai-chat-vllm-adapter, openai-embedding, ant-messages, openai-responses, openai-chat."
138
152
  )
139
153
 
140
154
  return client_class(model=model, api_key=api_key, base_url=base_url, default_headers=default_headers)
@@ -176,8 +176,14 @@ class Claude5Client(LLMClient):
176
176
 
177
177
  if config.get("thinking_level") is not None:
178
178
  claude_config.update(self._convert_thinking_level_to_thinking_config(config["thinking_level"]))
179
- if config.get("thinking_summary") and claude_config.get("thinking") is not None:
180
- claude_config["thinking"]["display"] = "summarized"
179
+
180
+ if config.get("thinking_summary") is not None:
181
+ # display lives on the thinking block, so a summary asked for on its own selects
182
+ # adaptive thinking, which is what this family runs by default anyway; a block
183
+ # carrying display but no output_config is accepted on 4.6 through 5 (verified
184
+ # live 2026-09-03). NONE omits the block, so the request lands on that default.
185
+ thinking = claude_config.setdefault("thinking", {"type": "adaptive"})
186
+ thinking["display"] = "summarized" if config["thinking_summary"] else "omitted"
181
187
 
182
188
  # Convert tools to Claude's tool schema
183
189
  if config.get("tools") is not None:
@@ -14,6 +14,7 @@
14
14
 
15
15
  import json
16
16
  import os
17
+ import re
17
18
  from typing import Any, AsyncIterator
18
19
 
19
20
  from openai import AsyncOpenAI
@@ -36,6 +37,13 @@ from ..types import (
36
37
  from ..utils import is_debug_enabled
37
38
 
38
39
 
40
+ # The DeepSeek ids that read no image: the current V4 Flash and V4 Pro, bare or with a dated
41
+ # snapshot suffix (deepseek-v4-flash-0731). Every other id forwards its images. Matched against
42
+ # the bare id — the part after the last "/", lowercased — so a gateway prefix (deepseek/,
43
+ # deepseek-ai/) and the spelling a platform uses do not change the verdict.
44
+ _TEXT_ONLY_MODELS = re.compile(r"deepseek-v4-(flash|pro)(-\d{4})?")
45
+
46
+
39
47
  class DeepSeekV4Client(LLMClient):
40
48
  """DeepSeek V4-specific LLM client implementation using the OpenAI-compatible Responses API."""
41
49
 
@@ -103,10 +111,17 @@ class DeepSeekV4Client(LLMClient):
103
111
  self.__class__.__name__, "temperature", "DeepSeek V4 does not support setting temperature."
104
112
  )
105
113
 
106
- # a thinking summary is accepted but never generated, so the parameter is left out
107
114
  if config.get("thinking_level") is not None:
108
115
  deepseek_config["reasoning"] = {"effort": self._convert_thinking_level_to_effort(config["thinking_level"])}
109
116
 
117
+ if config.get("thinking_summary"):
118
+ # DeepSeek takes reasoning.summary with or without an effort and returns an empty
119
+ # summary list for now (verified live 2026-09-03 on api.deepseek.com and
120
+ # OpenRouter), so the request carries the preference instead of dropping it and
121
+ # picks up summaries as soon as the vendor generates them. False needs no key:
122
+ # the Responses API returns no summary unless one is asked for.
123
+ deepseek_config.setdefault("reasoning", {})["summary"] = "concise"
124
+
110
125
  if config.get("tools") is not None:
111
126
  deepseek_config["tools"] = [{"type": "function", **tool} for tool in config["tools"]]
112
127
 
@@ -135,10 +150,10 @@ class DeepSeekV4Client(LLMClient):
135
150
  Returns:
136
151
  List of input items for the Responses API
137
152
  """
138
- # only a vision model reads image parts; every other DeepSeek model answers from a
139
- # placeholder instead of failing (llmsdk_docs/deepseek_v4/docs/responses-api.md), so an
140
- # image is refused here rather than silently dropped
141
- supports_image = "vision" in self._model.lower()
153
+ # a text-only model answers from a placeholder instead of failing
154
+ # (llmsdk_docs/deepseek_v4/docs/responses-api.md), so an image is refused here rather
155
+ # than silently dropped
156
+ supports_image = not _TEXT_ONLY_MODELS.fullmatch(self._model.lower().rsplit("/", 1)[-1])
142
157
  input_list: list[ResponseInputParam] = []
143
158
 
144
159
  for msg in messages:
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from .client import Gemini3_7Client
15
+ from .client import Gemini3_8Client
16
16
 
17
17
 
18
- __all__ = ["Gemini3_7Client"]
18
+ __all__ = ["Gemini3_8Client"]
@@ -61,14 +61,14 @@ def _split_function_response_runs(parts: list[types.Part]) -> list[list[types.Pa
61
61
  return runs if runs else [parts]
62
62
 
63
63
 
64
- class Gemini3_7Client(LLMClient):
65
- """Unified client for the Gemini family, named for the newest generation it serves (3.7).
66
-
67
- It serves every generateContent model generation (3.7 back through 3.x text, image, TTS,
68
- and embedding models, with the 2.5 series reachable via an explicit client_type). The API
69
- deprecated the temperature/top_p/top_k sampling parameters starting with the 3.6
70
- generation (silently ignored today, HTTP 400 in future generations), and this client
71
- applies that contract to the whole family: temperature is rejected everywhere.
64
+ class Gemini3_8Client(LLMClient):
65
+ """Unified client for the Gemini family, named for the newest generation it serves (3.8).
66
+
67
+ It serves every generateContent model generation (3.8 back through 3.x text, image, TTS,
68
+ and embedding models). The API deprecated the temperature/top_p/top_k sampling parameters
69
+ starting with the 3.6 generation (silently ignored today, HTTP 400 in future generations),
70
+ and this client applies that contract to the whole family: temperature is rejected
71
+ everywhere.
72
72
  """
73
73
 
74
74
  def __init__(
@@ -78,7 +78,7 @@ class Gemini3_7Client(LLMClient):
78
78
  base_url: str | None = None,
79
79
  default_headers: dict[str, str] | None = None,
80
80
  ):
81
- """Initialize Gemini 3.7 client with model and API key."""
81
+ """Initialize Gemini 3.8 client with model and API key."""
82
82
  self._model = model
83
83
  api_key = api_key or os.getenv("GEMINI_API_KEY")
84
84
  base_url = base_url or os.getenv("GEMINI_BASE_URL")
@@ -139,15 +139,11 @@ class Gemini3_7Client(LLMClient):
139
139
  )
140
140
 
141
141
  def _supported_thinking_levels(self) -> tuple[types.ThinkingLevel, ...]:
142
- """Thinking levels the target model accepts (llmsdk_docs/gemini3_7/docs/thinking.md).
142
+ """Thinking levels the target model accepts (llmsdk_docs/gemini3_8/docs/thinking.md).
143
143
 
144
144
  An empty tuple means the model rejects the thinking_level parameter
145
145
  entirely, so it must be omitted from the request.
146
146
  """
147
- if "gemini-2.5" in self._model:
148
- # The vendor table claims low/medium/high, but the live API rejects
149
- # every thinking_level value for the 2.5 series (verified 2026-07-24).
150
- return ()
151
147
  if "-image" in self._model:
152
148
  return (types.ThinkingLevel.MINIMAL, types.ThinkingLevel.HIGH)
153
149
  if "gemini-3-pro" in self._model:
@@ -159,8 +155,9 @@ class Gemini3_7Client(LLMClient):
159
155
  # would have accepted costs a little accuracy, forwarding an
160
156
  # unsupported one is a 400).
161
157
  return (types.ThinkingLevel.LOW, types.ThinkingLevel.MEDIUM, types.ThinkingLevel.HIGH)
162
- if "gemini-3.7" in self._model:
163
- # The 3.7 generation rejects "minimal" with a 400 (verified live 2026-08-13).
158
+ if "gemini-3.7" in self._model or "gemini-3.8" in self._model:
159
+ # Both generations reject "minimal" with a 400 (3.7 verified live 2026-08-13;
160
+ # 3.8 documented at ai.google.dev/gemini-api/docs/latest-model).
164
161
  return (types.ThinkingLevel.LOW, types.ThinkingLevel.MEDIUM, types.ThinkingLevel.HIGH)
165
162
  return self._GEMINI_LEVEL_ORDER
166
163
 
@@ -180,8 +177,9 @@ class Gemini3_7Client(LLMClient):
180
177
  return None
181
178
  supported = self._supported_thinking_levels()
182
179
  if not supported:
183
- # The model takes no thinking_level at all; drop the parameter and
184
- # let the model use its default instead of forwarding a 400.
180
+ # A model that takes no thinking_level at all has nothing to clamp onto, so the
181
+ # parameter is omitted rather than turned into a failed request. thinking_summary
182
+ # is unaffected -- include_thoughts still rides along.
185
183
  return None
186
184
  if level in supported:
187
185
  return level
@@ -281,7 +279,7 @@ class Gemini3_7Client(LLMClient):
281
279
  config_params["system_instruction"] = config["system_prompt"]
282
280
 
283
281
  # include_thoughts asks for thought summaries, but whether generateContent returns any
284
- # is model-dependent (llmsdk_docs/gemini3_7/docs/thinking.md)
282
+ # is model-dependent (llmsdk_docs/gemini3_8/docs/thinking.md)
285
283
  thinking_summary = config.get("thinking_summary")
286
284
  thinking_level = config.get("thinking_level")
287
285
  if thinking_summary is not None or thinking_level is not None:
@@ -570,7 +568,7 @@ class Gemini3_7Client(LLMClient):
570
568
 
571
569
  for item in event["content_items"]:
572
570
  if item["type"] == "tool_call":
573
- # gemini 3.7 does not support partial tool call, mock a partial tool call event
571
+ # the Gemini API does not stream partial tool calls, mock a partial tool call event
574
572
  yield {
575
573
  "role": "assistant",
576
574
  "event_type": "delta",
@@ -75,9 +75,11 @@ class GLM5_3Client(LLMClient):
75
75
 
76
76
  GLM-5.3 accepts only low/high/max and errors on anything else, so the client
77
77
  clamps to the closest value; NONE rides on low because 5.3 cannot disable
78
- thinking. GLM-5.2 accepts the full vocabulary and maps it server-side
79
- (low/medium to high, xhigh to max); NONE disables thinking there instead.
80
- Models before 5.2 take no reasoning_effort parameter at all.
78
+ thinking. Every earlier generation takes the vocabulary unchanged: 5.2 maps it
79
+ server-side (low/medium to high, xhigh to max), and 5.1 and below accept the
80
+ parameter and ignore it (verified live 2026-09-03 on Z.AI, OpenRouter and
81
+ SiliconFlow), so the level is forwarded there rather than dropped. Outside 5.3
82
+ NONE disables thinking outright, which leaves no effort to send.
81
83
  """
82
84
  model = self._model.lower() # provider-hosted ids keep their own casing
83
85
  if "glm-5.3" in model:
@@ -90,17 +92,15 @@ class GLM5_3Client(LLMClient):
90
92
  ThinkingLevel.MAX: "max",
91
93
  }
92
94
  return mapping.get(thinking_level)
93
- if "glm-5.2" in model:
94
- mapping = {
95
- ThinkingLevel.NONE: None,
96
- ThinkingLevel.LOW: "low",
97
- ThinkingLevel.MEDIUM: "medium",
98
- ThinkingLevel.HIGH: "high",
99
- ThinkingLevel.XHIGH: "xhigh",
100
- ThinkingLevel.MAX: "max",
101
- }
102
- return mapping.get(thinking_level)
103
- return None
95
+ mapping = {
96
+ ThinkingLevel.NONE: None,
97
+ ThinkingLevel.LOW: "low",
98
+ ThinkingLevel.MEDIUM: "medium",
99
+ ThinkingLevel.HIGH: "high",
100
+ ThinkingLevel.XHIGH: "xhigh",
101
+ ThinkingLevel.MAX: "max",
102
+ }
103
+ return mapping.get(thinking_level)
104
104
 
105
105
  def _convert_tool_choice(self, tool_choice: ToolChoice) -> str:
106
106
  """Convert ToolChoice to OpenAI's tool_choice format."""
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from .client import GPT5_6Client
15
+ from .client import GPT6Client
16
16
 
17
17
 
18
- __all__ = ["GPT5_6Client"]
18
+ __all__ = ["GPT6Client"]
@@ -36,8 +36,8 @@ from ..types import (
36
36
  from ..utils import is_debug_enabled, openai_image_detail
37
37
 
38
38
 
39
- class GPT5_6Client(LLMClient):
40
- """GPT-5.6-specific LLM client implementation (also serves GPT-5.4 and GPT-5.5)."""
39
+ class GPT6Client(LLMClient):
40
+ """GPT-6-specific LLM client implementation (also serves GPT-5.6, GPT-5.5 and GPT-5.4)."""
41
41
 
42
42
  def __init__(
43
43
  self,
@@ -46,7 +46,7 @@ class GPT5_6Client(LLMClient):
46
46
  base_url: str | None = None,
47
47
  default_headers: dict[str, str] | None = None,
48
48
  ):
49
- """Initialize GPT-5.6 client with model and API key."""
49
+ """Initialize GPT-6 client with model and API key."""
50
50
  self._model = model
51
51
  api_key = api_key or os.getenv("OPENAI_API_KEY")
52
52
  base_url = base_url or os.getenv("OPENAI_BASE_URL")
@@ -55,6 +55,13 @@ class GPT5_6Client(LLMClient):
55
55
 
56
56
  def _convert_thinking_level_to_effort(self, thinking_level: ThinkingLevel) -> str:
57
57
  """Convert ThinkingLevel enum to OpenAI's reasoning effort."""
58
+ if thinking_level == ThinkingLevel.NONE and "gpt-6" in self._model:
59
+ # GPT-6 rejects both "none" and "minimal" with a 400 (verified live 2026-09-09:
60
+ # "Unsupported value: 'none' is not supported with the 'gpt-6-astra' model.
61
+ # Supported values are: 'low', 'medium', 'high', 'xhigh', and 'max'."), so NONE
62
+ # degrades to the lowest effort the generation accepts.
63
+ return "low"
64
+
58
65
  mapping = {
59
66
  ThinkingLevel.NONE: "none",
60
67
  ThinkingLevel.LOW: "low",
@@ -101,13 +108,17 @@ class GPT5_6Client(LLMClient):
101
108
 
102
109
  if config.get("temperature") is not None and config["temperature"] != 1.0:
103
110
  raise UnsupportedParameterError(
104
- self.__class__.__name__, "temperature", "GPT-5.6 does not support setting temperature."
111
+ self.__class__.__name__, "temperature", "GPT-6 does not support setting temperature."
105
112
  )
106
113
 
107
114
  if config.get("thinking_level") is not None:
108
115
  openai_config["reasoning"] = {"effort": self._convert_thinking_level_to_effort(config["thinking_level"])}
109
- if config.get("thinking_summary"):
110
- openai_config["reasoning"]["summary"] = "concise"
116
+
117
+ if config.get("thinking_summary"):
118
+ # reasoning.summary stands on its own, with or without an effort (verified live
119
+ # 2026-09-03 on the OpenAI and OpenRouter endpoints). False needs no key: the
120
+ # Responses API returns no summary unless one is asked for.
121
+ openai_config.setdefault("reasoning", {})["summary"] = "concise"
111
122
 
112
123
  if config.get("tools") is not None:
113
124
  openai_config["tools"] = [{"type": "function", **tool} for tool in config["tools"]]
@@ -120,7 +131,7 @@ class GPT5_6Client(LLMClient):
120
131
 
121
132
  if config.get("prompt_caching") is not None and config["prompt_caching"] != PromptCaching.ENABLE:
122
133
  raise UnsupportedParameterError(
123
- self.__class__.__name__, "prompt_caching", "prompt_caching must be ENABLE for GPT-5.6."
134
+ self.__class__.__name__, "prompt_caching", "prompt_caching must be ENABLE for GPT-6."
124
135
  )
125
136
 
126
137
  return openai_config
@@ -302,7 +313,12 @@ class GPT5_6Client(LLMClient):
302
313
  # the completed item carries the canonical wire fields to send back on the
303
314
  # next turn (identical to the response.completed copy, but adjacent to the
304
315
  # thinking deltas so the fidelity lands on the item that carried the text);
305
- # record the channel plus the fields the server demands back
316
+ # record the channel plus the fields the server demands back. This event is the
317
+ # only source of encrypted_content, because the streaming-events reference says
318
+ # of response.output_item.added: "For reasoning items, encrypted_content may be
319
+ # incomplete while the item is in progress. Use the reasoning item from the
320
+ # corresponding response.output_item.done event when passing it as input to a
321
+ # subsequent request."
306
322
  event_type = "delta"
307
323
  fidelity = {}
308
324
  if getattr(model_output.item, "summary", None):
@@ -173,7 +173,7 @@ class OpenaiChatClient(LLMClient):
173
173
  if "tool_call_id" not in item:
174
174
  raise ValueError("tool_call_id is required for tool result.")
175
175
 
176
- content = [{"type": "text", "text": item["text"]}]
176
+ image_parts = []
177
177
 
178
178
  if "images" in item and item["images"]:
179
179
  for image_url in item["images"]:
@@ -182,7 +182,12 @@ class OpenaiChatClient(LLMClient):
182
182
  # siliconflow does not support image_url in tool result
183
183
  content_parts.append(part)
184
184
  else:
185
- content.append(part)
185
+ image_parts.append(part)
186
+
187
+ # a plain string is the form every OpenAI-compatible server accepts for a text
188
+ # result; the content-part list is reserved for results carrying images, which
189
+ # only servers with multimodal tool messages take
190
+ content = [{"type": "text", "text": item["text"]}, *image_parts] if image_parts else item["text"]
186
191
 
187
192
  # Tool results are sent as separate messages
188
193
  openai_messages.append(
@@ -0,0 +1,4 @@
1
+ from .client import OpenaiChatVllmAdapterClient
2
+
3
+
4
+ __all__ = ["OpenaiChatVllmAdapterClient"]
@@ -0,0 +1,122 @@
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 typing import Any
16
+
17
+ from ..openai_chat import OpenaiChatClient
18
+ from ..types import ThinkingLevel, UniConfig
19
+
20
+
21
+ # vLLM passes chat_template_kwargs straight to the served model's chat template, so the
22
+ # switch that turns thinking on is whatever that template happens to read. Each profile
23
+ # below maps an AgentHub level onto one family's kwargs; an empty mapping means the
24
+ # request carries no chat_template_kwargs at all.
25
+ #
26
+ # The upstream artifacts these profiles are read off, and the clamping those artifacts
27
+ # force, are snapshotted in llmsdk_docs/openai_chat_vllm_adapter/. Update that snapshot
28
+ # whenever a model is added here.
29
+
30
+ # Qwen3 templates read a single enable_thinking boolean and no effort key at all.
31
+ _QWEN3_THINKING: dict[ThinkingLevel, dict[str, Any]] = {
32
+ ThinkingLevel.NONE: {"enable_thinking": False},
33
+ ThinkingLevel.LOW: {"enable_thinking": True},
34
+ ThinkingLevel.MEDIUM: {"enable_thinking": True},
35
+ ThinkingLevel.HIGH: {"enable_thinking": True},
36
+ ThinkingLevel.XHIGH: {"enable_thinking": True},
37
+ ThinkingLevel.MAX: {"enable_thinking": True},
38
+ }
39
+
40
+ # Qwen3.8-27B and Qwen3.8-Flash-Next ship the same chat template, byte for byte, so they
41
+ # share a profile. It keeps enable_thinking as the off switch and takes its adaptive modes
42
+ # as reasoning_effort, validated against low/medium/xhigh, so high and max clamp to xhigh.
43
+ # The template defaults the key to xhigh, so a model on this template that is not sent the
44
+ # key runs every level at full effort.
45
+ _QWEN3_8_THINKING: dict[ThinkingLevel, dict[str, Any]] = {
46
+ ThinkingLevel.NONE: {"enable_thinking": False},
47
+ ThinkingLevel.LOW: {"reasoning_effort": "low"},
48
+ ThinkingLevel.MEDIUM: {"reasoning_effort": "medium"},
49
+ ThinkingLevel.HIGH: {"reasoning_effort": "xhigh"},
50
+ ThinkingLevel.XHIGH: {"reasoning_effort": "xhigh"},
51
+ ThinkingLevel.MAX: {"reasoning_effort": "xhigh"},
52
+ }
53
+
54
+ # DeepSeek V4 publishes no chat template; vLLM reads a thinking flag paired with
55
+ # reasoning_effort, and thinking is off whenever the flag is absent, which is what NONE
56
+ # sends. DeepSeek-V4-Pro and DeepSeek-V4-Flash share an encoding module that asserts
57
+ # reasoning_effort in ['max', None, 'high'], so low is a failed request rather than a
58
+ # weaker answer and high is the lowest value they take. That module then branches on 'max'
59
+ # alone, which means LOW through XHIGH all render the same prompt on these two models.
60
+ _DEEPSEEK_V4_PRO_FLASH_THINKING: dict[ThinkingLevel, dict[str, Any]] = {
61
+ ThinkingLevel.NONE: {},
62
+ ThinkingLevel.LOW: {"thinking": True, "reasoning_effort": "high"},
63
+ ThinkingLevel.MEDIUM: {"thinking": True, "reasoning_effort": "high"},
64
+ ThinkingLevel.HIGH: {"thinking": True, "reasoning_effort": "high"},
65
+ ThinkingLevel.XHIGH: {"thinking": True, "reasoning_effort": "high"},
66
+ ThinkingLevel.MAX: {"thinking": True, "reasoning_effort": "max"},
67
+ }
68
+
69
+ # DeepSeek-V4-Flash-Vision-Exp ships a different copy of that encoding module, one that
70
+ # validates reasoning_effort against a low/high/max table, so it keeps the finer scale;
71
+ # medium and xhigh clamp to high.
72
+ _DEEPSEEK_V4_VISION_EXP_THINKING: dict[ThinkingLevel, dict[str, Any]] = {
73
+ ThinkingLevel.NONE: {},
74
+ ThinkingLevel.LOW: {"thinking": True, "reasoning_effort": "low"},
75
+ ThinkingLevel.MEDIUM: {"thinking": True, "reasoning_effort": "high"},
76
+ ThinkingLevel.HIGH: {"thinking": True, "reasoning_effort": "high"},
77
+ ThinkingLevel.XHIGH: {"thinking": True, "reasoning_effort": "high"},
78
+ ThinkingLevel.MAX: {"thinking": True, "reasoning_effort": "max"},
79
+ }
80
+
81
+ # Keys are matched as substrings of the lowercased model id, so a served id keeps whatever
82
+ # prefix the deployment gave it (Qwen/Qwen3.6-35B-A3B, deepseek-ai/DeepSeek-V4-Pro). The
83
+ # first match wins, so a key that contains another must come first: deepseek-v4-flash is a
84
+ # prefix of deepseek-v4-flash-vision-exp.
85
+ _MODEL_THINKING_PROFILES: tuple[tuple[str, dict[ThinkingLevel, dict[str, Any]]], ...] = (
86
+ ("qwen3.8-flash-next", _QWEN3_8_THINKING),
87
+ ("qwen3.8-27b", _QWEN3_8_THINKING),
88
+ ("qwen3.6-35b-a3b", _QWEN3_THINKING),
89
+ ("qwen3.5-0.8b", _QWEN3_THINKING),
90
+ ("qwen3.5-9b", _QWEN3_THINKING),
91
+ ("deepseek-v4-flash-vision-exp", _DEEPSEEK_V4_VISION_EXP_THINKING),
92
+ ("deepseek-v4-pro", _DEEPSEEK_V4_PRO_FLASH_THINKING),
93
+ ("deepseek-v4-flash", _DEEPSEEK_V4_PRO_FLASH_THINKING),
94
+ )
95
+
96
+
97
+ class OpenaiChatVllmAdapterClient(OpenaiChatClient):
98
+ """Models served through vLLM's OpenAI-compatible Chat Completions API."""
99
+
100
+ def _thinking_chat_template_kwargs(self, thinking_level: ThinkingLevel) -> dict[str, Any]:
101
+ """Return the chat_template_kwargs this model's template reads for the level.
102
+
103
+ A model outside the table falls back to Qwen3's enable_thinking, the most
104
+ widespread of the conventions and inert on a template that ignores the key.
105
+ """
106
+ model = self._model.lower()
107
+ for name, profile in _MODEL_THINKING_PROFILES:
108
+ if name in model:
109
+ return dict(profile[thinking_level])
110
+
111
+ return dict(_QWEN3_THINKING[thinking_level])
112
+
113
+ def transform_uni_config_to_model_config(self, config: UniConfig) -> dict[str, Any]:
114
+ """Map AgentHub's level onto the thinking switch this model's chat template reads."""
115
+ vllm_config = super().transform_uni_config_to_model_config(config)
116
+
117
+ if config.get("thinking_level") is not None:
118
+ chat_template_kwargs = self._thinking_chat_template_kwargs(config["thinking_level"])
119
+ if chat_template_kwargs:
120
+ vllm_config["chat_template_kwargs"] = chat_template_kwargs
121
+
122
+ return vllm_config
@@ -55,6 +55,12 @@ class OpenaiResponsesClient(LLMClient):
55
55
 
56
56
  def _convert_thinking_level_to_effort(self, thinking_level: ThinkingLevel) -> str:
57
57
  """Convert ThinkingLevel enum to the Responses API reasoning effort."""
58
+ if thinking_level == ThinkingLevel.NONE and "gpt-6" in self._model:
59
+ # a gateway serving GPT-6 forwards the effort to OpenAI, which rejects "none" and
60
+ # "minimal" with a 400 (verified live 2026-09-09 against api.openai.com), so NONE
61
+ # degrades to the lowest effort the generation accepts.
62
+ return "low"
63
+
58
64
  mapping = {
59
65
  ThinkingLevel.NONE: "none",
60
66
  ThinkingLevel.LOW: "low",
@@ -100,6 +106,11 @@ class OpenaiResponsesClient(LLMClient):
100
106
  if config.get("temperature") is not None:
101
107
  openai_config["temperature"] = config["temperature"]
102
108
 
109
+ # Unlike the model-specific Responses clients, the summary stays inside this branch:
110
+ # OpenRouter reads a reasoning object carrying no effort as "reasoning disabled" and
111
+ # refuses it on a forced-thinking model -- "Reasoning is mandatory for this endpoint
112
+ # and cannot be disabled" (400, verified live 2026-09-03 with z-ai/glm-5.3) -- so a
113
+ # summary sent on its own would turn a dropped value into a failed request.
103
114
  if config.get("thinking_level") is not None:
104
115
  openai_config["reasoning"] = {"effort": self._convert_thinking_level_to_effort(config["thinking_level"])}
105
116
  if config.get("thinking_summary"):
@@ -197,13 +208,20 @@ class OpenaiResponsesClient(LLMClient):
197
208
  raise ValueError("tool_call_id is required for tool result.")
198
209
 
199
210
  # NOTE: tool results are input items
200
- tool_result = [{"type": "input_text", "text": item["text"]}]
211
+ image_parts = []
201
212
  if "images" in item:
202
213
  for image_url in item["images"]:
203
- tool_result.append(self._convert_image_url(image_url))
214
+ image_parts.append(self._convert_image_url(image_url))
215
+
216
+ # a plain string is the form every OpenAI-compatible server accepts for a text
217
+ # result; the content-part list is reserved for results carrying images, which
218
+ # only servers with multimodal tool messages take
219
+ output = (
220
+ [{"type": "input_text", "text": item["text"]}, *image_parts] if image_parts else item["text"]
221
+ )
204
222
 
205
223
  input_list.append(
206
- {"type": "function_call_output", "call_id": item["tool_call_id"], "output": tool_result}
224
+ {"type": "function_call_output", "call_id": item["tool_call_id"], "output": output}
207
225
  )
208
226
  else:
209
227
  raise ValueError(f"Unknown item: {item}")
@@ -42,6 +42,9 @@ class SupportedModel(TypedDict):
42
42
  ``AutoLLMClient(model=entry["model"], base_url=entry["base_url"], client_type=entry["client"])``.
43
43
  Modalities describe what is usable through that client; ``context_window`` and
44
44
  ``pricing`` are omitted where the platform publishes no authoritative value.
45
+
46
+ ``pricing`` is always the LIST price. A running promotion is deliberately not recorded:
47
+ the registry's job is the catalog price, and applying a promotion is the consumer's.
45
48
  """
46
49
 
47
50
  model: str
@@ -95,20 +98,31 @@ def _cny(prompt: float, output: float, cached: float | None = None) -> ModelPric
95
98
  # /models APIs on 2026-07-22, SiliconFlow CNY prices from the vendors' official price lists.
96
99
  _SUPPORTED_MODELS: list[SupportedModel] = [
97
100
  # official vendor endpoints
101
+ {
102
+ "model": "gemini-3.8-flash",
103
+ "base_url": _GOOGLE,
104
+ "client": "gemini-3.8",
105
+ "input_modalities": ["Text", "Image", "Video", "Audio"],
106
+ "output_modalities": ["Text"],
107
+ "context_window": 1048576,
108
+ # Google runs a launch discount through 2026-12-31 on this row and on the two flash
109
+ # rows below; the list price is stored regardless, because applying a running
110
+ # promotion belongs to the consumer, not to the registry.
111
+ "pricing": _usd(1.5, 7.5, cached=0.15),
112
+ },
98
113
  {
99
114
  "model": "gemini-3.7-flash",
100
115
  "base_url": _GOOGLE,
101
- "client": "gemini-3.7",
116
+ "client": "gemini-3.8",
102
117
  "input_modalities": ["Text", "Image", "Video", "Audio"],
103
118
  "output_modalities": ["Text"],
104
119
  "context_window": 1048576,
105
- # official list price; a launch discount halves all three rates through 2026-12-31
106
120
  "pricing": _usd(1.5, 7.5, cached=0.15),
107
121
  },
108
122
  {
109
123
  "model": "gemini-3.6-flash",
110
124
  "base_url": _GOOGLE,
111
- "client": "gemini-3.7",
125
+ "client": "gemini-3.8",
112
126
  "input_modalities": ["Text", "Image", "Video", "Audio"],
113
127
  "output_modalities": ["Text"],
114
128
  "context_window": 1048576,
@@ -117,7 +131,7 @@ _SUPPORTED_MODELS: list[SupportedModel] = [
117
131
  {
118
132
  "model": "gemini-3.5-flash-lite",
119
133
  "base_url": _GOOGLE,
120
- "client": "gemini-3.7",
134
+ "client": "gemini-3.8",
121
135
  "input_modalities": ["Text", "Image", "Video", "Audio"],
122
136
  "output_modalities": ["Text"],
123
137
  "context_window": 1048576,
@@ -126,7 +140,7 @@ _SUPPORTED_MODELS: list[SupportedModel] = [
126
140
  {
127
141
  "model": "gemini-3.5-flash",
128
142
  "base_url": _GOOGLE,
129
- "client": "gemini-3.7",
143
+ "client": "gemini-3.8",
130
144
  "input_modalities": ["Text", "Image", "Video", "Audio"],
131
145
  "output_modalities": ["Text"],
132
146
  "context_window": 1048576,
@@ -135,21 +149,21 @@ _SUPPORTED_MODELS: list[SupportedModel] = [
135
149
  {
136
150
  "model": "gemini-3.1-flash-image",
137
151
  "base_url": _GOOGLE,
138
- "client": "gemini-3.7",
152
+ "client": "gemini-3.8",
139
153
  "input_modalities": ["Text", "Image"],
140
154
  "output_modalities": ["Image"],
141
155
  },
142
156
  {
143
157
  "model": "gemini-3.1-flash-tts-preview",
144
158
  "base_url": _GOOGLE,
145
- "client": "gemini-3.7",
159
+ "client": "gemini-3.8",
146
160
  "input_modalities": ["Text"],
147
161
  "output_modalities": ["Audio"],
148
162
  },
149
163
  {
150
164
  "model": "gemini-embedding-2",
151
165
  "base_url": _GOOGLE,
152
- "client": "gemini-3.7",
166
+ "client": "gemini-3.8",
153
167
  "input_modalities": ["Text"],
154
168
  "output_modalities": ["Embed"],
155
169
  },
@@ -198,11 +212,24 @@ _SUPPORTED_MODELS: list[SupportedModel] = [
198
212
  "context_window": 1000000,
199
213
  "pricing": _usd(3.0, 15.0, cached=0.3),
200
214
  },
215
+ {
216
+ # official list price per 1M tokens: $1 cached input, $10 uncached input, $12.5 cache
217
+ # writes, $50 output, all doubled above 272K input tokens except output, which is 1.5x.
218
+ # The prompt bucket carries the cache-write rate: the usage buckets do not separate
219
+ # cache-written input from plain input.
220
+ "model": "gpt-6-astra",
221
+ "base_url": _OPENAI,
222
+ "client": "gpt-6",
223
+ "input_modalities": ["Text", "Image"],
224
+ "output_modalities": ["Text"],
225
+ "context_window": 1050000,
226
+ "pricing": _usd(12.5, 50.0, cached=1.0),
227
+ },
201
228
  {
202
229
  # official standard-tier list price; the bare gpt-5.6 alias also routes here
203
230
  "model": "gpt-5.6-sol",
204
231
  "base_url": _OPENAI,
205
- "client": "gpt-5.6",
232
+ "client": "gpt-6",
206
233
  "input_modalities": ["Text", "Image"],
207
234
  "output_modalities": ["Text"],
208
235
  "context_window": 1050000,
@@ -211,7 +238,7 @@ _SUPPORTED_MODELS: list[SupportedModel] = [
211
238
  {
212
239
  "model": "gpt-5.6-terra",
213
240
  "base_url": _OPENAI,
214
- "client": "gpt-5.6",
241
+ "client": "gpt-6",
215
242
  "input_modalities": ["Text", "Image"],
216
243
  "output_modalities": ["Text"],
217
244
  "context_window": 1050000,
@@ -220,7 +247,7 @@ _SUPPORTED_MODELS: list[SupportedModel] = [
220
247
  {
221
248
  "model": "gpt-5.6-luna",
222
249
  "base_url": _OPENAI,
223
- "client": "gpt-5.6",
250
+ "client": "gpt-6",
224
251
  "input_modalities": ["Text", "Image"],
225
252
  "output_modalities": ["Text"],
226
253
  "context_window": 1050000,
@@ -309,6 +336,20 @@ _SUPPORTED_MODELS: list[SupportedModel] = [
309
336
  "context_window": 262144,
310
337
  "pricing": _cny(6.5, 27.0, cached=1.1),
311
338
  },
339
+ {
340
+ "model": "deepseek-v4.1-flash",
341
+ "base_url": _DEEPSEEK,
342
+ "client": "deepseek-v4",
343
+ # announced by DeepSeek for release after 2026-09-10 and not yet served on
344
+ # 2026-09-09; multimodal per the announcement
345
+ "input_modalities": ["Text", "Image"],
346
+ "output_modalities": ["Text"],
347
+ # assumed equal to deepseek-v4-flash until the official model page lists it
348
+ "context_window": 1000000,
349
+ # priced as the V4 Flash series: official off-peak list price effective 2026-09-10,
350
+ # and peak-hour rates (Beijing 9:00-12:00, 14:00-18:00) are double
351
+ "pricing": _cny(1.0, 4.0, cached=0.02),
352
+ },
312
353
  {
313
354
  "model": "deepseek-v4-flash",
314
355
  "base_url": _DEEPSEEK,
@@ -316,9 +357,10 @@ _SUPPORTED_MODELS: list[SupportedModel] = [
316
357
  "input_modalities": ["Text"],
317
358
  "output_modalities": ["Text"],
318
359
  "context_window": 1000000,
319
- # official off-peak list price (verified 2026-08-18); peak-hour rates
320
- # (Beijing 9:00-12:00, 14:00-18:00) are double
321
- "pricing": _cny(1.5, 4.5, cached=0.05),
360
+ # official off-peak list price effective 2026-09-10 (verified 2026-09-08 against
361
+ # the official adjustment); peak-hour rates (Beijing 9:00-12:00, 14:00-18:00) are
362
+ # double
363
+ "pricing": _cny(1.0, 4.0, cached=0.02),
322
364
  },
323
365
  {
324
366
  "model": "deepseek-v4-flash-vision-exp",
@@ -327,9 +369,10 @@ _SUPPORTED_MODELS: list[SupportedModel] = [
327
369
  "input_modalities": ["Text", "Image"],
328
370
  "output_modalities": ["Text"],
329
371
  "context_window": 1000000,
330
- # priced as deepseek-v4-flash; official off-peak list price (verified 2026-08-21), and
331
- # peak-hour rates (Beijing 9:00-12:00, 14:00-18:00) are double
332
- "pricing": _cny(1.5, 4.5, cached=0.05),
372
+ # priced as deepseek-v4-flash; official off-peak list price effective 2026-09-10
373
+ # (verified 2026-09-08 against the official adjustment), and peak-hour rates
374
+ # (Beijing 9:00-12:00, 14:00-18:00) are double
375
+ "pricing": _cny(1.0, 4.0, cached=0.02),
333
376
  },
334
377
  {
335
378
  "model": "deepseek-v4-pro",
@@ -451,6 +494,17 @@ _SUPPORTED_MODELS: list[SupportedModel] = [
451
494
  "context_window": 1000000,
452
495
  "pricing": _usd(0.0, 0.0),
453
496
  },
497
+ {
498
+ # models API 2026-09-09, default OpenAI endpoint, no discount: $10 input, $50 output,
499
+ # $1 cache read, $12.5 cache write per 1M tokens
500
+ "model": "openai/gpt-6-astra",
501
+ "base_url": _OPENROUTER,
502
+ "client": "openai-responses",
503
+ "input_modalities": ["Text", "Image"],
504
+ "output_modalities": ["Text"],
505
+ "context_window": 1050000,
506
+ "pricing": _usd(12.5, 50.0, cached=1.0),
507
+ },
454
508
  {
455
509
  "model": "openai/gpt-5.6-sol",
456
510
  "base_url": _OPENROUTER,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agenthub-python"
3
- version = "0.4.9"
3
+ version = "0.4.11"
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.9"
3
+ version = "0.4.11"
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"