Issues / #2037

#2037 serve: tool_call_recovery misses fenced JSON inside an explicit tool-call wrapper

open · @bettercallcaleb · 0 comments · View on GitHub

Server & API

Description

## Summary

With `tool_call_recovery` enabled, Strata recovers JSON for a declared tool inside an explicit `<tool_call>` wrapper, but fails to recognize the same JSON if the model wraps it in a Markdown ```json fence. The response is emitted as regular content, with no `tool_call`.

This is a code-fence handling issue, **not** a JSON-string escaping issue: literal backticks inside a JSON string work in the unfenced case.

## Reproduction (parser-only; no model required)

````python
import json
from serve.frontend import OutputParser

tools = [
    {
        "name": "write_file",
        "parameters": {
            "type": "object",
            "properties": {"content": {"type": "string"}},
        },
    }
]
payload = json.dumps({
    "name": "write_file",
    "arguments": {"content": "hello `world`"},
})
cases = {
    "bare": f"<tool_call>\n{payload}\n</tool_call>",
    "fenced": f"<tool_call>\n```json\n{payload}\n```\n</tool_call>",
}

for name, raw in cases.items():
    parser = OutputParser(thinking=False, tools=tools, recover=True)
    events = parser.feed(raw) + parser.finish()
    calls = [event.call.arguments for event in events if event.kind == "tool_call"]
    print(name, calls)
````

## Actual

```text
bare [{'content': 'hello `world`'}]
fenced []
```

The fenced case instead emits the wrapper and fenced JSON as `content`, without an error. Reproduced against the current `serve/frontend.py` on `main` with whole-input, 1-character, and 7-character chunks, both `stream_tools=False` and `True`.

## Expected

When recovery is **explicitly enabled**, an otherwise valid JSON call for a declared tool inside an explicit `<tool_call>` wrapper should either be recovered despite an optional Markdown JSON fence, or produce a clear diagnostic. It should not silently become a normal assistant reply.

Fenced JSON in ordinary prose/reasoning must **remain text**, not execute as a tool call. Any recovery should be narrowly scoped to the explicit wrapper and a declared tool.

## Where

- `serve/frontend.py`: `OutputParser._follower()` recognizes JSON only when the text following `<tool_call>` begins with `{`.
- Related to the opt-in recovery behavior in #1172; the quote/fence safeguards from #1058 should remain intact.

Related on strata.com

Editorial links to help you install, pick models, or read release notes — not part of the upstream thread.