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.
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/PKG-INFO +1 -1
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/ant_messages/client.py +9 -2
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/auto_client.py +25 -11
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/claude5/client.py +8 -2
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/deepseek_v4/client.py +20 -5
- {agenthub_python-0.4.9/agenthub/gemini3_7 → agenthub_python-0.4.11/agenthub/gemini3_8}/__init__.py +2 -2
- {agenthub_python-0.4.9/agenthub/gemini3_7 → agenthub_python-0.4.11/agenthub/gemini3_8}/client.py +18 -20
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/glm5_3/client.py +14 -14
- {agenthub_python-0.4.9/agenthub/gpt5_6 → agenthub_python-0.4.11/agenthub/gpt6}/__init__.py +2 -2
- {agenthub_python-0.4.9/agenthub/gpt5_6 → agenthub_python-0.4.11/agenthub/gpt6}/client.py +24 -8
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/openai_chat/client.py +7 -2
- agenthub_python-0.4.11/agenthub/openai_chat_vllm_adapter/__init__.py +4 -0
- agenthub_python-0.4.11/agenthub/openai_chat_vllm_adapter/client.py +122 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/openai_responses/client.py +21 -3
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/registry.py +71 -17
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/pyproject.toml +1 -1
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/pyproject.toml.orig +1 -1
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/README.md +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/__init__.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/abort_signal.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/ant_messages/__init__.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/base_client.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/claude5/__init__.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/deepseek_v4/__init__.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/errors.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/glm5_3/__init__.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/integration/__init__.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/integration/playground.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/integration/tracer.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/kimi_k3/__init__.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/kimi_k3/client.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/minimax_m3/__init__.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/minimax_m3/client.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/openai_chat/__init__.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/openai_embedding/__init__.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/openai_embedding/client.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/openai_responses/__init__.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/types.py +0 -0
- {agenthub_python-0.4.9 → agenthub_python-0.4.11}/agenthub/utils.py +0 -0
|
@@ -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
|
-
|
|
132
|
-
|
|
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 = (
|
|
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.
|
|
72
|
-
from .
|
|
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
|
|
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
|
|
82
|
-
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
supports_image =
|
|
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:
|
{agenthub_python-0.4.9/agenthub/gemini3_7 → agenthub_python-0.4.11/agenthub/gemini3_8}/__init__.py
RENAMED
|
@@ -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
|
|
15
|
+
from .client import Gemini3_8Client
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
__all__ = ["
|
|
18
|
+
__all__ = ["Gemini3_8Client"]
|
{agenthub_python-0.4.9/agenthub/gemini3_7 → agenthub_python-0.4.11/agenthub/gemini3_8}/client.py
RENAMED
|
@@ -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
|
|
65
|
-
"""Unified client for the Gemini family, named for the newest generation it serves (3.
|
|
66
|
-
|
|
67
|
-
It serves every generateContent model generation (3.
|
|
68
|
-
and embedding models
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
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/
|
|
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
|
-
#
|
|
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
|
-
#
|
|
184
|
-
#
|
|
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/
|
|
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
|
-
#
|
|
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.
|
|
79
|
-
(low/medium to high, xhigh to max)
|
|
80
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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."""
|
|
@@ -36,8 +36,8 @@ from ..types import (
|
|
|
36
36
|
from ..utils import is_debug_enabled, openai_image_detail
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
class
|
|
40
|
-
"""GPT-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
110
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
211
|
+
image_parts = []
|
|
201
212
|
if "images" in item:
|
|
202
213
|
for image_url in item["images"]:
|
|
203
|
-
|
|
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":
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
320
|
-
# (Beijing 9:00-12:00, 14:00-18:00) are
|
|
321
|
-
|
|
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
|
|
331
|
-
#
|
|
332
|
-
|
|
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,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|