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:
@@ -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.")
|
||||
|
||||
Reference in New Issue
Block a user