mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-20 13:52:28 +03:00
* 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>