Skip to content

Conversation

@mudler
Copy link
Owner

@mudler mudler commented Jan 4, 2026

Extend the function parsing system in LocalAI to support tools streaming, more robust parsing, XML-style tool calls, similar to how JSON tool calls are currently parsed. This will allow models that return XML format (like <tool_call><function=name><parameter=key>value</tool_call>) to be properly parsed alongside text content. The implementation mimics the great work on llama.cpp and extends it across all backends. This also makes it easier to maintain long-term.

@netlify
Copy link

netlify bot commented Jan 4, 2026

Deploy Preview for localai ready!

Name Link
🔨 Latest commit 0e4cdaf
🔍 Latest deploy log https://app.netlify.com/projects/localai/deploys/695be4fa954fbc00089ba655
😎 Deploy Preview https://deploy-preview-7865--localai.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Extend the function parsing system in LocalAI to support XML-style tool calls, similar to how JSON tool calls are currently parsed. This will allow models that return XML format (like <tool_call><function=name><parameter=key>value</parameter></function></tool_call>) to be properly parsed alongside text content.

Signed-off-by: Ettore Di Giacinto <[email protected]>
@mudler mudler force-pushed the feat/xml-tool-call branch from e800bbe to e4ea2f1 Compare January 5, 2026 00:02
mudler added 2 commits January 5, 2026 00:26
Signed-off-by: Ettore Di Giacinto <[email protected]>
@github-actions github-actions bot added the kind/documentation Improvements or additions to documentation label Jan 5, 2026
returnError := func(err error, canRecover bool) (bool, error) {
xlog.Debug("Failed to parse XML tool call", "error", err, "position", p.pos)
if canRecover && recovery {
p.MoveTo(startPos)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
valEndSize := len(format.ValEnd)

if format.LastValEnd != nil {
p.MoveTo(savedPos)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
p.MoveTo(savedPos)
tc2 := p.tryFind2LiteralSplitBySpaces(*format.LastValEnd, format.ToolEnd)
if format.LastToolEnd != nil {
p.MoveTo(savedPos)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
if tc.Groups[0].Begin+len(*format.LastValEnd) < len(p.input) {
tc.Groups[0].End = tc.Groups[0].Begin + len(*format.LastValEnd)
}
p.MoveTo(tc.Groups[0].End)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
// Try to find first literal
tc1 := p.TryFindLiteral(literal1)
if tc1 == nil {
p.MoveTo(savedPos)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
// Try to find second literal
tc2 := p.TryFindLiteral(literal2)
if tc2 == nil {
p.MoveTo(savedPos)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
for _, fmtPreset := range formats {
if fmtPreset.format != nil {
// Try parsing with this format
parser.MoveTo(0)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
Signed-off-by: Ettore Di Giacinto <[email protected]>
@mudler mudler force-pushed the feat/xml-tool-call branch from fdaf6a0 to 335a23d Compare January 5, 2026 00:54
p.MoveTo(tc.Groups[0].End)
toolEndSize = len(*format.LastToolEnd)
} else {
p.MoveTo(savedPos)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
}
if !AllSpace(tc.Prelude) {
// Non-whitespace before scope_start, stop parsing
p.MoveTo(tc.Groups[0].Begin - len(tc.Prelude))

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
testInput := p.input[testPos:]
if strings.HasPrefix(testInput, format.ScopeStart) {
// It's another scope_start, break to continue outer loop
p.MoveTo(testPos)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
Signed-off-by: Ettore Di Giacinto <[email protected]>

if !AllSpace(tc.Prelude) {
// Non-whitespace before tool_start, stop parsing
p.MoveTo(tc.Groups[0].Begin - len(tc.Prelude))

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
(format.LastToolEnd != nil && strings.Contains(functionNamePrelude, *format.LastToolEnd)) {
// Empty tool call - function name is empty, tool_end is in the prelude
// Move back to start of tool_start and find tool_end
p.MoveTo(tc.Groups[0].Begin)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
if AllSpace(format.ToolSep) {
// GLM 4.5 format: function name is on a separate line after tool_start, before key_start
// The prelude contains the function name
p.MoveTo(funcName.Groups[0].Begin)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
p.MoveTo(funcName.Groups[0].Begin)
} else {
// Standard format: function name is before tool_sep
p.MoveTo(funcName.Groups[0].End)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled

if !AllSpace(keyStart.Prelude) {
// Non-whitespace before key_start, stop parsing parameters
p.MoveTo(keyStart.Groups[0].Begin - len(keyStart.Prelude))

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
jsonParsed = true
} else {
// Reset position if JSON parsing failed
p.MoveTo(valStart)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
}

// Rewind to json_end and check if val_end follows
p.MoveTo(jsonEnd)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
}
} else {
// val_end doesn't follow - rewind and parse as text
p.MoveTo(valStart)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
}

// Parse tool calls
p.MoveTo(tc.Groups[0].Begin)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
endOfTool := p.pos
p.ConsumeSpaces()
if p.pos != len(p.input) {
p.MoveTo(endOfTool)

Check warning

Code scanning / gosec

Errors unhandled Warning

Errors unhandled
mudler added 2 commits January 5, 2026 08:10
Signed-off-by: Ettore Di Giacinto <[email protected]>
Signed-off-by: Ettore Di Giacinto <[email protected]>
@mudler mudler changed the title feat(function): Add XML Tool Call Parsing Support feat(function): Add tool streaming, XML Tool Call Parsing Support Jan 5, 2026
mudler added 2 commits January 5, 2026 13:35
Signed-off-by: Ettore Di Giacinto <[email protected]>
Signed-off-by: Ettore Di Giacinto <[email protected]>
// generateHealingMarker generates a unique marker that doesn't appear in the input
func generateHealingMarker(input string) string {
for {
id := fmt.Sprintf("%d", rand.Int63())

Check failure

Code scanning / gosec

Use of weak random number generator (math/rand or math/rand/v2 instead of crypto/rand) Error

Use of weak random number generator (math/rand or math/rand/v2 instead of crypto/rand)
@mudler mudler added enhancement New feature or request and removed kind/documentation Improvements or additions to documentation labels Jan 5, 2026
@github-actions github-actions bot added the kind/documentation Improvements or additions to documentation label Jan 5, 2026
@mudler mudler merged commit 21c84f4 into master Jan 5, 2026
31 of 32 checks passed
@mudler mudler deleted the feat/xml-tool-call branch January 5, 2026 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request kind/documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants