From e21ebaa3895adb633f6808fb94cd1c809d614fc8 Mon Sep 17 00:00:00 2001 From: Raxxoor Date: Fri, 1 May 2026 20:11:32 +0100 Subject: [PATCH] fix(grok-web): stabilize tool calling and response parsing (#1857) Integrated into release/v3.7.8 --- next.config.mjs | 6 +- open-sse/config/providerRegistry.ts | 8 +- open-sse/executors/grok-web.ts | 1068 +++++++++++++- tests/unit/grok-web.test.ts | 2027 +++++++++++++++++++++++++++ 4 files changed, 3077 insertions(+), 32 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index 79348ec773..fa07570931 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -102,11 +102,6 @@ const nextConfig = { }) ); - // Mark @ngrok/ngrok as external to prevent webpack from trying to bundle its .node binaries - config.externals = config.externals || []; - config.externals.push({ - "@ngrok/ngrok": "commonjs @ngrok/ngrok", - }); // ── Turbopack / Next.js 16 module-hash patch (#394, #396, #398) ──────── // // Next.js 16 (with or without Turbopack) compiles the instrumentation hook @@ -128,6 +123,7 @@ const nextConfig = { const KNOWN_EXTERNALS = new Set([ "better-sqlite3", "keytar", + "@ngrok/ngrok", "wreq-js", "zod", "pino", diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 60b11ae6bc..c65d7f8f64 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -1127,10 +1127,10 @@ export const REGISTRY: Record = { authHeader: "cookie", passthroughModels: true, models: [ - { id: "fast", name: "Grok Fast" }, - { id: "expert", name: "Grok 4.20 Thinking" }, - { id: "heavy", name: "Grok 4.20 Multi Agent" }, - { id: "grok-420-computer-use-sa", name: "Grok 4.3 (Beta)" }, + { id: "fast", name: "Grok Fast", toolCalling: true }, + { id: "expert", name: "Grok 4.20 Thinking", toolCalling: true }, + { id: "heavy", name: "Grok 4.20 Multi Agent", toolCalling: true }, + { id: "grok-420-computer-use-sa", name: "Grok 4.3 (Beta)", toolCalling: true }, ], }, diff --git a/open-sse/executors/grok-web.ts b/open-sse/executors/grok-web.ts index fc97f94799..9e9f79018c 100644 --- a/open-sse/executors/grok-web.ts +++ b/open-sse/executors/grok-web.ts @@ -84,25 +84,227 @@ function randomHex(bytes: number): string { // ─── OpenAI message → Grok query translation ─────────────────────────────── -function parseOpenAIMessages(messages: Array>): string { +interface OpenAIToolCall { + id: string; + type: "function"; + function: { + name: string; + arguments: string; + }; +} + +interface GrokToolRegistry { + enabled: boolean; + toolsByName: Map; + lastUserText: string; + executedToolKeys: Set; + completedToolCalls: string[]; +} + +interface GrokFunctionToolSummary { + name: string; + description?: string; + parameters: unknown; +} + +type NativeToolIntent = "bash" | "readFile" | "webSearch" | "browsePage"; + +interface ToolBridgeContext { + lastUserText: string; +} + +function stripInjectedRuntimeReminders(text: string): string { + return text + .replace(/\n?---\s*\n\s*[\s\S]*?<\/internal_reminder>/gi, "") + .replace(/[\s\S]*?<\/internal_reminder>/gi, "") + .replace(/\n{3,}/g, "\n\n") + .trim(); +} + +function extractTextContent(msg: Record): string { + if (typeof msg.content === "string") return stripInjectedRuntimeReminders(msg.content); + if (Array.isArray(msg.content)) { + return stripInjectedRuntimeReminders((msg.content as Array>) + .filter((c) => c.type === "text") + .map((c) => String(c.text || "")) + .join(" ")); + } + return ""; +} + +function getLastUserText(messages: Array>): string { + for (let i = messages.length - 1; i >= 0; i--) { + if (String(messages[i].role || "") === "user") return extractTextContent(messages[i]); + } + return ""; +} + +function normalizeToolArgumentObject(value: unknown): Record { + if (!value) return {}; + if (typeof value === "object") return value as Record; + if (typeof value === "string") { + try { + const parsed = JSON.parse(value); + return parsed && typeof parsed === "object" ? (parsed as Record) : { input: value }; + } catch { + return { input: value }; + } + } + return {}; +} + +function stableJson(value: unknown): string { + if (!value || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; + const record = value as Record; + return `{${Object.keys(record) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`) + .join(",")}}`; +} + +function toolCallKey(name: string, args: unknown): string { + return `${name}:${stableJson(normalizeToolArgumentObject(args))}`; +} + +function normalizeShellCommand(command: string): string { + return command.trim().replace(/\s+/g, " "); +} + +function normalizePathValue(path: string): string { + return path.trim().replace(/^['"]|['"]$/g, ""); +} + +function normalizeQueryValue(query: string): string { + return query.trim().replace(/\s+/g, " ").toLowerCase(); +} + +function semanticToolKey(name: string, args: unknown): string { + const normalizedName = name.trim(); + const record = normalizeToolArgumentObject(args); + const command = firstString(record.command, record.cmd, record.shell); + if (command) return `${normalizedName}:command:${normalizeShellCommand(command)}`; + + const url = firstString(record.url, record.uri); + if (url) return `${normalizedName}:url:${normalizePathValue(url)}`; + + const path = firstString(record.filePath, record.file_path, record.path, record.filename); + if (path) return `${normalizedName}:path:${normalizePathValue(path)}`; + + const query = firstString(record.query, record.search); + if (query) return `${normalizedName}:query:${normalizeQueryValue(query)}`; + + return toolCallKey(normalizedName, record); +} + +function summarizeCompletedToolCall(name: string, args: unknown): string { + const record = normalizeToolArgumentObject(args); + const command = firstString(record.command, record.cmd, record.shell); + if (command) return `${name}(command=${JSON.stringify(normalizeShellCommand(command))})`; + const url = firstString(record.url, record.uri); + if (url) return `${name}(url=${JSON.stringify(normalizePathValue(url))})`; + const path = firstString(record.filePath, record.file_path, record.path, record.filename); + if (path) return `${name}(path=${JSON.stringify(normalizePathValue(path))})`; + const query = firstString(record.query, record.search); + if (query) return `${name}(query=${JSON.stringify(normalizeQueryValue(query))})`; + return `${name}(${stableJson(record)})`; +} + +function getExecutedToolState(messages: Array>): { + keys: Set; + summaries: string[]; +} { + let lastUserIdx = -1; + for (let i = messages.length - 1; i >= 0; i--) { + if (String(messages[i].role || "") === "user") { + lastUserIdx = i; + break; + } + } + const callsById = new Map }>(); + const executed = new Set(); + const summaries: string[] = []; + for (let i = Math.max(0, lastUserIdx + 1); i < messages.length; i++) { + const msg = messages[i]; + if (String(msg.role || "") === "assistant" && Array.isArray(msg.tool_calls)) { + for (const call of msg.tool_calls as Array>) { + const id = typeof call.id === "string" ? call.id : ""; + const fn = call.function; + if (!id || !fn || typeof fn !== "object") continue; + const fnRecord = fn as Record; + const name = typeof fnRecord.name === "string" ? fnRecord.name : ""; + if (!name) continue; + callsById.set(id, { name, args: normalizeToolArgumentObject(fnRecord.arguments) }); + } + } + if (String(msg.role || "") === "tool") { + const id = typeof msg.tool_call_id === "string" ? msg.tool_call_id : ""; + const call = id ? callsById.get(id) : null; + if (call) { + const key = semanticToolKey(call.name, call.args); + executed.add(key); + const summary = summarizeCompletedToolCall(call.name, call.args); + if (!summaries.includes(summary)) summaries.push(summary); + } + } + } + return { keys: executed, summaries }; +} + +function extractFirstUrl(text: string): string | undefined { + const match = text.match(/https?:\/\/[^\s)\]}>"']+/i); + if (match?.[0]) return match[0].replace(/[.,;:!?]+$/, ""); + const domain = text.match(/(?:^|\s)((?:[a-z0-9-]+\.)+[a-z]{2,}(?:\/[^\s)\]}>"']*)?)/i)?.[1]; + return domain ? `https://${domain.replace(/[.,;:!?]+$/, "")}` : undefined; +} + +function wantsUrlFetch(text: string): boolean { + return /\b(webfetch|web_fetch|fetch|browse|open|read|lee|abre|extrae|investiga|analiza|resume|summarize|de qu[eé] va)\b/i.test(text) && !!extractFirstUrl(text); +} + +function forcedToolChoiceName(toolChoice: unknown): string | null { + if (!toolChoice || typeof toolChoice !== "object") return null; + const record = toolChoice as Record; + if (record.type !== "function" || !record.function || typeof record.function !== "object") return null; + const name = (record.function as Record).name; + return typeof name === "string" && name.trim() ? name.trim() : null; +} + +function parseOpenAIMessages(messages: Array>, beforeLatestUser = ""): string { const parts: string[] = []; let lastUserIdx = -1; + let lastUserSourceIdx = -1; + + for (let i = messages.length - 1; i >= 0; i--) { + if (String(messages[i].role || "") === "user") { + lastUserSourceIdx = i; + break; + } + } // Extract text from each message const extracted: Array<{ role: string; text: string }> = []; - for (const msg of messages) { + for (let msgIdx = 0; msgIdx < messages.length; msgIdx++) { + const msg = messages[msgIdx]; let role = String(msg.role || "user"); if (role === "developer") role = "system"; - let content = ""; - if (typeof msg.content === "string") { - content = msg.content; - } else if (Array.isArray(msg.content)) { - content = (msg.content as Array>) - .filter((c) => c.type === "text") - .map((c) => String(c.text || "")) - .join(" "); + let content = extractTextContent(msg); + if (role === "tool") { + if (msgIdx < lastUserSourceIdx) continue; + const toolName = typeof msg.name === "string" ? msg.name : "unknown_tool"; + const toolCallId = typeof msg.tool_call_id === "string" ? msg.tool_call_id : "unknown_call"; + content = `CLIENT TOOL RESULT from caller runtime for ${toolName} (${toolCallId}). Use this result to answer; do not call the same tool again:\n${content}`; + } else if (role === "assistant" && Array.isArray(msg.tool_calls)) { + if (msgIdx < lastUserSourceIdx) continue; + const calls = (msg.tool_calls as Array>).map((call) => ({ + id: call.id, + function: call.function, + })); + content = [content, `Previous assistant tool calls: ${JSON.stringify(calls)}`] + .filter(Boolean) + .join("\n"); } if (!content.trim()) continue; extracted.push({ role, text: content }); @@ -126,17 +328,517 @@ function parseOpenAIMessages(messages: Array>): string { } } + if (beforeLatestUser.trim()) { + parts.push(beforeLatestUser.trim()); + } + return parts.join("\n\n"); } +function buildGrokToolRegistry(body: Record): GrokToolRegistry { + const tools = Array.isArray(body.tools) ? (body.tools as Array>) : []; + const messages = Array.isArray(body.messages) ? (body.messages as Array>) : []; + const lastUserText = getLastUserText(messages); + const executedToolState = getExecutedToolState(messages); + const toolChoice = body.tool_choice ?? "auto"; + + if (toolChoice === "none") { + return { + enabled: false, + toolsByName: new Map(), + lastUserText, + executedToolKeys: executedToolState.keys, + completedToolCalls: executedToolState.summaries, + }; + } + + const functionTools: GrokFunctionToolSummary[] = tools + .map((tool) => { + const fn = tool?.function; + if (tool?.type !== "function" || !fn || typeof fn !== "object") return null; + const record = fn as Record; + const name = typeof record.name === "string" ? record.name.trim() : ""; + if (!name) return null; + return { + name, + ...(typeof record.description === "string" ? { description: record.description } : {}), + parameters: record.parameters || { type: "object", properties: {} }, + }; + }) + .filter((tool): tool is GrokFunctionToolSummary => Boolean(tool)); + const forcedName = forcedToolChoiceName(toolChoice); + const visibleTools = forcedName ? functionTools.filter((tool) => tool.name === forcedName) : functionTools; + + return { + enabled: visibleTools.length > 0, + toolsByName: new Map(visibleTools.map((tool) => [tool.name, tool])), + lastUserText, + executedToolKeys: executedToolState.keys, + completedToolCalls: executedToolState.summaries, + }; +} + +function getSchemaProperties(parameters: unknown): Record { + if (!parameters || typeof parameters !== "object") return {}; + const properties = (parameters as Record).properties; + return properties && typeof properties === "object" ? (properties as Record) : {}; +} + +function getSchemaRequired(parameters: unknown): string[] { + if (!parameters || typeof parameters !== "object") return []; + const required = (parameters as Record).required; + return Array.isArray(required) ? required.filter((key): key is string => typeof key === "string") : []; +} + +function formatToolArgsSummary(parameters: unknown): string { + const properties = getSchemaProperties(parameters); + const propNames = Object.keys(properties); + const required = getSchemaRequired(parameters); + const segments: string[] = []; + if (propNames.length > 0) segments.push(`args=${propNames.join(",")}`); + if (required.length > 0) segments.push(`required=${required.join(",")}`); + return segments.length > 0 ? ` (${segments.join("; ")})` : ""; +} + +function toolText(tool: GrokFunctionToolSummary): string { + return `${tool.name} ${tool.description || ""}`.toLowerCase(); +} + +function hasAnyProperty(tool: GrokFunctionToolSummary, names: string[]): boolean { + const properties = getSchemaProperties(tool.parameters); + const lowerProps = new Set(Object.keys(properties).map((key) => key.toLowerCase())); + return names.some((name) => lowerProps.has(name.toLowerCase())); +} + +function isTerminalTool(tool: GrokFunctionToolSummary): boolean { + if (isMetaOrInfrastructureTool(tool)) return false; + const text = toolText(tool); + const name = tool.name.toLowerCase(); + const explicitName = /\b(bash|shell|terminal|run_command|execute_command|exec|command)\b/.test(name); + const explicitText = /\b(?:run|execute).{0,24}\b(?:shell|bash|terminal|command)\b|\b(?:shell|bash|terminal)\b/.test(text); + return explicitName || (hasAnyProperty(tool, ["command", "cmd", "shell"]) && explicitText); +} + +function isFileReadTool(tool: GrokFunctionToolSummary): boolean { + const text = toolText(tool); + return ( + hasAnyProperty(tool, ["filePath", "file_path", "path"]) && + /\b(read|file|filesystem|open)\b/.test(text) && + !/\b(write|edit|patch|delete|remove|grep|search|bash|shell|command)\b/.test(text) + ); +} + +function isUrlFetchTool(tool: GrokFunctionToolSummary): boolean { + const text = toolText(tool); + const name = tool.name.toLowerCase(); + const explicitName = /\b(webfetch|web.fetch|fetch_url|url_fetch|read_url|browse_page|browsepage)\b/.test(name); + const explicitUrlText = /\b(?:fetch|browse|read).{0,32}\b(?:url|uri|web page|page content)\b|\b(?:url|uri|web page|page content).{0,32}\b(?:fetch|browse|read)\b/.test(text); + return explicitName || (!isMetaOrInfrastructureTool(tool) && hasAnyProperty(tool, ["url", "uri"]) && explicitUrlText); +} + +function isWebSearchTool(tool: GrokFunctionToolSummary): boolean { + const text = toolText(tool); + return ( + hasAnyProperty(tool, ["query", "search"]) && + /\b(web|internet|exa|browser|browse|serp)\b/.test(text) && + !isMetaOrInfrastructureTool(tool) && + !isContextMemoryTool(tool) + ); +} + +function isContextMemoryTool(tool: GrokFunctionToolSummary): boolean { + const text = toolText(tool); + return /\b(ctx_|memory|memories|conversation history|session notes|git commits|project memories|context.db|magic context)\b/.test(text); +} + +function isMetaOrInfrastructureTool(tool: GrokFunctionToolSummary): boolean { + const text = toolText(tool); + return /\b(mcp|mcpproxy|upstream|registry|registries|quarantine|oauth|cache key|token usage|session notes|conversation transcript|handoff|context management|memory|memories|lsp|language server|plan file|server management|tool discovery|tools? using bm25)\b/.test(text); +} + +function baseToolOrderScore(tool: GrokFunctionToolSummary): number { + if (isUrlFetchTool(tool)) return 90; + if (isWebSearchTool(tool)) return 85; + if (isFileReadTool(tool)) return 75; + if (isTerminalTool(tool)) return 70; + if (/\b(glob|grep|search files?|file search|content search)\b/.test(toolText(tool))) return 60; + if (/\b(edit|write|patch|modify|apply)\b/.test(toolText(tool))) return 50; + if (/\b(task|agent|delegate|subagent)\b/.test(toolText(tool))) return 40; + if (isMetaOrInfrastructureTool(tool)) return 10; + if (isContextMemoryTool(tool)) return 20; + return 30; +} + +function latestUserIntentScore(tool: GrokFunctionToolSummary, lastUserText: string): number { + const user = lastUserText.toLowerCase(); + const hasPath = /(?:^|\s|["'`])(?:~|\.?\.?\/|\/)[^\s"'`]+/.test(lastUserText); + const hasUrl = !!extractFirstUrl(lastUserText); + const asksLineCount = /\b(l[ií]neas?|line count|cu[aá]ntas? l[ií]neas?|wc\s+-l)\b/.test(user); + const asksFileContent = /\b(lee|leer|read|archivo|file|json|config|modelo|default|por defecto|de qu[eé] va|consiste|contenido)\b/.test(user) && hasPath; + const asksContext = /\b(contexto|memoria|historial|conversation history|project memories|ctx_|memory|memories|recordabas?)\b/.test(user); + const asksWeb = !asksContext && /\b(web|internet|fuente|oficial|release|versi[oó]n|ubuntu|latest|actual|contrasta|busca|search)\b/.test(user); + let score = 0; + + if (asksFileContent && isFileReadTool(tool)) score += 160; + if (asksFileContent && isTerminalTool(tool)) score += asksLineCount ? 70 : 20; + if (asksLineCount && isTerminalTool(tool)) score += 120; + if (asksLineCount && isFileReadTool(tool)) score += asksFileContent ? 90 : 30; + if (asksWeb && !hasUrl && isWebSearchTool(tool)) score += 170; + if (asksWeb && !hasUrl && isUrlFetchTool(tool)) score += 35; + if (asksWeb && isContextMemoryTool(tool)) score -= 120; + if (asksContext && isContextMemoryTool(tool)) score += 170; + if (asksContext && isWebSearchTool(tool)) score -= 80; + + if (isContextMemoryTool(tool) && (asksFileContent || asksWeb)) score -= 80; + return score; +} + +function orderedToolsForManifest(toolRegistry: GrokToolRegistry): Array<{ tool: GrokFunctionToolSummary; score: number }> { + return [...toolRegistry.toolsByName.values()] + .map((tool, index) => ({ + tool, + score: latestUserIntentScore(tool, toolRegistry.lastUserText) + baseToolOrderScore(tool), + meta: isMetaOrInfrastructureTool(tool), + index, + })) + .sort((a, b) => b.score - a.score || Number(a.meta) - Number(b.meta) || a.index - b.index) + .map(({ tool, score }) => ({ tool, score })); +} + +function formatToolManifestEntry(tool: GrokFunctionToolSummary, rank: number): string { + const desc = tool.description ? `\n description: ${tool.description}` : ""; + const args = formatToolArgsSummary(tool.parameters).trim(); + return `${rank}. name: ${tool.name}${args ? `\n ${args.slice(1, -1)}` : ""}${desc ? desc.replace(/\n /g, "\n ") : ""}`; +} + +function buildClientToolManifest(toolRegistry: GrokToolRegistry, toolChoice: unknown): string { + if (!toolRegistry.enabled) return ""; + const orderedTools = orderedToolsForManifest(toolRegistry); + const lines = [ + "CLIENT_TOOLS: use this caller-runtime tool list as the tool interface for this request. To call one, respond only with {\"name\":\"exact_tool_name\",\"arguments\":{...}}. After tool results, answer normally.", + `tool_choice=${JSON.stringify(toolChoice ?? "auto")}`, + ...(toolRegistry.completedToolCalls.length > 0 + ? [ + "completed_tool_calls:", + ...toolRegistry.completedToolCalls.map((summary) => `- ${summary}`), + "Do not repeat completed tool calls unless a different result is required; use their tool results to answer.", + ] + : []), + "tools (priority order for this request):", + ...orderedTools.map(({ tool }, index) => formatToolManifestEntry(tool, index + 1)), + ]; + return lines.join("\n"); +} + +function buildGrokMessage(messages: Array>, toolRegistry: GrokToolRegistry, toolChoice: unknown): string { + const manifest = buildClientToolManifest(toolRegistry, toolChoice); + return parseOpenAIMessages(messages, manifest); +} + +function propertyType(properties: Record, key: string): string | undefined { + const prop = properties[key]; + if (!prop || typeof prop !== "object") return undefined; + const type = (prop as Record).type; + return typeof type === "string" ? type : undefined; +} + +function hasValue(value: unknown): boolean { + return value !== undefined && value !== null && value !== ""; +} + +function firstString(...values: unknown[]): string | undefined { + for (const value of values) { + if (typeof value === "string" && value.trim()) return value; + } + return undefined; +} + +function defaultRequiredValue(key: string, type: string | undefined, args: Record, intent: string): unknown { + const lower = key.toLowerCase(); + const command = firstString(args.command, args.cmd, args.shell, args.input); + const path = firstString(args.filePath, args.file_path, args.path, args.filename); + const query = firstString(args.query, args.search, args.input); + const url = firstString(args.url, args.uri); + + if (lower === "command" || lower === "cmd") return command; + if (lower === "filepath" || lower === "file_path" || lower === "path") return path; + if (lower === "query" || lower === "search") return query; + if (lower === "url" || lower === "uri") return url; + if (lower === "input") return query || url || command || path; + if (lower === "description" || lower === "reason" || lower === "intent_reason") { + if (command) return `Execute shell command: ${command}`; + if (path) return `Read file: ${path}`; + if (url) return `Fetch URL: ${url}`; + if (query) return `Search: ${query}`; + return `Grok Web ${intent} tool call`; + } + if (lower === "intent_data_sensitivity") return "private"; + return undefined; +} + +function extractNumericUserParam(text: string, names: string[]): number | undefined { + const escaped = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"); + const re = new RegExp(`\\b(?:${escaped})\\s*(?:=|:|a|de)?\\s*(\\d+)`, "i"); + const match = text.match(re); + if (!match) return undefined; + const value = Number(match[1]); + return Number.isFinite(value) ? value : undefined; +} + +function adaptArgumentsToDeclaredTool( + toolName: string, + args: Record, + toolRegistry: GrokToolRegistry, + intent: string, + options: { preserveUnknownArgs?: boolean } = { preserveUnknownArgs: true } +): Record { + const tool = toolRegistry.toolsByName.get(toolName); + if (!tool) return args; + const properties = getSchemaProperties(tool.parameters); + const required = getSchemaRequired(tool.parameters); + const out: Record = { ...args }; + + // Normalize common aliases only when the declared schema expects them. + if ("filePath" in properties && !hasValue(out.filePath)) out.filePath = firstString(args.filePath, args.file_path, args.path); + if ("file_path" in properties && !hasValue(out.file_path)) out.file_path = firstString(args.file_path, args.filePath, args.path); + if ("path" in properties && !hasValue(out.path)) out.path = firstString(args.path, args.filePath, args.file_path); + if ("query" in properties && !hasValue(out.query)) out.query = firstString(args.query, args.search, args.input); + if ("url" in properties && !hasValue(out.url)) out.url = firstString(args.url, args.uri); + if ("uri" in properties && !hasValue(out.uri)) out.uri = firstString(args.uri, args.url); + if ("input" in properties && !hasValue(out.input)) out.input = firstString(args.input, args.query, args.command); + + for (const key of required) { + if (hasValue(out[key])) continue; + const value = defaultRequiredValue(key, propertyType(properties, key), out, intent); + if (value !== undefined) out[key] = value; + } + + if (options.preserveUnknownArgs !== false || Object.keys(properties).length === 0) return out; + + const filtered: Record = {}; + for (const key of Object.keys(properties)) { + if (hasValue(out[key])) filtered[key] = out[key]; + } + for (const key of required) { + if (hasValue(out[key])) filtered[key] = out[key]; + } + return filtered; +} + +function normalizeArbitraryToolArguments(value: unknown): Record { + if (!value) return {}; + if (typeof value === "object") return value as Record; + if (typeof value === "string") { + try { + const parsed = JSON.parse(value); + return parsed && typeof parsed === "object" ? (parsed as Record) : {}; + } catch { + return { input: value }; + } + } + return {}; +} + +function parseClientToolCallMarkup(text: string, toolRegistry: GrokToolRegistry): OpenAIToolCall[] | null { + if (!toolRegistry.enabled || !text.includes("")) return null; + const calls: OpenAIToolCall[] = []; + const re = /\s*([\s\S]*?)\s*<\/tool_call>/g; + for (const match of text.matchAll(re)) { + let parsed: unknown; + try { + parsed = JSON.parse(match[1]); + } catch { + continue; + } + if (!parsed || typeof parsed !== "object") continue; + const record = parsed as Record; + const name = typeof record.name === "string" ? record.name.trim() : ""; + if (!name || !toolRegistry.toolsByName.has(name)) continue; + const rawArgs = normalizeArbitraryToolArguments(record.arguments); + const args = adaptArgumentsToDeclaredTool(name, rawArgs, toolRegistry, "clientTool", { preserveUnknownArgs: true }); + if (toolRegistry.executedToolKeys.has(semanticToolKey(name, args))) continue; + calls.push({ + id: typeof record.id === "string" && record.id.trim() ? record.id.trim() : `call_${crypto.randomUUID()}`, + type: "function", + function: { name, arguments: JSON.stringify(args) }, + }); + } + return calls.length > 0 ? calls : null; +} + +function hasOpenToolCallMarkup(text: string): boolean { + return /]*$/.test(text) || (text.includes("") && !text.includes("")); +} + +function toolScore(tool: GrokFunctionToolSummary, intent: NativeToolIntent, context: ToolBridgeContext): number { + const name = tool.name.toLowerCase(); + const description = (tool.description || "").toLowerCase(); + const properties = getSchemaProperties(tool.parameters); + const propNames = new Set(Object.keys(properties).map((key) => key.toLowerCase())); + const text = `${name} ${description}`; + const userText = context.lastUserText.toLowerCase(); + let score = 0; + + if (intent === "bash") { + if (!isTerminalTool(tool)) score -= 80; + if (name === "bash") score += 100; + if (["shell", "terminal", "run_command", "execute_command", "exec", "command"].includes(name)) score += 80; + if (propNames.has("command") || propNames.has("cmd")) score += 60; + if (/bash|shell|terminal|command|execute|run/.test(text)) score += 25; + if (/read|search|grep|web|http|browser|context|note|memory/.test(name)) score -= 50; + } else if (intent === "readFile") { + if (!isFileReadTool(tool)) score -= 60; + if (["read", "read_file", "readfile", "file_read"].includes(name)) score += 100; + if (propNames.has("filepath") || propNames.has("file_path") || propNames.has("path")) score += 50; + if (/read.*file|file.*read|filesystem/.test(text)) score += 25; + if (/write|edit|delete|remove|bash|shell|command/.test(text)) score -= 50; + } else if (intent === "webSearch" || intent === "browsePage") { + const preferUrlFetch = wantsUrlFetch(userText); + if (isContextMemoryTool(tool) || isMetaOrInfrastructureTool(tool)) score -= 180; + if (intent === "browsePage" || preferUrlFetch) { + if (!isUrlFetchTool(tool)) score -= 60; + if (/webfetch|web_fetch|fetch|browse|browse_page|read_url|url_fetch|page/.test(name)) score += 140; + if (propNames.has("url") || propNames.has("uri")) score += 90; + if (/fetch|browse|url|web page|page content|extract.*url|read.*url/.test(text)) score += 55; + if (/websearch|web_search|search/.test(name) && !(propNames.has("url") || propNames.has("uri"))) score -= 80; + } + if (intent === "webSearch" && !isWebSearchTool(tool)) score -= 60; + if (intent === "browsePage" && /\b(websearch|web_search|search)\b/.test(name) && !(propNames.has("url") || propNames.has("uri"))) score -= 120; + if (["web_search", "websearch", "search"].includes(name)) score += 100; + if (propNames.has("query") || propNames.has("search")) score += 50; + if (/web.*search|search.*web|internet|browse/.test(text)) score += 25; + if (/file|bash|shell|command|write|edit/.test(text)) score -= 50; + } + + return score; +} + +function pickDeclaredToolForIntent(intent: NativeToolIntent, toolRegistry: GrokToolRegistry): string | null { + let best: { name: string; score: number } | null = null; + for (const tool of toolRegistry.toolsByName.values()) { + const score = toolScore(tool, intent, { lastUserText: toolRegistry.lastUserText }); + if (score <= 0) continue; + if (!best || score > best.score) best = { name: tool.name, score }; + } + return best?.name || null; +} + +function mapGrokNativeToolToOpenAI( + resp: GrokStreamResponse, + toolRegistry: GrokToolRegistry +): OpenAIToolCall | null { + if (!toolRegistry.enabled || !resp.toolUsageCard) return null; + const card = resp.toolUsageCard as Record; + const id = resp.toolUsageCardId || String(card.toolUsageCardId || `call_${crypto.randomUUID()}`); + + const bash = card.bash as { args?: Record } | undefined; + if (bash?.args) { + const name = pickDeclaredToolForIntent("bash", toolRegistry); + if (name) { + const args = adaptArgumentsToDeclaredTool(name, bash.args, toolRegistry, "bash", { preserveUnknownArgs: false }); + if (toolRegistry.executedToolKeys.has(semanticToolKey(name, args))) return null; + return { id, type: "function", function: { name, arguments: JSON.stringify(args) } }; + } + } + + const readFile = (card.readFile || card.read_file) as { args?: Record } | undefined; + if (readFile?.args) { + const rawPath = readFile.args.filePath || readFile.args.file_path || readFile.args.path; + const name = pickDeclaredToolForIntent("readFile", toolRegistry); + if (name && typeof rawPath === "string") { + const userOffset = extractNumericUserParam(toolRegistry.lastUserText, ["offset"]); + const userLimit = extractNumericUserParam(toolRegistry.lastUserText, ["limit", "limite", "límite"]); + const rawArgs = { + ...readFile.args, + ...(userOffset !== undefined ? { offset: userOffset } : {}), + ...(userLimit !== undefined ? { limit: userLimit } : {}), + filePath: rawPath, + file_path: rawPath, + path: rawPath, + }; + const args = adaptArgumentsToDeclaredTool( + name, + rawArgs, + toolRegistry, + "readFile", + { preserveUnknownArgs: false } + ); + if (toolRegistry.executedToolKeys.has(semanticToolKey(name, args))) return null; + return { id, type: "function", function: { name, arguments: JSON.stringify(args) } }; + } + } + + const webSearch = card.webSearch as { args?: Record } | undefined; + if (webSearch?.args) { + const name = pickDeclaredToolForIntent("webSearch", toolRegistry); + if (name) { + const requestedUrl = wantsUrlFetch(toolRegistry.lastUserText) ? extractFirstUrl(toolRegistry.lastUserText) : undefined; + const args = adaptArgumentsToDeclaredTool( + name, + requestedUrl ? { ...webSearch.args, url: requestedUrl, uri: requestedUrl } : webSearch.args, + toolRegistry, + requestedUrl ? "webFetch" : "webSearch", + { preserveUnknownArgs: false } + ); + if (toolRegistry.executedToolKeys.has(semanticToolKey(name, args))) return null; + return { id, type: "function", function: { name, arguments: JSON.stringify(args) } }; + } + } + + const browsePage = (card.browsePage || card.browse_page) as { args?: Record } | undefined; + if (browsePage?.args) { + const url = firstString(browsePage.args.url, browsePage.args.uri); + const name = pickDeclaredToolForIntent("browsePage", toolRegistry); + if (name && url) { + const args = adaptArgumentsToDeclaredTool( + name, + { ...browsePage.args, url, uri: url, input: url }, + toolRegistry, + "browsePage", + { preserveUnknownArgs: false } + ); + if (toolRegistry.executedToolKeys.has(semanticToolKey(name, args))) return null; + return { id, type: "function", function: { name, arguments: JSON.stringify(args) } }; + } + } + + return null; +} + // ─── NDJSON stream types ──────────────────────────────────────────────────── interface GrokStreamResponse { token?: string; + isThinking?: boolean; + reasoning?: string; + reasoningContent?: string; + reasoning_content?: string; + thinking?: string; + thought?: string; responseId?: string; + messageTag?: string; + messageStepId?: number; + toolUsageCardId?: string; + toolUsageCard?: { + toolUsageCardId?: string; + bash?: { args?: Record }; + readFile?: { args?: Record }; + read_file?: { args?: Record }; + webSearch?: { args?: Record }; + browsePage?: { args?: Record }; + browse_page?: { args?: Record }; + }; + webSearchResults?: { + results?: Array>; + }; llmInfo?: { modelHash?: string }; modelResponse?: { message?: string; + reasoning?: string; + reasoningContent?: string; + reasoning_content?: string; + thinking?: string; + thought?: string; responseId?: string; generatedImageUrls?: string[]; metadata?: { llm_info?: { modelHash?: string } }; @@ -196,11 +898,147 @@ async function* readGrokNdjsonEvents( } } +// ─── Grok markup cleanup ──────────────────────────────────────────────────── + +const BLOCKED_GROK_MARKUP = [ + { start: "" }, +] as const; + +const PARTIAL_GROK_MARKER_KEEP = 32; + +function stripLooseGrokMarkup(text: string): string { + return text + .replace(/<\/?xai:[^>]*>/g, "") + .replace(/<\/?grok:[^>]*>/g, "") + .replace(/<\/?argument\b[^>]*>/g, "") + .replace(//g, ""); +} + +class GrokMarkupFilter { + private buffer = ""; + private suppressedUntil: string | null = null; + + feed(text: string): string { + if (!text) return ""; + this.buffer += text; + return this.drain(false); + } + + flush(): string { + const out = this.drain(true); + this.buffer = ""; + this.suppressedUntil = null; + return out; + } + + private drain(flush: boolean): string { + let out = ""; + + while (this.buffer) { + if (this.suppressedUntil) { + const endIdx = this.buffer.indexOf(this.suppressedUntil); + if (endIdx < 0) { + this.buffer = this.buffer.slice(this.longestEndPrefixStart(this.suppressedUntil)); + return out; + } + this.buffer = this.buffer.slice(endIdx + this.suppressedUntil.length); + this.suppressedUntil = null; + continue; + } + + let nextStart = -1; + let nextEnd = ""; + for (const marker of BLOCKED_GROK_MARKUP) { + const idx = this.buffer.indexOf(marker.start); + if (idx >= 0 && (nextStart < 0 || idx < nextStart)) { + nextStart = idx; + nextEnd = marker.end; + } + } + + if (nextStart < 0) { + if (!flush) { + const lastLt = this.buffer.lastIndexOf("<"); + if (lastLt >= 0 && this.buffer.length - lastLt <= PARTIAL_GROK_MARKER_KEEP) { + out += stripLooseGrokMarkup(this.buffer.slice(0, lastLt)); + this.buffer = this.buffer.slice(lastLt); + return out; + } + } + out += stripLooseGrokMarkup(this.buffer); + this.buffer = ""; + return out; + } + + out += stripLooseGrokMarkup(this.buffer.slice(0, nextStart)); + this.buffer = this.buffer.slice(nextStart); + const endIdx = this.buffer.indexOf(nextEnd); + const openTagEndIdx = this.buffer.indexOf(">"); + if (openTagEndIdx >= 0 && /\/\s*>$/.test(this.buffer.slice(0, openTagEndIdx + 1))) { + this.buffer = this.buffer.slice(openTagEndIdx + 1); + continue; + } + if (endIdx < 0) { + this.suppressedUntil = nextEnd; + this.buffer = this.buffer.slice(this.longestEndPrefixStart(nextEnd)); + return out; + } + this.buffer = this.buffer.slice(endIdx + nextEnd.length); + } + + return out; + } + + private longestEndPrefixStart(end: string): number { + const max = Math.min(this.buffer.length, end.length - 1); + for (let len = max; len > 0; len--) { + if (this.buffer.slice(-len) === end.slice(0, len)) return this.buffer.length - len; + } + return this.buffer.length; + } +} + +function cleanGrokText(text: string): string { + const filter = new GrokMarkupFilter(); + return filter.feed(text) + filter.flush(); +} + +function cleanGrokContentText(text: string): string { + return cleanGrokText(text); +} + +function cleanGrokThinkingText(resp: GrokStreamResponse): string { + const text = resp.token || ""; + const cleaned = cleanGrokText(text); + const trimmed = cleaned.trim(); + if (!trimmed) return ""; + const isGenericOpeningHeader = + resp.messageTag === "header" && + resp.messageStepId === 0 && + /^(?:\.{3}|thinking(?: about your request)?)$/i.test(trimmed); + if (isGenericOpeningHeader) return ""; + if (resp.messageTag === "header") return `${trimmed}\n`; + if (resp.messageTag === "summary") return `${trimmed}\n`; + return cleaned; +} + +function extractStructuredReasoning(value: object | undefined): string { + if (!value) return ""; + const record = value as Record; + for (const key of ["reasoning", "reasoningContent", "reasoning_content", "thinking", "thought"]) { + const candidate = record[key]; + if (typeof candidate === "string" && candidate.trim()) return cleanGrokText(candidate); + } + return ""; +} + // ─── Content extraction ───────────────────────────────────────────────────── interface ContentChunk { delta?: string; thinking?: string; + toolCalls?: OpenAIToolCall[]; fingerprint?: string; responseId?: string; fullMessage?: string; @@ -211,11 +1049,16 @@ interface ContentChunk { async function* extractContent( eventStream: ReadableStream, isThinkingModel: boolean, - signal?: AbortSignal | null + toolRegistry: GrokToolRegistry, + signal?: AbortSignal | null, + suppressThinkingAfterVisibleContent = false ): AsyncGenerator { let fingerprint = ""; let responseId = ""; - let thinkOpened = false; + const contentFilter = new GrokMarkupFilter(); + const thinkingFilter = new GrokMarkupFilter(); + let emittedThinking = ""; + let emittedVisibleContent = false; for await (const event of readGrokNdjsonEvents(eventStream, signal)) { // Error handling @@ -235,21 +1078,37 @@ async function* extractContent( responseId = resp.responseId; } + const nativeToolCall = mapGrokNativeToolToOpenAI(resp, toolRegistry); + if (nativeToolCall) { + yield { toolCalls: [nativeToolCall], fingerprint, responseId }; + return; + } + + if (resp.messageTag === "raw_function_result" || resp.messageTag === "tool_usage_card") { + continue; + } + // modelResponse = final/complete response if (resp.modelResponse) { const mr = resp.modelResponse; - // Close thinking block if open - if (thinkOpened && isThinkingModel) { - if (mr.message) { - yield { thinking: mr.message }; + const finalThinking = isThinkingModel ? extractStructuredReasoning(mr) : ""; + if ((!suppressThinkingAfterVisibleContent || !emittedVisibleContent) && finalThinking) { + const cleanedThinking = thinkingFilter.feed(finalThinking); + const thinkingDelta = cleanedThinking.startsWith(emittedThinking) + ? cleanedThinking.slice(emittedThinking.length) + : cleanedThinking; + if (thinkingDelta) { + emittedThinking += thinkingDelta; + yield { thinking: thinkingDelta }; } - thinkOpened = false; } // Extract final message if (mr.message) { - yield { fullMessage: mr.message, fingerprint, responseId }; + const fullMessage = cleanGrokContentText(mr.message); + if (fullMessage) emittedVisibleContent = true; + yield { fullMessage, fingerprint, responseId }; } // Extract fingerprint from metadata @@ -260,11 +1119,44 @@ async function* extractContent( } // Streaming token + const thinking = isThinkingModel ? extractStructuredReasoning(resp) : ""; + if ((!suppressThinkingAfterVisibleContent || !emittedVisibleContent) && thinking) { + const cleanedThinking = thinkingFilter.feed(thinking); + const thinkingDelta = cleanedThinking.startsWith(emittedThinking) + ? cleanedThinking.slice(emittedThinking.length) + : cleanedThinking; + if (thinkingDelta) { + emittedThinking += thinkingDelta; + yield { thinking: thinkingDelta, fingerprint, responseId }; + } + } if (resp.token != null) { - yield { delta: resp.token, fingerprint, responseId }; + if (resp.isThinking) { + const thinkingDelta = + suppressThinkingAfterVisibleContent && emittedVisibleContent ? "" : cleanGrokThinkingText(resp); + if (thinkingDelta) yield { thinking: thinkingDelta, fingerprint, responseId }; + continue; + } + const cleanedDelta = contentFilter.feed(resp.token); + if (cleanedDelta) { + emittedVisibleContent = true; + yield { delta: cleanedDelta, fingerprint, responseId }; + } } } + const trailingThinking = + suppressThinkingAfterVisibleContent && emittedVisibleContent ? "" : thinkingFilter.flush(); + if (trailingThinking) { + const thinkingDelta = trailingThinking.startsWith(emittedThinking) + ? trailingThinking.slice(emittedThinking.length) + : trailingThinking; + if (thinkingDelta) yield { thinking: thinkingDelta, fingerprint, responseId }; + } + const trailingContent = contentFilter.flush(); + const trailingContentWithTrace = trailingContent; + if (trailingContentWithTrace) yield { delta: trailingContentWithTrace, fingerprint, responseId }; + yield { done: true, fingerprint, responseId }; } @@ -274,12 +1166,60 @@ function sseChunk(data: unknown): string { return `data: ${JSON.stringify(data)}\n\n`; } +function enqueueStreamingToolCalls( + controller: ReadableStreamDefaultController, + encoder: TextEncoder, + params: { + id: string; + created: number; + model: string; + fingerprint: string; + toolCalls: OpenAIToolCall[]; + } +): void { + for (let i = 0; i < params.toolCalls.length; i++) { + controller.enqueue( + encoder.encode( + sseChunk({ + id: params.id, + object: "chat.completion.chunk", + created: params.created, + model: params.model, + system_fingerprint: params.fingerprint || null, + choices: [ + { + index: 0, + delta: { tool_calls: [{ index: i, ...params.toolCalls[i] }] }, + finish_reason: null, + logprobs: null, + }, + ], + }) + ) + ); + } + controller.enqueue( + encoder.encode( + sseChunk({ + id: params.id, + object: "chat.completion.chunk", + created: params.created, + model: params.model, + system_fingerprint: params.fingerprint || null, + choices: [{ index: 0, delta: {}, finish_reason: "tool_calls", logprobs: null }], + }) + ) + ); + controller.enqueue(encoder.encode("data: [DONE]\n\n")); +} + function buildStreamingResponse( eventStream: ReadableStream, model: string, cid: string, created: number, isThinkingModel: boolean, + toolRegistry: GrokToolRegistry, signal?: AbortSignal | null ): ReadableStream { const encoder = new TextEncoder(); @@ -304,8 +1244,15 @@ function buildStreamingResponse( ); let fp = ""; + let buffered = ""; - for await (const chunk of extractContent(eventStream, isThinkingModel, signal)) { + for await (const chunk of extractContent( + eventStream, + isThinkingModel, + toolRegistry, + signal, + true + )) { if (chunk.fingerprint) fp = chunk.fingerprint; if (chunk.error) { @@ -354,9 +1301,29 @@ function buildStreamingResponse( continue; } + if (chunk.toolCalls) { + enqueueStreamingToolCalls(controller, encoder, { id: cid, created, model, fingerprint: fp, toolCalls: chunk.toolCalls }); + return; + } + if (chunk.done) break; + if (chunk.fullMessage) { + const toolCalls = parseClientToolCallMarkup(chunk.fullMessage, toolRegistry); + if (toolCalls) { + enqueueStreamingToolCalls(controller, encoder, { id: cid, created, model, fingerprint: fp, toolCalls }); + return; + } + } + if (chunk.delta) { + buffered += chunk.delta; + const toolCalls = parseClientToolCallMarkup(buffered, toolRegistry); + if (toolCalls) { + enqueueStreamingToolCalls(controller, encoder, { id: cid, created, model, fingerprint: fp, toolCalls }); + return; + } + if (hasOpenToolCallMarkup(buffered)) continue; controller.enqueue( encoder.encode( sseChunk({ @@ -429,13 +1396,14 @@ async function buildNonStreamingResponse( cid: string, created: number, isThinkingModel: boolean, + toolRegistry: GrokToolRegistry, signal?: AbortSignal | null ): Promise { let fullContent = ""; let fingerprint = ""; const thinkingParts: string[] = []; - for await (const chunk of extractContent(eventStream, isThinkingModel, signal)) { + for await (const chunk of extractContent(eventStream, isThinkingModel, toolRegistry, signal)) { if (chunk.fingerprint) fingerprint = chunk.fingerprint; if (chunk.error) { @@ -450,6 +1418,27 @@ async function buildNonStreamingResponse( thinkingParts.push(chunk.thinking); continue; } + if (chunk.toolCalls) { + return new Response( + JSON.stringify({ + id: cid, + object: "chat.completion", + created, + model, + system_fingerprint: fingerprint || null, + choices: [ + { + index: 0, + message: { role: "assistant", content: null, tool_calls: chunk.toolCalls }, + finish_reason: "tool_calls", + logprobs: null, + }, + ], + usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } if (chunk.done) break; if (chunk.fullMessage) { fullContent = chunk.fullMessage; @@ -458,6 +1447,29 @@ async function buildNonStreamingResponse( } } + const manifestToolCalls = parseClientToolCallMarkup(fullContent, toolRegistry); + if (manifestToolCalls) { + return new Response( + JSON.stringify({ + id: cid, + object: "chat.completion", + created, + model, + system_fingerprint: fingerprint || null, + choices: [ + { + index: 0, + message: { role: "assistant", content: null, tool_calls: manifestToolCalls }, + finish_reason: "tool_calls", + logprobs: null, + }, + ], + usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + const msg: Record = { role: "assistant", content: fullContent }; if (thinkingParts.length > 0) { msg.reasoning_content = thinkingParts.join("\n"); @@ -473,7 +1485,14 @@ async function buildNonStreamingResponse( created, model, system_fingerprint: fingerprint || null, - choices: [{ index: 0, message: msg, finish_reason: "stop", logprobs: null }], + choices: [ + { + index: 0, + message: msg, + finish_reason: "stop", + logprobs: null, + }, + ], usage: { prompt_tokens: promptTokens, completion_tokens: completionTokens, @@ -518,10 +1537,11 @@ export class GrokWebExecutor extends BaseExecutor { if (!modelInfo) { log?.info?.("GROK-WEB", `Unmapped model ${model}, defaulting to fast mode`); } + const toolRegistry = buildGrokToolRegistry(body as Record); const { modeId, isThinking } = modelInfo || MODEL_MAP.fast; // Parse OpenAI messages → single Grok message string - const message = parseOpenAIMessages(messages); + const message = buildGrokMessage(messages, toolRegistry, (body as Record).tool_choice); if (!message.trim()) { const errResp = new Response( JSON.stringify({ @@ -674,6 +1694,7 @@ export class GrokWebExecutor extends BaseExecutor { cid, created, isThinking, + toolRegistry, signal ); finalResponse = new Response(sseStream, { @@ -691,6 +1712,7 @@ export class GrokWebExecutor extends BaseExecutor { cid, created, isThinking, + toolRegistry, signal ); } diff --git a/tests/unit/grok-web.test.ts b/tests/unit/grok-web.test.ts index d674df960b..0cbd45a15e 100644 --- a/tests/unit/grok-web.test.ts +++ b/tests/unit/grok-web.test.ts @@ -104,6 +104,1755 @@ test("Non-streaming: simple response", async () => { } }); +test("Non-streaming: strips internal Grok tags but preserves render content", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + modelResponse: { + message: + 'Before web_search after 1.', + }, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { messages: [{ role: "user", content: "hi" }], stream: false }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + const content = json.choices[0].message.content; + assert.equal(content, "Before after 1."); + assert.ok(!content.includes("xai:tool_usage_card")); + assert.ok(!content.includes("grok:render")); + } finally { + restore(); + } +}); + +test("Non-streaming: maps structured Grok thinking to reasoning_content", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + modelResponse: { + thinking: "I should inspect the request.", + message: "Final answer.", + }, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.20", + body: { messages: [{ role: "user", content: "hi" }], stream: false }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].message.reasoning_content, "I should inspect the request."); + assert.equal(json.choices[0].message.content, "Final answer."); + } finally { + restore(); + } +}); + +test("Non-streaming: routes Grok isThinking events to reasoning_content", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + token: "Thinking about your request", + isThinking: true, + messageTag: "header", + messageStepId: 0, + }, + }, + }, + { + result: { + response: { + token: "Buscando fecha de lanzamiento", + isThinking: true, + messageTag: "header", + }, + }, + }, + { + result: { + response: { + token: "Respuesta final.", + isThinking: false, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { messages: [{ role: "user", content: "haz otra tool call" }], stream: false }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + const message = json.choices[0].message; + assert.equal(message.reasoning_content, "Buscando fecha de lanzamiento\n"); + assert.equal(message.content, "Respuesta final."); + } finally { + restore(); + } +}); + +test("Non-streaming: preserves late thinking as metadata", async () => { + const restore = mockFetch(200, [ + { result: { response: { token: "Respuesta visible." } } }, + { + result: { + response: { + token: "Explaining to user that the result was verified", + isThinking: true, + messageTag: "summary", + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { messages: [{ role: "user", content: "test" }], stream: false }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + const message = json.choices[0].message; + assert.equal(message.content, "Respuesta visible."); + assert.equal(message.reasoning_content, "Explaining to user that the result was verified\n"); + } finally { + restore(); + } +}); + +test("Non-streaming: preserves non-generic first thinking header", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + token: "Checking file path", + isThinking: true, + messageTag: "header", + messageStepId: 0, + }, + }, + }, + { result: { response: { token: "Respuesta final." } } }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { messages: [{ role: "user", content: "test" }], stream: false }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + const message = json.choices[0].message; + assert.equal(message.reasoning_content, "Checking file path\n"); + assert.equal(message.content, "Respuesta final."); + } finally { + restore(); + } +}); + +test("Non-streaming: leaves textual JSON tool_calls as normal content", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + modelResponse: { + message: + '{"tool_calls":[{"id":"call_weather","type":"function","function":{"name":"get_weather","arguments":"{\\"city\\":\\"Madrid\\"}"}}]}', + }, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "weather in Madrid" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get weather for a city", + parameters: { type: "object", properties: { city: { type: "string" } } }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].finish_reason, "stop"); + assert.equal(json.choices[0].message.tool_calls, undefined); + assert.ok(json.choices[0].message.content.includes('"tool_calls"')); + } finally { + restore(); + } +}); + +test("Non-streaming: converts manifest tool_call markup to arbitrary client tool", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + modelResponse: { + message: + '{"name":"memory_context_tool","arguments":{"query":"grok tool calling","limit":5}}', + }, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "busca en memoria grok tool calling" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "memory_context_tool", + description: "Search memory and history", + parameters: { + type: "object", + properties: { query: { type: "string" }, limit: { type: "number" } }, + required: ["query"], + }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].finish_reason, "tool_calls"); + assert.equal(json.choices[0].message.tool_calls[0].function.name, "memory_context_tool"); + assert.equal( + json.choices[0].message.tool_calls[0].function.arguments, + JSON.stringify({ query: "grok tool calling", limit: 5 }) + ); + } finally { + restore(); + } +}); + +test("Non-streaming: converts native Grok bash toolUsageCard to OpenAI tool_calls", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + token: "...", + isThinking: true, + messageTag: "tool_usage_card", + toolUsageCardId: "ded06cb6-deaf-4f01-8337-c31c0c75932c", + toolUsageCard: { + toolUsageCardId: "ded06cb6-deaf-4f01-8337-c31c0c75932c", + intent: "Counting lines in config.json", + bash: { args: { command: "wc -l /tmp/project/config.json" } }, + }, + }, + }, + }, + { + result: { + response: { + codeExecutionResult: { stdout: "", stderr: "remote sandbox result should not leak" }, + messageTag: "raw_function_result", + toolUsageCardId: "ded06cb6-deaf-4f01-8337-c31c0c75932c", + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "count lines" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "bash", + parameters: { + type: "object", + properties: { + command: { type: "string" }, + description: { type: "string" }, + }, + required: ["command", "description"], + }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].finish_reason, "tool_calls"); + assert.deepEqual(json.choices[0].message.tool_calls, [ + { + id: "ded06cb6-deaf-4f01-8337-c31c0c75932c", + type: "function", + function: { + name: "bash", + arguments: JSON.stringify({ + command: "wc -l /tmp/project/config.json", + description: "Execute shell command: wc -l /tmp/project/config.json", + }), + }, + }, + ]); + } finally { + restore(); + } +}); + +test("Non-streaming: maps native Grok bash to environment terminal tool by schema", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + messageTag: "tool_usage_card", + toolUsageCardId: "terminal-call-1", + toolUsageCard: { + bash: { args: { command: "pwd" } }, + }, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "run pwd" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "run_command", + description: "Execute a terminal command in the current environment", + parameters: { + type: "object", + properties: { + cmd: { type: "string" }, + reason: { type: "string" }, + }, + required: ["cmd", "reason"], + }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].message.tool_calls[0].function.name, "run_command"); + assert.equal( + json.choices[0].message.tool_calls[0].function.arguments, + JSON.stringify({ cmd: "pwd", reason: "Execute shell command: pwd" }) + ); + } finally { + restore(); + } +}); + +test("Non-streaming: converts native Grok readFile toolUsageCard to OpenAI read tool_calls", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + messageTag: "tool_usage_card", + toolUsageCardId: "a7f2e241-8513-4329-b685-c1f563c6d246", + toolUsageCard: { + toolUsageCardId: "a7f2e241-8513-4329-b685-c1f563c6d246", + readFile: { + args: { + filePath: "/tmp/project/config.json", + fileType: "FILE_TYPE_FILE", + offset: 1, + limit: 5, + }, + }, + }, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "read file" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "read", + parameters: { + type: "object", + properties: { + filePath: { type: "string" }, + offset: { type: "number" }, + limit: { type: "number" }, + }, + required: ["filePath"], + }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].message.tool_calls[0].function.name, "read"); + const args = JSON.parse(json.choices[0].message.tool_calls[0].function.arguments); + assert.equal(args.filePath, "/tmp/project/config.json"); + assert.equal(args.offset, 1); + assert.equal(args.limit, 5); + assert.equal(args.fileType, undefined); + } finally { + restore(); + } +}); + +test("Non-streaming: routes native Grok webSearch to URL fetch tool when user asks webfetch", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + messageTag: "tool_usage_card", + toolUsageCardId: "fetch-call-1", + toolUsageCard: { + webSearch: { args: { query: "site:endless.horse" } }, + }, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "Haz webfetch de http://endless.horse/ y dime que hay" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "public_search_tool", + description: "Search the web for any topic and get clean content from top results", + parameters: { + type: "object", + properties: { query: { type: "string" }, numResults: { type: "number" } }, + required: ["query"], + }, + }, + }, + { + type: "function", + function: { + name: "webfetch", + description: "Fetch a URL and extract the main page content", + parameters: { + type: "object", + properties: { url: { type: "string" }, format: { type: "string" } }, + required: ["url"], + }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].message.tool_calls[0].function.name, "webfetch"); + assert.equal(json.choices[0].message.tool_calls[0].function.arguments, JSON.stringify({ url: "http://endless.horse/" })); + } finally { + restore(); + } +}); + +test("Non-streaming: keeps native Grok webSearch on search tool when user asks search", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + messageTag: "tool_usage_card", + toolUsageCardId: "search-call-1", + toolUsageCard: { + webSearch: { args: { query: "Ubuntu 24.04.4 Noble Numbat release notes" } }, + }, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "Haz un websearch de Ubuntu 24.04.4" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "public_search_tool", + description: "Search the web for any topic", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + { + type: "function", + function: { + name: "webfetch", + description: "Fetch a URL and extract page content", + parameters: { type: "object", properties: { url: { type: "string" } }, required: ["url"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].message.tool_calls[0].function.name, "public_search_tool"); + } finally { + restore(); + } +}); + +test("Non-streaming: native Grok webSearch does not choose context memory search", async () => { + const response = [ + { + result: { + response: { + messageTag: "tool_usage_card", + toolUsageCardId: "grok_web_2", + toolUsageCard: { webSearch: { args: { query: "noticias España" } } }, + }, + }, + }, + ]; + const restore = mockFetch(200, response); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "busca en internet noticias de España" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "memory_context_tool", + description: "Search across project memories and conversation history", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + { + type: "function", + function: { + name: "public_search_tool", + description: "Search the web for current public information", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const data = await result.response.json(); + assert.equal(data.choices[0].message.tool_calls[0].function.name, "public_search_tool"); + } finally { + restore(); + } +}); + +test("Non-streaming: maps native Grok browsePage to URL fetch tool", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + messageTag: "tool_usage_card", + toolUsageCardId: "browse-call-1", + toolUsageCard: { + browsePage: { + args: { + url: "https://discourse.ubuntu.com/t/ubuntu-24-04-4-lts-released/76854", + instructions: "Summarize the official announcement", + }, + }, + }, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "Busca la release oficial de Ubuntu y abre la pagina del anuncio" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "public_search_tool", + description: "Search the web for any topic", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + { + type: "function", + function: { + name: "webfetch", + description: "Fetch a URL with better extraction for static/docs pages", + parameters: { type: "object", properties: { url: { type: "string" }, prompt: { type: "string" } }, required: ["url"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].message.tool_calls[0].function.name, "webfetch"); + assert.equal( + json.choices[0].message.tool_calls[0].function.arguments, + JSON.stringify({ url: "https://discourse.ubuntu.com/t/ubuntu-24-04-4-lts-released/76854" }) + ); + } finally { + restore(); + } +}); + +test("Non-streaming: does not repeat a tool call that already has a tool result", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + messageTag: "tool_usage_card", + toolUsageCardId: "repeat-bash", + toolUsageCard: { bash: { args: { command: "wc -l /tmp/a" } } }, + }, + }, + }, + { result: { response: { modelResponse: { message: "413 líneas." } } } }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [ + { role: "user", content: "cuenta lineas" }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_1", + type: "function", + function: { name: "bash", arguments: JSON.stringify({ command: "wc -l /tmp/a" }) }, + }, + ], + }, + { role: "tool", tool_call_id: "call_1", name: "bash", content: "413 /tmp/a" }, + ], + stream: false, + tools: [{ type: "function", function: { name: "bash", parameters: { type: "object", properties: { command: { type: "string" } } } } }], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].finish_reason, "stop"); + assert.equal(json.choices[0].message.tool_calls, undefined); + assert.equal(json.choices[0].message.content, "413 líneas."); + } finally { + restore(); + } +}); + +test("Non-streaming: does not repeat equivalent terminal command with different metadata", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + messageTag: "tool_usage_card", + toolUsageCardId: "repeat-bash-semantic", + toolUsageCard: { bash: { args: { command: 'wc -l "/tmp/a"' } } }, + }, + }, + }, + { result: { response: { modelResponse: { message: "413 líneas." } } } }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [ + { role: "user", content: "cuenta lineas" }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_1", + type: "function", + function: { + name: "bash", + arguments: JSON.stringify({ command: 'wc -l "/tmp/a"', description: "previous run" }), + }, + }, + ], + }, + { role: "tool", tool_call_id: "call_1", name: "bash", content: "413 /tmp/a" }, + ], + stream: false, + tools: [ + { + type: "function", + function: { + name: "bash", + parameters: { + type: "object", + properties: { command: { type: "string" }, description: { type: "string" } }, + required: ["command", "description"], + }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].finish_reason, "stop"); + assert.equal(json.choices[0].message.tool_calls, undefined); + assert.equal(json.choices[0].message.content, "413 líneas."); + } finally { + restore(); + } +}); + +test("Non-streaming: allows a different tool after a completed call", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + messageTag: "tool_usage_card", + toolUsageCardId: "read-after-bash", + toolUsageCard: { readFile: { args: { filePath: "/tmp/a" } } }, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [ + { role: "user", content: "ahora lee el archivo" }, + { + role: "assistant", + content: null, + tool_calls: [ + { id: "call_1", type: "function", function: { name: "bash", arguments: JSON.stringify({ command: "wc -l /tmp/a" }) } }, + ], + }, + { role: "tool", tool_call_id: "call_1", name: "bash", content: "413 /tmp/a" }, + ], + stream: false, + tools: [ + { type: "function", function: { name: "bash", parameters: { type: "object", properties: { command: { type: "string" } } } } }, + { type: "function", function: { name: "read", parameters: { type: "object", properties: { filePath: { type: "string" } } } } }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].finish_reason, "tool_calls"); + assert.equal(json.choices[0].message.tool_calls[0].function.name, "read"); + } finally { + restore(); + } +}); + +test("Non-streaming: raw_function_result is not emitted as final content", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + token: "", + isThinking: false, + messageTag: "raw_function_result", + webSearchResults: { results: [{ title: "Ignored" }] }, + }, + }, + }, + { + result: { + response: { + modelResponse: { message: "El archivo tiene 413 líneas." }, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [ + { role: "user", content: "cuantas lineas" }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_1", + type: "function", + function: { name: "bash", arguments: JSON.stringify({ command: "wc -l /tmp/project/config.json" }) }, + }, + ], + }, + { role: "tool", tool_call_id: "call_1", name: "bash", content: "413 /tmp/project/config.json" }, + ], + stream: false, + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + const content = json.choices[0].message.content; + assert.ok(!content.includes("Ignored")); + assert.ok(content.includes("413 líneas")); + } finally { + restore(); + } +}); + +test("Non-streaming: skips raw_function_result even if it carries modelResponse text", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + messageTag: "raw_function_result", + modelResponse: { message: "internal tool result should not be content" }, + }, + }, + }, + { result: { response: { modelResponse: { message: "Respuesta final limpia." } } } }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { messages: [{ role: "user", content: "resume" }], stream: false }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].message.content, "Respuesta final limpia."); + } finally { + restore(); + } +}); + +test("Non-streaming: skips unmapped tool_usage_card text structurally", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + messageTag: "tool_usage_card", + token: "plain tool card text should not be content", + }, + }, + }, + { result: { response: { modelResponse: { message: "Final visible." } } } }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { messages: [{ role: "user", content: "test" }], stream: false }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].message.content, "Final visible."); + } finally { + restore(); + } +}); + +test("Request: forwards tool results into Grok prompt for the next turn", async () => { + let capturedBody = ""; + const originalFetch = global.fetch; + global.fetch = async (_url: string | URL | Request, init?: RequestInit) => { + capturedBody = String(init?.body || ""); + return new Response(mockGrokStream([{ result: { response: { modelResponse: { message: "It is sunny." } } } }]), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [ + { role: "user", content: "weather in Madrid" }, + { + role: "assistant", + content: null, + tool_calls: [ + { id: "call_weather", type: "function", function: { name: "get_weather", arguments: "{}" } }, + ], + }, + { role: "tool", tool_call_id: "call_weather", name: "get_weather", content: "sunny" }, + ], + stream: false, + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const payload = JSON.parse(capturedBody); + assert.ok(payload.message.includes("Previous assistant tool calls")); + assert.ok(payload.message.includes("CLIENT TOOL RESULT from caller runtime for get_weather (call_weather)")); + assert.ok(payload.message.includes("do not call the same tool again")); + assert.ok(payload.message.includes("sunny")); + } finally { + global.fetch = originalFetch; + } +}); + +test("Request: injects dynamic client tool manifest without truncating tools", async () => { + const capture = mockFetchCapture([ + { result: { response: { modelResponse: { message: "{}" } } } }, + ]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [ + { + role: "user", + content: "dime cuantas lineas tiene /tmp/project/config.json", + }, + ], + stream: false, + tools: [ + { + type: "function", + function: { + name: "bash", + description: "Run a shell command and return stdout/stderr", + parameters: { + type: "object", + properties: { command: { type: "string" }, timeout: { type: "number" } }, + required: ["command"], + }, + }, + }, + ], + tool_choice: "required", + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + assert.ok(prompt.includes("CLIENT_TOOLS:")); + assert.equal(capture.body.disableSearch, false); + assert.ok(prompt.includes("name: bash")); + assert.ok(prompt.includes("args=command,timeout; required=command")); + assert.ok(prompt.includes("description: Run a shell command and return stdout/stderr")); + assert.ok(prompt.includes("")); + assert.ok(prompt.includes("/tmp/project/config.json")); + assert.ok(prompt.indexOf("CLIENT_TOOLS:") > prompt.indexOf("/tmp/project/config.json")); + } finally { + capture.restore(); + } +}); + +test("Request: leaves native Grok search enabled when client tools are absent", async () => { + const capture = mockFetchCapture(SIMPLE_RESPONSE); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "Busca noticias" }], + stream: false, + }, + stream: false, + credentials: { apiKey: "test" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + assert.equal(capture.body.disableSearch, false); + assert.ok(!String(capture.body.message).includes("CLIENT_TOOLS:")); + } finally { + capture.restore(); + } +}); + +test("Request: places tool manifest next to latest user after noisy history", async () => { + const capture = mockFetchCapture([{ result: { response: { modelResponse: { message: "{}" } } } }]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [ + { role: "user", content: "old question" }, + { role: "assistant", content: "old answer claiming file does not exist" }, + { role: "user", content: "/tmp/project/config.json dime cuantas lineas tiene este archivo" }, + ], + stream: false, + tools: [ + { + type: "function", + function: { + name: "bash", + description: "Run a shell command", + parameters: { type: "object", properties: { command: { type: "string" } }, required: ["command"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + const oldIdx = prompt.indexOf("old answer claiming file does not exist"); + const manifestIdx = prompt.lastIndexOf("CLIENT_TOOLS:"); + const latestIdx = prompt.indexOf("/tmp/project/config.json"); + assert.ok(oldIdx >= 0); + assert.ok(manifestIdx > oldIdx); + assert.ok(manifestIdx > latestIdx); + } finally { + capture.restore(); + } +}); + +test("Request: strips injected internal reminders from Grok prompt", async () => { + const capture = mockFetchCapture([{ result: { response: { modelResponse: { message: "{}" } } } }]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [ + { + role: "user", + content: + "investiga example.com\n\n---\n\n!IMPORTANT! hidden runtime workflow reminder", + }, + ], + stream: false, + tools: [ + { + type: "function", + function: { + name: "fetch_url_tool", + description: "Fetch URL or browse web page content", + parameters: { type: "object", properties: { url: { type: "string" } }, required: ["url"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + assert.ok(prompt.includes("investiga example.com")); + assert.ok(!prompt.includes("internal_reminder")); + assert.ok(!prompt.includes("hidden runtime workflow reminder")); + } finally { + capture.restore(); + } +}); + +test("Request: old completed tools do not suppress fresh latest-user tool calls", async () => { + const capture = mockFetchCapture([{ result: { response: { modelResponse: { message: "{}" } } } }]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [ + { role: "user", content: "old count" }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "old_call", + type: "function", + function: { name: "bash", arguments: JSON.stringify({ command: "wc -l /tmp/a" }) }, + }, + ], + }, + { role: "tool", tool_call_id: "old_call", name: "bash", content: "10 /tmp/a" }, + { role: "assistant", content: "10 lines" }, + { role: "user", content: "wc -l /tmp/a otra vez" }, + ], + stream: false, + tools: [ + { + type: "function", + function: { + name: "bash", + description: "Run a shell command", + parameters: { type: "object", properties: { command: { type: "string" } }, required: ["command"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + assert.ok(prompt.includes("CLIENT_TOOLS:")); + assert.ok(!prompt.includes("completed_tool_calls:")); + } finally { + capture.restore(); + } +}); + +test("Request: appends tool manifest after tool results during multi-step continuation", async () => { + const capture = mockFetchCapture([{ result: { response: { modelResponse: { message: "{}" } } } }]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [ + { role: "user", content: "lee archivo y busca release oficial" }, + { + role: "assistant", + content: "", + tool_calls: [ + { id: "read_call", type: "function", function: { name: "read", arguments: JSON.stringify({ filePath: "/tmp/a" }) } }, + ], + }, + { role: "tool", tool_call_id: "read_call", name: "read", content: "file content" }, + ], + stream: false, + tools: [ + { + type: "function", + function: { + name: "memory_context_tool", + description: "Search across project memories and raw conversation history.", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + { + type: "function", + function: { + name: "public_search_tool", + description: "Search the web for any topic and get clean content.", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + const toolResultIdx = prompt.indexOf("CLIENT TOOL RESULT from caller runtime for read"); + const manifestIdx = prompt.lastIndexOf("CLIENT_TOOLS:"); + assert.ok(toolResultIdx >= 0); + assert.ok(manifestIdx > toolResultIdx); + assert.ok(prompt.includes("name: public_search_tool")); + } finally { + capture.restore(); + } +}); + +test("Request: keeps generic manifest ordered for file understanding tasks", async () => { + const capture = mockFetchCapture([{ result: { response: { modelResponse: { message: "{}" } } } }]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [ + { + role: "user", + content: "lee /tmp/project/config.json y dime de que va y que modelo usa por defecto", + }, + ], + stream: false, + tools: [ + { + type: "function", + function: { + name: "bash", + description: "Execute shell command", + parameters: { type: "object", properties: { command: { type: "string" }, description: { type: "string" } }, required: ["command"] }, + }, + }, + { + type: "function", + function: { + name: "read", + description: "Read a file or directory from the local filesystem", + parameters: { type: "object", properties: { filePath: { type: "string" } }, required: ["filePath"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + assert.ok(!prompt.includes("recommended_tools:")); + assert.ok(!prompt.includes("primary_tools:")); + assert.ok(!prompt.includes("other_tools:")); + assert.ok(!prompt.includes("scope_guide:")); + assert.ok(!prompt.includes("request_scopes:")); + const readIdx = prompt.indexOf("name: read"); + const bashIdx = prompt.indexOf("name: bash"); + assert.ok(readIdx >= 0); + assert.ok(bashIdx >= 0); + assert.ok(readIdx < bashIdx); + assert.ok(prompt.includes("args=filePath; required=filePath")); + } finally { + capture.restore(); + } +}); + +test("Request: keeps generic manifest ordered for official web facts", async () => { + const capture = mockFetchCapture([{ result: { response: { modelResponse: { message: "{}" } } } }]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "contrasta con una fuente web oficial la ultima release de Ubuntu 24.04" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "memory_context_tool", + description: "Search across project memories, indexed git commits, and raw conversation history.", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + { + type: "function", + function: { + name: "public_search_tool", + description: "Search the web for any topic and get clean, ready-to-use content.", + parameters: { type: "object", properties: { query: { type: "string" }, numResults: { type: "number" } }, required: ["query"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + assert.ok(!prompt.includes("recommended_tools:")); + assert.ok(!prompt.includes("primary_tools:")); + assert.ok(!prompt.includes("other_tools:")); + assert.ok(!prompt.includes("scope_guide:")); + assert.ok(!prompt.includes("request_scopes:")); + const webIdx = prompt.indexOf("name: public_search_tool"); + const ctxIdx = prompt.indexOf("name: memory_context_tool"); + assert.ok(webIdx >= 0); + assert.ok(ctxIdx >= 0); + assert.ok(webIdx < ctxIdx); + assert.ok(prompt.includes("args=query,numResults; required=query")); + } finally { + capture.restore(); + } +}); + +test("Request: base manifest order puts public web search before context memory", async () => { + const capture = mockFetchCapture([{ result: { response: { modelResponse: { message: "{}" } } } }]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "Dime que herramientas ves" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "memory_context_tool", + description: "Search across project memories and conversation history", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + { + type: "function", + function: { + name: "public_search_tool", + description: "Search the web for current public information", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + assert.ok(prompt.indexOf("name: public_search_tool") < prompt.indexOf("name: memory_context_tool")); + } finally { + capture.restore(); + } +}); + +test("Request: ranks public web search before infrastructure search", async () => { + const capture = mockFetchCapture([{ result: { response: { modelResponse: { message: "{}" } } } }]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "busca en web la última release de Ubuntu 24.04" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "tool_discovery_search", + description: "Search and discover available upstream tools using BM25 full-text search", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + { + type: "function", + function: { + name: "public_search_tool", + description: "Search the web for current public information", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + assert.ok(prompt.indexOf("name: public_search_tool") < prompt.indexOf("name: tool_discovery_search")); + } finally { + capture.restore(); + } +}); + +test("Request: ranks URL fetch before generic MCP read", async () => { + const capture = mockFetchCapture([{ result: { response: { modelResponse: { message: "{}" } } } }]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "lee https://example.com/docs" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "mcp_read_tool", + description: "Execute a read-only upstream tool such as fetch, get, query, list, or search", + parameters: { type: "object", properties: { name: { type: "string" }, args: { type: "object" } }, required: ["name"] }, + }, + }, + { + type: "function", + function: { + name: "fetch_url_tool", + description: "Fetch URL or browse web page content", + parameters: { type: "object", properties: { url: { type: "string" } }, required: ["url"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + assert.ok(prompt.indexOf("name: fetch_url_tool") < prompt.indexOf("name: mcp_read_tool")); + } finally { + capture.restore(); + } +}); + +test("Request: ranks shell command before infrastructure command config", async () => { + const capture = mockFetchCapture([{ result: { response: { modelResponse: { message: "{}" } } } }]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "cuántas líneas tiene /tmp/a" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "upstream_server_config", + description: "Manage upstream MCP servers, including stdio command configuration", + parameters: { type: "object", properties: { command: { type: "string" }, name: { type: "string" } }, required: ["name"] }, + }, + }, + { + type: "function", + function: { + name: "shell_command_tool", + description: "Execute a shell command and return output", + parameters: { type: "object", properties: { command: { type: "string" } }, required: ["command"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + assert.ok(prompt.indexOf("name: shell_command_tool") < prompt.indexOf("name: upstream_server_config")); + } finally { + capture.restore(); + } +}); + +test("Request: commit wording does not prioritize memory over shell", async () => { + const capture = mockFetchCapture([{ result: { response: { modelResponse: { message: "{}" } } } }]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "ejecuta git rev-parse HEAD para ver el commit actual" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "memory_context_tool", + description: "Search project memories and conversation history", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + { + type: "function", + function: { + name: "shell_command_tool", + description: "Execute a shell command and return output", + parameters: { type: "object", properties: { command: { type: "string" } }, required: ["command"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + assert.ok(prompt.indexOf("name: shell_command_tool") < prompt.indexOf("name: memory_context_tool")); + } finally { + capture.restore(); + } +}); + +test("Request: explicit memory request prioritizes context over public web", async () => { + const capture = mockFetchCapture([{ result: { response: { modelResponse: { message: "{}" } } } }]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "busca en memoria si hablamos antes de Ubuntu" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "public_search_tool", + description: "Search the web for current public information", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + { + type: "function", + function: { + name: "memory_context_tool", + description: "Search project memories and conversation history", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + assert.ok(prompt.indexOf("name: memory_context_tool") < prompt.indexOf("name: public_search_tool")); + } finally { + capture.restore(); + } +}); + +test("Request: explicit tool_choice exposes only the forced tool", async () => { + const capture = mockFetchCapture([{ result: { response: { modelResponse: { message: "{}" } } } }]); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "usa una herramienta concreta" }], + stream: false, + tool_choice: { type: "function", function: { name: "forced_tool" } }, + tools: [ + { + type: "function", + function: { + name: "other_tool", + description: "Other available tool", + parameters: { type: "object", properties: { input: { type: "string" } }, required: ["input"] }, + }, + }, + { + type: "function", + function: { + name: "forced_tool", + description: "Forced tool", + parameters: { type: "object", properties: { input: { type: "string" } }, required: ["input"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const prompt = String(capture.body.message); + assert.ok(prompt.includes("name: forced_tool")); + assert.ok(!prompt.includes("name: other_tool")); + } finally { + capture.restore(); + } +}); + +test("Non-streaming: routes URL-like webfetch requests conservatively", async () => { + const cases = [ + { + title: "trailing punctuation", + user: "haz webfetch de https://example.com/docs.", + query: "example docs", + expectedName: "fetch_url_tool", + expectedArgs: { url: "https://example.com/docs" }, + }, + { + title: "bare domain", + user: "haz webfetch en rtve.es", + query: "rtve.es", + expectedName: "fetch_url_tool", + expectedArgs: { url: "https://rtve.es" }, + }, + { + title: "www domain", + user: "haz webfetch en www.example.com/docs", + query: "www.example.com/docs", + expectedName: "fetch_url_tool", + expectedArgs: { url: "https://www.example.com/docs" }, + }, + { + title: "http URL", + user: "haz webfetch en .", + query: "http example", + expectedName: "fetch_url_tool", + expectedArgs: { url: "http://example.com/path" }, + }, + { + title: "websocket URL", + user: "haz webfetch en wss://example.com/socket", + query: "wss endpoint", + expectedName: "public_search_tool", + expectedArgs: { query: "wss endpoint" }, + }, + ]; + + for (const item of cases) { + const restore = mockFetch(200, [ + { + result: { + response: { + messageTag: "tool_usage_card", + toolUsageCardId: `grok_web_${item.title.replace(/\s+/g, "_")}`, + toolUsageCard: { webSearch: { args: { query: item.query } } }, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: item.user }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "fetch_url_tool", + description: "Fetch URL or browse web page content", + parameters: { type: "object", properties: { url: { type: "string" } }, required: ["url"] }, + }, + }, + { + type: "function", + function: { + name: "public_search_tool", + description: "Search the web for current public information", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const json = (await result.response.json()) as any; + assert.equal(json.choices[0].message.tool_calls[0].function.name, item.expectedName, item.title); + assert.equal(json.choices[0].message.tool_calls[0].function.arguments, JSON.stringify(item.expectedArgs), item.title); + } finally { + restore(); + } + } +}); + // ─── Streaming ────────────────────────────────────────────────────────────── test("Streaming: produces valid SSE chunks", async () => { @@ -137,6 +1886,250 @@ test("Streaming: produces valid SSE chunks", async () => { } }); +test("Streaming: strips internal Grok cards across chunk boundaries", async () => { + const restore = mockFetch(200, [ + { result: { response: { token: "Visible " } } }, + { result: { response: { token: "" } } }, + { result: { response: { token: "web_search text" } } }, + { result: { response: { modelResponse: { message: "Visible text" } } } }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { messages: [{ role: "user", content: "hello" }], stream: true }, + stream: true, + credentials: { apiKey: "test-sso" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + + const text = await result.response.text(); + assert.ok(text.includes('"content":"Visible "')); + assert.ok(text.includes('"content":" text"')); + assert.ok(!text.includes("xai:tool_usage_card")); + assert.ok(!text.includes("web_search")); + } finally { + restore(); + } +}); + +test("Streaming: handles Grok card closing tags split across chunks", async () => { + const restore = mockFetch(200, [ + { result: { response: { token: "Before " } } }, + { result: { response: { token: "hidden" } } }, + { result: { response: { token: " after" } } }, + { result: { response: { modelResponse: { message: "Before after" } } } }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { messages: [{ role: "user", content: "hello" }], stream: true }, + stream: true, + credentials: { apiKey: "test-sso" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + + const text = await result.response.text(); + assert.ok(text.includes('"content":"Before "')); + assert.ok(text.includes('"content":" after"')); + assert.ok(!text.includes("xai:tool_usage_card")); + assert.ok(!text.includes("hidden")); + } finally { + restore(); + } +}); + +test("Streaming: strips self-closing Grok render cards without swallowing later text", async () => { + const restore = mockFetch(200, [ + { result: { response: { token: "Alpha " } } }, + { result: { response: { token: ' omega' } } }, + { result: { response: { modelResponse: { message: "Alpha omega" } } } }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { messages: [{ role: "user", content: "hello" }], stream: true }, + stream: true, + credentials: { apiKey: "test-sso" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + + const text = await result.response.text(); + assert.ok(text.includes('"content":"Alpha "')); + assert.ok(text.includes('"content":" omega"')); + assert.ok(!text.includes("grok:render")); + } finally { + restore(); + } +}); + +test("Streaming: does not end suppressed Grok card on slash-close text inside the card", async () => { + const restore = mockFetch(200, [ + { result: { response: { token: "Start " } } }, + { result: { response: { token: "hidden /> still hidden" } } }, + { result: { response: { token: " end" } } }, + { result: { response: { modelResponse: { message: "Start end" } } } }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { messages: [{ role: "user", content: "hello" }], stream: true }, + stream: true, + credentials: { apiKey: "test-sso" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + + const text = await result.response.text(); + assert.ok(text.includes('"content":"Start "')); + assert.ok(text.includes('"content":" end"')); + assert.ok(!text.includes("hidden")); + } finally { + restore(); + } +}); + +test("Streaming: maps structured Grok thinking to reasoning_content", async () => { + const restore = mockFetch(200, [ + { result: { response: { thinking: "Plan first. " } } }, + { result: { response: { token: "Answer" } } }, + { result: { response: { modelResponse: { message: "Answer" } } } }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.20", + body: { messages: [{ role: "user", content: "hello" }], stream: true }, + stream: true, + credentials: { apiKey: "test-sso" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + + const text = await result.response.text(); + assert.ok(text.includes('"reasoning_content":"Plan first. "')); + assert.ok(text.includes('"content":"Answer"')); + } finally { + restore(); + } +}); + +test("Streaming: routes Grok thinking tokens separately from content", async () => { + const restore = mockFetch(200, [ + { result: { response: { token: "Thinking about your request", isThinking: true, messageTag: "header", messageStepId: 0 } } }, + { result: { response: { token: "Buscando fecha de lanzamiento", isThinking: true, messageTag: "header" } } }, + { result: { response: { token: "- Tool calls succeeded, confirming Ubuntu 24.04.4.\n", isThinking: true, messageTag: "summary" } } }, + { result: { response: { token: "Tool call ejecutado.\nweb_search ejecutado correctamente.\n" } } }, + { result: { response: { token: "Resultados clave:\n- Ubuntu 24.04.4 LTS liberado.\n" } } }, + { + result: { + response: { + modelResponse: { + message: + "Tool call ejecutado.\nweb_search ejecutado correctamente.\nResultados clave:\n- Ubuntu 24.04.4 LTS liberado.\n", + }, + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { messages: [{ role: "user", content: "prueba otra vez" }], stream: true }, + stream: true, + credentials: { apiKey: "test-sso" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + + const text = await result.response.text(); + assert.ok(!text.includes('"reasoning_content":"..."')); + assert.ok(text.includes('"reasoning_content":"Buscando fecha de lanzamiento\\n"')); + assert.ok(!text.includes('"content":"Buscando fecha de lanzamiento"')); + assert.ok(text.includes("Tool calls succeeded")); + assert.ok(text.includes("Tool call ejecutado.")); + assert.ok(text.includes("Resultados clave:")); + } finally { + restore(); + } +}); + +test("Streaming: ignores late thinking after content starts", async () => { + const restore = mockFetch(200, [ + { result: { response: { token: "413 líneas" } } }, + { + result: { + response: { + token: "Explaining to user that file cannot be accessed", + isThinking: true, + messageTag: "summary", + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { messages: [{ role: "user", content: "line count" }], stream: true }, + stream: true, + credentials: { apiKey: "test-sso" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + + const text = await result.response.text(); + assert.ok(text.includes('"content":"413 líneas"')); + assert.ok(!text.includes("Explaining to user")); + assert.ok(!text.includes("reasoning_content")); + } finally { + restore(); + } +}); + +test("Streaming: leaves textual JSON tool_calls as content", async () => { + const restore = mockFetch(200, [ + { + result: { + response: { + token: + '{"tool_calls":[{"id":"call_search","type":"function","function":{"name":"search_docs","arguments":"{\\"query\\":\\"Ubuntu 24.04.4\\"}"}}]}', + }, + }, + }, + ]); + try { + const executor = new GrokWebExecutor(); + const result = await executor.execute({ + model: "grok-4.1-fast", + body: { + messages: [{ role: "user", content: "search docs" }], + stream: true, + tools: [{ type: "function", function: { name: "search_docs" } }], + }, + stream: true, + credentials: { apiKey: "test-sso-token" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + const text = await result.response.text(); + assert.ok(text.includes("tool_calls")); + assert.ok(text.includes('"finish_reason":"stop"')); + assert.ok(text.includes("search_docs")); + assert.ok(text.includes('"content"')); + } finally { + restore(); + } +}); + // ─── Error handling ───────────────────────────────────────────────────────── test("Error: 401 returns auth error", async () => { @@ -297,6 +2290,40 @@ test("Request: payload has correct model mapping", async () => { } }); +test("Request: preserves selected mode and native search state when client tools are present", async () => { + const cap = mockFetchCapture(SIMPLE_RESPONSE); + try { + const executor = new GrokWebExecutor(); + await executor.execute({ + model: "grok-420-computer-use-sa", + body: { + messages: [{ role: "user", content: "Busca noticias" }], + stream: false, + tools: [ + { + type: "function", + function: { + name: "public_search_tool", + description: "Search the public web", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + }, + }, + ], + }, + stream: false, + credentials: { apiKey: "test" }, + signal: AbortSignal.timeout(10000), + log: null, + }); + assert.equal(cap.body.modeId, "grok-420-computer-use-sa"); + assert.equal(cap.body.disableSearch, false); + assert.deepEqual(cap.body.toolOverrides, {}); + assert.ok(String(cap.body.message).includes("CLIENT_TOOLS:")); + } finally { + cap.restore(); + } +}); + test("Request: grok-4-heavy maps to heavy mode", async () => { const cap = mockFetchCapture(SIMPLE_RESPONSE); try {