Skip to content

feat(duckduckgo-web): add tool-call translation via webTools prompt injection - #3253

Closed
oyi77 wants to merge 1 commit into
diegosouzapw:release/v3.8.11from
oyi77:feat/ddg-web-tool-calls
Closed

oyi77 wants to merge 1 commit into
diegosouzapw:release/v3.8.11from
oyi77:feat/ddg-web-tool-calls

Conversation

@oyi77

@oyi77 oyi77 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Description

DuckDuckGo AI Chat has no native function calling support. This PR adds tool-call translation using the same pattern as deepseek-web (#2820) and qwen-web (#3252).

How it works

  1. Request side: Serialize OpenAI tools[] into a system prompt instruction via serializeToolsToPrompt() from webTools.ts. Prepends tool instructions to messages array.

  2. Response side: Parse <tool>{...}</tool> blocks from model text response via parseToolCallsFromText(). Returns proper OpenAI tool_calls format.

When tools are requested, buffers full response for tool-call parsing. When no tools are requested, standard streaming passthrough is used (no behavior change).

Files Changed

  • open-sse/executors/duckduckgo-web.ts — +34 lines (tool injection + parsing)

Tests

All 15 existing tests pass.

…njection

DuckDuckGo AI Chat has no native function calling. Serialize OpenAI tools[] into system prompt instructions (webTools.ts), parse <tool>{...}</tool> blocks from response text back into OpenAI tool_calls.

Pattern: same as deepseek-web (diegosouzapw#2820) and qwen-web (diegosouzapw#3252). When tools requested, buffers full response for tool-call parsing. When no tools, standard streaming passthrough.
@oyi77
oyi77 requested a review from diegosouzapw as a code owner June 5, 2026 20:54

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces tool-calling support to the DuckDuckGo web executor by serializing requested tools into a system prompt and parsing tool calls from the response. Feedback on these changes highlights a critical issue where returning a standard JSON response when streaming is enabled with tools will break clients expecting an SSE stream; a code suggestion was provided to format the tool calls as SSE chunks. Additionally, the changes violate the repository style guide as no tests were added for the new production code under open-sse/.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +316 to +332
if (hasTools) {
const { content: cleanedContent, toolCalls } = parseToolCallsFromText(
fullContent, `call-${Date.now()}`, requestedTools
);
const choice: Record<string, unknown> = {
index: 0,
message: { role: "assistant", content: cleanedContent },
finish_reason: toolCalls ? "tool_calls" : "stop",
};
if (toolCalls) {
(choice.message as Record<string, unknown>).tool_calls = toolCalls;
(choice.message as Record<string, unknown>).content = null;
}
return new Response(JSON.stringify({ choices: [choice] }), {
headers: { "Content-Type": "application/json" },
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When streaming is true and hasTools is true, returning a standard JSON response with Content-Type: application/json will break clients that expect an SSE stream (text/event-stream). Instead, if streaming is true, we should format the tool calls (or plain text) as SSE chunks and return a text/event-stream response.

      if (hasTools) {
        const { content: cleanedContent, toolCalls } = parseToolCallsFromText(
          fullContent,
          "call-" + Date.now(),
          requestedTools
        );
        if (streaming) {
          const chunks: string[] = [];
          if (toolCalls) {
            chunks.push(JSON.stringify({
              choices: [{
                index: 0,
                delta: { role: "assistant", content: null, tool_calls: toolCalls },
                finish_reason: null
              }]
            }));
            chunks.push(JSON.stringify({
              choices: [{
                index: 0,
                delta: {},
                finish_reason: "tool_calls"
              }]
            }));
          } else {
            if (cleanedContent) {
              chunks.push(JSON.stringify({
                choices: [{
                  index: 0,
                  delta: { role: "assistant", content: cleanedContent },
                  finish_reason: null
                }]
              }));
            }
            chunks.push(JSON.stringify({
              choices: [{
                index: 0,
                delta: {},
                finish_reason: "stop"
              }]
            }));
          }
          const sseData = chunks.map(function(c) { return "data: " + c + "\n\n"; }).join("") + "data: [DONE]\n\n";
          return new Response(sseData, {
            headers: { "Content-Type": "text/event-stream" },
          });
        } else {
          const choice: Record<string, unknown> = {
            index: 0,
            message: { role: "assistant", content: cleanedContent },
            finish_reason: toolCalls ? "tool_calls" : "stop",
          };
          if (toolCalls) {
            (choice.message as Record<string, unknown>).tool_calls = toolCalls;
            (choice.message as Record<string, unknown>).content = null;
          }
          return new Response(JSON.stringify({ choices: [choice] }), {
            headers: { "Content-Type": "application/json" },
          });
        }
      }

@@ -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";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the Repository Style Guide (Rule 9), you must always include tests when changing production code under open-sse/. Please add unit/integration tests for the new tool-call translation logic in the tests/ directory.

References
  1. Always include tests when changing production code (src/, open-sse/, electron/, bin/). (link)

const requestedTools = bodyObj.tools;
const hasTools = Array.isArray(requestedTools) && requestedTools.length > 0;
const toolSystemPrompt = hasTools ? serializeToolsToPrompt(requestedTools) : "";
const effectiveMessages = toolSystemPrompt

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Unconditionally prepending a system message can collide with an existing system message in the user's messages array. If messages[0].role === "system", the tool instructions will precede the user's intended system prompt, potentially causing the upstream model to process them in an unintended order or ignore the user's system message entirely. Consider merging tool instructions into the existing system message if one is present, or appending after any existing system message.

}

if (streaming) {
if (streaming && !hasTools) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: When stream !== false and tools are present, this condition bypasses the SSE streaming path entirely and returns a buffered non-streaming JSON response. The client's stream=true request is silently downgraded to a single JSON payload, breaking the streaming contract for clients that always expect SSE chunks. Tool calls should either be streamed as SSE delta events or the executor should reject stream=true with tools rather than silently changing the response format.

@kilo-code-bot

kilo-code-bot Bot commented Jun 5, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
WARNING 2
SUGGESTION 0
CRITICAL 0
Issue Details (click to expand)

WARNING

File Line Issue
open-sse/executors/duckduckgo-web.ts 81 Unconditionally prepending a system message can collide with an existing user-provided system message. If messages[0].role === "system", tool instructions will precede or replace the user's intended system prompt. Consider merging into the existing system message instead of blindly prepending.
open-sse/executors/duckduckgo-web.ts 243 When stream !== false and tools are present, the streaming path is bypassed and a non-streaming JSON response is returned. This silently downgrades the client's streaming request, breaking the SSE contract. Tool calls should be streamed as SSE delta events, or the executor should reject stream=true with an explicit error rather than changing the response format.
Other Observations (not in diff)
File Line Issue
open-sse/executors/duckduckgo-web.ts 332 Existing review comment on tool-call parsing logic.
open-sse/executors/duckduckgo-web.ts 3 Existing review comment on import statement.
Files Reviewed (1 file)
  • open-sse/executors/duckduckgo-web.ts - 2 issues

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash-20260528 · 290,000 tokens

@oyi77

oyi77 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #3259 (consolidated PR with shared helpers)

@oyi77 oyi77 closed this Jun 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant