Files
OmniRoute/tests/unit/deepseek-web.test.ts
Diego Rodrigues de Sa e Souza 50896699d3 fix+feat(sse): per-model 403 lockout (#3027) + deepseek-web memory (#2942) & tool-calls (#2820) (#3101)
* fix(sse): per-model 403 on passthrough providers locks the model, not the connection (#3027)

A per-model subscription 403 from a passthrough / per-model-quota provider
(e.g. ollama-cloud "this model requires a subscription, upgrade for access" on
deepseek-v4-pro) cooled down the ENTIRE connection instead of locking out only
the paid model, knocking out the free models on the same key and escalating an
exponential connection-wide backoff on repeats.

markAccountUnavailable's per-model lockout gate only covered 404/429/>=500, and
the only 403 -> model-lockout special case was hard-coded to grok-web. Generalize
it: a model-scoped 403 on an isPerModelQuotaProvider becomes a model lockout
(connection stays active). Terminal whole-key 403s (permanent/banned, account
deactivated, credits exhausted, project-route) keep their connection-level path.

Test-first: reproduction (paid model locked, connection active, free model still
eligible), regression guard (deactivated key still terminal -> banned, not
downgraded), and backoff guard (repeated 403s do not escalate connection backoff).

* feat(sse): persistent session + rolling-window memory for deepseek-web (#2942)

The DeepSeek web API takes only a single `prompt` string (no messages array) and
the executor created a fresh chat session per request, deleting it afterward — so
agentic multi-turn clients got per-turn amnesia.

Add two opt-in, per-connection settings (providerSpecificData), both defaulting to
the legacy behavior so plain-chat users are unaffected:

- historyWindow (number, default 0): when > 0, messagesToPrompt stitches the last N
  non-system turns into a role-tagged transcript ("User:"/"Assistant:") so context
  carries across turns within the single prompt string.
- persistSession (bool, default false): reuse one upstream chat session per userToken
  (cached in the existing sessionCache) instead of creating/deleting one per request.
  A reused session that fails is treated as stale (deleted in the DeepSeek UI): the
  cache entry is dropped, a fresh session is created, and the completion is retried
  once. Error paths invalidate the cached session so the next turn self-heals.

Test-first: pure prompt-builder window semantics (legacy/window/cap/empty) and
execute()-level session behavior via mocked fetch (fresh-per-request default, reuse,
stale-session fresh retry, history threaded into the prompt). Full existing
deepseek-web suite (35 tests) still green.

Note: chat.deepseek.com is an unofficial reverse-engineered surface; the live
round-trip can't be exercised in CI (no userToken/session). Treated as best-effort
per the issue; the prompt/session logic is unit-tested in isolation.

* feat(sse): tool-call translation for deepseek-web (#2820)

deepseek-web previously hard-failed any request carrying tools[] with a 400 (#2848),
so agentic clients could not use it for function calling at all. Add a bidirectional
translation layer (new open-sse/translator/webTools.ts, reusable by the other
web-cookie executors):

- Request: serializeToolsToPrompt() turns the OpenAI tools[] into a <tool>{...}</tool>
  prompt contract, injected as a leading system message.
- Response: parseToolCallsFromText() extracts the model's <tool> blocks into OpenAI
  tool_calls (arguments as a JSON string), strips them from content, and the executor
  emits finish_reason "tool_calls" — for both non-stream and stream clients (the reply
  is buffered since a tool block must be parsed whole).

A plain reply (no <tool> block) still streams normally with finish_reason "stop".
The superseded #2848 400-contract test is updated to assert the new translation.

Test-first: pure serializer/parser units + execute() round-trip via mocked fetch
(no-400, prompt serialization, non-stream tool_calls, stream tool_calls, plain reply).

Note: chat.deepseek.com is an unofficial reverse-engineered surface and the exact
<tool> emission depends on the model following the injected contract; the live
round-trip can't be exercised in CI. Best-effort per the issue; translation logic is
unit-tested in isolation.

* fix(sse): drop duplicate per-model 403 block — already in release via #3096

The release branch already scopes per-model 403 to model lockout (PR #3096,
commit 7042d562c) with the canonical !terminalStatus guard + exactCooldownMs
upstream hint. This PR's separate isTerminalOrRoute403 block shadowed it and
omitted the retry hint. Keep only the deepseek-web (#2942) + webTools (#2820)
changes here; the #3027 regression test is retained as coverage for #3096.
2026-06-03 18:29:26 -03:00

38 KiB