mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 22:02:08 +03:00
* chore(config): ignore additional agent workflow command files Add newly introduced agent workflow and Claude command files to .gitignore so proprietary automation assets are not committed. * feat(deepseek-web): fix auth to use userToken + WASM PoW solver Rewrite deepseek-web executor from broken cookie auth to userToken Bearer flow (like Chat2API). Replace pure JS Keccak PoW with WASM solver (5.8s → 86ms). Add 14 models, validation, and dashboard UX. * fix(deepseek-web): update target_path to use challenge property * refactor(deepseek-web): streamline token handling and implement cache eviction * fix(deepseek-web): fix SSE parser, prompt format, and error handling - Handle all 3 DeepSeek SSE stream formats: initial fragments, APPEND operations, and bare string tokens (fixes truncated responses) - Simplify prompt builder to send system + last user message only (DeepSeek web API is single-turn, full history caused marker leakage) - Check json.code before token extraction (fixes "did not return access token: Authorization" on code 40003 with HTTP 200) - Clear session cache alongside token cache on auth errors - Add dev origin for remote testing Co-authored-by: Cursor <cursoragent@cursor.com> * chore: ignore memory-bank and cursor agent rules from tracking Co-authored-by: Cursor <cursoragent@cursor.com> * feat: enhance documentation and configuration for Fumadocs integration - Added Fumadocs MDX support in the Next.js configuration. - Updated transpile packages to include fumadocs-ui and fumadocs-core. - Implemented a comprehensive set of redirects for documentation paths to improve navigation. - Removed the generate-docs-index script as it is no longer needed. - Updated various documentation titles for consistency and clarity. - Enhanced global styles to incorporate Fumadocs UI themes and styles. * refactor(docs): cleanup fumadocs PR — revert deepseek, add i18n fallback, restore LanguageSelector - Revert unrelated deepseek-web.ts changes (should be separate PR) - Add .source/ to .gitignore (Fumadocs generated files) - Remove contributor IP from allowedDevOrigins - Add i18n runtime fallback: reads NEXT_LOCALE cookie, loads translated .md from docs/i18n/<locale>/docs/ (preserves existing translation pipeline) - Restore LanguageSelector in Fumadocs layout nav - Restore SEO metadata (title template, description, robots) * fix(codex): use allowlist to strip non-Responses-API fields in non-passthrough path (#2608) (#2615) Integrated into release/v3.8.3 — fix(codex): allowlist-based sanitization for gpt-5.5 Responses API * fix(deepseek-web): fix SSE parser, prompt format, error handling, and cache keys (#2616) Integrated into release/v3.8.3 — fix(deepseek-web): SSE parser (APPEND + bare tokens), prompt builder, error handling, session cache cleanup * chore(config): ignore additional agent workflow command files Add newly introduced agent workflow and Claude command files to .gitignore so proprietary automation assets are not committed. * feat(docs): migrate /docs to Fumadocs MDX with nested routes (#2614) Integrated into release/v3.8.3 — Fumadocs MDX migration with nested routes, search API, and 50+ URL redirects * fix(catalog): skip static PROVIDER_MODELS when synced models exist (#2625) Integrated into release/v3.8.3 * fix(qoder): Cosy auth fallback for PAT tokens + vision support for qwen3-vl-plus (#2629) Integrated into release/v3.8.3 * fix(cli): register tsx loader and add opencode config subcommand (#2631) Integrated into release/v3.8.3 * feat(dashboard): add search and filters to /dashboard/api-manager (#2628) Integrated into release/v3.8.3 * fix(claude): improve Pi and OpenCode compatibility (#2621) Integrated into release/v3.8.3 * fix: restore semantic passthrough system-role-only extraction instead of full normalization (#2620) Integrated into release/v3.8.3 * fix(kiro): stabilize conversationId across prompt compression (#2630) Integrated into release/v3.8.3 * fix(deepseek-web): SSE thinking/search routing and session lifecycle (#2624) Integrated into release/v3.8.3 — DeepSeek Web SSE thinking/search routing overhaul * feat(dashboard): free-tier grouping with symbolic link in /providers (#2632) Integrated into release/v3.8.3 * fix: close implementation gaps — t3-chat-web, stream_options, combo_strategy, batch config (#2634) Integrated into release/v3.8.3 * feat(dashboard): risk notice modal for sensitive providers (#2633) Integrated into release/v3.8.3 * fix(reasoning): extend reasoning_content injection to Kimi K2 and other replay models (#2639) Integrated into release/v3.8.3 * fix(cli): Linux autostart via systemd user service (fixes #2627) (#2635) Integrated into release/v3.8.3 * Refactor/providers free tier (#2640) Integrated into release/v3.8.3 * fix(tests): remove duplicate assertion in schema coercion & fix(cli): ignore system vars in env check * fix(combo): preserve omniModel tag in streaming output for round-trip context pinning (#2646) Integrated into release/v3.8.3 * feat(dashboard): media providers pages + Web Fetch category (#2645) Integrated into release/v3.8.3 * Feature provider adapta org com tutorial de conexão em modal (#2643) Integrated into release/v3.8.3 * fix(rtk): skip content-based filter matching for non-shell tool results (#2642) Integrated into release/v3.8.3 * fix(translator): enable Claude extended thinking for Copilot Responses-API requests (#2647) Integrated into release/v3.8.3 * feat(dashboard): add search and filters to /dashboard/api-manager (#2641) Integrated into release/v3.8.3 * feat(dashboard): risk notice modal for sensitive providers (#2638) Integrated into release/v3.8.3 * feat(dashboard): mini-playground inline (Phase 4) (#2648) Integrated into release/v3.8.3 * fix(settings): fix Require Login modal Cancel button text and dismissal (#2649) Integrated into release/v3.8.3 * feat(combos): universal context handoff for cross-model conversation continuity (#2653) Integrated into release/v3.8.3 * chore(release): bump to v3.8.3 — changelog, docs, version sync * feat(i18n): complete zh-CN translations for 1220 missing keys (#2655) Integrated into release/v3.8.3 * chore(release): include electron package changes in v3.8.3 * docs(changelog): integrate PR #2655 into v3.8.3 * feat(i18n): translate 377 additional zh-CN entries (81 new keys + 296 same-as-en) (#2659) Integrated into release/v3.8.3 * feat(dashboard): add Cmd+K / Ctrl+K command palette for sidebar navigation (#2656) Integrated into release/v3.8.3 * docs: update changelog for PR integrations under v3.8.3 * feat(cli): integrate native updates, autostart and headless CLI mode (#2662) Integrated into release/v3.8.3 * fix(proxy): save dashboard custom proxies in registry (#2661) Integrated into release/v3.8.3 * feat(dashboard): chat-first test slide-over (Option A) (#2660) Integrated into release/v3.8.3 * docs: update changelog with Batch 2 PR merges for v3.8.3 * fix: add xhigh+max to effortLevel schema; add opencode-plugin publish job (#2666) Integrated into release/v3.8.3 * docs: update changelog with Batch 3 PR #2666 merge for v3.8.3 * feat(quota+providers): card-grid layout, provider group headers, Codex race fix (#2667) Integrated into release/v3.8.3 * feat(dashboard): real-time live WebSocket monitoring (#2668) Integrated into release/v3.8.3 * feat(copilot): AI assistant with CodeGraph + CLI + knowledge base (#2669) Integrated into release/v3.8.3 * feat(pipeline): pre-request middleware hooks (#2670) Integrated into release/v3.8.3 * feat(resilience): credential health check + adaptive circuit breaker (#2671) Integrated into release/v3.8.3 * feat(playground): combo routing visual simulator (#2672) Integrated into release/v3.8.3 * feat(auth): API key groups with model-level permissions (#2673) Integrated into release/v3.8.3 * feat(pwa): enhanced manifest + push notification support (#2674) Integrated into release/v3.8.3 * feat(proxy): serverless relay endpoints with rate limiting (#2675) Integrated into release/v3.8.3 * docs(changelog): update changelog for PRs 2667-2675 & fix: resolve typescript compile-time errors * fix(db): remove transactions from migrations Remove explicit transaction wrappers from recent migrations and correct the API key groups migration metadata. Also fix codegraph path resolution for ESM environments and refresh generated fumadocs source output. --------- Co-authored-by: Ömer Vehbe <ovehbe@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Mr. Meowgi <mr@meowgi.dev> Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com> Co-authored-by: amogus22877769 <y.lev357@gmail.com> Co-authored-by: Halil Tezcan KARABULUT <info@hlltzcnkb.com> Co-authored-by: Tentoxa <53821604+Tentoxa@users.noreply.github.com> Co-authored-by: HALDRO <121296348+HALDRO@users.noreply.github.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: janeza2 <49841619+janeza2@users.noreply.github.com> Co-authored-by: df4p <38404+df4p@users.noreply.github.com> Co-authored-by: ivan-mezentsev <ivan@mezentsev.me> Co-authored-by: Chewji <126886556+Chewji9875@users.noreply.github.com> Co-authored-by: L-aros <107354918+L-aros@users.noreply.github.com> Co-authored-by: M.M <mr.maatoug@gmail.com> Co-authored-by: Benson K B <bensonkbmca@gmail.com> Co-authored-by: terence71-glitch <mcdowellterence71@gmail.com>
916 lines
32 KiB
TypeScript
916 lines
32 KiB
TypeScript
// @ts-nocheck
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { DeepSeekWebExecutor, DEEPSEEK_WEB_BASE } =
|
|
await import("../../open-sse/executors/deepseek-web.ts");
|
|
const { DeepSeekWebWithAutoRefreshExecutor } =
|
|
await import("../../open-sse/executors/deepseek-web-with-auto-refresh.ts");
|
|
const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts");
|
|
|
|
const COMPLETION_URL = `${DEEPSEEK_WEB_BASE}/api/v0/chat/completion`;
|
|
|
|
// ─── Registration ────────────────────────────────────────────────────────
|
|
|
|
test("DeepSeekWebExecutor registered as deepseek-web and ds-web", () => {
|
|
assert.ok(hasSpecializedExecutor("deepseek-web"));
|
|
assert.ok(hasSpecializedExecutor("ds-web"));
|
|
});
|
|
|
|
test("getExecutor returns DeepSeekWebWithAutoRefreshExecutor", () => {
|
|
const exec = getExecutor("deepseek-web");
|
|
assert.ok(exec instanceof DeepSeekWebWithAutoRefreshExecutor);
|
|
});
|
|
|
|
test("alias ds-web resolves same executor", () => {
|
|
assert.ok(getExecutor("ds-web") instanceof DeepSeekWebWithAutoRefreshExecutor);
|
|
});
|
|
|
|
test("provider name is deepseek-web", () => {
|
|
assert.equal(new DeepSeekWebExecutor().getProvider(), "deepseek-web");
|
|
});
|
|
|
|
// ─── Credential validation ───────────────────────────────────────────────
|
|
|
|
test("execute returns 400 without apiKey (userToken)", async () => {
|
|
const executor = new DeepSeekWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "default",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: true,
|
|
credentials: {},
|
|
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 ─────────────────────────────────────────────────────
|
|
|
|
test("testConnection returns false with empty credentials", async () => {
|
|
const executor = new DeepSeekWebExecutor();
|
|
assert.equal(await executor.testConnection({}), false);
|
|
});
|
|
|
|
test("testConnection returns false without apiKey", async () => {
|
|
const executor = new DeepSeekWebExecutor();
|
|
assert.equal(await executor.testConnection({ apiKey: "" }), false);
|
|
});
|
|
|
|
// ─── API flow (mocked) ──────────────────────────────────────────────────
|
|
|
|
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, headers: opts?.headers });
|
|
|
|
if (urlStr.includes("/users/current")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
code: 0,
|
|
data: { biz_data: { token: "test-access-token-123", email: "test@test.com" } },
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
if (urlStr.includes("/chat_session/create")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
code: 0,
|
|
data: { biz_data: { chat_session: { id: "session-abc-123" } } },
|
|
}),
|
|
{ status: 200, 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: "311b26ae1e0fe7375e242958ce46db5552a6c67fea3f96880dcd846c63a74286",
|
|
salt: "1122334455667788",
|
|
signature: "sig123",
|
|
difficulty: 1000,
|
|
expire_at: 1778891543095,
|
|
expire_after: 300000,
|
|
target_path: "/api/v0/chat/completion",
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
if (urlStr.includes("/chat_session/delete")) {
|
|
return new Response(JSON.stringify({ code: 0 }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
if (urlStr.includes("/chat/completion")) {
|
|
const encoder = new TextEncoder();
|
|
const sse = [
|
|
"event: ready\n",
|
|
'data: {"request_message_id":1,"response_message_id":2}\n',
|
|
"\n",
|
|
'data: {"v":{"response":{"message_id":2,"fragments":[{"id":1,"type":"RESPONSE","content":"Hello"}]}}}\n',
|
|
"\n",
|
|
'data: {"p":"response/status","o":"SET","v":"FINISHED"}\n',
|
|
"\n",
|
|
"event: close\n",
|
|
'data: {"click_behavior":"none"}\n',
|
|
].join("");
|
|
return new Response(encoder.encode(sse), {
|
|
status: 200,
|
|
headers: { "Content-Type": "text/event-stream" },
|
|
});
|
|
}
|
|
|
|
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 = await mockDeepSeekFlow();
|
|
try {
|
|
const executor = new DeepSeekWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "default",
|
|
body: { messages: [{ role: "user", content: "Say hello" }] },
|
|
stream: true,
|
|
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");
|
|
|
|
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");
|
|
assert.ok(text.includes("[DONE]"), "Should have [DONE]");
|
|
|
|
// Verify API call sequence
|
|
assert.ok(
|
|
mock.calls.some((c) => c.url.includes("/users/current")),
|
|
"Called /users/current"
|
|
);
|
|
assert.ok(
|
|
mock.calls.some((c) => c.url.includes("/chat_session/create")),
|
|
"Created session"
|
|
);
|
|
assert.ok(
|
|
mock.calls.some((c) => c.url.includes("/create_pow_challenge")),
|
|
"Got PoW challenge"
|
|
);
|
|
assert.ok(
|
|
mock.calls.some((c) => c.url.includes("/chat/completion")),
|
|
"Called completion"
|
|
);
|
|
|
|
// 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.ok(body.prompt.includes("Say hello"), "Prompt should contain user message");
|
|
} finally {
|
|
mock.restore();
|
|
}
|
|
});
|
|
|
|
test("execute: full flow with mocked API (non-streaming)", async () => {
|
|
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: { apiKey: "test-user-token-ns" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
|
|
assert.ok(result.response.ok);
|
|
const json = JSON.parse(await result.response.text());
|
|
assert.equal(json.object, "chat.completion");
|
|
assert.equal(json.choices[0].message.role, "assistant");
|
|
assert.equal(json.choices[0].message.content, "Hello");
|
|
assert.equal(json.choices[0].finish_reason, "stop");
|
|
} finally {
|
|
mock.restore();
|
|
}
|
|
});
|
|
|
|
test("execute: sends PoW response header", async () => {
|
|
const mock = await mockDeepSeekFlow();
|
|
try {
|
|
const executor = new DeepSeekWebExecutor();
|
|
await executor.execute({
|
|
model: "default",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: true,
|
|
credentials: { apiKey: "test-user-token-pow" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
|
|
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 {
|
|
mock.restore();
|
|
}
|
|
});
|
|
|
|
test("execute: handles API error (token fetch fails)", async () => {
|
|
const original = globalThis.fetch;
|
|
globalThis.fetch = async (url) => {
|
|
if (url.includes("/users/current")) {
|
|
return new Response(JSON.stringify({ code: 0, data: { biz_data: null } }), {
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
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: { 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" } } }), {
|
|
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")) {
|
|
return new Response("Unauthorized", { status: 401 });
|
|
}
|
|
return new Response("", { status: 404 });
|
|
};
|
|
try {
|
|
const executor = new DeepSeekWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "default",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: true,
|
|
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" } } }), {
|
|
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")) {
|
|
return new Response(JSON.stringify({ code: 40003, msg: "INVALID_TOKEN", data: null }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
return new Response("", { status: 404 });
|
|
};
|
|
try {
|
|
const executor = new DeepSeekWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "default",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: true,
|
|
credentials: { apiKey: "test-invalid-token" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
assert.equal(result.response.status, 401);
|
|
const text = await result.response.text();
|
|
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 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: { apiKey: "test-user-token-r1" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
assert.equal(capturedBody.model_type, "default");
|
|
assert.equal(capturedBody.thinking_enabled, true);
|
|
} finally {
|
|
mock.restore();
|
|
}
|
|
});
|
|
|
|
// ─── Auto-refresh executor ───────────────────────────────────────────────
|
|
|
|
test("DeepSeekWebWithAutoRefresh extends DeepSeekWebExecutor", () => {
|
|
const exec = new DeepSeekWebWithAutoRefreshExecutor({ autoRefresh: false });
|
|
assert.ok(exec instanceof DeepSeekWebExecutor);
|
|
});
|
|
|
|
test("isSessionValid starts false", () => {
|
|
const exec = new DeepSeekWebWithAutoRefreshExecutor({ autoRefresh: false });
|
|
assert.equal(exec.isSessionValid(), 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();
|
|
const result = await executor.execute({
|
|
model: "default",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: true,
|
|
credentials: { apiKey: "test-token-abort" },
|
|
signal: controller.signal,
|
|
});
|
|
assert.ok(result.response, "Should return response");
|
|
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 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: { apiKey: "test-token-search" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
assert.equal(capturedBody.search_enabled, true);
|
|
} finally {
|
|
mock.restore();
|
|
}
|
|
});
|
|
|
|
test("execute: search_enabled defaults to false", async () => {
|
|
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: { apiKey: "test-token-nosearch" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
assert.equal(capturedBody.search_enabled, false);
|
|
} finally {
|
|
mock.restore();
|
|
}
|
|
});
|
|
|
|
// ─── Thinking enabled via body ───────────────────────────────────────────
|
|
|
|
test("execute: thinking_enabled from body overrides model mapping", async () => {
|
|
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: "think" }], thinking_enabled: true },
|
|
stream: true,
|
|
credentials: { apiKey: "test-token-think" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
assert.equal(capturedBody.thinking_enabled, true);
|
|
} finally {
|
|
mock.restore();
|
|
}
|
|
});
|
|
|
|
// ─── File IDs ────────────────────────────────────────────────────────────
|
|
|
|
test("execute: passes ref_file_ids from body", async () => {
|
|
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: "analyze this" }],
|
|
ref_file_ids: ["file-abc-123", "file-def-456"],
|
|
},
|
|
stream: true,
|
|
credentials: { apiKey: "test-token-files" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
assert.deepEqual(capturedBody.ref_file_ids, ["file-abc-123", "file-def-456"]);
|
|
} finally {
|
|
mock.restore();
|
|
}
|
|
});
|
|
|
|
// ─── Expert model ────────────────────────────────────────────────────────
|
|
|
|
test("execute: maps expert model with thinking", async () => {
|
|
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: { apiKey: "test-token-expert" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
assert.equal(capturedBody.model_type, "expert");
|
|
assert.equal(capturedBody.thinking_enabled, false);
|
|
} finally {
|
|
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();
|
|
}
|
|
});
|
|
|
|
// ─── Session management ──────────────────────────────────────────────────
|
|
|
|
test("execute: always creates a new session per request (no caching)", async () => {
|
|
const mock = await mockDeepSeekFlow();
|
|
try {
|
|
const executor = new DeepSeekWebExecutor();
|
|
await executor.execute({
|
|
model: "deepseek-v4-flash",
|
|
body: { messages: [{ role: "user", content: "first" }] },
|
|
stream: true,
|
|
credentials: { apiKey: "test-session-fresh-key" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
await executor.execute({
|
|
model: "deepseek-v4-flash",
|
|
body: { messages: [{ role: "user", content: "second" }] },
|
|
stream: true,
|
|
credentials: { apiKey: "test-session-fresh-key" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
const sessionCalls = mock.calls.filter((c) => c.url.includes("/chat_session/create"));
|
|
assert.equal(sessionCalls.length, 2, "Should create a fresh session for every request");
|
|
} finally {
|
|
mock.restore();
|
|
}
|
|
});
|
|
|
|
test("execute: always deletes session after non-streaming response", async () => {
|
|
const mock = await mockDeepSeekFlow();
|
|
try {
|
|
const executor = new DeepSeekWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "deepseek-v4-flash",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: false,
|
|
credentials: { apiKey: "test-delete-always-key" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
assert.ok(result.response.ok, "Should succeed");
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
const deleteCalls = mock.calls.filter((c) => c.url.includes("/chat_session/delete"));
|
|
assert.equal(deleteCalls.length, 1, "Should always delete session after response");
|
|
} finally {
|
|
mock.restore();
|
|
}
|
|
});
|
|
|
|
test("execute: always deletes session after streaming response completes", async () => {
|
|
const mock = await mockDeepSeekFlow();
|
|
try {
|
|
const executor = new DeepSeekWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "deepseek-v4-flash",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: true,
|
|
credentials: { apiKey: "test-delete-stream-key" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
assert.ok(result.response.ok, "Should succeed");
|
|
await result.response.text();
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
const deleteCalls = mock.calls.filter((c) => c.url.includes("/chat_session/delete"));
|
|
assert.equal(deleteCalls.length, 1, "Should delete session after stream ends");
|
|
} finally {
|
|
mock.restore();
|
|
}
|
|
});
|
|
|
|
// ─── Thinking content separation ─────────────────────────────────────────
|
|
|
|
test("execute: THINK fragments emit as reasoning_content, not content", async () => {
|
|
const dsMod = await import("../../open-sse/executors/deepseek-web.ts");
|
|
if (dsMod.tokenCache) dsMod.tokenCache.clear();
|
|
|
|
const original = globalThis.fetch;
|
|
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-think" } } }), {
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
if (urlStr.includes("/chat_session/create")) {
|
|
return new Response(
|
|
JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s-think" } } } }),
|
|
{ headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
if (urlStr.includes("/chat_session/delete")) {
|
|
return new Response(JSON.stringify({ code: 0 }), {
|
|
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: "311b26ae1e0fe7375e242958ce46db5552a6c67fea3f96880dcd846c63a74286",
|
|
salt: "1122334455667788",
|
|
signature: "sig123",
|
|
difficulty: 1000,
|
|
expire_at: 1778891543095,
|
|
expire_after: 300000,
|
|
target_path: "/api/v0/chat/completion",
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
{ headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
if (urlStr.includes("/chat/completion")) {
|
|
const enc = new TextEncoder();
|
|
const sse = [
|
|
'data: {"v":{"response":{"thinking_enabled":true,"fragments":[{"id":2,"type":"THINK","content":""}]}}}\n',
|
|
"\n",
|
|
'data: {"p":"response/fragments/-1/content","o":"APPEND","v":"I am thinking..."}\n',
|
|
"\n",
|
|
'data: {"p":"response/fragments","o":"APPEND","v":{"id":3,"type":"RESPONSE","content":""}}\n',
|
|
"\n",
|
|
'data: {"p":"response/fragments/-1/content","v":"Here is the answer."}\n',
|
|
"\n",
|
|
'data: {"p":"response/status","o":"SET","v":"FINISHED"}\n',
|
|
"\n",
|
|
].join("");
|
|
return new Response(enc.encode(sse), {
|
|
status: 200,
|
|
headers: { "Content-Type": "text/event-stream" },
|
|
});
|
|
}
|
|
return new Response("", { status: 404 });
|
|
};
|
|
try {
|
|
const executor = new DeepSeekWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "deepseek-r1",
|
|
body: { messages: [{ role: "user", content: "think about this" }] },
|
|
stream: true,
|
|
credentials: { apiKey: "test-think-key" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
assert.ok(result.response.ok);
|
|
const text = await result.response.text();
|
|
assert.ok(
|
|
text.includes('"reasoning_content":"I am thinking..."'),
|
|
"Thinking should be reasoning_content"
|
|
);
|
|
assert.ok(text.includes('"content":"Here is the answer."'), "Response should be content");
|
|
assert.ok(
|
|
!text.includes('"content":"I am thinking..."'),
|
|
"Thinking should NOT be in content field"
|
|
);
|
|
} finally {
|
|
globalThis.fetch = original;
|
|
if (dsMod.tokenCache) dsMod.tokenCache.clear();
|
|
}
|
|
});
|
|
|
|
test("execute: search model converts citation tags and appends search results", async () => {
|
|
const dsMod = await import("../../open-sse/executors/deepseek-web.ts");
|
|
if (dsMod.tokenCache) dsMod.tokenCache.clear();
|
|
|
|
const original = globalThis.fetch;
|
|
globalThis.fetch = async (url) => {
|
|
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-search" } } }),
|
|
{ headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
if (urlStr.includes("/chat_session/create")) {
|
|
return new Response(
|
|
JSON.stringify({ code: 0, data: { biz_data: { chat_session: { id: "s-search" } } } }),
|
|
{ headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
if (urlStr.includes("/chat_session/delete")) {
|
|
return new Response(JSON.stringify({ code: 0 }), {
|
|
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: "311b26ae1e0fe7375e242958ce46db5552a6c67fea3f96880dcd846c63a74286",
|
|
salt: "1122334455667788",
|
|
signature: "sig123",
|
|
difficulty: 1000,
|
|
expire_at: 1778891543095,
|
|
expire_after: 300000,
|
|
target_path: "/api/v0/chat/completion",
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
{ headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
if (urlStr.includes("/chat/completion")) {
|
|
const enc = new TextEncoder();
|
|
const sse = [
|
|
'data: {"v":{"response":{"fragments":[{"id":1,"type":"RESPONSE","content":""}]}}}\n',
|
|
"\n",
|
|
'data: {"p":"response/fragments/-1/content","v":"Today is Monday [citation:10]."}\n',
|
|
"\n",
|
|
'data: {"p":"response/status","o":"SET","v":"FINISHED"}\n',
|
|
"\n",
|
|
'data: {"p":"response/search_results","v":[{"title":"Example","url":"https://example.com","cite_index":10}]}\n',
|
|
"\n",
|
|
].join("");
|
|
return new Response(enc.encode(sse), {
|
|
status: 200,
|
|
headers: { "Content-Type": "text/event-stream" },
|
|
});
|
|
}
|
|
return new Response("", { status: 404 });
|
|
};
|
|
try {
|
|
const executor = new DeepSeekWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "deepseek-v4-flash-search",
|
|
body: { messages: [{ role: "user", content: "what day is it" }] },
|
|
stream: true,
|
|
credentials: { apiKey: "test-search-key" },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
assert.ok(result.response.ok);
|
|
const text = await result.response.text();
|
|
assert.ok(
|
|
text.includes('"model":"deepseek-v4-flash-search"'),
|
|
"Should preserve client model id"
|
|
);
|
|
assert.ok(text.includes("Today is Monday [10]."), "Should convert citation tags");
|
|
assert.ok(
|
|
text.includes("[10]: [Example](https://example.com)"),
|
|
"Should append search citations"
|
|
);
|
|
assert.ok(!text.includes("[citation:10]"), "Should not leak raw citation tags");
|
|
} finally {
|
|
globalThis.fetch = original;
|
|
if (dsMod.tokenCache) dsMod.tokenCache.clear();
|
|
}
|
|
});
|