From 810399c27e97e184d4d77cca6ac5a530a9a60683 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Sat, 6 Jun 2026 03:54:31 +0700 Subject: [PATCH] feat(duckduckgo-web): add tool-call translation via webTools prompt injection DuckDuckGo AI Chat has no native function calling. Serialize OpenAI tools[] into system prompt instructions (webTools.ts), parse {...} blocks from response text back into OpenAI tool_calls. Pattern: same as deepseek-web (#2820) and qwen-web (#3252). When tools requested, buffers full response for tool-call parsing. When no tools, standard streaming passthrough. --- open-sse/executors/duckduckgo-web.ts | 41 +++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/open-sse/executors/duckduckgo-web.ts b/open-sse/executors/duckduckgo-web.ts index 0cea817dec44..e03d32fff211 100644 --- a/open-sse/executors/duckduckgo-web.ts +++ b/open-sse/executors/duckduckgo-web.ts @@ -1,5 +1,6 @@ import { BaseExecutor, type ExecuteInput } from "./base.ts"; import { FETCH_TIMEOUT_MS } from "../config/constants.ts"; +import { serializeToolsToPrompt, parseToolCallsFromText } from "../translator/webTools.ts"; export const DUCKDUCKGO_BASE = "https://duckduckgo.com"; const STATUS_URL = `${DUCKDUCKGO_BASE}/duckchat/v1/status`; @@ -64,7 +65,7 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { } async execute(input: ExecuteInput) { - const { model, messages, stream, signal, upstreamHeaders } = input; + const { model, messages, stream, signal, upstreamHeaders, body } = input; if (!messages || messages.length === 0) { return new Response( @@ -73,6 +74,14 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { ); } + const bodyObj = (body || {}) as Record; + const requestedTools = bodyObj.tools; + const hasTools = Array.isArray(requestedTools) && requestedTools.length > 0; + const toolSystemPrompt = hasTools ? serializeToolsToPrompt(requestedTools) : ""; + const effectiveMessages = toolSystemPrompt + ? [{ role: "system", content: toolSystemPrompt }, ...messages] + : messages; + // Acquire session from pool for fingerprint rotation const pool = this.getPool(); let session; @@ -117,7 +126,7 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { }, body: JSON.stringify({ model, - messages, + messages: effectiveMessages, stream: stream !== false, }), signal: mergedSignal, @@ -145,13 +154,13 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { }, body: JSON.stringify({ model, - messages, + messages: effectiveMessages, stream: stream !== false, }), signal: mergedSignal, }); - return this.processResponse(retryResponse, stream !== false); + return this.processResponse(retryResponse, stream !== false, hasTools, requestedTools); } return new Response( JSON.stringify({ error: { message: "Service unavailable" } }), @@ -167,7 +176,7 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { ); } - const result = this.processResponse(chatResponse, stream !== false); + const result = this.processResponse(chatResponse, stream !== false, hasTools, requestedTools); // Report pool status based on response if (pool && session) { @@ -222,7 +231,7 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { } } - private async processResponse(response: Response, streaming: boolean): Promise { + private async processResponse(response: Response, streaming: boolean, hasTools?: boolean, requestedTools?: unknown): Promise { if (!response.ok) { const body = await response.text(); return new Response(body, { @@ -231,7 +240,7 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { }); } - if (streaming) { + if (streaming && !hasTools) { const reader = response.body?.getReader(); if (!reader) { return new Response( @@ -304,6 +313,24 @@ export class DuckDuckGoWebExecutor extends BaseExecutor { } } + if (hasTools) { + const { content: cleanedContent, toolCalls } = parseToolCallsFromText( + fullContent, `call-${Date.now()}`, requestedTools + ); + const choice: Record = { + index: 0, + message: { role: "assistant", content: cleanedContent }, + finish_reason: toolCalls ? "tool_calls" : "stop", + }; + if (toolCalls) { + (choice.message as Record).tool_calls = toolCalls; + (choice.message as Record).content = null; + } + return new Response(JSON.stringify({ choices: [choice] }), { + headers: { "Content-Type": "application/json" }, + }); + } + const openaiResponse = { choices: [ {