[model] support TeleChat3#9727
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for the TeleChat3 model family, including base, thinking, and coder variants. It implements the corresponding templates and agent templates, updates the inference engines to support tool parsing with schema validation, and adds comprehensive test coverage. The review feedback suggests adding defensive checks when unwrapping tools to prevent potential AttributeError exceptions, and introducing a safe content concatenation helper to avoid TypeError crashes when merging message contents of mixed types.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| tool = self.unwrap_tool(tool) | ||
| if self._get_tool_name(tool) != tool_name: | ||
| continue |
There was a problem hiding this comment.
Add a defensive check to ensure tool is a dictionary after calling unwrap_tool. If unwrap_tool returns None or a non-dictionary object, calling _get_tool_name(tool) will raise an AttributeError.
| tool = self.unwrap_tool(tool) | |
| if self._get_tool_name(tool) != tool_name: | |
| continue | |
| tool = self.unwrap_tool(tool) | |
| if not isinstance(tool, dict): | |
| continue | |
| if self._get_tool_name(tool) != tool_name: | |
| continue |
| @staticmethod | ||
| def _to_content_segments(content): | ||
| if isinstance(content, list) and (not content or not isinstance(content[0], int)): | ||
| return content.copy() | ||
| return [content] |
There was a problem hiding this comment.
Add a helper method _concat_content to safely concatenate message contents. This avoids potential TypeError exceptions when merging consecutive assistant or user messages where one content is a list (e.g., multimodal or token IDs) and the other is a string.
| @staticmethod | |
| def _to_content_segments(content): | |
| if isinstance(content, list) and (not content or not isinstance(content[0], int)): | |
| return content.copy() | |
| return [content] | |
| @staticmethod | |
| def _to_content_segments(content): | |
| if isinstance(content, list) and (not content or not isinstance(content[0], int)): | |
| return content.copy() | |
| return [content] | |
| @staticmethod | |
| def _concat_content(pre_content, content): | |
| if isinstance(pre_content, list) and isinstance(content, list): | |
| return pre_content + content | |
| if isinstance(pre_content, list): | |
| return pre_content + [content] | |
| if isinstance(content, list): | |
| return [pre_content] + content | |
| return pre_content + content |
| pre_message['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'] = pre_content + content |
There was a problem hiding this comment.
Use the safe _concat_content helper to merge consecutive user/assistant messages, preventing potential TypeError crashes.
| pre_message['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'] = pre_content + content | |
| 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) |
| 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'] = pre_content + content | ||
| messages.pop(i) |
There was a problem hiding this comment.
Use the safe _concat_content helper when merging consecutive messages in _swift_prepare_inputs to avoid TypeError crashes.
| 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'] = pre_content + content | |
| messages.pop(i) | |
| 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) |
There was a problem hiding this comment.
Pull request overview
Adds first-class TeleChat3 support to ms-swift by introducing TeleChat3/TelChat3-Coder templates (Swift + Jinja parity), agent tool-call parsing/formatting for both tool-call styles, model registration, and alignment/inference tests. It also threads request tool schemas through multiple inference backends so TeleChat3-Coder can preserve string-typed tool arguments according to schema.
Changes:
- Implement TeleChat3 + TeleChat3-Coder templates (including thinking normalization, tool-call/tool-response preprocessing, and Swift/Jinja backend parity checks).
- Add TeleChat3 agent templates (JSON
<tool_call>blocks and coder XML param blocks), plus a schema-awareget_toolcall_with_tools(...)hook used by inference engines. - Register TeleChat3 models/templates and document them in CN/EN supported-model lists; add focused alignment/inference tests.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_align/test_template/test_agent.py | Adds TeleChat3/TeleChat3-Coder alignment + inference tests, including schema-aware string-arg parsing assertions. |
| swift/template/templates/llm.py | Introduces TeleChat3Template / TeleChat3CoderTemplate and registers new template metas. |
| swift/template/constant.py | Adds telechat3 and telechat3_coder template type constants. |
| swift/model/models/telechat.py | Registers TeleChat3 dense + coder models with appropriate templates and requirements. |
| swift/model/models/deepseek.py | Registers TeleChat3 MoE (105B-A4.7B) under deepseek_v3 with TeleChat3 template. |
| swift/model/constant.py | Adds telechat3 model type constant. |
| swift/infer_engine/vllm_engine.py | Passes request tools into tool-call parsing for streaming + non-streaming responses. |
| swift/infer_engine/transformers_engine.py | Passes request tools into tool-call parsing for streaming + full inference. |
| swift/infer_engine/sglang_engine.py | Passes request tools into tool-call parsing for streaming + non-streaming responses. |
| swift/infer_engine/lmdeploy_engine.py | Passes request tools into tool-call parsing for streaming + full inference. |
| swift/infer_engine/infer_engine.py | Extends _get_toolcall to accept request tools and use schema-aware agent hook. |
| swift/infer_engine/grpo_vllm_engine.py | Passes request tools into tool-call parsing. |
| swift/agent_template/telechat3.py | Adds TeleChat3 + TeleChat3-Coder agent templates, including schema-aware coder parsing. |
| swift/agent_template/mapping.py | Registers new agent templates in the mapping. |
| swift/agent_template/base.py | Adds backward-compatible get_toolcall_with_tools(response, tools=None) hook. |
| docs/source/Instruction/Supported-models-and-datasets.md | Documents TeleChat3 models/templates in the supported-model list (CN). |
| docs/source_en/Instruction/Supported-models-and-datasets.md | Documents TeleChat3 models/templates in the supported-model list (EN). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pre_content = pre_message.get('content') or '' | ||
| content = message.get('content') or '' | ||
| 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'] = 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'] = pre_content + content |
| 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 |
| has_reasoning = bool(reasoning_content) | ||
| reasoning_content = reasoning_content.strip() | ||
| content = content.strip() | ||
| if preserve_reasoning and has_reasoning: | ||
| return f'<think>\n{reasoning_content}\n</think>{content}' | ||
| return f'</think>{content}' |
PR type
PR information
Support the TeleChat3 dense, coder, and MoE thinking models:
TeleAI/TeleChat3-36B-ThinkingTeleAI/TeleChat3-Coder-36B-ThinkingTeleAI/TeleChat3-105B-A4.7B-ThinkingThis PR adds:
tests/test_align/test_template/test_agent.py.TeleChat3-Coder's parser needs the request tool schema to preserve string-typed arguments exactly as specified by the model's official parser. The first commit therefore adds a backward-compatible
get_toolcall_with_tools(response, tools=None)hook and passes the current request's tools through the Transformers, vLLM, LMDeploy, SGLang, and GRPO-vLLM inference paths. The base implementation delegates to the existingget_toolcall, so existing agent templates retain their previous behavior; only TeleChat3-Coder overrides the schema-aware hook.These inference-engine changes only propagate parser context; they do not claim that every TeleChat3 checkpoint is loadable by every backend. Runtime validation in this PR uses TransformersEngine, while accelerated-backend model compatibility remains determined by the corresponding vLLM, SGLang, or LMDeploy runtime.
Template behavior covered by the tests includes system/empty-system prompts, thinking removal and normalization, answer whitespace, current and historical thinking, assistant text before tool calls, single/parallel tool calls and responses, string arguments, and generation prompts. Native Swift rendering is compared with
tokenizer.apply_chat_template/the model Jinja output at theinput_idslevel.Experiment results
pre-commit run --all-files: passed.2 passed, 2 warnings:tests/test_align/test_template/test_agent.py::test_telechat3tests/test_align/test_template/test_agent.py::test_telechat3_coder2 passed, 2 warnings.