Files
OmniRoute/tests
VictorRP7 8288a4a012 fix(sse): deepseek-web resilience — premature session close + malformed tool-call recovery (#13226)
* fix(sse): deepseek-web collectSSEContent no longer returns a silent partial stub on premature session close

collectSSEContent() (used for the deepseek-web tool-calling / non-stream path)
drained the upstream SSE body and returned whatever content it had once the
reader reported done, with no check that DeepSeek had actually signalled
completion via response/status: "FINISHED".

When the upstream cookie session drops mid-generation (expired session,
anti-bot challenge, network interruption), the HTTP body simply closes
early. That was indistinguishable from a real completion: execute()
returned HTTP 200 with finish_reason "stop" and whatever partial stub text
had arrived so far. Observed in production call logs: a lone "I'll check
that..." / "Vou verificar..." with no continuation, reported as a
successful completion.

collectSSEContent now tracks whether the FINISHED status event was seen. If
the stream ends without it, it throws instead of returning the stub -
execute()'s existing try/catch turns that into a proper 502 that the
client, or a combo's retry/fallback logic, can react to.

Added tests/unit/deepseek-web-premature-close.test.ts covering both the
premature-close error path and the normal FINISHED completion path. Full
deepseek-web unit suite (97 tests) still passes.

* fix(sse): recover malformed deepseek-web tool-call replies and retry when unrecoverable

Two related failure modes on the deepseek-web tool-calling path, both
observed in production call logs from real agentic (VS Code Copilot-style)
usage of the deepseek combo:

1. DeepSeek's web session occasionally leaks malformed/internal formatting
   tokens right after an otherwise-complete <tool>{json} body, instead of
   a clean </tool> close (observed: a fully valid create_file JSON call
   immediately followed by corrupted pseudo-tags). parseLooseJsonObject's
   strict JSON.parse rejected the whole block over that trailing garbage,
   even though a perfectly valid object sat at the start - so the call was
   silently dropped and the raw tagged text was shown to the user instead
   of the file being created.

   deepseekWebTools.ts: added salvageLeadingJsonObject(), a quote/escape
   aware balanced-brace scanner that recovers just the leading JSON object
   when the strict parse fails, reusing the same salvage idea already used
   elsewhere in this file (findBareJsonCandidates) for bare-JSON detection.

2. When even that salvage cannot recover a call (genuinely truncated JSON,
   garbled beyond repair), execute() previously gave up on the first try.
   Since this is a scraped, non-deterministic web session rather than a
   real API, simply asking again is usually enough to get a clean reply.

   deepseek-web.ts: the hasTools branch now detects an unparsed <tool...>
   tag surviving in the cleaned content and retries with a brand-new
   session, bounded to MAX_TOOL_PARSE_ATTEMPTS (2) - never an unbounded
   retry loop, and a reply that parses cleanly on the first try costs no
   extra latency.

Builds on the collectSSEContent premature-close fix from the same PR -
that one covers the upstream session dropping mid-stream; this one covers
the session completing but returning malformed tool-call content.

Testing:
- tests/unit/deepseek-web-tools-salvage-leading-json.test.ts (4 tests):
  recovery from the exact production-observed corruption pattern, escaped
  quotes/nested braces before the garbage, correct non-promotion of
  genuinely truncated JSON, and no regression on well-formed blocks.
- tests/unit/deepseek-web-tool-call-retry.test.ts (3 tests): retry
  succeeds on a fresh session, retry is bounded (gives up after
  MAX_TOOL_PARSE_ATTEMPTS and surfaces the raw content rather than
  looping forever), and a clean first reply never triggers a retry.
- Full deepseek-web unit suite: 104/104 passing, no regressions.
- The salvage fix was additionally verified directly against the exact
  malformed content captured from a live production call log (not just
  the hand-written test fixture).

---------

Co-authored-by: VictorRP7 <187780317+VictorRP7@users.noreply.github.com>
2026-09-19 00:03:34 -03:00
..