feat(model-meta): register qwen-plus family with its own tokenizer and template

request_model values like qwen-plus matched no registered family, so
resolve_model_family fell through to its "glm5" default and the formatter
rendered canonical_prompt with the GLM-5 chat template and tokenized release
blocks with the GLM-5 tokenizer. The fallback is silent, so nothing surfaced it.

Register the family, ship Qwen3-plus tokenizer/chat_template (the template was
embedded in tokenizer_config.json upstream and is extracted here), and add a
QwenPlusTraceAdapter ahead of the GLM catch-all in get_raw_adapter -- the
registry entry alone is not enough, since the adapter is what picks the
template. The shared message/tool normalization is factored out of
build_glm5_canonical_prompt so both builders use one copy.

Measured against provider-reported usage.input_tokens on a 400-request sample,
median tokenized/reported ratio goes 0.9449 -> 1.0000, and unique release block
count on a 2190-request sample moves 5886 -> 5701.

Note deepseek-v4-flash is still unregistered and still resolves to glm5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-11 06:31:34 +00:00
parent 8b536cab2e
commit 7cb4cac56d
7 changed files with 303751 additions and 7 deletions

View File

@@ -57,6 +57,10 @@ def _load_qwen3_coder_chat_template():
return _load_chat_template(str(resolve_chat_template_path("qwen3-coder")))
def _load_qwen_plus_chat_template():
return _load_chat_template(str(resolve_chat_template_path("qwen-plus")))
@lru_cache(maxsize=1)
def _load_qwen3_coder_tool_parser_spec() -> Qwen3CoderToolParserSpec:
defaults = Qwen3CoderToolParserSpec()
@@ -101,6 +105,10 @@ def _stringify_message_content_for_template(content):
def _normalize_message_content_for_template(content, role=""):
if content is None:
return ""
if role == "tool" and not isinstance(content, (list, dict, str)):
return str(content)
if role == "tool" and isinstance(content, (list, dict)):
has_named_tool_refs = False
if isinstance(content, list):
@@ -261,7 +269,35 @@ def _normalize_qwen_message_with_tool_parser(message, spec: Qwen3CoderToolParser
return normalized_message
def build_glm5_canonical_prompt(payload):
_FALLBACK_COUNT = 0
def _fallback_canonical_prompt(messages):
global _FALLBACK_COUNT
_FALLBACK_COUNT += 1
parts = []
for m in messages:
if not isinstance(m, dict):
parts.append(str(m))
continue
role = str(m.get("role", ""))
content = _stringify_message_content_for_template(m.get("content"))
seg = "<|" + role + "|>" + content
tool_calls = m.get("tool_calls") or []
if isinstance(tool_calls, list):
for tc in tool_calls:
function = tc.get("function", {}) if isinstance(tc, dict) else {}
if not isinstance(function, dict):
function = {}
name = str(function.get("name", ""))
arguments = function.get("arguments", {})
seg += "<tool_call>" + name + compact_json(arguments) + "</tool_call>"
parts.append(seg)
return "".join(parts)
def _normalize_payload_for_template(payload):
"""Normalize one request payload into (messages, tools) ready for a chat template."""
input_payload = payload.get("input", {}) if isinstance(payload, dict) else {}
parameters = payload.get("parameters", {}) if isinstance(payload, dict) else {}
messages = []
@@ -289,11 +325,33 @@ def build_glm5_canonical_prompt(payload):
elif not isinstance(tools_payload, list):
tools_payload = [tools_payload]
tools = [_normalize_tool_spec_for_template(tool) for tool in tools_payload if isinstance(tool, dict)]
return messages, tools
def build_glm5_canonical_prompt(payload):
messages, tools = _normalize_payload_for_template(payload)
try:
return _load_glm5_chat_template().render(
messages=messages,
tools=tools,
add_generation_prompt=True,
)
except Exception:
return _fallback_canonical_prompt(messages)
def build_qwen_plus_canonical_prompt(payload):
messages, tools = _normalize_payload_for_template(payload)
if not messages:
messages = [{"role": "system", "content": ""}]
try:
return _load_qwen_plus_chat_template().render(
messages=messages,
tools=tools,
add_generation_prompt=True,
)
except Exception:
return _fallback_canonical_prompt(messages)
def build_qwen3_coder_canonical_prompt(payload):
@@ -408,8 +466,20 @@ class QwenTraceAdapter(GLMTraceAdapter):
return build_qwen3_coder_canonical_prompt(payload)
class QwenPlusTraceAdapter(GLMTraceAdapter):
name = "qwen-plus"
def detect(self, raw):
if "request_params" not in raw:
return False
return infer_model_family_from_request_model(raw.get("request_model")) == "qwen-plus"
def build_canonical_prompt(self, payload):
return build_qwen_plus_canonical_prompt(payload)
def get_raw_adapter(raw: dict[str, Any]):
for adapter in [QwenTraceAdapter(), GLMTraceAdapter()]:
for adapter in [QwenTraceAdapter(), QwenPlusTraceAdapter(), GLMTraceAdapter()]:
if adapter.detect(raw):
return adapter
raise ValueError("Unsupported raw trace format for trace_formatter.")

View File

@@ -0,0 +1,62 @@
{%- if tools %}
{{- '<|im_start|>system\n' }}
{%- if messages[0]['role'] == 'system' %}
{{- messages[0]['content'] }}
{%- else %}
{{- '' }}
{%- endif %}
{{- "\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
{%- for tool in tools %}
{{- "\n" }}
{{- tool | tojson }}
{%- endfor %}
{{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
{%- else %}
{%- if messages[0]['role'] == 'system' %}
{{- '<|im_start|>system\n' + messages[0]['content'] + '<|im_end|>\n' }}
{%- endif %}
{%- endif %}
{%- for message in messages %}
{%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
{{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
{%- elif message.role == "assistant" and not message.tool_calls %}
{%- set content = message.content %}
{%- if not loop.last %}
{%- set content = message.content.split('</think>')[-1].lstrip('\n') %}
{%- endif %}
{{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
{%- elif message.role == "assistant" %}
{%- set content = message.content %}
{%- if not loop.last %}
{%- set content = message.content.split('</think>')[-1].lstrip('\n') %}
{%- endif %}
{{- '<|im_start|>' + message.role }}
{%- if message.content %}
{{- '\n' + content }}
{%- endif %}
{%- for tool_call in message.tool_calls %}
{%- if tool_call.function is defined %}
{%- set tool_call = tool_call.function %}
{%- endif %}
{{- '\n<tool_call>\n{"name": "' }}
{{- tool_call.name }}
{{- '", "arguments": ' }}
{{- tool_call.arguments | tojson }}
{{- '}\n</tool_call>' }}
{%- endfor %}
{{- '<|im_end|>\n' }}
{%- elif message.role == "tool" %}
{%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") %}
{{- '<|im_start|>user' }}
{%- endif %}
{{- '\n<tool_response>\n' }}
{{- message.content }}
{{- '\n</tool_response>' }}
{%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
{{- '<|im_end|>\n' }}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- if add_generation_prompt %}
{{- '<|im_start|>assistant\n' }}
{%- endif %}

View File

@@ -0,0 +1,42 @@
{
"architectures": [
"Qwen3MoeForCausalLM"
],
"attention_bias": false,
"attention_dropout": 0.0,
"attn_output_gate": false,
"bos_token_id": 151643,
"decoder_sparse_step": 1,
"eos_token_id": 151645,
"head_dim": 128,
"hidden_act": "silu",
"hidden_size": 4096,
"initializer_range": 0.02,
"intermediate_size": 12288,
"max_position_embeddings": 35840,
"max_window_layers": 60,
"mlp_only_layers": [],
"model_type": "qwen3_moe",
"moe_intermediate_size": 1536,
"norm_topk_prob": true,
"num_attention_heads": 64,
"num_experts": 128,
"num_experts_per_tok": 8,
"num_hidden_layers": 60,
"num_key_value_heads": 4,
"output_router_logits": false,
"partial_rotary_factor": 1.0,
"rms_norm_eps": 1e-06,
"rope_scaling": null,
"rope_theta": 10000000,
"router_aux_loss_coef": 0.001,
"shared_expert_intermediate_size": 0,
"sliding_window": null,
"tie_word_embeddings": false,
"torch_dtype": "bfloat16",
"transformers_version": "4.52.3",
"use_cache": true,
"use_gemma_rms_norm": false,
"use_sliding_window": false,
"vocab_size": 151936
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,239 @@
{
"add_bos_token": false,
"add_prefix_space": false,
"added_tokens_decoder": {
"151643": {
"content": "<|endoftext|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151644": {
"content": "<|im_start|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151645": {
"content": "<|im_end|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151646": {
"content": "<|object_ref_start|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151647": {
"content": "<|object_ref_end|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151648": {
"content": "<|box_start|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151649": {
"content": "<|box_end|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151650": {
"content": "<|quad_start|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151651": {
"content": "<|quad_end|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151652": {
"content": "<|vision_start|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151653": {
"content": "<|vision_end|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151654": {
"content": "<|vision_pad|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151655": {
"content": "<|image_pad|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151656": {
"content": "<|video_pad|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"151657": {
"content": "<tool_call>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": false
},
"151658": {
"content": "</tool_call>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": false
},
"151659": {
"content": "<|fim_prefix|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": false
},
"151660": {
"content": "<|fim_middle|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": false
},
"151661": {
"content": "<|fim_suffix|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": false
},
"151662": {
"content": "<|fim_pad|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": false
},
"151663": {
"content": "<|repo_name|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": false
},
"151664": {
"content": "<|file_sep|>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": false
},
"151665": {
"content": "<tool_response>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": false
},
"151666": {
"content": "</tool_response>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": false
},
"151667": {
"content": "<think>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": false
},
"151668": {
"content": "</think>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": false
}
},
"additional_special_tokens": [
"<|im_start|>",
"<|im_end|>",
"<|object_ref_start|>",
"<|object_ref_end|>",
"<|box_start|>",
"<|box_end|>",
"<|quad_start|>",
"<|quad_end|>",
"<|vision_start|>",
"<|vision_end|>",
"<|vision_pad|>",
"<|image_pad|>",
"<|video_pad|>"
],
"bos_token": null,
"chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- '' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" and not message.tool_calls %}\n {%- set content = message.content %}\n {%- if not loop.last %}\n {%- set content = message.content.split('</think>')[-1].lstrip('\\n') %}\n {%- endif %}\n {{- '<|im_start|>' + message.role + '\\n' + content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set content = message.content %}\n {%- if not loop.last %}\n {%- set content = message.content.split('</think>')[-1].lstrip('\\n') %}\n {%- endif %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- message.content }}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n",
"clean_up_tokenization_spaces": false,
"eos_token": "<|im_end|>",
"errors": "replace",
"model_max_length": 131072,
"pad_token": "<|endoftext|>",
"split_special_tokens": false,
"tokenizer_class": "Qwen2Tokenizer",
"unk_token": null
}

File diff suppressed because one or more lines are too long

View File

@@ -42,6 +42,12 @@ MODEL_REGISTRY = {
model_name="Qwen3-Coder-480B-A35B-Instruct",
request_model_hints=("qwen3-coder", "qwen3 coder", "qwen3_coder"),
),
"qwen-plus": ModelMeta(
family="qwen-plus",
provider="Qwen",
model_name="Qwen3-plus",
request_model_hints=("qwen-plus", "qwen3-plus", "qwen_plus"),
),
}
MODEL_ALIASES = {
@@ -54,6 +60,10 @@ MODEL_ALIASES = {
"qwen3-coder": "qwen3-coder",
"qwen3_coder": "qwen3-coder",
"coder": "qwen3-coder",
"qwen-plus": "qwen-plus",
"qwen3-plus": "qwen-plus",
"qwen_plus": "qwen-plus",
"plus": "qwen-plus",
}
@@ -73,6 +83,8 @@ def _infer_model_family_from_path(input_path: str | Path | None) -> str | None:
return None
if "qwen3-coder" in text or "qwen3_coder" in text:
return "qwen3-coder"
if "qwen-plus" in text or "qwen3-plus" in text or "qwen_plus" in text:
return "qwen-plus"
if "glm5" in text or "trace-glm" in text:
return "glm5"
return None