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 0000000000..ac92b1d87e Binary files /dev/null and b/open-sse/lib/sha3_wasm_bg.wasm differ 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(); } });