Skip to content

fix: add XML tool call parsing fallback for Qwen3-coder via Ollama#6882

Merged
codefromthecrypt merged 1 commit intoblock:mainfrom
kuccello:fix/qwen3-coder-xml-tool-parsing
Feb 7, 2026
Merged

fix: add XML tool call parsing fallback for Qwen3-coder via Ollama#6882
codefromthecrypt merged 1 commit intoblock:mainfrom
kuccello:fix/qwen3-coder-xml-tool-parsing

Conversation

@kuccello
Copy link
Contributor

@kuccello kuccello commented Feb 1, 2026

When using Qwen3-coder model through Ollama with many tools (6+), the model outputs XML-style tool calls in the content field instead of using the native JSON tool_calls format. This causes Goose to fail to execute any tools.

This commit adds XML parsing as a fallback mechanism:

  • Add parse_xml_tool_calls() helper function using regex to parse <function=name><parameter=key>value format
  • Modify response_to_message() to check for XML tool calls when no JSON tool_calls are found in the response
  • Modify response_to_streaming_message() to accumulate text and parse XML tool calls when stream completes
  • Add comprehensive unit tests for XML parsing

The fix is backward-compatible and only activates when JSON tool_calls are absent, ensuring existing providers continue to work normally.

Tested with Qwen3-coder:latest via Ollama with 11 developer tools.

Summary

Type of Change

  • Feature
  • Bug fix
  • Refactor / Code quality
  • Performance improvement
  • Documentation
  • Tests
  • Security fix
  • Build / Release
  • Other (specify below)
    • qwen3-coder behavior (other qwen models too)

AI Assistance

  • This PR was created or reviewed with AI assistance

Testing

  • Unit tests passing: cargo test -p goose --lib -- formats::openai::tests - green
  • Clippy check: cargo fmt -p goose && cargo clippy -p goose --lib -- -D warnings

Related Issues

Relates to #6883
Discussion: LINK (if any)

Screenshots/Demos (for UX changes)

Before:

After:

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e3ee90bcba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@michaelneale michaelneale self-assigned this Feb 2, 2026
@michaelneale
Copy link
Collaborator

michaelneale commented Feb 2, 2026

nice - have pushed this here: #6886 to get to run live tests, but I think this looks good. If that passes can merge this. thanks @kuccello

Copy link
Collaborator

@michaelneale michaelneale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice to have these tests

@kuccello
Copy link
Contributor Author

kuccello commented Feb 2, 2026

I added a commit to address the comments:

  • Added xml_detected flag - Tracks when XML tool call markers (<function=) are detected in the accumulated text
    • Modified streaming logic - Once XML is detected:
    • Text chunks are buffered instead of yielded
  • On final chunk, only the parsed tool calls are emitted (or the accumulated text if parsing fails)

This change is to prevent noisy duplicate response text showing up in the conversation flow.

@michaelneale
Copy link
Collaborator

thanks @kuccello - looks nice - BTW I am working on a extensive model/extension combo to test with open models - and this will solve a recurring issue I see in test suites.

@codefromthecrypt
Copy link
Collaborator

@kuccello so I'm concerned about doing more stuff like this in the openai provider as it puts main openai at risk..

you can see conditional body handling scattered per provider in some cases, such as this in the tetrate one

        // Handle Google-compatible model responses differently
        if is_google_model(payload) {
            return handle_response_google_compat(response).await;
        }

        // For OpenAI-compatible models, parse the response body to JSON
        let response_body = handle_response_openai_compat(response)
            .await
            .map_err(|e| ProviderError::RequestFailed(format!("Failed to parse response: {e}")))?;

Agree normal responses are a lot less tricky than tool responses.

#6832 introduces a different type OpenAiCompatibleProvider to help isolate code that is about clones and not in any way about openai the platform (openai.rs). In its first revision, this does not support body customizations. It only migrated xai and azure to it, not yet ollama.

For now, it would be best to move this code to the ollama provider because it has templating that isn't guaranteed to act the same in other providers. Since it is left alone and not using OpenAiCompatibleProvider, #6832 shouldn't interfere too much with your work.

If you run into a problem in refactoring and would prefer a hand, I can try to reproduce your work in a way that doesn't affect the main OpenAiProvider.

@michaelneale
Copy link
Collaborator

@codefromthecrypt @kuccello we could also move to a branch so it is covered by e2e tests with live providers while at it? really good to get this in

@kuccello
Copy link
Contributor Author

kuccello commented Feb 3, 2026

@codefromthecrypt I will make an adjustment based on your comment

Copy link
Collaborator

@codefromthecrypt codefromthecrypt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick turnaround!

nit try to remove some of the single line comments and also rename stream_ollama_compat -> stream_ollama since the _compat thing is not really relevant here.

@kuccello kuccello force-pushed the fix/qwen3-coder-xml-tool-parsing branch from b8c76de to 86dfeb5 Compare February 3, 2026 02:25
@kuccello
Copy link
Contributor Author

kuccello commented Feb 3, 2026

Here's a summary of the changes for the Qwen3-coder XML tool call fix:

Problem

When using Qwen3-coder through Ollama with many tools (6+), the model outputs XML-style tool calls instead of the expected JSON format:

<function=developer__text_editor><parameter=command>view</parameter><parameter=path>/some/path</parameter></function>

Instead of:

{"tool_calls": [{"function": {"name": "developer__text_editor", "arguments": {"command": "view", "path": "/some/path"}}}]}

Architectural Solution

I implemented an Ollama-specific post-processing wrapper that isolates the XML parsing logic to the Ollama provider only, without modifying the core OpenAI format handling.

Files Changed

  1. crates/goose/src/providers/formats/ollama.rs (new file)

    • Contains parse_xml_tool_calls() - regex-based XML parser
    • Contains response_to_message() - wraps standard OpenAI parsing with XML fallback
    • Contains response_to_streaming_message_ollama() - streaming wrapper that buffers text when XML markers are detected
  2. crates/goose/src/providers/formats/mod.rs

    • Added pub mod ollama; to expose the new module
  3. crates/goose/src/providers/openai_compatible.rs

    • Added stream_ollama() function that uses the Ollama-specific streaming handler
  4. crates/goose/src/providers/ollama.rs

    • Updated imports to use formats::ollama instead of formats::openai
    • Calls stream_ollama() for streaming responses

Key Design Decisions

  1. Isolation: The XML parsing is completely isolated to Ollama. The core formats/openai.rs remains unchanged, so other OpenAI-compatible providers (Azure, XAI, etc.) are unaffected.

  2. Post-Processing Wrapper Pattern: Rather than exposing internal streaming types from openai.rs, the Ollama streaming handler wraps the standard OpenAI stream and post-processes Message objects. This avoids tight coupling.

  3. Streaming Buffering: When XML markers (<function=) are detected during streaming, text chunks are buffered instead of being emitted immediately. At stream end, the buffered content is parsed for XML tool calls. This prevents duplicate output (raw XML fragments followed by parsed tool calls).

  4. Re-exports: formats/ollama.rs re-exports the standard OpenAI utilities (create_request, format_messages, format_tools, get_usage, validate_tool_schemas) so the Ollama provider can use them without importing from two places.

Data Flow

Ollama API Response
        │
        ▼
┌─────────────────────────────────┐
│  openai::response_to_message()  │  ← Standard JSON parsing
└─────────────────────────────────┘
        │
        ▼
   Has tool_calls?
        │
   ┌────┴────┐
   │ Yes     │ No
   ▼         ▼
 Return   Contains "<function="?
           │
      ┌────┴────┐
      │ Yes     │ No
      ▼         ▼
  Parse XML   Return original
  tool calls  message
      │
      ▼
  Return message
  with tool requests

Why This Approach?

  • Minimal risk: Core OpenAI handling untouched
  • No public API changes: Internal types in openai.rs remain private
  • Follows existing patterns: Similar to how handle_response_google_compat is used for Google-specific handling in other providers
  • Extensible: If other Ollama models exhibit similar behavior, they'll automatically benefit

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an abstraction break, as we are putting provider-specific logic into the openai_compatible.rs

can you please contain changes related to ollama to files named ollama?

@kuccello kuccello force-pushed the fix/qwen3-coder-xml-tool-parsing branch from 252948e to fe2aca1 Compare February 7, 2026 15:10
@kuccello kuccello requested a review from a team as a code owner February 7, 2026 15:10
@kuccello
Copy link
Contributor Author

kuccello commented Feb 7, 2026

All comments addressed

When using Qwen3-coder model through Ollama with many tools (6+), the
model outputs XML-style tool calls in the content field instead of
using the native JSON tool_calls format. This causes Goose to fail
to execute any tools.

This commit adds XML parsing as a fallback mechanism:

- Create formats/ollama.rs with XML tool call parsing logic
- Add parse_xml_tool_calls() to parse <function=name><parameter=key>value</parameter></function> format
- Wrap standard OpenAI response parsing with XML fallback for Ollama
- Add response_to_streaming_message_ollama() that buffers text when XML markers detected
- Update ollama.rs to use Ollama-specific format module and streaming
- Add comprehensive unit tests for XML parsing

The fix is backward-compatible and only activates when JSON tool_calls
are absent, ensuring existing providers continue to work normally.
The XML parsing is isolated to the Ollama provider only.

Tested with Qwen3-coder:latest via Ollama with 11 developer tools.

Signed-off-by: kuccello <kuccello@users.noreply.github.com>
@kuccello kuccello force-pushed the fix/qwen3-coder-xml-tool-parsing branch from 84311d0 to 80a7719 Compare February 7, 2026 15:27
@codefromthecrypt codefromthecrypt added this pull request to the merge queue Feb 7, 2026
@codefromthecrypt
Copy link
Collaborator

@kuccello thanks for your patience. very helpful!

Merged via the queue into block:main with commit 584f710 Feb 7, 2026
18 checks passed
tlongwell-block added a commit that referenced this pull request Feb 9, 2026
* origin/main: (55 commits)
  test(mcp): add image tool test and consolidate MCP test fixtures (#7019)
  fix: remove Option from model listing return types, propagate errors (#7074)
  fix: lazy provider creation for goose acp (#7026) (#7066)
  Smoke tests: split compaction test and use debug build (#6984)
  fix(deps): trim bat to resolve RUSTSEC-2024-0320 (#7061)
  feat: expose AGENT_SESSION_ID env var to extension child processes (#7072)
  fix: add XML tool call parsing fallback for Qwen3-coder via Ollama (#6882)
  Remove clippy too_many_lines lint and decompose long functions (#7064)
  refactor: move disable_session_naming into AgentConfig (#7062)
  Add global config switch to disable automatic session naming (#7052)
  docs: add blog post - 8 Things You Didn't Know About Code Mode (#7059)
  fix: ensure animated elements are visible when prefers-reduced-motion is enabled (#7047)
  Show recommended model on failture (#7040)
  feat(ui): add session content search via API (#7050)
  docs: fix img url (#7053)
  Desktop UI for deleting custom providers (#7042)
  Add blog post: How I Used RPI to Build an OpenClaw Alternative (#7051)
  Remove build-dependencies section from Cargo.toml (#6946)
  add /rp-why skill blog post (#6997)
  fix: fix snake_case function names in code_execution instructions (#7035)
  ...

# Conflicts:
#	scripts/test_subrecipes.sh
lifeizhou-ap added a commit that referenced this pull request Feb 9, 2026
* main: (101 commits)
  fix: lazy provider creation for goose acp (#7026) (#7066)
  Smoke tests: split compaction test and use debug build (#6984)
  fix(deps): trim bat to resolve RUSTSEC-2024-0320 (#7061)
  feat: expose AGENT_SESSION_ID env var to extension child processes (#7072)
  fix: add XML tool call parsing fallback for Qwen3-coder via Ollama (#6882)
  Remove clippy too_many_lines lint and decompose long functions (#7064)
  refactor: move disable_session_naming into AgentConfig (#7062)
  Add global config switch to disable automatic session naming (#7052)
  docs: add blog post - 8 Things You Didn't Know About Code Mode (#7059)
  fix: ensure animated elements are visible when prefers-reduced-motion is enabled (#7047)
  Show recommended model on failture (#7040)
  feat(ui): add session content search via API (#7050)
  docs: fix img url (#7053)
  Desktop UI for deleting custom providers (#7042)
  Add blog post: How I Used RPI to Build an OpenClaw Alternative (#7051)
  Remove build-dependencies section from Cargo.toml (#6946)
  add /rp-why skill blog post (#6997)
  fix: fix snake_case function names in code_execution instructions (#7035)
  Document max_turns settings for recipes and subagents (#7044)
  feat: update Groq declarative data with Preview Models (#7023)
  ...
jh-block added a commit that referenced this pull request Feb 9, 2026
* origin/main: (54 commits)
  chore: strip posthog for sessions/models/daily only (#7079)
  tidy: clean up old benchmark and add gym (#7081)
  fix: use command.process_group(0) for CLI providers, not just MCP (#7083)
  added build notify (#6891)
  test(mcp): add image tool test and consolidate MCP test fixtures (#7019)
  fix: remove Option from model listing return types, propagate errors (#7074)
  fix: lazy provider creation for goose acp (#7026) (#7066)
  Smoke tests: split compaction test and use debug build (#6984)
  fix(deps): trim bat to resolve RUSTSEC-2024-0320 (#7061)
  feat: expose AGENT_SESSION_ID env var to extension child processes (#7072)
  fix: add XML tool call parsing fallback for Qwen3-coder via Ollama (#6882)
  Remove clippy too_many_lines lint and decompose long functions (#7064)
  refactor: move disable_session_naming into AgentConfig (#7062)
  Add global config switch to disable automatic session naming (#7052)
  docs: add blog post - 8 Things You Didn't Know About Code Mode (#7059)
  fix: ensure animated elements are visible when prefers-reduced-motion is enabled (#7047)
  Show recommended model on failture (#7040)
  feat(ui): add session content search via API (#7050)
  docs: fix img url (#7053)
  Desktop UI for deleting custom providers (#7042)
  ...
@timohuovinen
Copy link

🚀 !!! Thank you all

Tyler-Hardin pushed a commit to Tyler-Hardin/goose that referenced this pull request Feb 11, 2026
…lock#6882)

Signed-off-by: kuccello <kuccello@users.noreply.github.com>
Co-authored-by: kuccello <kuccello@users.noreply.github.com>
Tyler-Hardin pushed a commit to Tyler-Hardin/goose that referenced this pull request Feb 11, 2026
…lock#6882)

Signed-off-by: kuccello <kuccello@users.noreply.github.com>
Co-authored-by: kuccello <kuccello@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Comments