agenthub-python 0.4.0__py3-none-any.whl → 0.4.2__py3-none-any.whl

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/__init__.py CHANGED
@@ -13,15 +13,22 @@
13
13
  # limitations under the License.
14
14
 
15
15
  from .auto_client import AutoLLMClient
16
- from .errors import AgentHubError, EmptyResponseError, ToolCallArgumentParseError
16
+ from .errors import AgentHubError, EmptyResponseError, ToolCallArgumentParseError, UnsupportedParameterError
17
+ from .registry import Currency, Modality, ModelPricing, SupportedModel, list_supported_models
17
18
  from .types import PromptCaching, ThinkingLevel
18
19
 
19
20
 
20
21
  __all__ = [
21
22
  "AgentHubError",
22
23
  "AutoLLMClient",
24
+ "Currency",
23
25
  "EmptyResponseError",
26
+ "Modality",
27
+ "ModelPricing",
24
28
  "PromptCaching",
29
+ "SupportedModel",
25
30
  "ThinkingLevel",
26
31
  "ToolCallArgumentParseError",
32
+ "UnsupportedParameterError",
33
+ "list_supported_models",
27
34
  ]
@@ -0,0 +1,18 @@
1
+ # Copyright 2025 Prism Shadow. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from .client import AntMessagesClient
16
+
17
+
18
+ __all__ = ["AntMessagesClient"]
@@ -12,18 +12,15 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- import base64
16
- import mimetypes
17
15
  import os
18
16
  import re
19
17
  from typing import Any, AsyncIterator
20
18
 
21
- import httpx
22
- from anthropic import AsyncAnthropic, AsyncAnthropicBedrock
19
+ from anthropic import AsyncAnthropic
23
20
  from anthropic.types.beta import BetaMessageParam, BetaRawMessageStreamEvent
24
21
 
25
22
  from ..base_client import LLMClient
26
- from ..errors import parse_tool_call_arguments
23
+ from ..errors import UnsupportedParameterError, parse_tool_call_arguments
27
24
  from ..types import (
28
25
  EventType,
29
26
  FinishReason,
@@ -36,91 +33,60 @@ from ..types import (
36
33
  UniMessage,
37
34
  UsageMetadata,
38
35
  )
36
+ from ..utils import fix_openrouter_usage_metadata
39
37
 
40
38
 
41
39
  REDACTED_THINKING = "_REDACTED_THINKING"
42
40
 
43
41
 
44
- class Claude4_6Client(LLMClient):
45
- """Claude 4.6-specific LLM client implementation."""
42
+ class AntMessagesClient(LLMClient):
43
+ """Anthropic Messages-compatible client implementation."""
46
44
 
47
45
  def __init__(self, model: str, api_key: str | None = None, base_url: str | None = None):
48
- """Initialize Claude 4.6 client with model and API key."""
46
+ """Initialize Anthropic Messages-compatible client with model, API key, and base URL."""
49
47
  self._model = model
50
48
  api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
51
49
  base_url = base_url or os.getenv("ANTHROPIC_BASE_URL")
52
- if base_url and base_url.startswith("bedrock://"): # example: bedrock://us-east-1
53
- region = base_url.replace("bedrock://", "")
54
- access_key, secret_key = api_key.split(",")
55
- self._client = AsyncAnthropicBedrock(
56
- aws_secret_key=secret_key, aws_access_key=access_key, aws_region=region
57
- )
58
- self._use_bedrock = True
59
- else:
60
- self._client = AsyncAnthropic(api_key=api_key, base_url=base_url)
61
- self._use_bedrock = False
62
-
50
+ # send the credential through both header conventions: Anthropic and DeepSeek read
51
+ # x-api-key while gateways such as OpenRouter and Z.AI read Authorization: Bearer
52
+ self._client = AsyncAnthropic(api_key=api_key, auth_token=api_key, base_url=base_url)
63
53
  self._history: list[UniMessage] = []
64
54
 
65
- async def _convert_image_url_to_source(self, url: str) -> dict[str, Any]:
66
- """Convert image URL to image source.
67
-
68
- Bedrock does not support image url sources, so we need to fetch the image bytes and encode them.
69
-
70
- Args:
71
- url: Image URL to convert
72
-
73
- Returns:
74
- Image source
75
- """
55
+ def _convert_image_url_to_source(self, url: str) -> dict[str, Any]:
56
+ """Convert image URL to an Anthropic image source block."""
76
57
  if url.startswith("data:"):
77
58
  match = re.match(r"data:([^;]+);base64,(.+)", url)
78
- if match:
79
- media_type = match.group(1)
80
- base64_data = match.group(2)
81
- source = {
82
- "type": "image",
83
- "source": {"type": "base64", "media_type": media_type, "data": base64_data},
84
- }
85
- else:
59
+ if not match:
86
60
  raise ValueError(f"Invalid base64 image: {url}")
87
- elif self._use_bedrock:
88
- async with httpx.AsyncClient() as client:
89
- response = await client.get(url)
90
- response.raise_for_status()
91
- image_bytes = response.content
92
- mime_type = mimetypes.guess_type(url)[0] or "image/jpeg"
93
- source = {
94
- "type": "image",
95
- "source": {
96
- "type": "base64",
97
- "media_type": mime_type,
98
- "data": base64.b64encode(image_bytes).decode("utf-8"),
99
- },
100
- }
101
- else:
102
- source = {"type": "image", "source": {"type": "url", "url": url}}
103
61
 
104
- return source
62
+ return {
63
+ "type": "image",
64
+ "source": {"type": "base64", "media_type": match.group(1), "data": match.group(2)},
65
+ }
66
+
67
+ return {"type": "image", "source": {"type": "url", "url": url}}
105
68
 
106
69
  def _convert_thinking_level_to_thinking_config(self, thinking_level: ThinkingLevel) -> dict[str, Any]:
107
- """Convert ThinkingLevel enum to Claude's adaptive thinking config."""
70
+ """Convert ThinkingLevel enum to the Messages API thinking config."""
71
+ # NONE is explicit rather than omitted because some servers (e.g. Z.AI) think by default
108
72
  mapping = {
109
- ThinkingLevel.NONE: {}, # omit thinking config
73
+ ThinkingLevel.NONE: {"thinking": {"type": "disabled"}},
110
74
  ThinkingLevel.LOW: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "low"}},
111
75
  ThinkingLevel.MEDIUM: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "medium"}},
112
76
  ThinkingLevel.HIGH: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "high"}},
113
- ThinkingLevel.XHIGH: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "high"}},
77
+ ThinkingLevel.XHIGH: {"thinking": {"type": "adaptive"}, "output_config": {"effort": "xhigh"}},
114
78
  }
115
79
  return mapping.get(thinking_level)
116
80
 
117
81
  def _convert_tool_choice(self, tool_choice: ToolChoice) -> dict[str, str]:
118
- """Convert ToolChoice to Claude's tool_choice format."""
82
+ """Convert ToolChoice to the Messages API tool_choice format."""
119
83
  if isinstance(tool_choice, list):
120
84
  if len(tool_choice) > 1:
121
- raise ValueError("Claude supports only one tool choice.")
85
+ raise UnsupportedParameterError(
86
+ self.__class__.__name__, "tool_choice", "The Messages API does not support multiple tool choices."
87
+ )
122
88
 
123
- return {"type": "any", "name": tool_choice[0]}
89
+ return {"type": "tool", "name": tool_choice[0]}
124
90
  elif tool_choice == "none":
125
91
  return {"type": "none"}
126
92
  elif tool_choice == "auto":
@@ -130,70 +96,70 @@ class Claude4_6Client(LLMClient):
130
96
 
131
97
  def transform_uni_config_to_model_config(self, config: UniConfig) -> dict[str, Any]:
132
98
  """
133
- Transform universal configuration to Claude-specific configuration.
99
+ Transform universal configuration to Anthropic Messages-compatible configuration.
134
100
 
135
101
  Args:
136
102
  config: Universal configuration dict
137
103
 
138
104
  Returns:
139
- Claude configuration dictionary
105
+ Anthropic Messages API configuration dictionary
140
106
  """
141
- claude_config = {"model": self._model, "stream": True}
107
+ ant_config = {"model": self._model, "stream": True}
142
108
 
143
109
  if config.get("system_prompt") is not None:
144
- claude_config["system"] = config["system_prompt"]
110
+ ant_config["system"] = config["system_prompt"]
145
111
 
146
112
  if config.get("max_tokens") is not None:
147
- claude_config["max_tokens"] = config["max_tokens"]
113
+ ant_config["max_tokens"] = config["max_tokens"]
148
114
  else:
149
- claude_config["max_tokens"] = 64000 # Claude requires max_tokens to be specified
115
+ ant_config["max_tokens"] = 64000 # the Messages API requires max_tokens to be specified
150
116
 
151
117
  if config.get("temperature") is not None:
152
- claude_config["temperature"] = config["temperature"]
118
+ ant_config["temperature"] = config["temperature"]
153
119
 
154
- # NOTE: Claude always provides thinking summary
155
120
  if config.get("thinking_level") is not None:
156
- claude_config["temperature"] = 1.0 # `temperature` may only be set to 1 when thinking is enabled
157
- claude_config.update(self._convert_thinking_level_to_thinking_config(config["thinking_level"]))
121
+ ant_config.update(self._convert_thinking_level_to_thinking_config(config["thinking_level"]))
122
+ if config.get("thinking_summary") and ant_config.get("thinking", {}).get("type") == "adaptive":
123
+ ant_config["thinking"]["display"] = "summarized"
158
124
 
159
- # Convert tools to Claude's tool schema
125
+ # Convert tools to the Messages API tool schema
160
126
  if config.get("tools") is not None:
161
- claude_tools = []
127
+ ant_tools = []
162
128
  for tool in config["tools"]:
163
- claude_tool = {}
129
+ ant_tool = {}
164
130
  for key, value in tool.items():
165
- claude_tool[key.replace("parameters", "input_schema")] = value
131
+ ant_tool[key.replace("parameters", "input_schema")] = value
166
132
 
167
- claude_tools.append(claude_tool)
133
+ ant_tools.append(ant_tool)
168
134
 
169
- claude_config["tools"] = claude_tools
135
+ ant_config["tools"] = ant_tools
170
136
 
171
137
  # Convert tool_choice
172
138
  if config.get("tool_choice") is not None:
173
- claude_config["tool_choice"] = self._convert_tool_choice(config["tool_choice"])
139
+ ant_config["tool_choice"] = self._convert_tool_choice(config["tool_choice"])
140
+
141
+ if config.get("fast_mode"):
142
+ ant_config["speed"] = "fast"
143
+ ant_config["betas"] = ["fast-mode-2026-02-01"]
174
144
 
175
- # Add cache_control if prompt caching is enabled
176
- # TODO: wait for bedrock to support cache_control in config
177
- if not self._use_bedrock:
178
- prompt_caching = config.get("prompt_caching", PromptCaching.ENABLE)
179
- if prompt_caching == PromptCaching.ENABLE:
180
- claude_config["cache_control"] = {"type": "ephemeral"}
181
- elif prompt_caching == PromptCaching.ENHANCE:
182
- claude_config["cache_control"] = {"type": "ephemeral", "ttl": "1h"}
145
+ if config.get("prompt_caching") is not None and config["prompt_caching"] != PromptCaching.ENABLE:
146
+ raise UnsupportedParameterError(
147
+ self.__class__.__name__, "prompt_caching", "prompt_caching must be ENABLE for the Messages API."
148
+ )
183
149
 
184
- return claude_config
150
+ return ant_config
185
151
 
186
- async def transform_uni_message_to_model_input(self, messages: list[UniMessage]) -> list[BetaMessageParam]:
152
+ def transform_uni_message_to_model_input(self, messages: list[UniMessage]) -> list[BetaMessageParam]:
187
153
  """
188
- Transform universal message format to Claude's BetaMessageParam format.
154
+ Transform universal message format to the Messages API BetaMessageParam format.
189
155
 
190
156
  Args:
191
157
  messages: List of universal message dictionaries
192
158
 
193
159
  Returns:
194
- List of Claude BetaMessageParam objects
160
+ List of Messages API BetaMessageParam objects
195
161
  """
196
- claude_messages: list[BetaMessageParam] = []
162
+ ant_messages: list[BetaMessageParam] = []
197
163
 
198
164
  for msg in messages:
199
165
  content_blocks = []
@@ -201,18 +167,19 @@ class Claude4_6Client(LLMClient):
201
167
  if item["type"] == "text":
202
168
  content_blocks.append({"type": "text", "text": item["text"]})
203
169
  elif item["type"] == "image_url":
204
- content_blocks.append(await self._convert_image_url_to_source(item["image_url"]))
170
+ content_blocks.append(self._convert_image_url_to_source(item["image_url"]))
205
171
  elif item["type"] == "thinking":
206
172
  if item["thinking"] == REDACTED_THINKING:
207
173
  content_blocks.append({"type": "redacted_thinking", "data": item["fidelity"]["signature"]})
208
174
  else:
209
- content_blocks.append(
210
- {
211
- "type": "thinking",
212
- "thinking": item["thinking"],
213
- "signature": item["fidelity"]["signature"],
214
- }
215
- )
175
+ # third-party servers accept thinking without a signature, but the
176
+ # official API requires the one it emitted
177
+ thinking_block = {"type": "thinking", "thinking": item["thinking"]}
178
+ signature = (item.get("fidelity") or {}).get("signature")
179
+ if signature is not None:
180
+ thinking_block["signature"] = signature
181
+
182
+ content_blocks.append(thinking_block)
216
183
  elif item["type"] == "tool_call":
217
184
  content_blocks.append(
218
185
  {
@@ -229,7 +196,7 @@ class Claude4_6Client(LLMClient):
229
196
  tool_result = [{"type": "text", "text": item["text"]}]
230
197
  if "images" in item:
231
198
  for image_url in item["images"]:
232
- tool_result.append(await self._convert_image_url_to_source(image_url))
199
+ tool_result.append(self._convert_image_url_to_source(image_url))
233
200
 
234
201
  content_blocks.append(
235
202
  {"type": "tool_result", "content": tool_result, "tool_use_id": item["tool_call_id"]}
@@ -237,18 +204,18 @@ class Claude4_6Client(LLMClient):
237
204
  else:
238
205
  raise ValueError(f"Unknown item: {item}")
239
206
 
240
- claude_messages.append({"role": msg["role"], "content": content_blocks})
207
+ ant_messages.append({"role": msg["role"], "content": content_blocks})
241
208
 
242
- return claude_messages
209
+ return ant_messages
243
210
 
244
211
  def transform_model_output_to_uni_event(self, model_output: BetaRawMessageStreamEvent) -> UniEvent:
245
212
  """
246
- Transform Claude model output to universal event format.
213
+ Transform a Messages API streaming event to universal event format.
247
214
 
248
- NOTE: Claude always has only one content item per event.
215
+ NOTE: the Messages API always has only one content item per event.
249
216
 
250
217
  Args:
251
- model_output: Claude streaming event
218
+ model_output: Messages API streaming event
252
219
 
253
220
  Returns:
254
221
  Universal event dictionary
@@ -258,8 +225,8 @@ class Claude4_6Client(LLMClient):
258
225
  usage_metadata: UsageMetadata | None = None
259
226
  finish_reason: FinishReason | None = None
260
227
 
261
- claude_event_type = model_output.type
262
- if claude_event_type == "content_block_start":
228
+ ant_event_type = model_output.type
229
+ if ant_event_type == "content_block_start":
263
230
  event_type = "start"
264
231
  block = model_output.content_block
265
232
  if block.type == "tool_use":
@@ -271,7 +238,7 @@ class Claude4_6Client(LLMClient):
271
238
  {"type": "thinking", "thinking": REDACTED_THINKING, "fidelity": {"signature": block.data}}
272
239
  )
273
240
 
274
- elif claude_event_type == "content_block_delta":
241
+ elif ant_event_type == "content_block_delta":
275
242
  event_type = "delta"
276
243
  delta = model_output.delta
277
244
  if delta.type == "thinking_delta":
@@ -285,10 +252,10 @@ class Claude4_6Client(LLMClient):
285
252
  elif delta.type == "signature_delta":
286
253
  content_items.append({"type": "thinking", "thinking": "", "fidelity": {"signature": delta.signature}})
287
254
 
288
- elif claude_event_type == "content_block_stop":
255
+ elif ant_event_type == "content_block_stop":
289
256
  event_type = "stop"
290
257
 
291
- elif claude_event_type == "message_start":
258
+ elif ant_event_type == "message_start":
292
259
  event_type = "start"
293
260
  message = model_output.message
294
261
  if getattr(message, "usage", None):
@@ -300,7 +267,7 @@ class Claude4_6Client(LLMClient):
300
267
  "response_tokens": None,
301
268
  }
302
269
 
303
- elif claude_event_type == "message_delta":
270
+ elif ant_event_type == "message_delta":
304
271
  event_type = "stop"
305
272
  delta = model_output.delta
306
273
  if getattr(delta, "stop_reason", None):
@@ -312,19 +279,28 @@ class Claude4_6Client(LLMClient):
312
279
  }
313
280
  finish_reason = stop_reason_mapping.get(delta.stop_reason, "unknown")
314
281
 
315
- if getattr(model_output, "usage", None):
316
- # In message_delta, we only update response_tokens
282
+ usage = getattr(model_output, "usage", None)
283
+ if usage:
284
+ # gateways report zero usage in message_start and the full counts here, so the
285
+ # delta also carries the input-side fields (None on servers that omit them)
286
+ if usage.input_tokens is not None:
287
+ prompt_tokens = usage.input_tokens + (usage.cache_creation_input_tokens or 0)
288
+ else:
289
+ prompt_tokens = None
290
+
291
+ output_details = getattr(usage, "output_tokens_details", None)
292
+ thinking_tokens = getattr(output_details, "thinking_tokens", None) if output_details else None
317
293
  usage_metadata = {
318
- "cached_tokens": None,
319
- "prompt_tokens": None,
320
- "thoughts_tokens": None,
321
- "response_tokens": model_output.usage.output_tokens,
294
+ "cached_tokens": usage.cache_read_input_tokens,
295
+ "prompt_tokens": prompt_tokens,
296
+ "thoughts_tokens": thinking_tokens,
297
+ "response_tokens": usage.output_tokens - (thinking_tokens or 0),
322
298
  }
323
299
 
324
- elif claude_event_type == "message_stop":
300
+ elif ant_event_type == "message_stop":
325
301
  event_type = "stop"
326
302
 
327
- elif claude_event_type in ["text", "thinking", "signature", "input_json"]:
303
+ elif ant_event_type in ["text", "thinking", "signature", "input_json"]:
328
304
  event_type = "unused"
329
305
 
330
306
  else:
@@ -343,32 +319,17 @@ class Claude4_6Client(LLMClient):
343
319
  messages: list[UniMessage],
344
320
  config: UniConfig,
345
321
  ) -> AsyncIterator[UniEvent]:
346
- """Stream generate using Claude SDK with unified conversion methods."""
322
+ """Stream generate using an Anthropic Messages-compatible API with unified conversion methods."""
347
323
  # Use unified config conversion
348
- claude_config = self.transform_uni_config_to_model_config(config)
324
+ ant_config = self.transform_uni_config_to_model_config(config)
349
325
 
350
326
  # Use unified message conversion
351
- claude_messages = await self.transform_uni_message_to_model_input(messages)
352
-
353
- # Add cache_control to last user message's last item if using bedrock and enabled prompt caching
354
- # TODO: remove after bedrock supports cache_control in config
355
- if self._use_bedrock:
356
- prompt_caching = config.get("prompt_caching", PromptCaching.ENABLE)
357
- if prompt_caching != PromptCaching.DISABLE and claude_messages:
358
- try:
359
- last_user_message = next(filter(lambda x: x["role"] == "user", claude_messages[::-1]))
360
- last_content_item = last_user_message["content"][-1]
361
- last_content_item["cache_control"] = {
362
- "type": "ephemeral",
363
- "ttl": "1h" if prompt_caching == PromptCaching.ENHANCE else "5m",
364
- }
365
- except StopIteration:
366
- pass
327
+ ant_messages = self.transform_uni_message_to_model_input(messages)
367
328
 
368
329
  # Stream generate
369
330
  partial_tool_call = {}
370
331
  partial_usage = {}
371
- stream = await self._client.beta.messages.create(**claude_config, messages=claude_messages)
332
+ stream = await self._client.beta.messages.create(**ant_config, messages=ant_messages)
372
333
  async for event in stream:
373
334
  event = self.transform_model_output_to_uni_event(event)
374
335
  if event["event_type"] == "start":
@@ -423,18 +384,28 @@ class Claude4_6Client(LLMClient):
423
384
  }
424
385
  partial_tool_call = {}
425
386
 
426
- if "prompt_tokens" in partial_usage and event["usage_metadata"] is not None:
427
- # finish partial_usage
387
+ if event["usage_metadata"] is not None:
388
+ # finish partial_usage: the message_delta counts win over message_start
389
+ delta_usage = event["usage_metadata"]
390
+ usage_metadata = {
391
+ "prompt_tokens": (
392
+ delta_usage["prompt_tokens"]
393
+ if delta_usage["prompt_tokens"] is not None
394
+ else partial_usage.get("prompt_tokens")
395
+ ),
396
+ "cached_tokens": (
397
+ delta_usage["cached_tokens"]
398
+ if delta_usage["cached_tokens"] is not None
399
+ else partial_usage.get("cached_tokens")
400
+ ),
401
+ "thoughts_tokens": delta_usage["thoughts_tokens"],
402
+ "response_tokens": delta_usage["response_tokens"],
403
+ }
428
404
  yield {
429
405
  "role": "assistant",
430
406
  "event_type": "stop",
431
407
  "content_items": [],
432
- "usage_metadata": {
433
- "prompt_tokens": partial_usage["prompt_tokens"],
434
- "thoughts_tokens": None,
435
- "response_tokens": event["usage_metadata"]["response_tokens"],
436
- "cached_tokens": partial_usage["cached_tokens"],
437
- },
408
+ "usage_metadata": fix_openrouter_usage_metadata(usage_metadata, str(self._client.base_url)),
438
409
  "finish_reason": event["finish_reason"],
439
410
  }
440
411
  partial_usage = {}
agenthub/auto_client.py CHANGED
@@ -46,51 +46,65 @@ class AutoLLMClient(LLMClient):
46
46
  self, model: str, api_key: str | None = None, base_url: str | None = None, client_type: str | None = None
47
47
  ) -> LLMClient:
48
48
  """Create the appropriate client for the given model."""
49
- client_type = (client_type or os.getenv("CLIENT_TYPE", model)).lower()
49
+ client_type = (client_type or os.getenv("CLIENT_TYPE") or model).lower()
50
+ if client_type == "minimax-m3":
51
+ from .minimax_m3 import MiniMaxM3Client
52
+
53
+ return MiniMaxM3Client(model=model, api_key=api_key, base_url=base_url)
54
+ # every Gemini generation shares the unified client ("gemini-3" also matches the
55
+ # gemini-3.7/gemini-3.6/gemini-3.5-flash-lite client types)
50
56
  if any(
51
57
  prefix in client_type for prefix in ("gemini-3", "gemini-embedding")
52
- ): # e.g., gemini-3-flash-preview, gemini-embedding-2
53
- from .gemini3 import Gemini3Client
58
+ ): # e.g., gemini-3.7-flash, gemini-3-flash-preview, gemini-embedding-2
59
+ from .gemini3_7 import Gemini3_7Client
54
60
 
55
- return Gemini3Client(model=model, api_key=api_key, base_url=base_url)
61
+ return Gemini3_7Client(model=model, api_key=api_key, base_url=base_url)
56
62
  elif "claude" in client_type and (
57
- "4-7" in client_type or "4-8" in client_type or "-5" in client_type
58
- ): # e.g., claude-opus-4-7
63
+ "4-6" in client_type or "4-7" in client_type or "4-8" in client_type or "-5" in client_type
64
+ ): # the whole Claude 4.6+ series shares the unified client, e.g., claude-sonnet-4-6
59
65
  from .claude5 import Claude5Client
60
66
 
61
67
  return Claude5Client(model=model, api_key=api_key, base_url=base_url)
62
- elif "claude" in client_type and "4-6" in client_type: # e.g., claude-sonnet-4-6
63
- from .claude4_6 import Claude4_6Client
64
-
65
- return Claude4_6Client(model=model, api_key=api_key, base_url=base_url)
66
- elif "gpt-5.4" in client_type or "gpt-5.5" in client_type: # e.g., gpt-5.5
67
- from .gpt5_5 import GPT5_5Client
68
+ 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
69
+ from .gpt5_6 import GPT5_6Client
68
70
 
69
- return GPT5_5Client(model=model, api_key=api_key, base_url=base_url)
70
- elif "glm-5" in client_type or "glm-5.1" in client_type:
71
- from .glm5_1 import GLM5_1Client
71
+ return GPT5_6Client(model=model, api_key=api_key, base_url=base_url)
72
+ elif "glm-5" in client_type: # the whole GLM series shares the unified client
73
+ from .glm5_3 import GLM5_3Client
72
74
 
73
- return GLM5_1Client(model=model, api_key=api_key, base_url=base_url)
74
- elif "kimi-k2.5" in client_type or "kimi-k2.6" in client_type:
75
- from .kimi_k2_6 import KimiK2_6Client
75
+ return GLM5_3Client(model=model, api_key=api_key, base_url=base_url)
76
+ elif "kimi-k3" in client_type or "kimi-k2.5" in client_type or "kimi-k2.6" in client_type:
77
+ # the whole Kimi K2.5+ series shares the unified client
78
+ from .kimi_k3 import KimiK3Client
76
79
 
77
- return KimiK2_6Client(model=model, api_key=api_key, base_url=base_url)
80
+ return KimiK3Client(model=model, api_key=api_key, base_url=base_url)
78
81
  elif "deepseek-v4" in client_type:
79
82
  from .deepseek_v4 import DeepSeekV4Client
80
83
 
81
84
  return DeepSeekV4Client(model=model, api_key=api_key, base_url=base_url)
85
+ elif "ant-messages" in client_type:
86
+ from .ant_messages import AntMessagesClient
87
+
88
+ return AntMessagesClient(model=model, api_key=api_key, base_url=base_url)
89
+ elif "openai-responses" in client_type:
90
+ from .openai_responses import OpenaiResponsesClient
91
+
92
+ return OpenaiResponsesClient(model=model, api_key=api_key, base_url=base_url)
82
93
  elif "openai" in client_type and "embedding" in client_type:
83
94
  from .openai_embedding import OpenaiEmbeddingClient
84
95
 
85
96
  return OpenaiEmbeddingClient(model=model, api_key=api_key, base_url=base_url)
86
- elif "openai" in client_type and "embedding" not in client_type:
87
- from .openai import OpenaiClient
97
+ elif "openai" in client_type and "embedding" not in client_type: # openai-chat, plus bare "openai" as alias
98
+ from .openai_chat import OpenaiChatClient
88
99
 
89
- return OpenaiClient(model=model, api_key=api_key, base_url=base_url)
100
+ return OpenaiChatClient(model=model, api_key=api_key, base_url=base_url)
90
101
  else:
91
102
  raise ValueError(
92
103
  f"{client_type} is not supported. "
93
- "Supported client types: gemini-3, claude-5, claude-4-8, claude-4-7, claude-4-6, gpt-5.5, gpt-5.4, glm-5.1, kimi-k2.6, kimi-k2.5, deepseek-v4, openai-embedding, openai."
104
+ "Supported client types: minimax-m3, gemini-3.7, gemini-3.6, gemini-3, "
105
+ "claude-5, claude-4-8, claude-4-7, claude-4-6, gpt-5.6, gpt-5.5, gpt-5.4, "
106
+ "glm-5.3, glm-5.2, glm-5.1, kimi-k3, kimi-k2.6, kimi-k2.5, deepseek-v4, "
107
+ "openai-embedding, ant-messages, openai-responses, openai-chat."
94
108
  )
95
109
 
96
110
  def transform_uni_config_to_model_config(self, config: UniConfig) -> Any: