From 824f1768b26828eb0a9f937bfef41fba723db636 Mon Sep 17 00:00:00 2001 From: Michael Verrilli Date: Sun, 19 Apr 2026 17:17:29 +0000 Subject: [PATCH] model/parsers/qwen3coder: prevent tag regex from matching across newlines qwenTagRegex used [^>]+ which matches newlines. A parameter value containing a pattern like "C<1w=expr" with no ">" on the same line caused the regex to greedily consume everything through the real closing tag on a later line, leaving as the next structural element. xml.Unmarshal then reported "element closed by ". Restricting the match to [^>\n]+ keeps each tag on a single line, matching the Qwen3-coder format's actual structure. Fixes #15574 --- model/parsers/qwen3coder.go | 4 +- model/parsers/qwen3coder_test.go | 63 ++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/model/parsers/qwen3coder.go b/model/parsers/qwen3coder.go index dfa604acc..1779a2c67 100644 --- a/model/parsers/qwen3coder.go +++ b/model/parsers/qwen3coder.go @@ -392,7 +392,9 @@ func parseValue(raw string, paramType api.PropertyType) any { } var ( - qwenTagRegex = regexp.MustCompile(`<(\w+)=([^>]+)>`) + qwenTagRegex = regexp.MustCompile(`<(\w+)=([^>\r\n]+)>`) + // [^"] is safe here because xml.EscapeText has already encoded any literal + // newlines in attribute values as before this regex runs. qwenXMLTagRegex = regexp.MustCompile(``) ) diff --git a/model/parsers/qwen3coder_test.go b/model/parsers/qwen3coder_test.go index 7142567ed..9bee7f431 100644 --- a/model/parsers/qwen3coder_test.go +++ b/model/parsers/qwen3coder_test.go @@ -1121,6 +1121,34 @@ func TestQwen3CoderParserToolCallIndexResetOnInit(t *testing.T) { } } +func TestQwen3CoderParserAngleBracketsInParameterValue(t *testing.T) { + tools := []api.Tool{tool("run_code", map[string]api.ToolProperty{ + "source": {Type: api.PropertyType{"string"}}, + })} + parser := Qwen3CoderParser{} + parser.Init(tools, nil, nil) + + // Parameter value contains " closing tag + // and producing "element closed by " from xml.Unmarshal. + input := "\n\n\nIF C<1w=P-SQR(1-C)\nNEXT\n\n\n" + _, _, calls, err := parser.Add(input, true) + if err != nil { + t.Fatalf("parse failed: %v", err) + } + if len(calls) != 1 { + t.Fatalf("expected 1 tool call, got %d", len(calls)) + } + want := "IF C<1w=P-SQR(1-C)\nNEXT" + got, ok := calls[0].Function.Arguments.Get("source") + if !ok { + t.Fatal("missing source argument") + } + if got != want { + t.Errorf("source: got %q, want %q", got, want) + } +} + func TestQwenXMLTransform(t *testing.T) { cases := []struct { desc string @@ -1181,6 +1209,41 @@ celsius `, }, + { + // qwenTagRegex must not match across newlines: a pattern like <1w=expr + // with no ">" on the same line should not greedily consume the real + // and closing tags that appear on later lines. + desc: "angle brackets in parameter values do not corrupt closing tags", + raw: ` + +IF C<1w=P-SQR(1-C) +NEXT + +`, + want: ` + +IF C<1w=P-SQR(1-C) +NEXT + +`, + }, + { + desc: "multiple angle-bracket patterns in same parameter value", + raw: ` + +IF A<1x=FOO(A) +IF B<2y=BAR(B) +NEXT + +`, + want: ` + +IF A<1x=FOO(A) +IF B<2y=BAR(B) +NEXT + +`, + }, } for _, tc := range cases {