From bf5d12e3dbe182a98ae43b7a8a11c8c6818838e8 Mon Sep 17 00:00:00 2001 From: "Mr. Meowgi" <63689864+ovehbe@users.noreply.github.com> Date: Thu, 21 May 2026 03:23:50 +0300 Subject: [PATCH] fix(deepseek-web): rewrite auth to userToken Bearer + WASM PoW solver (#2452) Integrated into release/v3.8.1 --- next.config.mjs | 2 + open-sse/config/providerRegistry.ts | 34 ++ .../deepseek-web-with-auto-refresh.ts | 54 +- open-sse/executors/deepseek-web.ts | 366 ++++++++---- open-sse/lib/deepseek-pow.ts | 159 +++++- open-sse/lib/sha3_wasm_bg.wasm | Bin 0 -> 26612 bytes .../dashboard/providers/[id]/page.tsx | 73 +-- src/app/api/v1/agents/tasks/[id]/route.ts | 11 +- src/lib/providers/validation.ts | 53 ++ src/shared/constants/providers.ts | 3 +- tests/unit/deepseek-web.test.ts | 538 ++++++------------ 11 files changed, 734 insertions(+), 559 deletions(-) create mode 100644 open-sse/lib/sha3_wasm_bg.wasm diff --git a/next.config.mjs b/next.config.mjs index b0c1cdb32c..2877257d8f 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -96,6 +96,8 @@ const nextConfig = { "./src/mitm/server.cjs", "./open-sse/services/compression/engines/rtk/filters/**/*.json", "./open-sse/services/compression/rules/**/*.json", + "./open-sse/lib/sha3_wasm_bg.wasm", + "./open-sse/lib/deepseek-pow-solver.cjs", ], }, outputFileTracingExcludes: { diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index c878d946bb..6a2e82a660 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -2175,6 +2175,40 @@ export const REGISTRY: Record = { ], }, + "deepseek-web": { + id: "deepseek-web", + alias: "ds-web", + format: "openai", + executor: "deepseek-web", + baseUrl: "https://chat.deepseek.com/api/v0/chat/completion", + authType: "apikey", + authHeader: "bearer", + models: [ + { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro" }, + { id: "deepseek-v4-pro-think", name: "DeepSeek V4 Pro Think", supportsReasoning: true }, + { id: "deepseek-v4-pro-search", name: "DeepSeek V4 Pro Search" }, + { + id: "deepseek-v4-pro-think-search", + name: "DeepSeek V4 Pro Think+Search", + supportsReasoning: true, + }, + { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" }, + { id: "deepseek-v4-flash-think", name: "DeepSeek V4 Flash Think", supportsReasoning: true }, + { id: "deepseek-v4-flash-search", name: "DeepSeek V4 Flash Search" }, + { + id: "deepseek-v4-flash-think-search", + name: "DeepSeek V4 Flash Think+Search", + supportsReasoning: true, + }, + { id: "deepseek-chat", name: "DeepSeek Chat" }, + { id: "deepseek-reasoner", name: "DeepSeek Reasoner", supportsReasoning: true }, + { id: "DeepSeek-R1", name: "DeepSeek R1", supportsReasoning: true }, + { id: "DeepSeek-R1-Search", name: "DeepSeek R1 Search", supportsReasoning: true }, + { id: "DeepSeek-V3.2", name: "DeepSeek V3.2" }, + { id: "DeepSeek-Search", name: "DeepSeek Search" }, + ], + }, + "grok-web": { id: "grok-web", alias: "gw", diff --git a/open-sse/executors/deepseek-web-with-auto-refresh.ts b/open-sse/executors/deepseek-web-with-auto-refresh.ts index ce890eb493..5bf92404f9 100644 --- a/open-sse/executors/deepseek-web-with-auto-refresh.ts +++ b/open-sse/executors/deepseek-web-with-auto-refresh.ts @@ -1,5 +1,5 @@ import type { ExecuteInput } from "./base.ts"; -import { DeepSeekWebExecutor, DEEPSEEK_WEB_BASE } from "./deepseek-web.ts"; +import { DeepSeekWebExecutor, acquireAccessToken, tokenCache } from "./deepseek-web.ts"; interface AutoRefreshConfig { sessionRefreshInterval?: number; @@ -18,12 +18,12 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor { private sessionValid = false; private retryCount = 0; private readonly maxRetries = 2; - private currentCookies = ""; + private currentUserToken = ""; constructor(config: AutoRefreshConfig = {}) { super(); this.refreshConfig = { - sessionRefreshInterval: 20 * 60 * 60 * 1000, + sessionRefreshInterval: 50 * 60 * 1000, maxRefreshRetries: 3, autoRefresh: true, ...config, @@ -35,8 +35,8 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor { override async execute(input: ExecuteInput) { this.retryCount = 0; - this.currentCookies = - ((input.credentials as unknown as Record).cookies as string) || ""; + const creds = input.credentials as unknown as Record; + this.currentUserToken = (creds.apiKey as string) || (creds.accessToken as string) || ""; return this.executeWithRetry(input); } @@ -71,34 +71,26 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor { } private async doRefreshSession(): Promise { - if (!this.currentCookies) { + if (!this.currentUserToken) { this.sessionValid = false; - throw new Error("No cookies available for session refresh"); + throw new Error("No userToken available for session refresh"); } const { maxRefreshRetries } = this.refreshConfig; for (let attempt = 0; attempt < maxRefreshRetries; attempt++) { try { - // Validate session by fetching current user (lightweight, no PoW needed) - const response = await fetch(`${DEEPSEEK_WEB_BASE}/api/v0/users/current`, { - headers: { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", - Cookie: this.currentCookies, - }, - }); - if (response.ok) { - const json = await response.json(); - if (json?.data?.biz_data?.token) { - this.lastRefreshTime = Date.now(); - this.sessionValid = true; - return; - } + tokenCache.delete(this.currentUserToken); + const accessToken = await acquireAccessToken(this.currentUserToken); + if (accessToken) { + this.lastRefreshTime = Date.now(); + this.sessionValid = true; + return; } - if (response.status === 401 || response.status === 403) { - this.sessionValid = false; - throw new Error("Session expired - requires re-authentication"); - } - await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1000)); } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + if (msg.includes("invalid") || msg.includes("expired")) { + this.sessionValid = false; + throw new Error("Token expired — get a new userToken from DeepSeek localStorage"); + } if (attempt >= maxRefreshRetries - 1) throw error; await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1000)); } @@ -106,18 +98,22 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor { throw new Error("Failed to refresh session after max retries"); } + private executeBase(input: ExecuteInput) { + return super.execute(input); + } + private async executeWithRetry(input: ExecuteInput) { try { - return await super.execute(input); + return await this.executeBase(input); } catch (error: unknown) { const msg = error instanceof Error ? error.message : String(error); const isUnauthorized = - msg.includes("401") || msg.includes("Unauthorized") || msg.includes("Session expired"); + msg.includes("401") || msg.includes("Unauthorized") || msg.includes("expired"); if (isUnauthorized && this.retryCount < this.maxRetries) { this.retryCount++; try { await this.doRefreshSession(); - return await super.execute(input); + return await this.executeBase(input); } catch (refreshError) { console.error( `[DeepSeek-WEB] Session refresh failed (attempt ${this.retryCount}/${this.maxRetries}):`, diff --git a/open-sse/executors/deepseek-web.ts b/open-sse/executors/deepseek-web.ts index 498aec34f9..6b88576709 100644 --- a/open-sse/executors/deepseek-web.ts +++ b/open-sse/executors/deepseek-web.ts @@ -1,26 +1,26 @@ import { BaseExecutor, type ExecuteInput } from "./base.ts"; -import { solveDeepSeekPow } from "../lib/deepseek-pow.ts"; +import { solveDeepSeekPowAsync } from "../lib/deepseek-pow.ts"; export const DEEPSEEK_WEB_BASE = "https://chat.deepseek.com"; -const COMPLETION_URL = `${DEEPSEEK_WEB_BASE}/api/v0/chat/completion`; -const USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; +const DEEPSEEK_API_BASE = `${DEEPSEEK_WEB_BASE}/api`; +const COMPLETION_URL = `${DEEPSEEK_API_BASE}/v0/chat/completion`; -// DeepSeek native API headers -const BASE_HEADERS: Record = { - "User-Agent": USER_AGENT, - "x-app-version": "2.0.0", - "x-client-platform": "web", - "x-client-version": "2.0.0", - "x-client-locale": "en_US", +const FAKE_HEADERS: Record = { + Accept: "*/*", + "Accept-Encoding": "gzip, deflate, br, zstd", + "Accept-Language": "en-US,en;q=0.9", + Origin: DEEPSEEK_WEB_BASE, + Referer: `${DEEPSEEK_WEB_BASE}/`, + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", + "X-App-Version": "20241129.1", + "X-Client-Locale": "en-US", + "X-Client-Platform": "web", + "X-Client-Version": "1.8.0", }; // ── Types ──────────────────────────────────────────────────────────────── -export interface DeepSeekCredentials { - cookies: string; -} - interface PowChallenge { algorithm: string; challenge: string; @@ -32,14 +32,39 @@ interface PowChallenge { target_path: string; } +interface TokenInfo { + accessToken: string; + expiresAt: number; +} + +// ── Token cache (keyed by userToken → short-lived access token) ───────── + +const tokenCache = new Map(); +const sessionCache = new Map(); + +const SESSION_CACHE_TTL_MS = 5 * 60 * 1000; +const CACHE_MAX_SIZE = 100; + +function evictOldest(cache: Map): void { + if (cache.size >= CACHE_MAX_SIZE) { + const first = cache.keys().next().value; + if (first) cache.delete(first); + } +} + // ── Helpers ────────────────────────────────────────────────────────────── -function validateCredentials(creds: unknown): creds is DeepSeekCredentials { - const raw = - typeof creds === "object" && creds !== null - ? (creds as Record).cookies - : undefined; - return typeof raw === "string" && raw.includes("ds_session_id="); +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":"..."}) + try { + const parsed = JSON.parse(raw); + if (typeof parsed?.value === "string") return parsed.value; + } catch { + // not JSON, use raw + } + return raw; } function errorResponse(status: number, message: string, dsCode?: number): Response { @@ -51,20 +76,47 @@ function errorResponse(status: number, message: string, dsCode?: number): Respon ); } -function mapModelToType(model?: string): { modelType: string; thinking: boolean } { - if (!model) return { modelType: "default", thinking: false }; - const m = model.toLowerCase(); - if (m.includes("r1") || m.includes("reason") || m.includes("think")) - return { modelType: "deepseek_r1", thinking: true }; - if (m.includes("v3")) return { modelType: "deepseek_v3", thinking: false }; - if (m.includes("expert")) return { modelType: "expert", thinking: true }; - return { modelType: "default", thinking: false }; +function resolveModelOptions( + model?: string, + bodyObj?: Record +): { + modelType: string; + thinkingEnabled: boolean; + searchEnabled: boolean; +} { + const m = (model || "").toLowerCase(); + const modelType = m.includes("pro") || m.includes("expert") ? "expert" : "default"; + const thinkingEnabled = + m.includes("r1") || + m.includes("think") || + m.includes("reason") || + bodyObj?.thinking_enabled === true || + bodyObj?.thinking === true || + !!bodyObj?.reasoning_effort; + const searchEnabled = + m.includes("search") || + bodyObj?.search_enabled === true || + bodyObj?.search === true || + bodyObj?.web_search === true; + return { modelType, thinkingEnabled, searchEnabled }; +} + +function generateFakeCookie(): string { + const ts = Date.now(); + const hex = (n: number) => + Array.from({ length: n }, () => Math.floor(Math.random() * 16).toString(16)).join(""); + const uid = () => + "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + return (c === "x" ? r : (r & 0x3) | 0x8).toString(16); + }); + return `intercom-HWWAFSESTIME=${ts}; HWWAFSESID=${hex(18)}; Hm_lvt_${uid()}=${Math.floor(ts / 1000)}; _frid=${uid()}`; } // ── PoW Solver (DeepSeekHashV1) ───────────────────────────────────────── -function solvePow(challenge: PowChallenge): Record { - const answer = solveDeepSeekPow( +async function solvePow(challenge: PowChallenge): Promise { + const answer = await solveDeepSeekPowAsync( challenge.algorithm, challenge.challenge, challenge.salt, @@ -72,14 +124,16 @@ function solvePow(challenge: PowChallenge): Record { challenge.expire_at ); if (answer < 0) throw new Error("PoW solver failed"); - return { - algorithm: challenge.algorithm, - challenge: challenge.challenge, - salt: challenge.salt, - answer, - signature: challenge.signature, - target_path: challenge.target_path, - }; + return Buffer.from( + JSON.stringify({ + algorithm: challenge.algorithm, + challenge: challenge.challenge, + salt: challenge.salt, + answer, + signature: challenge.signature, + target_path: challenge.target_path, + }) + ).toString("base64"); } // ── SSE Transform (DeepSeek → OpenAI) ─────────────────────────────────── @@ -120,8 +174,8 @@ function transformSSE(deepseekStream: ReadableStream, model: string): ReadableSt buffer = lines.pop() || ""; for (const line of lines) { - if (!line.startsWith("data: ")) continue; - const payload = line.slice(6).trim(); + if (!line.startsWith("data: ") && !line.startsWith("data:")) continue; + const payload = line.replace(/^data:\s*/, "").trim(); if (payload === "[DONE]") { if (!emittedRole) { @@ -141,7 +195,6 @@ function transformSSE(deepseekStream: ReadableStream, model: string): ReadableSt continue; } - // Extract content from DeepSeek fragments const fragments = (data as any)?.v?.response?.fragments; if (Array.isArray(fragments)) { if (!emittedRole) { @@ -150,12 +203,32 @@ function transformSSE(deepseekStream: ReadableStream, model: string): ReadableSt } for (const frag of fragments) { if (typeof frag.content === "string" && frag.content.length > 0) { - chunk({ content: frag.content }); + if (frag.type === "THINK") { + chunk({ reasoning_content: frag.content }); + } else { + chunk({ content: frag.content }); + } + } + } + } + + // response/fragments path (incremental updates) + if ((data as any)?.p === "response/fragments" && Array.isArray((data as any)?.v)) { + if (!emittedRole) { + emittedRole = true; + chunk({ role: "assistant", content: "" }); + } + for (const frag of (data as any).v) { + if (typeof frag.content === "string" && frag.content.length > 0) { + if (frag.type === "THINK") { + chunk({ reasoning_content: frag.content }); + } else { + chunk({ content: frag.content }); + } } } } - // Check for stream end if ((data as any)?.p === "response/status" && (data as any)?.v === "FINISHED") { if (!emittedRole) { emittedRole = true; @@ -166,18 +239,13 @@ function transformSSE(deepseekStream: ReadableStream, model: string): ReadableSt controller.close(); return; } - - // Also check event: close - if ((data as any)?.click_behavior !== undefined) { - // close event — emit [DONE] if not already - } } } } catch (err) { - // Stream error — emit what we have + controller.error(err); + return; } - // Fallback close if (!emittedRole) { emittedRole = true; chunk({ role: "assistant", content: "" }); @@ -203,8 +271,8 @@ async function collectSSEContent(deepseekStream: ReadableStream): Promise { - const resp = await fetch(`${DEEPSEEK_WEB_BASE}/api/v0/users/current`, { - headers: { ...BASE_HEADERS, Cookie: cookies }, - signal, + const cached = tokenCache.get(userToken); + if (cached && cached.expiresAt > Math.floor(Date.now() / 1000)) { + return cached.accessToken; + } + + log?.info?.("DEEPSEEK-WEB", "Acquiring access token from /users/current..."); + const resp = await fetch(`${DEEPSEEK_API_BASE}/v0/users/current`, { + headers: { + Authorization: `Bearer ${userToken}`, + ...FAKE_HEADERS, + }, + signal: signal ?? undefined, }); + + if (resp.status === 401 || resp.status === 403) { + throw new Error("Token invalid or expired — get a new userToken from DeepSeek localStorage"); + } if (!resp.ok) { throw new Error(`users/current HTTP ${resp.status}`); } + const json = await resp.json(); - const token = json?.data?.biz_data?.token; - if (!token) { - throw new Error(`No token in users/current response: code=${json?.code} msg=${json?.msg}`); + const bizData = json?.data?.biz_data || json?.biz_data; + if (!bizData?.token) { + const errMsg = json?.msg || json?.data?.biz_msg || "Unknown error"; + throw new Error(`Failed to acquire token: ${errMsg}`); } - log?.info?.("DEEPSEEK-WEB", `Got bearer token (${token.length} chars)`); - return token; + + const accessToken = bizData.token; + evictOldest(tokenCache); + tokenCache.set(userToken, { + accessToken, + expiresAt: Math.floor(Date.now() / 1000) + 3600, + }); + + log?.info?.("DEEPSEEK-WEB", `Access token acquired (${accessToken.length} chars)`); + return accessToken; } async function createSession( - token: string, - cookies: string, - signal?: AbortSignal + accessToken: string, + connectionId: string | undefined, + signal?: AbortSignal | null ): Promise { - const resp = await fetch(`${DEEPSEEK_WEB_BASE}/api/v0/chat_session/create`, { + const cacheKey = connectionId || accessToken; + const cached = sessionCache.get(cacheKey); + if (cached && Date.now() - cached.createdAt < SESSION_CACHE_TTL_MS) { + return cached.sessionId; + } + + const resp = await fetch(`${DEEPSEEK_API_BASE}/v0/chat_session/create`, { method: "POST", headers: { - ...BASE_HEADERS, + ...FAKE_HEADERS, "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - Cookie: cookies, + Authorization: `Bearer ${accessToken}`, + Cookie: generateFakeCookie(), }, body: JSON.stringify({}), - signal, + signal: signal ?? undefined, }); + if (!resp.ok) throw new Error(`chat_session/create HTTP ${resp.status}`); const json = await resp.json(); - const id = json?.data?.biz_data?.chat_session?.id; + const bizData = json?.data?.biz_data || json?.biz_data; + const id = bizData?.chat_session?.id; if (!id) throw new Error(`No session id: code=${json?.code}`); + + evictOldest(sessionCache); + sessionCache.set(cacheKey, { sessionId: id, createdAt: Date.now() }); return id; } async function getPowChallenge( - token: string, - cookies: string, - signal?: AbortSignal + accessToken: string, + signal?: AbortSignal | null ): Promise { - const resp = await fetch(`${DEEPSEEK_WEB_BASE}/api/v0/chat/create_pow_challenge`, { + const resp = await fetch(`${DEEPSEEK_API_BASE}/v0/chat/create_pow_challenge`, { method: "POST", headers: { - ...BASE_HEADERS, + ...FAKE_HEADERS, "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - Cookie: cookies, + Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ target_path: "/api/v0/chat/completion" }), - signal, + signal: signal ?? undefined, }); if (!resp.ok) throw new Error(`create_pow_challenge HTTP ${resp.status}`); const json = await resp.json(); - const challenge = json?.data?.biz_data?.challenge; - if (!challenge?.challenge) throw new Error(`No PoW challenge: code=${json?.code}`); - return challenge as PowChallenge; + const bizData = json?.data?.biz_data || json?.biz_data; + if (!bizData?.challenge?.challenge) throw new Error(`No PoW challenge: code=${json?.code}`); + return bizData.challenge as PowChallenge; } // ── Executor ───────────────────────────────────────────────────────────── @@ -303,10 +408,10 @@ export class DeepSeekWebExecutor extends BaseExecutor { signal?: AbortSignal ): Promise { try { - const cookies = String((credentials as any)?.cookies || ""); - if (!cookies.includes("ds_session_id=")) return false; - const token = await getBearerToken(cookies, signal); - return !!token; + const userToken = extractUserToken(credentials); + if (!userToken) return false; + const accessToken = await acquireAccessToken(userToken, signal); + return !!accessToken; } catch { return false; } @@ -320,32 +425,42 @@ export class DeepSeekWebExecutor extends BaseExecutor { }>; const rawCreds = credentials as unknown as Record; - // 1. Validate credentials - if (!validateCredentials(rawCreds)) { + // 1. Extract userToken from credentials.apiKey + const userToken = extractUserToken(rawCreds); + if (!userToken) { return { - response: errorResponse(400, "Invalid credentials: requires ds_session_id cookie"), + response: errorResponse( + 400, + "Invalid credentials: paste your userToken from DeepSeek localStorage " + + "(DevTools → Application → Local Storage → chat.deepseek.com → userToken)" + ), url: COMPLETION_URL, headers: {}, transformedBody: body, }; } - const cookies = rawCreds.cookies; try { - // 2. Get bearer token from session cookie - log?.info?.("DEEPSEEK-WEB", "Getting bearer token..."); - const token = await getBearerToken(cookies, signal, log); + // 2. Exchange userToken for short-lived access token (cached 1h) + let t0 = Date.now(); + const accessToken = await acquireAccessToken(userToken, signal, log); + log?.info?.("DEEPSEEK-WEB", `Token acquired in ${Date.now() - t0}ms`); - // 3. Create chat session - log?.info?.("DEEPSEEK-WEB", "Creating chat session..."); - const sessionId = await createSession(token, cookies, signal); + // 3. Create chat session (cached 5min) + t0 = Date.now(); + const sessionId = await createSession(accessToken, rawCreds.connectionId as string, signal); + log?.info?.("DEEPSEEK-WEB", `Session created in ${Date.now() - t0}ms`); // 4. Get PoW challenge and solve - log?.info?.("DEEPSEEK-WEB", "Getting PoW challenge..."); - const powChallenge = await getPowChallenge(token, cookies, signal); - log?.info?.("DEEPSEEK-WEB", `Solving PoW (difficulty=${powChallenge.difficulty})...`); - const powSolution = solvePow(powChallenge); - log?.info?.("DEEPSEEK-WEB", `PoW solved: nonce=${powSolution.answer}`); + t0 = Date.now(); + const powChallenge = await getPowChallenge(accessToken, signal); + log?.info?.( + "DEEPSEEK-WEB", + `PoW challenge fetched in ${Date.now() - t0}ms (difficulty=${powChallenge.difficulty})` + ); + t0 = Date.now(); + const powAnswer = await solvePow(powChallenge); + log?.info?.("DEEPSEEK-WEB", `PoW solved in ${Date.now() - t0}ms`); // 5. Build prompt from messages const prompt = messages @@ -356,12 +471,11 @@ export class DeepSeekWebExecutor extends BaseExecutor { }) .join("\n"); - // 6. Map model and extract features from request body - const { modelType, thinking } = mapModelToType(model as string); - const thinkingEnabled = - thinking || bodyObj.thinking_enabled === true || bodyObj.thinking === true; - const searchEnabled = - bodyObj.search_enabled === true || bodyObj.search === true || bodyObj.web_search === true; + // 6. Resolve model type, thinking, and search from model name + body flags + const { modelType, thinkingEnabled, searchEnabled } = resolveModelOptions( + model as string, + bodyObj + ); const refFileIds = Array.isArray(bodyObj.ref_file_ids) ? bodyObj.ref_file_ids : []; log?.info?.( "DEEPSEEK-WEB", @@ -370,18 +484,13 @@ export class DeepSeekWebExecutor extends BaseExecutor { // 7. Send completion request const headers: Record = { - ...BASE_HEADERS, + ...FAKE_HEADERS, "Content-Type": "application/json", - Accept: "*/*", - Authorization: `Bearer ${token}`, - "x-ds-pow-response": Buffer.from(JSON.stringify(powSolution)).toString("base64"), - "x-client-timezone-offset": String(new Date().getTimezoneOffset() * -60), - Cookie: cookies, - Referer: `${DEEPSEEK_WEB_BASE}/`, + Authorization: `Bearer ${accessToken}`, + "X-Ds-Pow-Response": powAnswer, + "X-Client-Timezone-Offset": String(new Date().getTimezoneOffset() * -60), + Cookie: generateFakeCookie(), }; - if (thinkingEnabled) { - headers["x-thinking-enabled"] = "1"; - } const requestPayload = { chat_session_id: sessionId, @@ -394,25 +503,31 @@ export class DeepSeekWebExecutor extends BaseExecutor { preempt: false, }; + t0 = Date.now(); log?.info?.("DEEPSEEK-WEB", `POST ${COMPLETION_URL}`); const resp = await fetch(COMPLETION_URL, { method: "POST", headers, body: JSON.stringify(requestPayload), - signal, + signal: signal ?? undefined, }); + log?.info?.( + "DEEPSEEK-WEB", + `Completion response in ${Date.now() - t0}ms, status=${resp.status}` + ); + if (!resp.ok) { const status = resp.status; let errMsg = `DeepSeek API error (${status})`; if (status === 401 || status === 403) { - errMsg = "DeepSeek session expired — re-paste your ds_session_id cookie."; + tokenCache.delete(userToken); + errMsg = "DeepSeek token expired — get a fresh userToken from localStorage."; } else if (status === 429) { errMsg = "DeepSeek rate limited. Wait and retry."; } log?.warn?.("DEEPSEEK-WEB", errMsg); - // Check for DeepSeek JSON error body try { const errBody = await resp.json(); if (errBody?.code && errBody.code !== 0) { @@ -439,6 +554,7 @@ export class DeepSeekWebExecutor extends BaseExecutor { const errMsg = `DeepSeek error ${json.code}: ${json.msg}`; log?.warn?.("DEEPSEEK-WEB", errMsg); const status = json.code === 40003 ? 401 : json.code === 40002 ? 429 : 502; + if (json.code === 40003) tokenCache.delete(userToken); return { response: errorResponse(status, errMsg, json.code), url: COMPLETION_URL, @@ -446,7 +562,6 @@ export class DeepSeekWebExecutor extends BaseExecutor { transformedBody: requestPayload, }; } - // Valid JSON response (shouldn't happen for streaming, but handle it) return { response: new Response(JSON.stringify(json), { status: 200, @@ -524,3 +639,6 @@ export class DeepSeekWebExecutor extends BaseExecutor { } export const deepseekWebExecutor = new DeepSeekWebExecutor(); + +// Re-export for auto-refresh executor and tests +export { acquireAccessToken, tokenCache, sessionCache }; diff --git a/open-sse/lib/deepseek-pow.ts b/open-sse/lib/deepseek-pow.ts index 35ac919a2a..29382c6fb3 100644 --- a/open-sse/lib/deepseek-pow.ts +++ b/open-sse/lib/deepseek-pow.ts @@ -1,12 +1,111 @@ -// DeepSeek PoW Solver - loads exact implementation from extracted worker module -// The Keccak sponge has non-standard byte packing that's difficult to replicate exactly, -// so we use the verified extracted module. - import { createRequire } from "node:module"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +// ── WASM solver (fast — ~50-100ms at difficulty 144000) ────────────────── + +class DeepSeekHashWasm { + private wasmInstance: any; + private offset = 0; + private cachedUint8Memory: Uint8Array | null = null; + private cachedTextEncoder = new TextEncoder(); + + private getCachedUint8Memory(): Uint8Array { + if (!this.cachedUint8Memory?.byteLength) { + this.cachedUint8Memory = new Uint8Array(this.wasmInstance.memory.buffer); + } + return this.cachedUint8Memory; + } + + private encodeString( + text: string, + allocate: (size: number, align: number) => number, + reallocate: (ptr: number, oldSize: number, newSize: number, align: number) => number + ): number { + const strLength = text.length; + let ptr = allocate(strLength, 1) >>> 0; + const memory = this.getCachedUint8Memory(); + let asciiLength = 0; + + for (; asciiLength < strLength; asciiLength++) { + if (text.charCodeAt(asciiLength) > 127) break; + memory[ptr + asciiLength] = text.charCodeAt(asciiLength); + } + + if (asciiLength !== strLength) { + if (asciiLength > 0) text = text.slice(asciiLength); + ptr = reallocate(ptr, strLength, asciiLength + text.length * 3, 1) >>> 0; + const result = this.cachedTextEncoder.encodeInto( + text, + this.getCachedUint8Memory().subarray(ptr + asciiLength, ptr + asciiLength + text.length * 3) + ); + asciiLength += result.written!; + ptr = reallocate(ptr, asciiLength + text.length * 3, asciiLength, 1) >>> 0; + } + + this.offset = asciiLength; + return ptr; + } + + calculateHash(challenge: string, prefix: string, difficulty: number): number | undefined { + try { + const retptr = this.wasmInstance.__wbindgen_add_to_stack_pointer(-16); + + const ptr0 = this.encodeString( + challenge, + this.wasmInstance.__wbindgen_export_0, + this.wasmInstance.__wbindgen_export_1 + ); + const len0 = this.offset; + + const ptr1 = this.encodeString( + prefix, + this.wasmInstance.__wbindgen_export_0, + this.wasmInstance.__wbindgen_export_1 + ); + const len1 = this.offset; + + this.wasmInstance.wasm_solve(retptr, ptr0, len0, ptr1, len1, difficulty); + + const dv = new DataView(this.wasmInstance.memory.buffer); + const status = dv.getInt32(retptr + 0, true); + const value = dv.getFloat64(retptr + 8, true); + + return status === 0 ? undefined : value; + } finally { + this.wasmInstance.__wbindgen_add_to_stack_pointer(16); + } + } + + async init(wasmPath: string): Promise { + const wasmBuffer = await fs.promises.readFile(wasmPath); + const { instance } = await WebAssembly.instantiate(wasmBuffer, { wbg: {} }); + this.wasmInstance = instance.exports; + } +} + +let _wasmSolver: DeepSeekHashWasm | null = null; +let _wasmInitFailed = false; + +async function getWasmSolver(): Promise { + if (_wasmInitFailed) return null; + if (_wasmSolver) return _wasmSolver; + + try { + const solver = new DeepSeekHashWasm(); + const wasmPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "sha3_wasm_bg.wasm"); + await solver.init(wasmPath); + _wasmSolver = solver; + return solver; + } catch { + _wasmInitFailed = true; + return null; + } +} + +// ── JS fallback solver (slow — ~5-6s at difficulty 144000) ─────────────── -// Load the exact solver extracted from DeepSeek's worker chunk. -// Lazy-loaded inside the function so the standalone Next build can collect -// page data without executing a dynamic require() at module-load time. const require = createRequire(import.meta.url); let _U: any | undefined; function loadU(): any { @@ -16,16 +115,7 @@ function loadU(): any { return _U; } -export function solveDeepSeekPow( - algorithm: string, - challenge: string, - salt: string, - difficulty: number, - expireAt: number -): number { - if (algorithm !== "DeepSeekHashV1") throw new Error(`Unsupported: ${algorithm}`); - const prefix = `${salt}_${expireAt}_`; - +function solveWithJS(challenge: string, prefix: string, difficulty: number): number { const U = loadU(); const createHash = () => { const self: any = {}; @@ -62,3 +152,38 @@ export function solveDeepSeekPow( } return -1; } + +// ── Public API ─────────────────────────────────────────────────────────── + +export async function solveDeepSeekPowAsync( + algorithm: string, + challenge: string, + salt: string, + difficulty: number, + expireAt: number +): Promise { + if (algorithm !== "DeepSeekHashV1") throw new Error(`Unsupported: ${algorithm}`); + const prefix = `${salt}_${expireAt}_`; + + const wasm = await getWasmSolver(); + if (wasm) { + const answer = wasm.calculateHash(challenge, prefix, difficulty); + if (answer === undefined) return -1; + return answer; + } + + return solveWithJS(challenge, prefix, difficulty); +} + +// Sync wrapper kept for backward compat (uses JS fallback only) +export function solveDeepSeekPow( + algorithm: string, + challenge: string, + salt: string, + difficulty: number, + expireAt: number +): number { + if (algorithm !== "DeepSeekHashV1") throw new Error(`Unsupported: ${algorithm}`); + const prefix = `${salt}_${expireAt}_`; + return solveWithJS(challenge, prefix, difficulty); +} diff --git a/open-sse/lib/sha3_wasm_bg.wasm b/open-sse/lib/sha3_wasm_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..ac92b1d87e8cdf3a5c2af4d189743c1f9d5feba7 GIT binary patch literal 26612 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 literal 0 HcmV?d00001 diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 11e5b51b13..c6f47a0bb5 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -6856,6 +6856,7 @@ function AddApiKeyModal({ const isPerplexityWeb = provider === "perplexity-web"; const isBlackboxWeb = provider === "blackbox-web"; const isMuseSparkWeb = provider === "muse-spark-web"; + const isDeepSeekWeb = provider === "deepseek-web"; const isWebSessionProvider = isGrokWeb || isPerplexityWeb || isBlackboxWeb || isMuseSparkWeb; const apiKeyOptional = providerAllowsOptionalApiKey(provider); const commandCodeAuthPhaseLabel = commandCodeAuthState @@ -6908,43 +6909,49 @@ function AddApiKeyModal({ const [bulkWarnings, setBulkWarnings] = useState([]); const apiCredentialLabel = isQoder ? t("personalAccessTokenLabel") - : isWebSessionProvider - ? t("sessionCookieLabel") - : apiKeyOptional - ? `${t("apiKeyLabel")} (${t("optional").toLowerCase()})` - : t("apiKeyLabel"); + : isDeepSeekWeb + ? "User Token" + : isWebSessionProvider + ? t("sessionCookieLabel") + : apiKeyOptional + ? `${t("apiKeyLabel")} (${t("optional").toLowerCase()})` + : t("apiKeyLabel"); const apiCredentialPlaceholder = isVertex ? t("vertexServiceAccountPlaceholder") - : isGrokWeb - ? t("grokWebCookiePlaceholder") - : isPerplexityWeb - ? t("perplexityWebCookiePlaceholder") - : isBlackboxWeb - ? t("blackboxWebCookiePlaceholder") - : isMuseSparkWeb - ? t("museSparkWebCookiePlaceholder") - : isQoder - ? t("qoderPatPlaceholder") - : apiKeyOptional - ? t("optional") - : undefined; + : isDeepSeekWeb + ? "Paste userToken value from localStorage" + : isGrokWeb + ? t("grokWebCookiePlaceholder") + : isPerplexityWeb + ? t("perplexityWebCookiePlaceholder") + : isBlackboxWeb + ? t("blackboxWebCookiePlaceholder") + : isMuseSparkWeb + ? t("museSparkWebCookiePlaceholder") + : isQoder + ? t("qoderPatPlaceholder") + : apiKeyOptional + ? t("optional") + : undefined; const apiCredentialHint = isQoder ? t("qoderPatHint") - : isGrokWeb - ? t("grokWebCookieHint") - : isPerplexityWeb - ? t("perplexityWebCookieHint") - : isBlackboxWeb - ? t("blackboxWebCookieHint") - : isMuseSparkWeb - ? t("museSparkWebCookieHint") - : isLocalSelfHostedProvider - ? t("localProviderApiKeyOptionalHint", { - provider: localProviderMetadata?.name || providerName || provider || "", - }) - : apiKeyOptional - ? t("apiKeyOptionalHint") - : undefined; + : isDeepSeekWeb + ? "Found in browser DevTools → Application → Local Storage → chat.deepseek.com → userToken" + : isGrokWeb + ? t("grokWebCookieHint") + : isPerplexityWeb + ? t("perplexityWebCookieHint") + : isBlackboxWeb + ? t("blackboxWebCookieHint") + : isMuseSparkWeb + ? t("museSparkWebCookieHint") + : isLocalSelfHostedProvider + ? t("localProviderApiKeyOptionalHint", { + provider: localProviderMetadata?.name || providerName || provider || "", + }) + : apiKeyOptional + ? t("apiKeyOptionalHint") + : undefined; const handleValidate = async () => { setValidating(true); diff --git a/src/app/api/v1/agents/tasks/[id]/route.ts b/src/app/api/v1/agents/tasks/[id]/route.ts index 6b89aa3573..0ae5d9ccb0 100644 --- a/src/app/api/v1/agents/tasks/[id]/route.ts +++ b/src/app/api/v1/agents/tasks/[id]/route.ts @@ -18,7 +18,13 @@ import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; const logger = pino({ name: "cloud-agents-api" }); -createCloudAgentTaskTable(); +let _tableInit = false; +function ensureTable() { + if (!_tableInit) { + createCloudAgentTaskTable(); + _tableInit = true; + } +} export async function OPTIONS(request: NextRequest) { return new NextResponse(null, { headers: getCloudAgentCorsHeaders(request) }); @@ -52,6 +58,7 @@ function cloudAgentCredentialsRequiredResponse(providerId: string, request: Next export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { + ensureTable(); const authError = await requireCloudAgentManagementAuth(request); if (authError) return authError; @@ -110,6 +117,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { + ensureTable(); const authError = await requireCloudAgentManagementAuth(request); if (authError) return authError; @@ -192,6 +200,7 @@ export async function DELETE( { params }: { params: Promise<{ id: string }> } ) { try { + ensureTable(); const authError = await requireCloudAgentManagementAuth(request); if (authError) return authError; diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index be71bfa7e4..107775e186 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -2643,6 +2643,58 @@ function buildMetaAiValidationBody() { }; } +async function validateDeepSeekWebProvider({ apiKey }: any) { + if (!apiKey) { + return { + valid: false, + error: + "Missing userToken — paste the value from DevTools → Application → Local Storage → chat.deepseek.com → userToken", + }; + } + let token = apiKey; + try { + const parsed = JSON.parse(token); + if (typeof parsed?.value === "string") token = parsed.value; + } catch { + // not JSON, use as-is + } + + try { + const resp = await fetch("https://chat.deepseek.com/api/v0/users/current", { + headers: { + Authorization: `Bearer ${token}`, + Accept: "*/*", + Origin: "https://chat.deepseek.com", + Referer: "https://chat.deepseek.com/", + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", + "X-App-Version": "20241129.1", + "X-Client-Platform": "web", + }, + }); + if (resp.status === 401 || resp.status === 403) { + return { + valid: false, + error: "userToken is invalid or expired — get a fresh one from localStorage", + }; + } + if (!resp.ok) { + return { valid: false, error: `DeepSeek returned HTTP ${resp.status}` }; + } + const json = await resp.json(); + const bizData = json?.data?.biz_data || json?.biz_data; + if (!bizData?.token) { + return { + valid: false, + error: `DeepSeek did not return an access token: ${json?.msg || "unknown error"}`, + }; + } + return { valid: true, error: null }; + } catch (error: any) { + return toValidationErrorResult(error); + } +} + async function validateGrokWebProvider({ apiKey, providerSpecificData = {} }: any) { try { const token = extractCookieValue(apiKey, "sso"); @@ -3252,6 +3304,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi runwayml: validateRunwayProvider, snowflake: validateSnowflakeProvider, gigachat: validateGigachatProvider, + "deepseek-web": validateDeepSeekWebProvider, "grok-web": validateGrokWebProvider, "chatgpt-web": validateChatGptWebProvider, "perplexity-web": validatePerplexityWebProvider, diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index f1f31485eb..f71099b1b8 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -224,7 +224,8 @@ export const WEB_COOKIE_PROVIDERS = { color: "#4D6BFE", textIcon: "DS", website: "https://chat.deepseek.com", - authHint: "Paste your ds_session_id cookie from chat.deepseek.com", + authHint: + "Paste your userToken from chat.deepseek.com — DevTools → Application → Local Storage → userToken", }, "copilot-web": { id: "copilot-web", diff --git a/tests/unit/deepseek-web.test.ts b/tests/unit/deepseek-web.test.ts index 8cdbb6bba7..8a13b5ef96 100644 --- a/tests/unit/deepseek-web.test.ts +++ b/tests/unit/deepseek-web.test.ts @@ -32,21 +32,7 @@ test("provider name is deepseek-web", () => { // ─── Credential validation ─────────────────────────────────────────────── -test("execute returns 400 without ds_session_id cookie", async () => { - const executor = new DeepSeekWebExecutor(); - const result = await executor.execute({ - model: "default", - body: { messages: [{ role: "user", content: "hi" }] }, - stream: true, - credentials: { cookies: "foo=bar" }, - signal: AbortSignal.timeout(5000), - }); - assert.equal(result.response.status, 400); - const text = await result.response.text(); - assert.ok(text.includes("ds_session_id")); -}); - -test("execute returns 400 with empty credentials", async () => { +test("execute returns 400 without apiKey (userToken)", async () => { const executor = new DeepSeekWebExecutor(); const result = await executor.execute({ model: "default", @@ -56,6 +42,20 @@ test("execute returns 400 with empty credentials", async () => { signal: AbortSignal.timeout(5000), }); assert.equal(result.response.status, 400); + const text = await result.response.text(); + assert.ok(text.includes("userToken")); +}); + +test("execute returns 400 with empty apiKey", async () => { + const executor = new DeepSeekWebExecutor(); + const result = await executor.execute({ + model: "default", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: true, + credentials: { apiKey: "" }, + signal: AbortSignal.timeout(5000), + }); + assert.equal(result.response.status, 400); }); // ─── Test connection ───────────────────────────────────────────────────── @@ -65,33 +65,31 @@ test("testConnection returns false with empty credentials", async () => { assert.equal(await executor.testConnection({}), false); }); -test("testConnection returns false without ds_session_id", async () => { +test("testConnection returns false without apiKey", async () => { const executor = new DeepSeekWebExecutor(); - assert.equal(await executor.testConnection({ cookies: "foo=bar" }), false); + assert.equal(await executor.testConnection({ apiKey: "" }), false); }); // ─── API flow (mocked) ────────────────────────────────────────────────── -function mockDeepSeekFlow() { +async function mockDeepSeekFlow() { const original = globalThis.fetch; const calls = []; globalThis.fetch = async (url, opts) => { const urlStr = typeof url === "string" ? url : url.toString(); - calls.push({ url: urlStr, method: opts?.method, body: opts?.body }); + calls.push({ url: urlStr, method: opts?.method, body: opts?.body, headers: opts?.headers }); - // /users/current → return token if (urlStr.includes("/users/current")) { return new Response( JSON.stringify({ code: 0, - data: { biz_data: { token: "test-bearer-token-123", email: "test@test.com" } }, + data: { biz_data: { token: "test-access-token-123", email: "test@test.com" } }, }), { status: 200, headers: { "Content-Type": "application/json" } } ); } - // /chat_session/create → return session id if (urlStr.includes("/chat_session/create")) { return new Response( JSON.stringify({ @@ -102,7 +100,6 @@ function mockDeepSeekFlow() { ); } - // /create_pow_challenge → return challenge if (urlStr.includes("/create_pow_challenge")) { return new Response( JSON.stringify({ @@ -126,7 +123,6 @@ function mockDeepSeekFlow() { ); } - // /chat/completion → return SSE stream if (urlStr.includes("/chat/completion")) { const encoder = new TextEncoder(); const sse = [ @@ -149,30 +145,36 @@ function mockDeepSeekFlow() { return new Response("Not found", { status: 404 }); }; + // Clear token/session caches between tests + const dsMod = await import("../../open-sse/executors/deepseek-web.ts"); + if (dsMod.tokenCache) dsMod.tokenCache.clear(); + if (dsMod.sessionCache) dsMod.sessionCache.clear(); + return { calls, restore: () => { globalThis.fetch = original; + if (dsMod.tokenCache) dsMod.tokenCache.clear(); + if (dsMod.sessionCache) dsMod.sessionCache.clear(); }, }; } test("execute: full flow with mocked API (streaming)", async () => { - const mock = mockDeepSeekFlow(); + const mock = await mockDeepSeekFlow(); try { const executor = new DeepSeekWebExecutor(); const result = await executor.execute({ model: "default", body: { messages: [{ role: "user", content: "Say hello" }] }, stream: true, - credentials: { cookies: "ds_session_id=test-session-id-1234" }, + credentials: { apiKey: "test-user-token-1234" }, signal: AbortSignal.timeout(10000), }); assert.ok(result.response.ok); assert.equal(result.response.headers.get("content-type"), "text/event-stream"); - // Read SSE stream const text = await result.response.text(); assert.ok(text.includes('"content":"Hello"'), "Should contain Hello"); assert.ok(text.includes('"finish_reason":"stop"'), "Should have stop"); @@ -196,8 +198,19 @@ test("execute: full flow with mocked API (streaming)", async () => { "Called completion" ); - // Verify completion request had Bearer token + // Verify /users/current used Bearer auth (userToken) + const usersCall = mock.calls.find((c) => c.url.includes("/users/current")); + assert.ok( + usersCall.headers?.Authorization === "Bearer test-user-token-1234", + "Should use userToken as Bearer for /users/current" + ); + + // Verify completion used the access token (not the userToken) const compCall = mock.calls.find((c) => c.url.includes("/chat/completion")); + assert.ok( + compCall.headers?.Authorization === "Bearer test-access-token-123", + "Should use access token for /completion" + ); const body = JSON.parse(compCall.body); assert.equal(body.chat_session_id, "session-abc-123"); assert.equal(body.prompt, "Say hello"); @@ -207,14 +220,14 @@ test("execute: full flow with mocked API (streaming)", async () => { }); test("execute: full flow with mocked API (non-streaming)", async () => { - const mock = mockDeepSeekFlow(); + const mock = await mockDeepSeekFlow(); try { const executor = new DeepSeekWebExecutor(); const result = await executor.execute({ model: "default", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, - credentials: { cookies: "ds_session_id=abc123" }, + credentials: { apiKey: "test-user-token-ns" }, signal: AbortSignal.timeout(10000), }); @@ -230,70 +243,27 @@ test("execute: full flow with mocked API (non-streaming)", async () => { }); test("execute: sends PoW response header", async () => { - const original = globalThis.fetch; - const capturedHeaders = {}; - - globalThis.fetch = async (url, opts) => { - const urlStr = typeof url === "string" ? url : url.toString(); - if (urlStr.includes("/users/current")) { - return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "tok" } } }), { - headers: { "Content-Type": "application/json" }, - }); - } - if (urlStr.includes("/chat_session/create")) { - return new Response( - JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s1" } } } }), - { headers: { "Content-Type": "application/json" } } - ); - } - if (urlStr.includes("/create_pow_challenge")) { - return new Response( - JSON.stringify({ - code: 0, - data: { - biz_data: { - challenge: { - algorithm: "DeepSeekHashV1", - challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64", - salt: "bb", - signature: "s", - difficulty: 1000, - expire_at: 1778891543095, - expire_after: 300000, - target_path: "/api/v0/chat/completion", - }, - }, - }, - }), - { headers: { "Content-Type": "application/json" } } - ); - } - if (urlStr.includes("/chat/completion")) { - Object.assign(capturedHeaders, opts.headers); - const encoder = new TextEncoder(); - return new Response(encoder.encode("data: [DONE]\n\n"), { - headers: { "Content-Type": "text/event-stream" }, - }); - } - return new Response("", { status: 404 }); - }; - + const mock = await mockDeepSeekFlow(); try { const executor = new DeepSeekWebExecutor(); await executor.execute({ model: "default", body: { messages: [{ role: "user", content: "hi" }] }, stream: true, - credentials: { cookies: "ds_session_id=x" }, + credentials: { apiKey: "test-user-token-pow" }, signal: AbortSignal.timeout(10000), }); - assert.ok(capturedHeaders["Authorization"]?.startsWith("Bearer tok"), "Has Bearer token"); - assert.ok(capturedHeaders["x-ds-pow-response"], "Has PoW header"); - assert.ok(capturedHeaders["x-app-version"] === "2.0.0", "Has x-app-version"); - assert.ok(capturedHeaders["x-client-platform"] === "web", "Has x-client-platform"); + const compCall = mock.calls.find((c) => c.url.includes("/chat/completion")); + assert.ok( + compCall.headers["Authorization"]?.startsWith("Bearer test-access-token"), + "Has Bearer token" + ); + assert.ok(compCall.headers["X-Ds-Pow-Response"], "Has PoW header"); + assert.ok(compCall.headers["X-App-Version"], "Has X-App-Version"); + assert.ok(compCall.headers["X-Client-Platform"] === "web", "Has X-Client-Platform"); } finally { - globalThis.fetch = original; + mock.restore(); } }); @@ -307,23 +277,29 @@ test("execute: handles API error (token fetch fails)", async () => { } return new Response("", { status: 404 }); }; + const dsMod1 = await import("../../open-sse/executors/deepseek-web.ts"); + if (dsMod1.tokenCache) dsMod1.tokenCache.clear(); try { const executor = new DeepSeekWebExecutor(); const result = await executor.execute({ model: "default", body: { messages: [{ role: "user", content: "hi" }] }, stream: true, - credentials: { cookies: "ds_session_id=abc" }, + credentials: { apiKey: "test-bad-token" }, signal: AbortSignal.timeout(10000), }); assert.ok(result.response.status >= 400, "Should return error status"); } finally { globalThis.fetch = original; + if (dsMod1.tokenCache) dsMod1.tokenCache.clear(); } }); test("execute: handles 401 from DeepSeek", async () => { const original = globalThis.fetch; + const dsMod2 = await import("../../open-sse/executors/deepseek-web.ts"); + if (dsMod2.tokenCache) dsMod2.tokenCache.clear(); + if (dsMod2.sessionCache) dsMod2.sessionCache.clear(); globalThis.fetch = async (url) => { if (url.includes("/users/current")) { return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "tok" } } }), { @@ -369,17 +345,22 @@ test("execute: handles 401 from DeepSeek", async () => { model: "default", body: { messages: [{ role: "user", content: "hi" }] }, stream: true, - credentials: { cookies: "ds_session_id=abc" }, + credentials: { apiKey: "test-expired-token" }, signal: AbortSignal.timeout(10000), }); assert.equal(result.response.status, 401); } finally { globalThis.fetch = original; + if (dsMod2.tokenCache) dsMod2.tokenCache.clear(); + if (dsMod2.sessionCache) dsMod2.sessionCache.clear(); } }); test("execute: handles DeepSeek JSON error (40003 INVALID_TOKEN)", async () => { const original = globalThis.fetch; + const dsMod3 = await import("../../open-sse/executors/deepseek-web.ts"); + if (dsMod3.tokenCache) dsMod3.tokenCache.clear(); + if (dsMod3.sessionCache) dsMod3.sessionCache.clear(); globalThis.fetch = async (url) => { if (url.includes("/users/current")) { return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "tok" } } }), { @@ -428,7 +409,7 @@ test("execute: handles DeepSeek JSON error (40003 INVALID_TOKEN)", async () => { model: "default", body: { messages: [{ role: "user", content: "hi" }] }, stream: true, - credentials: { cookies: "ds_session_id=abc" }, + credentials: { apiKey: "test-invalid-token" }, signal: AbortSignal.timeout(10000), }); assert.equal(result.response.status, 401); @@ -436,66 +417,37 @@ test("execute: handles DeepSeek JSON error (40003 INVALID_TOKEN)", async () => { assert.ok(text.includes("40003")); } finally { globalThis.fetch = original; + if (dsMod3.tokenCache) dsMod3.tokenCache.clear(); + if (dsMod3.sessionCache) dsMod3.sessionCache.clear(); } }); // ─── Model mapping ─────────────────────────────────────────────────────── test("execute: maps model to deepseek_r1 with thinking", async () => { - const original = globalThis.fetch; - let capturedBody = null; - globalThis.fetch = async (url, opts) => { - if (url.includes("/users/current")) - return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "t" } } }), { - headers: { "Content-Type": "application/json" }, - }); - if (url.includes("/chat_session/create")) - return new Response( - JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s" } } } }), - { headers: { "Content-Type": "application/json" } } - ); - if (url.includes("/create_pow_challenge")) - return new Response( - JSON.stringify({ - code: 0, - data: { - biz_data: { - challenge: { - algorithm: "DeepSeekHashV1", - challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64", - salt: "bb", - signature: "s", - difficulty: 1000, - expire_at: 1778891543095, - expire_after: 300000, - target_path: "/api/v0/chat/completion", - }, - }, - }, - }), - { headers: { "Content-Type": "application/json" } } - ); - if (url.includes("/chat/completion")) { - capturedBody = JSON.parse(opts.body); - const enc = new TextEncoder(); - return new Response(enc.encode("data: [DONE]\n\n"), { - headers: { "Content-Type": "text/event-stream" }, - }); - } - return new Response("", { status: 404 }); - }; + const mock = await mockDeepSeekFlow(); try { + let capturedBody = null; + const origFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + const resp = await origFetch(url, opts); + if (url.toString().includes("/chat/completion")) { + capturedBody = JSON.parse(opts.body); + } + return resp; + }; + await new DeepSeekWebExecutor().execute({ model: "deepseek-r1", body: { messages: [{ role: "user", content: "think" }] }, stream: true, - credentials: { cookies: "ds_session_id=x" }, + credentials: { apiKey: "test-user-token-r1" }, signal: AbortSignal.timeout(10000), }); - assert.equal(capturedBody.model_type, "deepseek_r1"); + assert.equal(capturedBody.model_type, "default"); assert.equal(capturedBody.thinking_enabled, true); } finally { - globalThis.fetch = original; + mock.restore(); } }); @@ -514,6 +466,8 @@ test("isSessionValid starts false", () => { // ─── Abort handling ────────────────────────────────────────────────────── test("execute: handles abort signal gracefully", async () => { + const dsMod4 = await import("../../open-sse/executors/deepseek-web.ts"); + if (dsMod4.tokenCache) dsMod4.tokenCache.clear(); const executor = new DeepSeekWebExecutor(); const controller = new AbortController(); controller.abort(); @@ -521,233 +475,113 @@ test("execute: handles abort signal gracefully", async () => { model: "default", body: { messages: [{ role: "user", content: "hi" }] }, stream: true, - credentials: { cookies: "ds_session_id=test" }, + credentials: { apiKey: "test-token-abort" }, signal: controller.signal, }); assert.ok(result.response, "Should return response"); - assert.ok(result.response.status >= 400, "Should indicate error"); + assert.ok( + result.response.status >= 400 || result.response.status === 499, + "Should indicate error or abort" + ); }); // ─── Search enabled ────────────────────────────────────────────────────── test("execute: passes search_enabled from body", async () => { - const original = globalThis.fetch; - let capturedBody = null; - globalThis.fetch = async (url, opts) => { - if (url.includes("/users/current")) - return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "t" } } }), { - headers: { "Content-Type": "application/json" }, - }); - if (url.includes("/chat_session/create")) - return new Response( - JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s1" } } } }), - { headers: { "Content-Type": "application/json" } } - ); - if (url.includes("/create_pow_challenge")) - return new Response( - JSON.stringify({ - code: 0, - data: { - biz_data: { - challenge: { - algorithm: "DeepSeekHashV1", - challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64", - salt: "bb", - signature: "s", - difficulty: 1000, - expire_at: 1778891543095, - expire_after: 300000, - target_path: "/api/v0/chat/completion", - }, - }, - }, - }), - { headers: { "Content-Type": "application/json" } } - ); - if (url.includes("/chat/completion")) { - capturedBody = JSON.parse(opts.body); - return new Response(new TextEncoder().encode("data: [DONE]\n\n"), { - headers: { "Content-Type": "text/event-stream" }, - }); - } - return new Response("", { status: 404 }); - }; + const mock = await mockDeepSeekFlow(); try { + let capturedBody = null; + const origFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + const resp = await origFetch(url, opts); + if (url.toString().includes("/chat/completion")) { + capturedBody = JSON.parse(opts.body); + } + return resp; + }; + await new DeepSeekWebExecutor().execute({ model: "default", body: { messages: [{ role: "user", content: "hi" }], search_enabled: true }, stream: true, - credentials: { cookies: "ds_session_id=x" }, + credentials: { apiKey: "test-token-search" }, signal: AbortSignal.timeout(10000), }); assert.equal(capturedBody.search_enabled, true); } finally { - globalThis.fetch = original; + mock.restore(); } }); test("execute: search_enabled defaults to false", async () => { - const original = globalThis.fetch; - let capturedBody = null; - globalThis.fetch = async (url, opts) => { - if (url.includes("/users/current")) - return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "t" } } }), { - headers: { "Content-Type": "application/json" }, - }); - if (url.includes("/chat_session/create")) - return new Response( - JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s1" } } } }), - { headers: { "Content-Type": "application/json" } } - ); - if (url.includes("/create_pow_challenge")) - return new Response( - JSON.stringify({ - code: 0, - data: { - biz_data: { - challenge: { - algorithm: "DeepSeekHashV1", - challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64", - salt: "bb", - signature: "s", - difficulty: 1000, - expire_at: 1778891543095, - expire_after: 300000, - target_path: "/api/v0/chat/completion", - }, - }, - }, - }), - { headers: { "Content-Type": "application/json" } } - ); - if (url.includes("/chat/completion")) { - capturedBody = JSON.parse(opts.body); - return new Response(new TextEncoder().encode("data: [DONE]\n\n"), { - headers: { "Content-Type": "text/event-stream" }, - }); - } - return new Response("", { status: 404 }); - }; + const mock = await mockDeepSeekFlow(); try { + let capturedBody = null; + const origFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + const resp = await origFetch(url, opts); + if (url.toString().includes("/chat/completion")) { + capturedBody = JSON.parse(opts.body); + } + return resp; + }; + await new DeepSeekWebExecutor().execute({ model: "default", body: { messages: [{ role: "user", content: "hi" }] }, stream: true, - credentials: { cookies: "ds_session_id=x" }, + credentials: { apiKey: "test-token-nosearch" }, signal: AbortSignal.timeout(10000), }); assert.equal(capturedBody.search_enabled, false); } finally { - globalThis.fetch = original; + mock.restore(); } }); // ─── Thinking enabled via body ─────────────────────────────────────────── test("execute: thinking_enabled from body overrides model mapping", async () => { - const original = globalThis.fetch; - let capturedBody = null; - let capturedHeaders = null; - globalThis.fetch = async (url, opts) => { - if (url.includes("/users/current")) - return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "t" } } }), { - headers: { "Content-Type": "application/json" }, - }); - if (url.includes("/chat_session/create")) - return new Response( - JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s1" } } } }), - { headers: { "Content-Type": "application/json" } } - ); - if (url.includes("/create_pow_challenge")) - return new Response( - JSON.stringify({ - code: 0, - data: { - biz_data: { - challenge: { - algorithm: "DeepSeekHashV1", - challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64", - salt: "bb", - signature: "s", - difficulty: 1000, - expire_at: 1778891543095, - expire_after: 300000, - target_path: "/api/v0/chat/completion", - }, - }, - }, - }), - { headers: { "Content-Type": "application/json" } } - ); - if (url.includes("/chat/completion")) { - capturedBody = JSON.parse(opts.body); - capturedHeaders = opts.headers; - return new Response(new TextEncoder().encode("data: [DONE]\n\n"), { - headers: { "Content-Type": "text/event-stream" }, - }); - } - return new Response("", { status: 404 }); - }; + const mock = await mockDeepSeekFlow(); try { + let capturedBody = null; + const origFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + const resp = await origFetch(url, opts); + if (url.toString().includes("/chat/completion")) { + capturedBody = JSON.parse(opts.body); + } + return resp; + }; + await new DeepSeekWebExecutor().execute({ - model: "default", // not r1/expert + model: "default", body: { messages: [{ role: "user", content: "think" }], thinking_enabled: true }, stream: true, - credentials: { cookies: "ds_session_id=x" }, + credentials: { apiKey: "test-token-think" }, signal: AbortSignal.timeout(10000), }); assert.equal(capturedBody.thinking_enabled, true); - assert.equal(capturedHeaders["x-thinking-enabled"], "1"); } finally { - globalThis.fetch = original; + mock.restore(); } }); // ─── File IDs ──────────────────────────────────────────────────────────── test("execute: passes ref_file_ids from body", async () => { - const original = globalThis.fetch; - let capturedBody = null; - globalThis.fetch = async (url, opts) => { - if (url.includes("/users/current")) - return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "t" } } }), { - headers: { "Content-Type": "application/json" }, - }); - if (url.includes("/chat_session/create")) - return new Response( - JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s1" } } } }), - { headers: { "Content-Type": "application/json" } } - ); - if (url.includes("/create_pow_challenge")) - return new Response( - JSON.stringify({ - code: 0, - data: { - biz_data: { - challenge: { - algorithm: "DeepSeekHashV1", - challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64", - salt: "bb", - signature: "s", - difficulty: 1000, - expire_at: 1778891543095, - expire_after: 300000, - target_path: "/api/v0/chat/completion", - }, - }, - }, - }), - { headers: { "Content-Type": "application/json" } } - ); - if (url.includes("/chat/completion")) { - capturedBody = JSON.parse(opts.body); - return new Response(new TextEncoder().encode("data: [DONE]\n\n"), { - headers: { "Content-Type": "text/event-stream" }, - }); - } - return new Response("", { status: 404 }); - }; + const mock = await mockDeepSeekFlow(); try { + let capturedBody = null; + const origFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + const resp = await origFetch(url, opts); + if (url.toString().includes("/chat/completion")) { + capturedBody = JSON.parse(opts.body); + } + return resp; + }; + await new DeepSeekWebExecutor().execute({ model: "default", body: { @@ -755,70 +589,66 @@ test("execute: passes ref_file_ids from body", async () => { ref_file_ids: ["file-abc-123", "file-def-456"], }, stream: true, - credentials: { cookies: "ds_session_id=x" }, + credentials: { apiKey: "test-token-files" }, signal: AbortSignal.timeout(10000), }); assert.deepEqual(capturedBody.ref_file_ids, ["file-abc-123", "file-def-456"]); } finally { - globalThis.fetch = original; + mock.restore(); } }); // ─── Expert model ──────────────────────────────────────────────────────── test("execute: maps expert model with thinking", async () => { - const original = globalThis.fetch; - let capturedBody = null; - globalThis.fetch = async (url, opts) => { - if (url.includes("/users/current")) - return new Response(JSON.stringify({ code: 0, data: { biz_data: { token: "t" } } }), { - headers: { "Content-Type": "application/json" }, - }); - if (url.includes("/chat_session/create")) - return new Response( - JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s1" } } } }), - { headers: { "Content-Type": "application/json" } } - ); - if (url.includes("/create_pow_challenge")) - return new Response( - JSON.stringify({ - code: 0, - data: { - biz_data: { - challenge: { - algorithm: "DeepSeekHashV1", - challenge: "705e5d630f02d09a8179c6a0fcb0caf7265f08fb206fadca0301224f4422fc64", - salt: "bb", - signature: "s", - difficulty: 1000, - expire_at: 1778891543095, - expire_after: 300000, - target_path: "/api/v0/chat/completion", - }, - }, - }, - }), - { headers: { "Content-Type": "application/json" } } - ); - if (url.includes("/chat/completion")) { - capturedBody = JSON.parse(opts.body); - return new Response(new TextEncoder().encode("data: [DONE]\n\n"), { - headers: { "Content-Type": "text/event-stream" }, - }); - } - return new Response("", { status: 404 }); - }; + const mock = await mockDeepSeekFlow(); try { + let capturedBody = null; + const origFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + const resp = await origFetch(url, opts); + if (url.toString().includes("/chat/completion")) { + capturedBody = JSON.parse(opts.body); + } + return resp; + }; + await new DeepSeekWebExecutor().execute({ model: "expert", body: { messages: [{ role: "user", content: "deep think" }] }, stream: true, - credentials: { cookies: "ds_session_id=x" }, + credentials: { apiKey: "test-token-expert" }, signal: AbortSignal.timeout(10000), }); assert.equal(capturedBody.model_type, "expert"); - assert.equal(capturedBody.thinking_enabled, true); + assert.equal(capturedBody.thinking_enabled, false); } finally { - globalThis.fetch = original; + mock.restore(); + } +}); + +// ─── JSON-wrapped userToken ────────────────────────────────────────────── + +test("execute: handles JSON-wrapped userToken", async () => { + const mock = await mockDeepSeekFlow(); + try { + const executor = new DeepSeekWebExecutor(); + const result = await executor.execute({ + model: "default", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: true, + credentials: { apiKey: JSON.stringify({ value: "test-json-wrapped-token" }) }, + signal: AbortSignal.timeout(10000), + }); + + assert.ok(result.response.ok, "Should succeed with JSON-wrapped token"); + + const usersCall = mock.calls.find((c) => c.url.includes("/users/current")); + assert.ok( + usersCall.headers?.Authorization === "Bearer test-json-wrapped-token", + "Should unwrap JSON and use inner value" + ); + } finally { + mock.restore(); } });