fix(response_codec): suppress deepseek-v4-pro tool markup leaked into content - #55
fix(response_codec): suppress deepseek-v4-pro tool markup leaked into content#55jatmn wants to merge 3 commits into
Conversation
7790f33 to
d8895ad
Compare
d8895ad to
5bbf1f2
Compare
… content DeepSeek-v4-pro emits native tool_calls AND leaks XML-ish tool markup (e.g. <parameter, <tool, <invoke, <function, <function_call, <think) as delta.content. Those fragments were being forwarded verbatim as assistant output text, causing the reported <parameter> spam. This can happen even before the first native tool_calls chunk arrives, so suppress any content fragment that begins with a tool markup tag as soon as it is seen. - Detect tool-markup content and replace it with an empty string before it is accumulated or emitted as output_text.delta. - Preserve native tool_calls and legitimate prose/content. - Add regression test deepseek_v4_pro_tool_markup_in_content_is_suppressed covering markup arriving before tool_calls, legitimate prose, and non-tool XML-like content. - Run rustfmt, which also reformatted one existing assertion in the same test file.
5bbf1f2 to
4105a4c
Compare
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The suppression check using
chars().take_while(...).last().is_some_and(...)is a bit convoluted; consider simplifying this logic (e.g., by directly trimming leading whitespace and checking the first non-whitespace character) to improve readability. - In
looks_like_tool_markup, converting the entire string to lowercase allocates a newString; if this function is called frequently, consider a cheaper case-insensitive prefix check (e.g., manual char comparison or limiting the lowercase conversion to the maximum tag prefix length).
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The suppression check using `chars().take_while(...).last().is_some_and(...)` is a bit convoluted; consider simplifying this logic (e.g., by directly trimming leading whitespace and checking the first non-whitespace character) to improve readability.
- In `looks_like_tool_markup`, converting the entire string to lowercase allocates a new `String`; if this function is called frequently, consider a cheaper case-insensitive prefix check (e.g., manual char comparison or limiting the lowercase conversion to the maximum tag prefix length).
## Individual Comments
### Comment 1
<location path="src/response_codec_tests.rs" line_range="4991-5000" />
<code_context>
+ }]}
+ }]
+ }));
+ accum.apply_chat_chunk(&json!({
+ "choices": [{
+ "delta": {"content": "<tool>"}
+ }]
+ }));
+ accum.apply_chat_chunk(&json!({
+ "choices": [{
+ "delta": {"content": "Working on it."}
+ }]
+ }));
+ accum.apply_chat_chunk(&json!({
+ "choices": [{
+ "delta": {"content": "the `<parameter>` tag is not markup"}
+ }]
+ }));
+ accum.apply_chat_chunk(&json!({
+ "choices": [{
+ "delta": {"content": "<hello>this is not a tool tag</hello>"}
+ }]
+ }));
+ accum.apply_chat_chunk(&json!({
+ "choices": [{
+ "delta": {"content": "<function>fn x() {}</function>"}
+ }]
+ }));
+ accum.apply_chat_chunk(&json!({
+ "choices": [{
+ "delta": {"content": "<think>some reasoning</think>"}
+ }]
+ }));
</code_context>
<issue_to_address>
**suggestion (testing):** Extend the regression to cover all tool tags handled by `looks_like_tool_markup`, especially `<invoke` and `<function_call`.
The production code suppresses `<parameter`, `<tool`, `<invoke`, `<function`, `<function_call`, and `<think>`, but this test only covers a subset (`<parameter`, `<tool`, `<function`, `<think>`). Please add chunks containing `<invoke ...>` and `<function_call ...>` and assert that their content is also excluded from `output_text` to fully exercise `looks_like_tool_markup` behavior.
Suggested implementation:
```rust
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "<tool>"}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "Working on it."}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "the `<parameter>` tag is not markup"}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "<hello>this is not a tool tag</hello>"}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "<function>fn x() {}</function>"}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "<think>some reasoning</think>"}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "<invoke name=\"exec_command\">echo hi</invoke>"}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "<function_call name=\"exec_command\">{\"cmd\":\"ls\"}</function_call>"}
}]
}));
```
You will also need to update the final assertion(s) in `deepseek_v4_pro_tool_markup_in_content_is_suppressed` to ensure the `<invoke ...>` and `<function_call ...>` chunks are excluded from `output_text`. For example, if the test currently asserts only the non-tool-markup content is present (like `"Working on it.the `<parameter>` tag is not markup<hello>this is not a tool tag</hello>"`), extend the expected string so that it still **does not** include any of `<parameter ...>`, `<tool>`, `<function>...`, `<think>...`, `<invoke ...>`, or `<function_call ...>`, while preserving any legitimate assistant text that follows these chunks.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| accum.apply_chat_chunk(&json!({ | ||
| "choices": [{ | ||
| "delta": {"content": "<parameter name=\"cmd\">rg -n spam</parameter>"} | ||
| }] | ||
| })); | ||
| // Then the native function call stream starts. | ||
| accum.apply_chat_chunk(&json!({ | ||
| "choices": [{ | ||
| "delta": {"tool_calls": [{ | ||
| "index": 0, |
There was a problem hiding this comment.
suggestion (testing): Extend the regression to cover all tool tags handled by looks_like_tool_markup, especially <invoke and <function_call.
The production code suppresses <parameter, <tool, <invoke, <function, <function_call, and <think>, but this test only covers a subset (<parameter, <tool, <function, <think>). Please add chunks containing <invoke ...> and <function_call ...> and assert that their content is also excluded from output_text to fully exercise looks_like_tool_markup behavior.
Suggested implementation:
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "<tool>"}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "Working on it."}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "the `<parameter>` tag is not markup"}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "<hello>this is not a tool tag</hello>"}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "<function>fn x() {}</function>"}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "<think>some reasoning</think>"}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "<invoke name=\"exec_command\">echo hi</invoke>"}
}]
}));
accum.apply_chat_chunk(&json!({
"choices": [{
"delta": {"content": "<function_call name=\"exec_command\">{\"cmd\":\"ls\"}</function_call>"}
}]
}));You will also need to update the final assertion(s) in deepseek_v4_pro_tool_markup_in_content_is_suppressed to ensure the <invoke ...> and <function_call ...> chunks are excluded from output_text. For example, if the test currently asserts only the non-tool-markup content is present (like "Working on it.the tag is not markup<hello>this is not a tool tag</hello>"), extend the expected string so that it still does not include any of <parameter ...>, <tool>, <function>..., <think>..., <invoke ...>, or <function_call ...>, while preserving any legitimate assistant text that follows these chunks.
…n test
Address review feedback on the deepseek-v4-pro tool-markup suppression.
- Replace the convoluted chars().take_while(...).last().is_some_and(...)
prefix guard with a direct trim_start().starts_with('<') check.
- Drop the allocating to_ascii_lowercase() copy in looks_like_tool_markup in
favor of a cheap, case-insensitive ASCII prefix helper
(starts_with_ci_ascii) that compares bytes without allocating.
- Extend deepseek_v4_pro_tool_markup_in_content_is_suppressed to also feed
<invoke ...> and <function_call ...> chunks and assert their content is
suppressed from output_text, so every tag handled by looks_like_tool_markup
is exercised.
Validation: cargo test deepseek_v4_pro_tool_markup_in_content_is_suppressed
and the full response_codec::tests module pass; cargo clippy --all-targets is
warning-free; cargo build succeeds.
e167c6d to
b623124
Compare
|
@sourcery-ai review |
Problem
DeepSeek-v4-pro emits native
tool_callsAND leaks XML-ish tool markup (e.g.<parameter,<tool,<invoke,<function,<function_call,<think) asdelta.content. Those fragments were forwarded verbatim as assistant output text, causing the reported<parameter>spam. This can happen even before the first nativetool_callschunk arrives.Fix
output_text.delta.tool_callsand legitimate prose/content.deepseek_v4_pro_tool_markup_in_content_is_suppressedcovering markup arriving before tool_calls, legitimate prose, and non-tool XML-like content.Validation
cargo mutants --in-diff git.diff: 14/14 mutants caught (previously 6 missed).response_codecmodule: 183 passed / 0 failed.webui/upstreamtests unrelated to this change.Final reviewed SHA
4105a4c7b9d420a2b0157ccd2889da06c7792c32Summary by Sourcery
Prevent DeepSeek-v4-pro tool markup from appearing as assistant output while retaining valid content and native tool calls.
Bug Fixes:
Enhancements:
Tests: