From 475393748ed290fa9147975e57df673b00a66946 Mon Sep 17 00:00:00 2001 From: "Thanet S." Date: Wed, 27 May 2026 01:09:40 +0700 Subject: [PATCH] fix(deepseek-web): lazy start session refresh (#2742) Integrated into release/v3.8.4 --- .../deepseek-web-with-auto-refresh.ts | 30 ++++- open-sse/executors/deepseek-web.ts | 2 +- tests/unit/deepseek-web.test.ts | 121 ++++++++++++++++++ 3 files changed, 147 insertions(+), 6 deletions(-) diff --git a/open-sse/executors/deepseek-web-with-auto-refresh.ts b/open-sse/executors/deepseek-web-with-auto-refresh.ts index 5bf92404f9..1f53cf25e5 100644 --- a/open-sse/executors/deepseek-web-with-auto-refresh.ts +++ b/open-sse/executors/deepseek-web-with-auto-refresh.ts @@ -1,5 +1,10 @@ import type { ExecuteInput } from "./base.ts"; -import { DeepSeekWebExecutor, acquireAccessToken, tokenCache } from "./deepseek-web.ts"; +import { + DeepSeekWebExecutor, + acquireAccessToken, + extractUserToken, + tokenCache, +} from "./deepseek-web.ts"; interface AutoRefreshConfig { sessionRefreshInterval?: number; @@ -28,15 +33,12 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor { autoRefresh: true, ...config, }; - if (this.refreshConfig.autoRefresh) { - this.startAutoRefresh(); - } } override async execute(input: ExecuteInput) { this.retryCount = 0; const creds = input.credentials as unknown as Record; - this.currentUserToken = (creds.apiKey as string) || (creds.accessToken as string) || ""; + this.setCurrentUserToken(extractUserToken(creds)); return this.executeWithRetry(input); } @@ -62,6 +64,10 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor { private startAutoRefresh(): void { if (this.refreshTimer) clearInterval(this.refreshTimer); this.refreshTimer = setInterval(async () => { + if (!this.currentUserToken) { + this.sessionValid = false; + return; + } try { await this.doRefreshSession(); } catch (error) { @@ -70,6 +76,20 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor { }, this.refreshConfig.sessionRefreshInterval); } + private setCurrentUserToken(userToken: string | null): void { + if (!userToken) { + return; + } + if (this.currentUserToken === userToken) { + return; + } + this.currentUserToken = userToken; + this.sessionValid = false; + if (this.refreshConfig.autoRefresh) { + this.startAutoRefresh(); + } + } + private async doRefreshSession(): Promise { if (!this.currentUserToken) { this.sessionValid = false; diff --git a/open-sse/executors/deepseek-web.ts b/open-sse/executors/deepseek-web.ts index 3b18b676a3..376bafbab2 100644 --- a/open-sse/executors/deepseek-web.ts +++ b/open-sse/executors/deepseek-web.ts @@ -53,7 +53,7 @@ function evictOldest(cache: Map): void { // ── Helpers ────────────────────────────────────────────────────────────── -function extractUserToken(credentials: Record): string | null { +export function extractUserToken(credentials: Record): string | null { const raw = credentials?.apiKey || credentials?.accessToken; if (typeof raw !== "string" || raw.length === 0) return null; // Handle JSON-wrapped tokens (DeepSeek stores token as {"value":"..."}) diff --git a/tests/unit/deepseek-web.test.ts b/tests/unit/deepseek-web.test.ts index 2671b5f027..bfc7ab0738 100644 --- a/tests/unit/deepseek-web.test.ts +++ b/tests/unit/deepseek-web.test.ts @@ -470,6 +470,127 @@ test("isSessionValid starts false", () => { assert.equal(exec.isSessionValid(), false); }); +test("auto-refresh stays idle until a userToken is provided", async () => { + const errors = []; + const originalError = console.error; + console.error = (...args) => { + errors.push(args); + }; + + const exec = new DeepSeekWebWithAutoRefreshExecutor({ + autoRefresh: true, + sessionRefreshInterval: 5, + }); + try { + await new Promise((resolve) => setTimeout(resolve, 25)); + assert.equal(errors.length, 0); + } finally { + exec.destroy(); + console.error = originalError; + } +}); + +test("execute without DeepSeek credentials does not start auto-refresh", async () => { + const errors = []; + const originalError = console.error; + console.error = (...args) => { + errors.push(args); + }; + + const exec = new DeepSeekWebWithAutoRefreshExecutor({ + autoRefresh: true, + sessionRefreshInterval: 5, + }); + try { + const result = await exec.execute({ + model: "default", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: true, + credentials: {}, + signal: AbortSignal.timeout(5000), + }); + assert.equal(result.response.status, 400); + + await new Promise((resolve) => setTimeout(resolve, 25)); + assert.equal(errors.length, 0); + } finally { + exec.destroy(); + console.error = originalError; + } +}); + +test("execute without DeepSeek credentials preserves an active auto-refresh session", async () => { + const mock = await mockDeepSeekFlow(); + const exec = new DeepSeekWebWithAutoRefreshExecutor({ + autoRefresh: true, + sessionRefreshInterval: 60_000, + }); + try { + const validResult = await exec.execute({ + model: "default", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: true, + credentials: { apiKey: "test-user-token-active-refresh" }, + signal: AbortSignal.timeout(10000), + }); + assert.ok(validResult.response.ok); + + const activeTimer = exec.refreshTimer; + assert.ok(activeTimer); + assert.equal(exec.currentUserToken, "test-user-token-active-refresh"); + + const invalidResult = await exec.execute({ + model: "default", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: true, + credentials: {}, + signal: AbortSignal.timeout(5000), + }); + assert.equal(invalidResult.response.status, 400); + assert.equal(exec.currentUserToken, "test-user-token-active-refresh"); + assert.equal(exec.refreshTimer, activeTimer); + } finally { + exec.destroy(); + mock.restore(); + } +}); + +test("execute with a new DeepSeek userToken restarts auto-refresh", async () => { + const mock = await mockDeepSeekFlow(); + const exec = new DeepSeekWebWithAutoRefreshExecutor({ + autoRefresh: true, + sessionRefreshInterval: 60_000, + }); + try { + const firstResult = await exec.execute({ + model: "default", + body: { messages: [{ role: "user", content: "first" }] }, + stream: true, + credentials: { apiKey: "test-user-token-refresh-1" }, + signal: AbortSignal.timeout(10000), + }); + assert.ok(firstResult.response.ok); + + const firstTimer = exec.refreshTimer; + assert.ok(firstTimer); + + const secondResult = await exec.execute({ + model: "default", + body: { messages: [{ role: "user", content: "second" }] }, + stream: true, + credentials: { apiKey: "test-user-token-refresh-2" }, + signal: AbortSignal.timeout(10000), + }); + assert.ok(secondResult.response.ok); + assert.equal(exec.currentUserToken, "test-user-token-refresh-2"); + assert.ok(exec.refreshTimer); + assert.notEqual(exec.refreshTimer, firstTimer); + } finally { + exec.destroy(); + mock.restore(); + } +}); + // ─── Abort handling ────────────────────────────────────────────────────── test("execute: handles abort signal gracefully", async () => {