diff --git a/model/parsers/glm46.go b/model/parsers/glm46.go
index 7befc711f..fb6ea888e 100644
--- a/model/parsers/glm46.go
+++ b/model/parsers/glm46.go
@@ -345,6 +345,47 @@ func escapeGLM46Content(s string) string {
return result.String()
}
+// repairUnclosedArgValues inserts missing closing tags.
+// GLM models sometimes omit the closing tag, producing XML like:
+//
+// value
+//
+// instead of:
+//
+// value
+func repairUnclosedArgValues(s string) string {
+ var result strings.Builder
+ for {
+ openIdx := strings.Index(s, "")
+ if openIdx == -1 {
+ result.WriteString(s)
+ break
+ }
+ afterOpen := openIdx + len("")
+ closeIdx := strings.Index(s[afterOpen:], "")
+ nextKeyIdx := strings.Index(s[afterOpen:], "")
+ // Check if properly closed before the next (or no next key)
+ if closeIdx != -1 && (nextKeyIdx == -1 || closeIdx < nextKeyIdx) {
+ end := afterOpen + closeIdx + len("")
+ result.WriteString(s[:end])
+ s = s[end:]
+ continue
+ }
+ // Unclosed — insert before the next or at end
+ if nextKeyIdx != -1 {
+ insertAt := afterOpen + nextKeyIdx
+ result.WriteString(s[:insertAt])
+ result.WriteString("")
+ s = s[insertAt:]
+ } else {
+ result.WriteString(s)
+ result.WriteString("")
+ break
+ }
+ }
+ return result.String()
+}
+
func parseGLM46ToolCall(raw glm46EventRawToolCall, tools []api.Tool) (api.ToolCall, error) {
// Escape any unescaped entities in text content
// We need to escape text between tags, but not the tags themselves
@@ -353,10 +394,14 @@ func parseGLM46ToolCall(raw glm46EventRawToolCall, tools []api.Tool) (api.ToolCa
// Wrap the content in a root element to make it valid XML
xmlString := "" + escaped + ""
- // Parse XML into struct
+ // Parse XML into struct, retrying once with repaired XML if it fails
var parsed GLMToolCallXML
if err := xml.Unmarshal([]byte(xmlString), &parsed); err != nil {
- return api.ToolCall{}, fmt.Errorf("failed to parse XML: %w", err)
+ parsed = GLMToolCallXML{}
+ repaired := "" + repairUnclosedArgValues(escaped) + ""
+ if err2 := xml.Unmarshal([]byte(repaired), &parsed); err2 != nil {
+ return api.ToolCall{}, fmt.Errorf("failed to parse XML: %w", err)
+ }
}
// Extract and trim function name
diff --git a/model/parsers/glm46_test.go b/model/parsers/glm46_test.go
index 341b93fbe..8cd88d196 100644
--- a/model/parsers/glm46_test.go
+++ b/model/parsers/glm46_test.go
@@ -846,6 +846,47 @@ line3`,
},
},
},
+ {
+ name: "unclosed arg_value at end",
+ tools: []api.Tool{},
+ rawToolCall: `get-weather
+city
+Paris`,
+ wantToolCall: api.ToolCall{
+ Function: api.ToolCallFunction{
+ Name: "get-weather",
+ Arguments: args(`{"city": "Paris"}`),
+ },
+ },
+ },
+ {
+ name: "unclosed arg_value before next arg_key",
+ tools: []api.Tool{},
+ rawToolCall: `get-weather
+city
+Parisunit
+celsius`,
+ wantToolCall: api.ToolCall{
+ Function: api.ToolCallFunction{
+ Name: "get-weather",
+ Arguments: args(`{"city": "Paris", "unit": "celsius"}`),
+ },
+ },
+ },
+ {
+ name: "multiple unclosed arg_values",
+ tools: []api.Tool{},
+ rawToolCall: `get-weather
+city
+Parisunit
+celsius`,
+ wantToolCall: api.ToolCall{
+ Function: api.ToolCallFunction{
+ Name: "get-weather",
+ Arguments: args(`{"city": "Paris", "unit": "celsius"}`),
+ },
+ },
+ },
}
for i, tc := range cases {
@@ -860,3 +901,45 @@ line3`,
})
}
}
+
+func TestRepairUnclosedArgValues(t *testing.T) {
+ cases := []struct {
+ name string
+ input string
+ want string
+ }{
+ {
+ name: "already valid",
+ input: `kv`,
+ want: `kv`,
+ },
+ {
+ name: "unclosed at end",
+ input: `kv`,
+ want: `kv`,
+ },
+ {
+ name: "unclosed before next arg_key",
+ input: `a1b2`,
+ want: `a1b2`,
+ },
+ {
+ name: "no arg_value tags",
+ input: `just plain text`,
+ want: `just plain text`,
+ },
+ {
+ name: "multiple unclosed",
+ input: `a1b2`,
+ want: `a1b2`,
+ },
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ got := repairUnclosedArgValues(tc.input)
+ if got != tc.want {
+ t.Errorf("got %q, want %q", got, tc.want)
+ }
+ })
+ }
+}
diff --git a/x/models/glm4_moe_lite/parser.go b/x/models/glm4_moe_lite/parser.go
index c81ec5a40..de1b2cc17 100644
--- a/x/models/glm4_moe_lite/parser.go
+++ b/x/models/glm4_moe_lite/parser.go
@@ -369,6 +369,45 @@ func escapeContent(s string) string {
return result.String()
}
+// repairUnclosedArgValues inserts missing closing tags.
+// GLM models sometimes omit the closing tag, producing XML like:
+//
+// value
+//
+// instead of:
+//
+// value
+func repairUnclosedArgValues(s string) string {
+ var result strings.Builder
+ for {
+ openIdx := strings.Index(s, "")
+ if openIdx == -1 {
+ result.WriteString(s)
+ break
+ }
+ afterOpen := openIdx + len("")
+ closeIdx := strings.Index(s[afterOpen:], "")
+ nextKeyIdx := strings.Index(s[afterOpen:], "")
+ if closeIdx != -1 && (nextKeyIdx == -1 || closeIdx < nextKeyIdx) {
+ end := afterOpen + closeIdx + len("")
+ result.WriteString(s[:end])
+ s = s[end:]
+ continue
+ }
+ if nextKeyIdx != -1 {
+ insertAt := afterOpen + nextKeyIdx
+ result.WriteString(s[:insertAt])
+ result.WriteString("")
+ s = s[insertAt:]
+ } else {
+ result.WriteString(s)
+ result.WriteString("")
+ break
+ }
+ }
+ return result.String()
+}
+
func parseToolCall(raw eventRawToolCall, tools []api.Tool) (api.ToolCall, error) {
// Escape any unescaped entities in text content
escaped := escapeContent(raw.raw)
@@ -376,10 +415,14 @@ func parseToolCall(raw eventRawToolCall, tools []api.Tool) (api.ToolCall, error)
// Wrap the content in a root element to make it valid XML
xmlString := "" + escaped + ""
- // Parse XML into struct
+ // Parse XML into struct, retrying once with repaired XML if it fails
var parsed ToolCallXML
if err := xml.Unmarshal([]byte(xmlString), &parsed); err != nil {
- return api.ToolCall{}, fmt.Errorf("failed to parse XML: %w", err)
+ parsed = ToolCallXML{}
+ repaired := "" + repairUnclosedArgValues(escaped) + ""
+ if err2 := xml.Unmarshal([]byte(repaired), &parsed); err2 != nil {
+ return api.ToolCall{}, fmt.Errorf("failed to parse XML: %w", err)
+ }
}
// Extract and trim function name