diff --git a/docs/source/Instruction/Supported-models-and-datasets.md b/docs/source/Instruction/Supported-models-and-datasets.md
index a9660ecd87..0e29612378 100644
--- a/docs/source/Instruction/Supported-models-and-datasets.md
+++ b/docs/source/Instruction/Supported-models-and-datasets.md
@@ -493,6 +493,7 @@
|[moonshotai/Kimi-K2-Instruct](https://modelscope.cn/models/moonshotai/Kimi-K2-Instruct)|deepseek_v3|kimi_k2||transformers>=4.39.3|✔|-|[moonshotai/Kimi-K2-Instruct](https://huggingface.co/moonshotai/Kimi-K2-Instruct)|
|[moonshotai/Kimi-K2-Instruct-0905](https://modelscope.cn/models/moonshotai/Kimi-K2-Instruct-0905)|deepseek_v3|kimi_k2||transformers>=4.39.3|✔|-|[moonshotai/Kimi-K2-Instruct-0905](https://huggingface.co/moonshotai/Kimi-K2-Instruct-0905)|
|[moonshotai/Kimi-K2-Thinking](https://modelscope.cn/models/moonshotai/Kimi-K2-Thinking)|deepseek_v3|kimi_k2||transformers>=4.39.3|✔|-|[moonshotai/Kimi-K2-Thinking](https://huggingface.co/moonshotai/Kimi-K2-Thinking)|
+|[TeleAI/TeleChat3-105B-A4.7B-Thinking](https://modelscope.cn/models/TeleAI/TeleChat3-105B-A4.7B-Thinking)|deepseek_v3|telechat3|telechat3|transformers>=4.46.3|✘|-|-|
|[deepseek-ai/DeepSeek-V3.1-Base](https://modelscope.cn/models/deepseek-ai/DeepSeek-V3.1-Base)|deepseek_v3|deepseek_v3_1|deepseek_v3_1|transformers>=4.39.3|✔|-|[deepseek-ai/DeepSeek-V3.1-Base](https://huggingface.co/deepseek-ai/DeepSeek-V3.1-Base)|
|[deepseek-ai/DeepSeek-V3.1](https://modelscope.cn/models/deepseek-ai/DeepSeek-V3.1)|deepseek_v3|deepseek_v3_1|deepseek_v3_1|transformers>=4.39.3|✔|-|[deepseek-ai/DeepSeek-V3.1](https://huggingface.co/deepseek-ai/DeepSeek-V3.1)|
|[deepseek-ai/DeepSeek-V3.1-Terminus](https://modelscope.cn/models/deepseek-ai/DeepSeek-V3.1-Terminus)|deepseek_v3|deepseek_v3_1|deepseek_v3_1|transformers>=4.39.3|✔|-|[deepseek-ai/DeepSeek-V3.1-Terminus](https://huggingface.co/deepseek-ai/DeepSeek-V3.1-Terminus)|
@@ -547,6 +548,8 @@
|[TeleAI/TeleChat2-7B-32K](https://modelscope.cn/models/TeleAI/TeleChat2-7B-32K)|telechat2|telechat2||-|✘|-|[Tele-AI/TeleChat2-7B-32K](https://huggingface.co/Tele-AI/TeleChat2-7B-32K)|
|[TeleAI/TeleChat2-35B-32K](https://modelscope.cn/models/TeleAI/TeleChat2-35B-32K)|telechat2|telechat2||-|✘|-|[Tele-AI/TeleChat2-35B-32K](https://huggingface.co/Tele-AI/TeleChat2-35B-32K)|
|[TeleAI/TeleChat2-35B-Nov](https://modelscope.cn/models/TeleAI/TeleChat2-35B-Nov)|telechat2|telechat2||-|✘|-|[Tele-AI/TeleChat2-35B-Nov](https://huggingface.co/Tele-AI/TeleChat2-35B-Nov)|
+|[TeleAI/TeleChat3-36B-Thinking](https://modelscope.cn/models/TeleAI/TeleChat3-36B-Thinking)|telechat3|telechat3|telechat3|transformers>=4.53.2|✘|-|[Tele-AI/TeleChat3-36B-Thinking](https://huggingface.co/Tele-AI/TeleChat3-36B-Thinking)|
+|[TeleAI/TeleChat3-Coder-36B-Thinking](https://modelscope.cn/models/TeleAI/TeleChat3-Coder-36B-Thinking)|telechat3|telechat3_coder|telechat3_coder|transformers>=4.53.2|✘|coding|[Tele-AI/TeleChat3-Coder-36B-Thinking](https://huggingface.co/Tele-AI/TeleChat3-Coder-36B-Thinking)|
|[AI-ModelScope/Mistral-7B-Instruct-v0.1](https://modelscope.cn/models/AI-ModelScope/Mistral-7B-Instruct-v0.1)|mistral|llama||transformers>=4.34|✘|-|[mistralai/Mistral-7B-Instruct-v0.1](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.1)|
|[AI-ModelScope/Mistral-7B-Instruct-v0.2](https://modelscope.cn/models/AI-ModelScope/Mistral-7B-Instruct-v0.2)|mistral|llama||transformers>=4.34|✘|-|[mistralai/Mistral-7B-Instruct-v0.2](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2)|
|[LLM-Research/Mistral-7B-Instruct-v0.3](https://modelscope.cn/models/LLM-Research/Mistral-7B-Instruct-v0.3)|mistral|llama||transformers>=4.34|✘|-|[mistralai/Mistral-7B-Instruct-v0.3](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.3)|
diff --git a/docs/source_en/Instruction/Supported-models-and-datasets.md b/docs/source_en/Instruction/Supported-models-and-datasets.md
index e7a6a3a951..670b13b79d 100644
--- a/docs/source_en/Instruction/Supported-models-and-datasets.md
+++ b/docs/source_en/Instruction/Supported-models-and-datasets.md
@@ -494,6 +494,7 @@ The table below introduces the models integrated with ms-swift:
|[moonshotai/Kimi-K2-Instruct](https://modelscope.cn/models/moonshotai/Kimi-K2-Instruct)|deepseek_v3|kimi_k2||transformers>=4.39.3|✔|-|[moonshotai/Kimi-K2-Instruct](https://huggingface.co/moonshotai/Kimi-K2-Instruct)|
|[moonshotai/Kimi-K2-Instruct-0905](https://modelscope.cn/models/moonshotai/Kimi-K2-Instruct-0905)|deepseek_v3|kimi_k2||transformers>=4.39.3|✔|-|[moonshotai/Kimi-K2-Instruct-0905](https://huggingface.co/moonshotai/Kimi-K2-Instruct-0905)|
|[moonshotai/Kimi-K2-Thinking](https://modelscope.cn/models/moonshotai/Kimi-K2-Thinking)|deepseek_v3|kimi_k2||transformers>=4.39.3|✔|-|[moonshotai/Kimi-K2-Thinking](https://huggingface.co/moonshotai/Kimi-K2-Thinking)|
+|[TeleAI/TeleChat3-105B-A4.7B-Thinking](https://modelscope.cn/models/TeleAI/TeleChat3-105B-A4.7B-Thinking)|deepseek_v3|telechat3|telechat3|transformers>=4.46.3|✘|-|-|
|[deepseek-ai/DeepSeek-V3.1-Base](https://modelscope.cn/models/deepseek-ai/DeepSeek-V3.1-Base)|deepseek_v3|deepseek_v3_1|deepseek_v3_1|transformers>=4.39.3|✔|-|[deepseek-ai/DeepSeek-V3.1-Base](https://huggingface.co/deepseek-ai/DeepSeek-V3.1-Base)|
|[deepseek-ai/DeepSeek-V3.1](https://modelscope.cn/models/deepseek-ai/DeepSeek-V3.1)|deepseek_v3|deepseek_v3_1|deepseek_v3_1|transformers>=4.39.3|✔|-|[deepseek-ai/DeepSeek-V3.1](https://huggingface.co/deepseek-ai/DeepSeek-V3.1)|
|[deepseek-ai/DeepSeek-V3.1-Terminus](https://modelscope.cn/models/deepseek-ai/DeepSeek-V3.1-Terminus)|deepseek_v3|deepseek_v3_1|deepseek_v3_1|transformers>=4.39.3|✔|-|[deepseek-ai/DeepSeek-V3.1-Terminus](https://huggingface.co/deepseek-ai/DeepSeek-V3.1-Terminus)|
@@ -548,6 +549,8 @@ The table below introduces the models integrated with ms-swift:
|[TeleAI/TeleChat2-7B-32K](https://modelscope.cn/models/TeleAI/TeleChat2-7B-32K)|telechat2|telechat2||-|✘|-|[Tele-AI/TeleChat2-7B-32K](https://huggingface.co/Tele-AI/TeleChat2-7B-32K)|
|[TeleAI/TeleChat2-35B-32K](https://modelscope.cn/models/TeleAI/TeleChat2-35B-32K)|telechat2|telechat2||-|✘|-|[Tele-AI/TeleChat2-35B-32K](https://huggingface.co/Tele-AI/TeleChat2-35B-32K)|
|[TeleAI/TeleChat2-35B-Nov](https://modelscope.cn/models/TeleAI/TeleChat2-35B-Nov)|telechat2|telechat2||-|✘|-|[Tele-AI/TeleChat2-35B-Nov](https://huggingface.co/Tele-AI/TeleChat2-35B-Nov)|
+|[TeleAI/TeleChat3-36B-Thinking](https://modelscope.cn/models/TeleAI/TeleChat3-36B-Thinking)|telechat3|telechat3|telechat3|transformers>=4.53.2|✘|-|[Tele-AI/TeleChat3-36B-Thinking](https://huggingface.co/Tele-AI/TeleChat3-36B-Thinking)|
+|[TeleAI/TeleChat3-Coder-36B-Thinking](https://modelscope.cn/models/TeleAI/TeleChat3-Coder-36B-Thinking)|telechat3|telechat3_coder|telechat3_coder|transformers>=4.53.2|✘|coding|[Tele-AI/TeleChat3-Coder-36B-Thinking](https://huggingface.co/Tele-AI/TeleChat3-Coder-36B-Thinking)|
|[AI-ModelScope/Mistral-7B-Instruct-v0.1](https://modelscope.cn/models/AI-ModelScope/Mistral-7B-Instruct-v0.1)|mistral|llama||transformers>=4.34|✘|-|[mistralai/Mistral-7B-Instruct-v0.1](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.1)|
|[AI-ModelScope/Mistral-7B-Instruct-v0.2](https://modelscope.cn/models/AI-ModelScope/Mistral-7B-Instruct-v0.2)|mistral|llama||transformers>=4.34|✘|-|[mistralai/Mistral-7B-Instruct-v0.2](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2)|
|[LLM-Research/Mistral-7B-Instruct-v0.3](https://modelscope.cn/models/LLM-Research/Mistral-7B-Instruct-v0.3)|mistral|llama||transformers>=4.34|✘|-|[mistralai/Mistral-7B-Instruct-v0.3](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.3)|
diff --git a/swift/agent_template/base.py b/swift/agent_template/base.py
index 6ec8df4527..39f57dd988 100644
--- a/swift/agent_template/base.py
+++ b/swift/agent_template/base.py
@@ -13,7 +13,7 @@
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
from swift.infer_engine import Function
-from swift.template import Prompt, split_str_parts_by
+from swift.template import Prompt, Tool, split_str_parts_by
@dataclass
@@ -168,6 +168,10 @@ def _add_tool_call_prefix(self, tool_content: str, pre_message=None) -> str:
"""
return tool_content
+ def get_toolcall_with_tools(self, response: str, tools: Optional[List[Tool]] = None) -> List[Function]:
+ """Parse tool calls with request tools; legacy parsers ignore the schemas."""
+ return self.get_toolcall(response)
+
@staticmethod
def _get_tool_name(tool):
return tool.get('name_for_model') or tool.get('name')
diff --git a/swift/agent_template/mapping.py b/swift/agent_template/mapping.py
index ddecd0044b..823372292c 100644
--- a/swift/agent_template/mapping.py
+++ b/swift/agent_template/mapping.py
@@ -17,6 +17,7 @@
from .qwen3_coder import Qwen3_5AgentTemplate, Qwen3CoderAgentTemplate
from .react import ReactEnAgentTemplate, ReactZnAgentTemplate
from .seed_oss import SeedAgentTemplate
+from .telechat3 import TeleChat3AgentTemplate, TeleChat3CoderAgentTemplate
from .toolbench import ToolBenchAgentTemplate
from .youtu import YoutuAgentTemplate
@@ -50,6 +51,8 @@
'minimax_m2': MinimaxM2AgentTemplate,
'minimax_m3': MinimaxM3AgentTemplate,
'seed_oss': SeedAgentTemplate,
+ 'telechat3': TeleChat3AgentTemplate,
+ 'telechat3_coder': TeleChat3CoderAgentTemplate,
# ref: https://modelscope.cn/models/google/gemma-4-12B-it
'gemma4': Gemma4AgentTemplate,
# extra
diff --git a/swift/agent_template/telechat3.py b/swift/agent_template/telechat3.py
new file mode 100644
index 0000000000..483172cbbb
--- /dev/null
+++ b/swift/agent_template/telechat3.py
@@ -0,0 +1,152 @@
+# Copyright (c) ModelScope Contributors. All rights reserved.
+import json
+import re
+from typing import List, Optional, Tuple, Union
+
+from swift.infer_engine import Function
+from swift.template import Prompt, Tool
+from .base import BaseAgentTemplate
+
+
+class TeleChat3AgentTemplate(BaseAgentTemplate):
+
+ @staticmethod
+ def _dump_tool(tool) -> str:
+ return json.dumps(tool, ensure_ascii=False)
+
+ @staticmethod
+ def _dump_arg_value(value) -> str:
+ return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
+
+ def get_toolcall(self, response: str) -> List[Function]:
+ functions = []
+ for tool_call in re.findall(r'\s*(.*?)\s*', response, re.DOTALL):
+ tool_call = self._parse_json(tool_call.strip())
+ if isinstance(tool_call, dict) and 'name' in tool_call:
+ functions.append(Function(name=tool_call['name'], arguments=tool_call.get('arguments') or {}))
+ return functions
+
+ def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
+ tool_descs = '\n'.join(self._dump_tool(tool) for tool in tools)
+ tool_instruction = ('\n\n# 可用工具\n'
+ '你可以调用标签中包含的一个或多个工具来辅助你回答问题,以下是可用工具详情:\n'
+ '\n'
+ f'{tool_descs}\n'
+ '\n\n'
+ '# 调用方法\n'
+ '你需要遵循工具的要求,使用json格式返回工具名称及参数,并用包含。下方是一个调用模板:\n'
+ '\n'
+ '{"name": , "arguments": }\n'
+ '\n')
+ return (system or '') + tool_instruction
+
+ def _format_tool_calls(self, tool_call_messages) -> str:
+ tool_calls = []
+ for message in tool_call_messages:
+ tool_call = self._parse_tool_call(message['content'])
+ tool_calls.append(f'\n{json.dumps(tool_call, ensure_ascii=False)}\n')
+ return '\n'.join(tool_calls)
+
+ @staticmethod
+ def _to_prompt(content: Union[str, Prompt]) -> Prompt:
+ return content if isinstance(content, list) else [content]
+
+ def _format_tool_responses(self, assistant_content: Union[str, Prompt], tool_messages) -> Tuple[Prompt, Prompt]:
+ res: Prompt = []
+ for i, tool_message in enumerate(tool_messages):
+ tool_content = tool_message['content']
+ if i == 0:
+ res.append('<_user>\n')
+ else:
+ res.append('\n\n')
+ res.append(tool_content)
+ res.append('\n')
+ res.append('<_bot>')
+ return self._to_prompt(assistant_content) + ['<_end>\n'], res
+
+
+class TeleChat3CoderAgentTemplate(TeleChat3AgentTemplate):
+
+ def _add_tool_call_prefix(self, tool_content: str, pre_message=None) -> str:
+ if pre_message is None or pre_message['role'] != 'assistant':
+ return '' + tool_content
+ return tool_content
+
+ def get_toolcall(self, response: str) -> List[Function]:
+ return self.get_toolcall_with_tools(response)
+
+ def get_toolcall_with_tools(self, response: str, tools: Optional[List[Tool]] = None) -> List[Function]:
+ functions = []
+ for block in re.findall(r'(.*?)', response, re.DOTALL):
+ name, args = self._parse_coder_tool_call(block, tools)
+ if name:
+ functions.append(Function(name=name, arguments=args))
+ return functions
+
+ def _parse_coder_tool_call(self, block: str, tools: Optional[List[Tool]] = None):
+ name = re.split(r'', block, maxsplit=1)[0].strip()
+ args = {}
+ pattern = re.compile(r'(.*?)\s*(.*?)', re.DOTALL)
+ for key, value in pattern.findall(block):
+ key = key.strip()
+ value = value.strip()
+ if self._is_string_arg(name, key, tools):
+ args[key] = value
+ else:
+ parsed_value = self._parse_json(value)
+ args[key] = parsed_value if parsed_value is not None or value in {'null', 'None'} else value
+ return name, args
+
+ def _is_string_arg(self, tool_name, arg_name, tools):
+ # Match the model's official parser: only an exact schema type of "string" keeps the raw value.
+ for tool in tools or []:
+ if isinstance(tool, str):
+ tool = self._parse_json(tool)
+ if not isinstance(tool, dict):
+ continue
+ tool = self.unwrap_tool(tool)
+ if self._get_tool_name(tool) != tool_name:
+ continue
+ parameters = self._parse_json(tool.get('parameters') or {})
+ if not isinstance(parameters, dict):
+ return False
+ properties = parameters.get('properties') or {}
+ if not isinstance(properties, dict):
+ return False
+ arg_schema = properties.get(arg_name) or {}
+ return isinstance(arg_schema, dict) and arg_schema.get('type') == 'string'
+ return False
+
+ def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
+ tool_descs = '\n'.join(self._dump_tool(tool) for tool in tools)
+ tool_instruction = (
+ '\n# Tools\n\n'
+ 'You may call one or more functions to assist with the user query.\n\n'
+ 'You are provided with function signatures within XML tags:\n'
+ '\n'
+ f'{tool_descs}\n'
+ '\n\n'
+ 'For each function call, output the function name and arguments within the following XML format:\n'
+ '{function-name}{param-key-1}{param-value-1}'
+ '{param-key-2}{param-value-2}...')
+ return (system or '') + tool_instruction
+
+ def _format_tool_calls(self, tool_call_messages) -> str:
+ tool_calls = []
+ for message in tool_call_messages:
+ tool_call = self._parse_tool_call(message['content'])
+ arguments = tool_call.get('arguments') or {}
+ parts = [f'{tool_call["name"]}']
+ for key, value in arguments.items():
+ parts.append(f'{key}{self._dump_arg_value(value)}')
+ parts.append('')
+ tool_calls.append(''.join(parts))
+ return ''.join(tool_calls)
+
+ def _format_tool_responses(self, assistant_content: Union[str, Prompt], tool_messages) -> Tuple[Prompt, Prompt]:
+ res: Prompt = ['<_observation>']
+ for tool_message in tool_messages:
+ tool_content = tool_message['content']
+ res += ['', tool_content, '']
+ res.append('<_bot>')
+ return self._to_prompt(assistant_content) + ['<_end>'], res
diff --git a/swift/infer_engine/grpo_vllm_engine.py b/swift/infer_engine/grpo_vllm_engine.py
index 2c9254e3d7..9bffb42c9e 100644
--- a/swift/infer_engine/grpo_vllm_engine.py
+++ b/swift/infer_engine/grpo_vllm_engine.py
@@ -110,7 +110,7 @@ def _create_chat_completion_response(self, result, inputs, request_config, reque
output.token_ids = list(output.token_ids)
response = self.template.decode_generate_ids(output.token_ids, template_inputs=inputs['template_inputs'])
logprobs = self._get_logprobs(output.logprobs, output.token_ids, request_config.top_logprobs)
- toolcall = self._get_toolcall(response)
+ toolcall = self._get_toolcall(response, inputs['template_inputs'].tools)
token_ids = output.token_ids if request_config.return_details else None
choice = ChatCompletionResponseChoice(
diff --git a/swift/infer_engine/infer_engine.py b/swift/infer_engine/infer_engine.py
index b6a2e932eb..cdcadea9a7 100644
--- a/swift/infer_engine/infer_engine.py
+++ b/swift/infer_engine/infer_engine.py
@@ -9,7 +9,7 @@
from swift.metrics import Metric
from swift.model import get_ckpt_dir
-from swift.template import Template, get_template
+from swift.template import Template, Tool, get_template
from swift.utils import Processor, ProcessorMixin, get_logger
from .base import BaseInferEngine
from .protocol import (ChatCompletionMessageToolCall, ChatCompletionResponse, ChatCompletionStreamResponse,
@@ -187,9 +187,11 @@ def infer(self,
use_tqdm = not request_config.stream and len(infer_requests) > 1
return self._batch_infer_stream(tasks, request_config.stream, use_tqdm, metrics)
- def _get_toolcall(self, response: str) -> Optional[List[ChatCompletionMessageToolCall]]:
+ def _get_toolcall(self,
+ response: str,
+ tools: Optional[List[Tool]] = None) -> Optional[List[ChatCompletionMessageToolCall]]:
try:
- functions = self.template.agent_template.get_toolcall(response)
+ functions = self.template.agent_template.get_toolcall_with_tools(response, tools)
except Exception:
functions = None
if functions:
diff --git a/swift/infer_engine/lmdeploy_engine.py b/swift/infer_engine/lmdeploy_engine.py
index 61a9409e7b..db1276b4cb 100644
--- a/swift/infer_engine/lmdeploy_engine.py
+++ b/swift/infer_engine/lmdeploy_engine.py
@@ -226,7 +226,8 @@ async def _infer_stream_async(
toolcall = None
if is_finished:
toolcall = self._get_toolcall(
- self.template.decode_generate_ids(output.token_ids, template_inputs=inputs['template_inputs']))
+ self.template.decode_generate_ids(output.token_ids, template_inputs=inputs['template_inputs']),
+ inputs['template_inputs'].tools)
finish_reason = self._get_finish_reason(generation_config.max_new_tokens, output.num_token,
output.status.name == 'FINISH')
choices = [
@@ -265,7 +266,7 @@ async def _infer_full_async(
logprobs = self._get_logprobs(output.logprobs, output.token_ids, request_config.top_logprobs)
usage_info = self._get_usage_info(len(inputs['input_ids']), output.num_token)
- toolcall = self._get_toolcall(response)
+ toolcall = self._get_toolcall(response, inputs['template_inputs'].tools)
finish_reason = self._get_finish_reason(generation_config.max_new_tokens, output.num_token,
output.status.name == 'FINISH')
token_ids = output.token_ids if request_config.return_details else None
diff --git a/swift/infer_engine/sglang_engine.py b/swift/infer_engine/sglang_engine.py
index 15045e9881..71bdc6697c 100644
--- a/swift/infer_engine/sglang_engine.py
+++ b/swift/infer_engine/sglang_engine.py
@@ -186,7 +186,7 @@ def _create_chat_completion_response(self, output, inputs, return_details: bool
meta_info = output['meta_info']
usage_info = self._get_usage_info(meta_info['prompt_tokens'], meta_info['completion_tokens'])
response = self.template.decode_generate_ids(output['output_ids'], template_inputs=inputs['template_inputs'])
- toolcall = self._get_toolcall(response)
+ toolcall = self._get_toolcall(response, inputs['template_inputs'].tools)
token_ids = output['output_ids'] if return_details else None
choice = ChatCompletionResponseChoice(
index=0,
@@ -290,7 +290,8 @@ def _create_chat_completion_stream_response(self, output, infer_streamer) -> Opt
if is_finished:
finish_reason = finish_reason['type']
toolcall = self._get_toolcall(
- self.template.decode_generate_ids(output['output_ids'], **infer_streamer.decode_kwargs))
+ self.template.decode_generate_ids(output['output_ids'], **infer_streamer.decode_kwargs),
+ infer_streamer.decode_kwargs['template_inputs'].tools)
meta_info = output['meta_info']
usage_info = self._get_usage_info(meta_info['prompt_tokens'], meta_info['completion_tokens'])
# TODO: logprobs
diff --git a/swift/infer_engine/transformers_engine.py b/swift/infer_engine/transformers_engine.py
index 98c356789d..e4f406ef7a 100644
--- a/swift/infer_engine/transformers_engine.py
+++ b/swift/infer_engine/transformers_engine.py
@@ -310,7 +310,8 @@ def _model_generate(**kwargs):
toolcall = None
if is_finished[i]:
toolcall = self._get_toolcall(
- self.template.decode_generate_ids(generate_ids, template_inputs=template_inputs[i]))
+ self.template.decode_generate_ids(generate_ids, template_inputs=template_inputs[i]),
+ template_inputs[i].tools)
finish_reason = self._get_finish_reason(generation_config.max_new_tokens, usage_info.completion_tokens,
is_finished[i])
@@ -436,7 +437,7 @@ def _infer_full(self, inputs: Dict[str, Any], *, generation_config: GenerationCo
usage_info = self._update_usage_info(usage_info, len(generate_ids))
response = self.template.decode_generate_ids(generate_ids, template_inputs=template_inputs[i])
finish_reason = self._get_finish_reason(generation_config.max_new_tokens, len(generate_ids), True)
- toolcall = self._get_toolcall(response)
+ toolcall = self._get_toolcall(response, template_inputs[i].tools)
token_ids = generate_ids if request_config.return_details else None
choices.append(
ChatCompletionResponseChoice(
diff --git a/swift/infer_engine/vllm_engine.py b/swift/infer_engine/vllm_engine.py
index 03e5784f1b..b2b4a84350 100644
--- a/swift/infer_engine/vllm_engine.py
+++ b/swift/infer_engine/vllm_engine.py
@@ -641,7 +641,8 @@ def _create_chat_completion_stream_response(self, result, request_config, reques
toolcall = None
if output.is_finished:
toolcall = self._get_toolcall(
- self.template.decode_generate_ids(output.token_ids, **infer_streamers[i].decode_kwargs))
+ self.template.decode_generate_ids(output.token_ids, **infer_streamers[i].decode_kwargs),
+ infer_streamers[i].decode_kwargs['template_inputs'].tools)
choice = ChatCompletionResponseStreamChoice(
index=i,
@@ -711,7 +712,8 @@ def _create_chat_completion_response(
content = response
logprobs = self._get_logprobs(output.logprobs, output.token_ids, request_config.top_logprobs)
- toolcall = self._get_toolcall(content) # Use content instead of response for tool calls
+ # Use content instead of response for tool calls.
+ toolcall = self._get_toolcall(content, inputs['template_inputs'].tools)
token_ids = output.token_ids if request_config.return_details else None
choice = ChatCompletionResponseChoice(
index=output.index,
diff --git a/swift/model/constant.py b/swift/model/constant.py
index 5568f488e6..71b8649dc2 100644
--- a/swift/model/constant.py
+++ b/swift/model/constant.py
@@ -62,6 +62,7 @@ class LLMModelType:
telechat = 'telechat'
telechat2 = 'telechat2'
+ telechat3 = 'telechat3'
mistral = 'mistral'
devstral = 'devstral'
diff --git a/swift/model/models/deepseek.py b/swift/model/models/deepseek.py
index 3bcd40c8c9..267f164ed4 100644
--- a/swift/model/models/deepseek.py
+++ b/swift/model/models/deepseek.py
@@ -114,6 +114,11 @@ def get_model(self, model_dir: str, *args, **kwargs) -> PreTrainedModel:
Model('moonshotai/Kimi-K2-Instruct-0905', 'moonshotai/Kimi-K2-Instruct-0905'),
Model('moonshotai/Kimi-K2-Thinking', 'moonshotai/Kimi-K2-Thinking'),
], TemplateType.kimi_k2),
+ ModelGroup([
+ Model('TeleAI/TeleChat3-105B-A4.7B-Thinking'),
+ ],
+ TemplateType.telechat3,
+ requires=['transformers>=4.46.3']),
ModelGroup([
Model('deepseek-ai/DeepSeek-V3.1-Base', 'deepseek-ai/DeepSeek-V3.1-Base'),
Model('deepseek-ai/DeepSeek-V3.1', 'deepseek-ai/DeepSeek-V3.1'),
diff --git a/swift/model/models/telechat.py b/swift/model/models/telechat.py
index 73217bf06e..ee4d4b6949 100644
--- a/swift/model/models/telechat.py
+++ b/swift/model/models/telechat.py
@@ -58,3 +58,22 @@ def get_model(self, model_dir: str, config, processor, **kwargs) -> PreTrainedMo
model_arch=ModelArch.telechat,
architectures=['TeleChat2ForCausalLM'],
))
+
+register_model(
+ ModelMeta(
+ LLMModelType.telechat3,
+ [
+ ModelGroup([
+ Model('TeleAI/TeleChat3-36B-Thinking', 'Tele-AI/TeleChat3-36B-Thinking'),
+ ]),
+ ModelGroup([
+ Model('TeleAI/TeleChat3-Coder-36B-Thinking', 'Tele-AI/TeleChat3-Coder-36B-Thinking'),
+ ],
+ template=TemplateType.telechat3_coder,
+ tags=['coding']),
+ ],
+ template=TemplateType.telechat3,
+ model_arch=ModelArch.llama,
+ architectures=['TeleChat3ForCausalLM'],
+ requires=['transformers>=4.53.2'],
+ ))
diff --git a/swift/template/constant.py b/swift/template/constant.py
index a77a0f04e3..0ac9cf7eb1 100644
--- a/swift/template/constant.py
+++ b/swift/template/constant.py
@@ -79,6 +79,8 @@ class LLMTemplateType:
minicpm5 = 'minicpm5'
telechat = 'telechat'
telechat2 = 'telechat2'
+ telechat3 = 'telechat3'
+ telechat3_coder = 'telechat3_coder'
codefuse = 'codefuse'
codefuse_codellama = 'codefuse_codellama'
diff --git a/swift/template/templates/llm.py b/swift/template/templates/llm.py
index deaec030de..195ec4b09f 100644
--- a/swift/template/templates/llm.py
+++ b/swift/template/templates/llm.py
@@ -1,4 +1,5 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
+import json
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
@@ -8,7 +9,7 @@
from ..constant import LLMTemplateType, MLLMTemplateType
from ..register import TemplateMeta, register_template
from ..template_inputs import StdTemplateInputs
-from ..utils import Prompt
+from ..utils import Prompt, get_last_user_round
from .llama import Llama3_2TemplateMeta
from .qwen import Qwen2VLTemplate, QwenTemplateMeta
from .utils import DEFAULT_SYSTEM, ChatmlTemplateMeta
@@ -197,6 +198,303 @@ class TeleChatTemplateMeta(TemplateMeta):
telechat_system = '你是中国电信星辰语义大模型,英文名是TeleChat,你是由中电信人工智能科技有限公司和中国电信人工智能研究院(TeleAI)研发的人工智能助手。'
register_template(TeleChatTemplateMeta(LLMTemplateType.telechat2, default_system=telechat_system))
+
+class TeleChat3Template(Template):
+
+ class _TruthyEmptySystem(str):
+
+ def __bool__(self):
+ return True
+
+ def _get_system(self, inputs):
+ system = super()._get_system(inputs)
+ if system == '' and not inputs.tools:
+ system = self._TruthyEmptySystem('')
+ return system
+
+ def _get_response_prefix(self, inputs=None):
+ response_prefix = None if inputs is None else inputs.chat_template_kwargs.get('response_prefix')
+ if response_prefix is None:
+ response_prefix = self.response_prefix
+ if response_prefix is not None:
+ return response_prefix
+ if not self.use_chat_template:
+ return ''
+ return self.template_meta.thinking_prefix
+
+ @staticmethod
+ def _to_content_segments(content):
+ if isinstance(content, list) and (not content or not isinstance(content[0], int)):
+ return content.copy()
+ return [content]
+
+ @classmethod
+ def _concat_content(cls, pre_content, content):
+ if isinstance(pre_content, str) and isinstance(content, str):
+ return pre_content + content
+ return cls._to_content_segments(pre_content) + cls._to_content_segments(content)
+
+ @staticmethod
+ def _expand_metadata(value, num_segments, key):
+ if isinstance(value, list):
+ if len(value) > num_segments:
+ raise ValueError(f'{key} has {len(value)} values for {num_segments} content segments.')
+ return value + [None] * (num_segments - len(value))
+ return [value] * num_segments
+
+ def _merge_assistant_messages(self, pre_message, message):
+ pre_segments = self._to_content_segments(pre_message['content'])
+ cur_segments = self._to_content_segments(message['content'])
+ pre_message['content'] = pre_segments + cur_segments
+ for key in ['loss', 'loss_scale']:
+ if key not in pre_message and key not in message:
+ continue
+ pre_values = self._expand_metadata(pre_message.get(key), len(pre_segments), key)
+ cur_values = self._expand_metadata(message.get(key), len(cur_segments), key)
+ pre_message[key] = pre_values + cur_values
+
+ def _extend_assistant_metadata(self, message, old_num_segments):
+ new_num_segments = len(self._to_content_segments(message['content']))
+ if new_num_segments <= old_num_segments:
+ return
+ for key in ['loss', 'loss_scale']:
+ if key not in message:
+ continue
+ values = message[key]
+ if not isinstance(values, list):
+ continue
+ values = self._expand_metadata(values, old_num_segments, key)
+ fill_value = values[-1] if values else None
+ message[key] = values + [fill_value] * (new_num_segments - old_num_segments)
+
+ def _merge_natural_messages(self, inputs):
+ messages = inputs.messages
+ i = 1
+ while i < len(messages):
+ pre_message, message = messages[i - 1], messages[i]
+ role = message['role']
+ if pre_message['role'] != role or role not in {'assistant', 'user'}:
+ i += 1
+ continue
+ pre_content = pre_message.get('content')
+ content = message.get('content')
+ pre_content = '' if pre_content is None else pre_content
+ content = '' if content is None else content
+ if role == 'assistant':
+ for key in ['loss', 'loss_scale']:
+ if key not in pre_message and key not in message:
+ continue
+ if key not in pre_message or key not in message or pre_message[key] != message[key]:
+ raise ValueError(
+ f'TeleChat3 cannot merge consecutive assistant messages with different `{key}` values. '
+ 'Merge the messages before encoding or use the same value.')
+ pre_message['content'] = self._concat_content(pre_content, content)
+ tool_calls = list(pre_message.get('tool_calls') or []) + list(message.get('tool_calls') or [])
+ if tool_calls:
+ pre_message['tool_calls'] = tool_calls
+ pre_reasoning = pre_message.get('reasoning_content')
+ reasoning = message.get('reasoning_content')
+ if isinstance(reasoning, str):
+ pre_message['reasoning_content'] = (pre_reasoning or '') + reasoning
+ else:
+ pre_message['content'] = self._concat_content(pre_content, content)
+ messages.pop(i)
+
+ def _prepare_assistant_thinking(self, inputs):
+ for message in inputs.messages:
+ if message['role'] != 'assistant':
+ continue
+ content = message['content']
+ if isinstance(content, list) and (not content or not isinstance(content[0], int)):
+ for i, value in enumerate(content):
+ if isinstance(value, str):
+ content[i] = value.split('')[-1].lstrip('\n')
+ elif isinstance(content, str):
+ message['content'] = content.split('')[-1].lstrip('\n')
+
+ def _preprocess_tool_call_jinja(self, inputs):
+ messages = inputs.messages
+ i = 0
+ while i < len(messages):
+ if messages[i]['role'] != 'tool_call':
+ i += 1
+ continue
+ i_start = i
+ while i + 1 < len(messages) and messages[i + 1]['role'] == 'tool_call':
+ i += 1
+ tool_calls = []
+ for message in messages[i_start:i + 1]:
+ tool_call = self.agent_template._parse_tool_call(message['content'])
+ tool_calls.append({'type': 'function', 'function': tool_call})
+ if i_start > 0 and messages[i_start - 1]['role'] == 'assistant':
+ assistant_message = messages[i_start - 1]
+ if assistant_message.get('content') is None:
+ assistant_message['content'] = ''
+ assistant_message['tool_calls'] = list(assistant_message.get('tool_calls') or []) + tool_calls
+ messages[i_start:i + 1] = []
+ i = i_start
+ else:
+ messages[i_start:i + 1] = [{'role': 'assistant', 'content': '', 'tool_calls': tool_calls}]
+ i = i_start + 1
+
+ def _preprocess_structured_tool_calls_swift(self, inputs):
+ messages = inputs.messages
+ i = 0
+ while i < len(messages):
+ message = messages[i]
+ tool_calls = message.pop('tool_calls', None) if message['role'] == 'assistant' else None
+ if not tool_calls:
+ i += 1
+ continue
+ tool_call_messages = []
+ for tool_call in tool_calls:
+ function = tool_call.get('function', tool_call)
+ arguments = function.get('arguments') or {}
+ tool_call_message = {
+ 'role': 'tool_call',
+ 'content': json.dumps({
+ 'name': function['name'],
+ 'arguments': arguments
+ }, ensure_ascii=False)
+ }
+ for key in ['loss', 'loss_scale']:
+ value = message.get(key)
+ if key in message:
+ tool_call_message[key] = value[-1] if isinstance(value, list) and value else value
+ tool_call_messages.append(tool_call_message)
+ messages[i + 1:i + 1] = tool_call_messages
+ i += len(tool_call_messages) + 1
+
+ def _normalize_structured_tool_calls(self, inputs):
+ for message in inputs.messages:
+ if message['role'] != 'assistant':
+ continue
+ for tool_call in message.get('tool_calls') or []:
+ function = tool_call.get('function', tool_call)
+ arguments = function.get('arguments')
+ if isinstance(arguments, str):
+ parsed_arguments = self.agent_template._parse_json(arguments)
+ if parsed_arguments is not None:
+ function['arguments'] = parsed_arguments
+
+ def _swift_prepare_inputs(self, inputs):
+ # Normalize natural assistant text before tool calls are expanded into strings.
+ self._normalize_structured_tool_calls(inputs)
+ self._merge_natural_messages(inputs)
+ if self.template_backend == 'jinja':
+ self._preprocess_tool_call_jinja(inputs)
+ return
+ self._prepare_assistant_thinking(inputs)
+ self._preprocess_structured_tool_calls_swift(inputs)
+ self._preprocess_tool_call(inputs)
+ messages = inputs.messages
+ if len(messages) < 2:
+ return
+ i = 1
+ while i < len(messages):
+ pre_message, message = messages[i - 1], messages[i]
+ pre_role, pre_content = pre_message['role'], pre_message['content']
+ role, content = message['role'], message['content']
+ if pre_role == 'assistant' and role == 'tool' and self.template_backend == 'swift':
+ i_start = i
+ while i + 1 < len(messages) and messages[i + 1]['role'] == 'tool':
+ i += 1
+ old_num_segments = len(self._to_content_segments(pre_content))
+ pre_message['content'], tool_content = self.agent_template._format_tool_responses(
+ pre_content, messages[i_start:i + 1])
+ self._extend_assistant_metadata(pre_message, old_num_segments)
+ messages[i_start:i + 1] = [{'role': 'tool', 'content': tool_content}]
+ i = i_start + 1
+ elif pre_role == 'assistant' and role == 'assistant' or pre_role == 'user' and role == 'user':
+ if self.template_backend == 'swift' and pre_role == 'assistant':
+ self._merge_assistant_messages(pre_message, message)
+ else:
+ pre_message['content'] = self._concat_content(pre_content, content)
+ messages.pop(i)
+ else:
+ i += 1
+
+ def _remove_history_thinking(self, inputs) -> None:
+ # Every assistant turn is already normalized against the model Jinja above.
+ pass
+
+
+@dataclass
+class TeleChat3TemplateMeta(TemplateMeta):
+ template_cls: type = TeleChat3Template
+ prefix: Prompt = field(default_factory=lambda: ['<_system>'])
+ prompt: Prompt = field(default_factory=lambda: ['<_user>{{QUERY}}<_bot>'])
+ chat_sep: Optional[Prompt] = field(default_factory=lambda: ['<_end>\n'])
+ suffix: Prompt = field(default_factory=lambda: ['<_end>\n'])
+ system_prefix: Optional[Prompt] = field(default_factory=lambda: ['<_system>{{SYSTEM}}\n'])
+ is_thinking: bool = True
+ thinking_prefix: str = '\n'
+ agent_template: Optional[str] = 'telechat3'
+
+
+register_template(TeleChat3TemplateMeta(LLMTemplateType.telechat3))
+
+
+class TeleChat3CoderTemplate(TeleChat3Template):
+
+ def __init__(self, *args, **kwargs):
+ if kwargs.get('enable_thinking') is None:
+ kwargs['enable_thinking'] = True
+ super().__init__(*args, **kwargs)
+
+ def _prepare_assistant_thinking(self, inputs):
+ messages = inputs.messages
+ last_user_round = get_last_user_round(messages)
+ clear_thinking_defined = ('clear_thinking' in self.chat_template_kwargs
+ or 'clear_thinking' in inputs.chat_template_kwargs)
+ clear_thinking = inputs.chat_template_kwargs.get('clear_thinking',
+ self.chat_template_kwargs.get('clear_thinking'))
+ preserve_all = clear_thinking_defined and not clear_thinking
+ for i, message in enumerate(messages):
+ if message['role'] != 'assistant':
+ continue
+ preserve_reasoning = preserve_all or i > last_user_round
+ content = message['content']
+ reasoning_content = message.pop('reasoning_content', None)
+ if isinstance(content, list) and (not content or not isinstance(content[0], int)):
+ for j, value in enumerate(content):
+ if isinstance(value, str):
+ reasoning = reasoning_content if j == 0 else None
+ content[j] = self._normalize_current_thinking(value, reasoning, preserve_reasoning)
+ elif isinstance(content, str):
+ message['content'] = self._normalize_current_thinking(content, reasoning_content, preserve_reasoning)
+
+ @staticmethod
+ def _normalize_current_thinking(content: str,
+ reasoning_content: Optional[str] = None,
+ preserve_reasoning: bool = True) -> str:
+ if not isinstance(reasoning_content, str):
+ reasoning_content = ''
+ if '' in content:
+ reasoning_content = content.split('')[0].rstrip('\n').split('')[-1].lstrip('\n')
+ content = content.split('')[-1].lstrip('\n')
+ has_reasoning = bool(reasoning_content)
+ reasoning_content = reasoning_content.strip()
+ content = content.strip()
+ if preserve_reasoning and has_reasoning:
+ return f'\n{reasoning_content}\n{content}'
+ return f'{content}'
+
+
+@dataclass
+class TeleChat3CoderTemplateMeta(TeleChat3TemplateMeta):
+ template_cls: type = TeleChat3CoderTemplate
+ chat_sep: Optional[Prompt] = field(default_factory=lambda: ['<_end>'])
+ suffix: Prompt = field(default_factory=lambda: ['<_end>'])
+ system_prefix: Optional[Prompt] = field(default_factory=lambda: ['<_system>{{SYSTEM}}'])
+ thinking_prefix: str = ''
+ non_thinking_prefix: str = ''
+ history_thinking_prefix: str = ''
+ agent_template: Optional[str] = 'telechat3_coder'
+
+
+register_template(TeleChat3CoderTemplateMeta(LLMTemplateType.telechat3_coder))
+
DBRX_SYSTEM = (
'You are DBRX, created by Databricks. You were last updated in December 2023. '
'You answer questions based on information available up to that point.\n'
diff --git a/tests/test_align/test_template/test_agent.py b/tests/test_align/test_template/test_agent.py
index 367341c107..b0969f0732 100644
--- a/tests/test_align/test_template/test_agent.py
+++ b/tests/test_align/test_template/test_agent.py
@@ -1,8 +1,12 @@
+import json
import os
+import pytest
+from types import SimpleNamespace
os.environ['SWIFT_DEBUG'] = '1'
os.environ['CUDA_VISIBLE_DEVICES'] = '0,1,2,3'
os.environ['ASCEND_RT_VISIBLE_DEVICES'] = '0,1,2,3'
+
system = 'You are a helpful assistant.'
tools = [{
@@ -61,6 +65,8 @@
def _infer(engine, num_tools: int = 1, agent_tools=None, tool_messages=None, query=None):
+ from swift import InferRequest, RequestConfig
+
if agent_tools is None:
agent_tools = tools
if tool_messages is None:
@@ -657,6 +663,477 @@ def test_deepseek_v4():
assert template.safe_decode(encoded['input_ids']) == expected_input_ids
+telechat3_tools = [{
+ 'type': 'function',
+ 'function': {
+ 'name': 'get_weather',
+ 'description': 'Get weather',
+ 'parameters': {
+ 'type': 'object',
+ 'properties': {
+ 'city': {
+ 'type': 'string'
+ },
+ 'unit': {
+ 'type': 'string'
+ },
+ },
+ 'required': ['city']
+ }
+ }
+}]
+
+telechat3_tool_call = '{"name": "get_weather", "arguments": {"city": "Beijing", "unit": "celsius"}}'
+telechat3_tool_call2 = '{"name": "get_weather", "arguments": {"city": "Shanghai", "unit": "fahrenheit"}}'
+
+
+def _assert_template_backend_equal(template, data):
+ template.template_backend = 'swift'
+ template.set_mode('train')
+ encoded = template.encode(data)
+ template.template_backend = 'jinja'
+ encoded2 = template.encode(data)
+ assert encoded['input_ids'] == encoded2['input_ids']
+ return encoded
+
+
+def _assert_generation_backend_equal(template, data):
+ template.set_mode('transformers')
+ template.template_backend = 'swift'
+ encoded = template.encode(data)
+ template.template_backend = 'jinja'
+ encoded2 = template.encode(data)
+ assert encoded['input_ids'] == encoded2['input_ids']
+ return template.safe_decode(encoded['input_ids'])
+
+
+def _assert_telechat3_agent_template(model_id: str, template_type: str):
+ from swift import get_processor, get_template
+ from swift.model import get_matched_model_meta
+
+ model_meta = get_matched_model_meta(model_id)
+ assert model_meta.model_type == 'telechat3'
+ assert model_meta.template == template_type
+ tokenizer = get_processor(model_id)
+ template = get_template(tokenizer)
+ assert template.template_meta.template_type == template_type
+
+ for role in ['user', 'assistant']:
+ messages = [{'role': role, 'content': [101, 102]}, {'role': role, 'content': 'tail'}]
+ template._merge_natural_messages(SimpleNamespace(messages=messages))
+ assert messages == [{'role': role, 'content': [[101, 102], 'tail']}]
+
+ data = {
+ 'tools':
+ telechat3_tools,
+ 'messages': [{
+ 'role': 'system',
+ 'content': 'You are a helpful assistant.'
+ }, {
+ 'role': 'user',
+ 'content': 'weather?'
+ }, {
+ 'role': 'tool_call',
+ 'content': telechat3_tool_call
+ }, {
+ 'role': 'tool_response',
+ 'content': '{"temperature": 22}'
+ }, {
+ 'role': 'assistant',
+ 'content': 'sunny'
+ }]
+ }
+ _assert_template_backend_equal(template, data)
+
+ data['messages'].insert(2, {'role': 'assistant', 'content': 'I will check.'})
+ _assert_template_backend_equal(template, data)
+
+ data = {
+ 'tools':
+ telechat3_tools,
+ 'messages': [{
+ 'role': 'system',
+ 'content': 'You are a helpful assistant.'
+ }, {
+ 'role': 'user',
+ 'content': 'weather in two cities?'
+ }, {
+ 'role': 'tool_call',
+ 'content': telechat3_tool_call
+ }, {
+ 'role': 'tool_call',
+ 'content': telechat3_tool_call2
+ }, {
+ 'role': 'tool_response',
+ 'content': '{"temperature": 22}'
+ }, {
+ 'role': 'tool_response',
+ 'content': '{"temperature": 28}'
+ }, {
+ 'role': 'assistant',
+ 'content': 'Beijing is 22 and Shanghai is 28.'
+ }]
+ }
+ _assert_template_backend_equal(template, data)
+
+ thinking_arg = 'beforeafter'
+ data = {
+ 'tools':
+ telechat3_tools,
+ 'messages': [{
+ 'role': 'user',
+ 'content': 'echo the text'
+ }, {
+ 'role': 'tool_call',
+ 'content': json.dumps({
+ 'name': 'get_weather',
+ 'arguments': {
+ 'city': thinking_arg
+ }
+ })
+ }]
+ }
+ encoded = _assert_template_backend_equal(template, data)
+ assert thinking_arg in template.safe_decode(encoded['input_ids'])
+
+ structured_tool_call = {
+ 'type': 'function',
+ 'function': {
+ 'name': 'get_weather',
+ 'arguments': {
+ 'city': thinking_arg
+ }
+ }
+ }
+ data = {
+ 'tools':
+ telechat3_tools,
+ 'messages': [{
+ 'role': 'user',
+ 'content': 'echo the text'
+ }, {
+ 'role': 'assistant',
+ 'content': '',
+ 'tool_calls': [structured_tool_call]
+ }]
+ }
+ encoded = _assert_template_backend_equal(template, data)
+ assert thinking_arg in template.safe_decode(encoded['input_ids'])
+
+ structured_tool_call['function']['arguments'] = json.dumps({'city': thinking_arg}, separators=(',', ':'))
+ encoded = _assert_template_backend_equal(template, data)
+ assert thinking_arg in template.safe_decode(encoded['input_ids'])
+
+ for key, pre_value, tool_value in [('loss', False, True), ('loss_scale', 0.2, 0.8)]:
+ data = {
+ 'tools':
+ telechat3_tools,
+ 'messages': [{
+ 'role': 'user',
+ 'content': 'weather?'
+ }, {
+ 'role': 'assistant',
+ 'content': 'I will check.',
+ key: pre_value
+ }, {
+ 'role': 'tool_call',
+ 'content': telechat3_tool_call,
+ key: tool_value
+ }, {
+ 'role': 'tool_response',
+ 'content': '{"temperature": 22}'
+ }, {
+ 'role': 'assistant',
+ 'content': 'sunny'
+ }]
+ }
+ template.template_backend = 'swift'
+ template.set_mode('train')
+ is_binary_loss_scale = template.is_binary_loss_scale
+ if key == 'loss_scale':
+ template.is_binary_loss_scale = False
+ encoded = template.encode(data)
+ template.is_binary_loss_scale = is_binary_loss_scale
+ assert len(encoded['input_ids']) == len(encoded['labels'])
+ if key == 'loss_scale':
+ assert len(encoded['input_ids']) == len(encoded['loss_scale'])
+ assert pre_value in encoded['loss_scale']
+ assert tool_value in encoded['loss_scale']
+ else:
+ labels = template.safe_decode(encoded['labels'])
+ assert 'I will check.' not in labels
+ assert 'Beijing' in labels
+ return template
+
+
+def test_telechat3():
+ from swift import agent_template_map
+ from swift.model import get_matched_model_meta
+
+ template = _assert_telechat3_agent_template('TeleAI/TeleChat3-36B-Thinking', 'telechat3')
+ model_meta = get_matched_model_meta('TeleAI/TeleChat3-105B-A4.7B-Thinking')
+ assert model_meta.model_type == 'deepseek_v3'
+ assert model_meta.template == 'telechat3'
+
+ data = {
+ 'messages': [{
+ 'role': 'system',
+ 'content': ''
+ }, {
+ 'role': 'user',
+ 'content': 'hi'
+ }, {
+ 'role': 'assistant',
+ 'content': 'answer'
+ }]
+ }
+ encoded = _assert_template_backend_equal(template, data)
+ assert template.safe_decode(encoded['input_ids']) == '<_system>\n<_user>hi<_bot>answer<_end>\n'
+
+ data = {
+ 'messages': [{
+ 'role': 'user',
+ 'content': 'question'
+ }, {
+ 'role': 'assistant',
+ 'content': '\nplan\n\nanswer'
+ }]
+ }
+ encoded = _assert_template_backend_equal(template, data)
+ assert template.safe_decode(encoded['input_ids']) == '<_system><_user>question<_bot>answer<_end>\n'
+ data['chat_template_kwargs'] = {'preserve_thinking': True}
+ encoded = _assert_template_backend_equal(template, data)
+ assert template.safe_decode(encoded['input_ids']) == '<_system><_user>question<_bot>answer<_end>\n'
+
+ data = {'messages': [{'role': 'user', 'content': 'q'}, {'role': 'assistant', 'content': ' answer\n'}]}
+ encoded = _assert_template_backend_equal(template, data)
+ assert template.safe_decode(encoded['input_ids']) == '<_system><_user>q<_bot> answer\n<_end>\n'
+
+ data = {'chat_template_kwargs': {'enable_thinking': False}, 'messages': [{'role': 'user', 'content': 'q'}]}
+ assert _assert_generation_backend_equal(template, data) == '<_system><_user>q<_bot>\n'
+
+ agent_template = agent_template_map['telechat3']()
+ functions = agent_template.get_toolcall(f'\n{telechat3_tool_call}\n\n'
+ f'\n{telechat3_tool_call2}\n')
+ assert len(functions) == 2
+ assert functions[0].arguments == '{"city": "Beijing", "unit": "celsius"}'
+ assert functions[1].arguments == '{"city": "Shanghai", "unit": "fahrenheit"}'
+
+
+def test_telechat3_infer():
+ from swift import TransformersEngine
+
+ engine = TransformersEngine('TeleAI/TeleChat3-36B-Thinking')
+ engine.template.template_backend = 'jinja'
+ messages = _infer(
+ engine,
+ num_tools=2,
+ agent_tools=telechat3_tools,
+ query='Use the get_weather tool to get the weather in Beijing. Return only a tool call.')
+ assert messages[-1]['content']
+
+
+def test_telechat3_coder():
+ from swift import agent_template_map
+
+ template = _assert_telechat3_agent_template('TeleAI/TeleChat3-Coder-36B-Thinking', 'telechat3_coder')
+
+ data = {'messages': [{'role': 'user', 'content': 'hi'}, {'role': 'assistant', 'content': 'answer'}]}
+ encoded = _assert_template_backend_equal(template, data)
+ assert template.safe_decode(encoded['input_ids']) == '<_system><_user>hi<_bot>answer<_end>'
+
+ data = {
+ 'messages': [{
+ 'role': 'user',
+ 'content': 'hi'
+ }, {
+ 'role': 'assistant',
+ 'content': 'plananswer'
+ }, {
+ 'role': 'user',
+ 'content': 'again'
+ }, {
+ 'role': 'assistant',
+ 'content': 'done'
+ }]
+ }
+ encoded = _assert_template_backend_equal(template, data)
+ expected_input = '<_system><_user>hi<_bot>answer<_end><_user>again<_bot>done<_end>'
+ assert template.safe_decode(encoded['input_ids']) == expected_input
+ data['chat_template_kwargs'] = {'preserve_thinking': True}
+ encoded = _assert_template_backend_equal(template, data)
+ assert template.safe_decode(encoded['input_ids']) == expected_input
+
+ for key, first_value, second_value in [('loss', False, True), ('loss_scale', 0.2, 0.8)]:
+ data = {
+ 'messages': [{
+ 'role': 'user',
+ 'content': 'q'
+ }, {
+ 'role': 'assistant',
+ 'content': 'left',
+ key: first_value
+ }, {
+ 'role': 'assistant',
+ 'content': 'right',
+ key: second_value
+ }]
+ }
+ with pytest.raises(ValueError, match=rf'different `{key}` values'):
+ template.encode(data)
+
+ data = {
+ 'messages': [{
+ 'role': 'user',
+ 'content': 'question'
+ }, {
+ 'role': 'assistant',
+ 'content': '\nplan\n\nanswer'
+ }]
+ }
+ encoded = _assert_template_backend_equal(template, data)
+ expected_input = '<_system><_user>question<_bot>\nplan\nanswer<_end>'
+ assert template.safe_decode(encoded['input_ids']) == expected_input
+
+ cases = [
+ ({
+ 'messages': [{
+ 'role': 'user',
+ 'content': 'q'
+ }, {
+ 'role': 'assistant',
+ 'content': 'answer',
+ 'reasoning_content': 'plan'
+ }]
+ }, '<_system><_user>q<_bot>\nplan\nanswer<_end>'),
+ ({
+ 'messages': [{
+ 'role': 'user',
+ 'content': 'q'
+ }, {
+ 'role': 'assistant',
+ 'content': '\n\nanswer'
+ }]
+ }, '<_system><_user>q<_bot>answer<_end>'),
+ ({
+ 'chat_template_kwargs': {
+ 'clear_thinking': False
+ },
+ 'messages': [{
+ 'role': 'user',
+ 'content': 'q'
+ }, {
+ 'role': 'assistant',
+ 'content': 'answer',
+ 'reasoning_content': ' '
+ }]
+ }, '<_system><_user>q<_bot>\n\nanswer<_end>'),
+ ({
+ 'messages': [{
+ 'role': 'user',
+ 'content': 'q'
+ }, {
+ 'role': 'assistant',
+ 'content': 'prefix'
+ }, {
+ 'role': 'assistant',
+ 'content': 'xanswer'
+ }]
+ }, '<_system><_user>q<_bot>\nx\nanswer<_end>'),
+ ]
+ for data, expected_input in cases:
+ encoded = _assert_template_backend_equal(template, data)
+ assert template.safe_decode(encoded['input_ids']) == expected_input
+
+ data = {
+ 'chat_template_kwargs': {
+ 'clear_thinking': False
+ },
+ 'messages': [{
+ 'role': 'user',
+ 'content': 'q1'
+ }, {
+ 'role': 'assistant',
+ 'content': 'p1a1'
+ }, {
+ 'role': 'user',
+ 'content': 'q2'
+ }, {
+ 'role': 'assistant',
+ 'content': 'p2a2'
+ }]
+ }
+ encoded = _assert_template_backend_equal(template, data)
+ expected_input = ('<_system><_user>q1<_bot>\np1\na1<_end>'
+ '<_user>q2<_bot>\np2\na2<_end>')
+ assert template.safe_decode(encoded['input_ids']) == expected_input
+
+ data = {'chat_template_kwargs': {'enable_thinking': False}, 'messages': [{'role': 'user', 'content': 'q'}]}
+ assert _assert_generation_backend_equal(template, data) == '<_system><_user>q<_bot>'
+
+ data = {
+ 'tools':
+ telechat3_tools,
+ 'messages': [{
+ 'role': 'user',
+ 'content': 'q1'
+ }, {
+ 'role': 'tool_call',
+ 'content': telechat3_tool_call
+ }, {
+ 'role': 'tool_response',
+ 'content': '{"temperature": 22}'
+ }, {
+ 'role': 'assistant',
+ 'content': 'done'
+ }, {
+ 'role': 'user',
+ 'content': 'q2'
+ }]
+ }
+ rendered = _assert_generation_backend_equal(template, data)
+ assert '<_user>q1<_bot>' in rendered
+
+ agent_template = agent_template_map['telechat3_coder']()
+ functions = agent_template.get_toolcall('get_weather'
+ 'cityBeijing'
+ 'get_weather'
+ 'cityShanghai'
+ '')
+ assert len(functions) == 2
+ assert functions[0].arguments == '{"city": "Beijing"}'
+ assert functions[1].arguments == '{"city": "Shanghai"}'
+ response = ('get_weather'
+ ' city \n 22 '
+ 'unit null'
+ '')
+ functions = agent_template.get_toolcall(response)
+ assert len(functions) == 1
+ assert functions[0].arguments == '{"city": 22, "unit": null}'
+ functions = agent_template.get_toolcall_with_tools(response, telechat3_tools)
+ assert len(functions) == 1
+ assert functions[0].arguments == '{"city": "22", "unit": "null"}'
+
+ from swift.infer_engine.infer_engine import InferEngine
+ engine = SimpleNamespace(template=SimpleNamespace(agent_template=agent_template))
+ tool_calls = InferEngine._get_toolcall(engine, response, telechat3_tools)
+ assert len(tool_calls) == 1
+ assert tool_calls[0].function.arguments == '{"city": "22", "unit": "null"}'
+
+
+def test_telechat3_coder_infer():
+ from swift import TransformersEngine
+
+ engine = TransformersEngine('TeleAI/TeleChat3-Coder-36B-Thinking', template_type='telechat3_coder')
+ engine.template.template_backend = 'jinja'
+ messages = _infer(
+ engine,
+ num_tools=2,
+ agent_tools=telechat3_tools,
+ query='Use the get_weather tool to get the weather in Beijing. Return only a tool call.')
+ assert messages[-1]['content']
+
+
def test_seed_oss():
engine = TransformersEngine('ByteDance-Seed/Seed-OSS-36B-Instruct', load_model=False)