From f3404227d29344e59e164ee93bc6bd6dc387f867 Mon Sep 17 00:00:00 2001 From: Tushar Agarwal <76201310+Tushar49@users.noreply.github.com> Date: Fri, 29 May 2026 09:26:16 +0530 Subject: [PATCH] fix(deepseek-web): return 400 when client sends tools[] - chat.deepseek.com has no tool support (#2854) Integrated into release/v3.8.6. --- open-sse/executors/deepseek-web.ts | 24 +++++++++++++ tests/unit/deepseek-web.test.ts | 57 ++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/open-sse/executors/deepseek-web.ts b/open-sse/executors/deepseek-web.ts index 376bafbab2..0076126ddb 100644 --- a/open-sse/executors/deepseek-web.ts +++ b/open-sse/executors/deepseek-web.ts @@ -683,6 +683,30 @@ export class DeepSeekWebExecutor extends BaseExecutor { async execute({ model, body, stream, credentials, signal, log }: ExecuteInput) { const bodyObj = (body || {}) as Record; + + // chat.deepseek.com's web API only accepts {prompt, ref_file_ids, + // thinking_enabled, search_enabled} - no tools field. Silently dropping + // tools[] is misleading: models reply as if no tools were ever offered, + // causing agentic clients (OpenAI-compatible) to hallucinate "I don't + // have that tool". Fail fast with a clear error so callers route to the + // official DeepSeek API (provider: 'deepseek') or a different provider + // for tool-using requests. See #2848. + const requestedTools = bodyObj.tools; + if (Array.isArray(requestedTools) && requestedTools.length > 0) { + return { + response: errorResponse( + 400, + "deepseek-web upstream (chat.deepseek.com) does not support function calling. " + + "The web interface only accepts text + thinking_enabled + search_enabled flags. " + + "Use provider 'deepseek' (official api.deepseek.com) for tool-using requests, " + + "or route through a different provider." + ), + url: COMPLETION_URL, + headers: {}, + transformedBody: body, + }; + } + const messages = (Array.isArray(bodyObj.messages) ? bodyObj.messages : []) as Array<{ role: string; content: string; diff --git a/tests/unit/deepseek-web.test.ts b/tests/unit/deepseek-web.test.ts index bfc7ab0738..3271b65c96 100644 --- a/tests/unit/deepseek-web.test.ts +++ b/tests/unit/deepseek-web.test.ts @@ -58,6 +58,63 @@ test("execute returns 400 with empty apiKey", async () => { assert.equal(result.response.status, 400); }); +test("execute returns 400 when client sends non-empty tools[] (chat.deepseek.com has no tool support) - issue #2848", async () => { + const executor = new DeepSeekWebExecutor(); + const result = await executor.execute({ + model: "default", + body: { + messages: [{ role: "user", content: "call my_tool" }], + tools: [ + { + type: "function", + function: { + name: "my_tool", + description: "test", + parameters: { type: "object", properties: {} }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "any-valid-looking-token" }, + signal: AbortSignal.timeout(5000), + }); + assert.equal(result.response.status, 400); + const text = await result.response.text(); + assert.ok( + /does not support function calling/i.test(text), + `expected explanatory error body, got: ${text}` + ); + assert.ok( + /provider 'deepseek'|api\.deepseek\.com/i.test(text), + `expected guidance to use official deepseek provider, got: ${text}` + ); +}); + +test("execute does NOT 400 on tools[]=[] (empty array, equivalent to no tools)", async () => { + const executor = new DeepSeekWebExecutor(); + const result = await executor.execute({ + model: "default", + body: { + messages: [{ role: "user", content: "hi" }], + tools: [], + }, + stream: false, + credentials: {}, + signal: AbortSignal.timeout(5000), + }); + assert.equal( + result.response.status, + 400, + "still returns 400 but for missing userToken, NOT for tools[]" + ); + const text = await result.response.text(); + assert.ok( + text.includes("userToken"), + `expected userToken error (not tools error) for empty tools[], got: ${text}` + ); +}); + // ─── Test connection ───────────────────────────────────────────────────── test("testConnection returns false with empty credentials", async () => {