import { appendDanglingToolIntentCorrection, buildChatToolSystemPrompt, executeToolCallAndBuildEvent, getEnabledChatTools, getUnstreamedText, looksLikeDanglingToolIntent, MAX_DANGLING_TOOL_INTENT_RETRIES, MAX_TOOL_ROUNDS, mergeUsage, normalizeModelToolCalls, prepareToolCallExecution, type NormalizedToolCall, type ToolAwareCompletionParams, type ToolAwareCompletionResult, type ToolAwareStreamingEvent, type ToolExecutionEvent, } from "../chat-tools.js"; import { buildImageSummaryText, buildSystemPromptAugmentationMessage, buildTextAttachmentPrompt, getImageAttachments, getTextAttachments, } from "../message-content.js"; import type { ChatMessage } from "../types.js"; function toContentParts(message: ChatMessage) { const imageAttachments = getImageAttachments(message); const textAttachments = getTextAttachments(message); if (!imageAttachments.length && !textAttachments.length) { return message.content; } const parts: Array> = []; for (const attachment of imageAttachments) { parts.push({ type: "image_url", image_url: { url: attachment.dataUrl, detail: "auto", }, }); } const imageSummary = buildImageSummaryText(imageAttachments); if (imageSummary) { parts.push({ type: "text", text: imageSummary }); } for (const attachment of textAttachments) { parts.push({ type: "text", text: buildTextAttachmentPrompt(attachment) }); } if (message.content.trim()) { parts.push({ type: "text", text: message.content }); } if (parts.length === 1 && parts[0]?.type === "text" && typeof parts[0].text === "string") { return parts[0].text; } return parts; } function buildConversationMessage(message: ChatMessage) { if (message.role === "tool") { const name = message.name?.trim() || "tool"; return { role: "user", content: `Tool output (${name}):\n${message.content}`, }; } const out: Record = { role: message.role, content: toContentParts(message), }; if (message.name && (message.role === "assistant" || message.role === "user")) { out.name = message.name; } return out; } function normalizeMessages(messages: ChatMessage[], userLocation?: string, params: Pick = {}) { const normalized = messages.map((message) => buildConversationMessage(message)); return [{ role: "system", content: buildChatToolSystemPrompt(params) }, buildSystemPromptAugmentationMessage(userLocation), ...normalized]; } function normalizePlainMessages(messages: ChatMessage[], userLocation?: string) { return [buildSystemPromptAugmentationMessage(userLocation), ...messages.map((message) => buildConversationMessage(message))]; } function extractContent(message: any) { if (typeof message?.content === "string") return message.content; if (!Array.isArray(message?.content)) return ""; return message.content .map((part: any) => { if (typeof part === "string") return part; if (typeof part?.text === "string") return part.text; if (typeof part?.content === "string") return part.content; return ""; }) .join(""); } export async function completeWithChatCompletionsApi(params: ToolAwareCompletionParams): Promise { const enabledTools = getEnabledChatTools(params); if (!enabledTools.length) { const completion = await params.client.chat.completions.create({ model: params.model, messages: normalizePlainMessages(params.messages, params.userLocation), temperature: params.temperature, max_tokens: params.maxTokens, } as any); const usageAcc: Required> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; const sawUsage = mergeUsage(usageAcc, completion?.usage); const message = completion?.choices?.[0]?.message; return { text: extractContent(message), usage: sawUsage ? usageAcc : undefined, raw: { response: completion, api: "chat.completions" }, toolEvents: [], }; } const conversation: any[] = normalizeMessages(params.messages, params.userLocation, params); const rawResponses: unknown[] = []; const toolEvents: ToolExecutionEvent[] = []; const usageAcc: Required> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; let sawUsage = false; let totalToolCalls = 0; let danglingToolIntentRetries = 0; for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) { const completion = await params.client.chat.completions.create({ model: params.model, messages: conversation, temperature: params.temperature, max_tokens: params.maxTokens, tools: enabledTools, tool_choice: "auto", } as any); rawResponses.push(completion); sawUsage = mergeUsage(usageAcc, completion?.usage) || sawUsage; const message = completion?.choices?.[0]?.message; if (!message) { return { text: "", usage: sawUsage ? usageAcc : undefined, raw: { responses: rawResponses, toolCallsUsed: totalToolCalls, missingMessage: true }, toolEvents, }; } const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : []; if (!toolCalls.length) { const text = typeof message.content === "string" ? message.content : ""; if (danglingToolIntentRetries < MAX_DANGLING_TOOL_INTENT_RETRIES && looksLikeDanglingToolIntent(text)) { danglingToolIntentRetries += 1; appendDanglingToolIntentCorrection(conversation, text); continue; } return { text, usage: sawUsage ? usageAcc : undefined, raw: { responses: rawResponses, toolCallsUsed: totalToolCalls }, toolEvents, }; } const normalizedToolCalls = normalizeModelToolCalls(toolCalls, round); totalToolCalls += normalizedToolCalls.length; const assistantToolCallMessage: any = { role: "assistant", tool_calls: normalizedToolCalls.map((call) => ({ id: call.id, type: "function", function: { name: call.name, arguments: call.arguments, }, })), }; if (typeof message.content === "string" && message.content.length) { assistantToolCallMessage.content = message.content; } conversation.push(assistantToolCallMessage); for (const call of normalizedToolCalls) { const { execution } = prepareToolCallExecution(call); const { event, toolResult } = await executeToolCallAndBuildEvent(call, execution, params); toolEvents.push(event); conversation.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(toolResult), }); } } return { text: "I reached the tool-call limit while gathering information. Please narrow the request and try again.", usage: sawUsage ? usageAcc : undefined, raw: { responses: rawResponses, toolCallsUsed: totalToolCalls, toolCallLimitReached: true }, toolEvents, }; } export async function* streamWithChatCompletionsApi(params: ToolAwareCompletionParams): AsyncGenerator { const enabledTools = getEnabledChatTools(params); if (!enabledTools.length) { const rawResponses: unknown[] = []; const usageAcc: Required> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; let sawUsage = false; let text = ""; const stream = await params.client.chat.completions.create({ model: params.model, messages: normalizePlainMessages(params.messages, params.userLocation), temperature: params.temperature, max_tokens: params.maxTokens, stream: true, } as any); for await (const chunk of stream as any as AsyncIterable) { rawResponses.push(chunk); sawUsage = mergeUsage(usageAcc, chunk?.usage) || sawUsage; const deltaText = chunk?.choices?.[0]?.delta?.content ?? ""; if (typeof deltaText === "string" && deltaText.length) { text += deltaText; yield { type: "delta", text: deltaText }; } } yield { type: "done", result: { text, usage: sawUsage ? usageAcc : undefined, raw: { streamed: true, responses: rawResponses, api: "chat.completions" }, toolEvents: [], }, }; return; } const conversation: any[] = normalizeMessages(params.messages, params.userLocation, params); const rawResponses: unknown[] = []; const toolEvents: ToolExecutionEvent[] = []; const usageAcc: Required> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; let sawUsage = false; let totalToolCalls = 0; let danglingToolIntentRetries = 0; for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) { const stream = await params.client.chat.completions.create({ model: params.model, messages: conversation, temperature: params.temperature, max_tokens: params.maxTokens, tools: enabledTools, tool_choice: "auto", stream: true, stream_options: { include_usage: true }, } as any); let roundText = ""; let streamedRoundText = ""; let roundHasToolCalls = false; const roundToolCalls = new Map(); for await (const chunk of stream as any as AsyncIterable) { rawResponses.push(chunk); sawUsage = mergeUsage(usageAcc, chunk?.usage) || sawUsage; const choice = chunk?.choices?.[0]; const deltaText = choice?.delta?.content ?? ""; if (typeof deltaText === "string" && deltaText.length) { roundText += deltaText; if (!roundHasToolCalls) { streamedRoundText += deltaText; yield { type: "delta", text: deltaText }; } } const deltaToolCalls = Array.isArray(choice?.delta?.tool_calls) ? choice.delta.tool_calls : []; if (deltaToolCalls.length) { roundHasToolCalls = true; } for (const toolCall of deltaToolCalls) { const idx = typeof toolCall?.index === "number" ? toolCall.index : 0; const entry = roundToolCalls.get(idx) ?? { arguments: "" }; if (typeof toolCall?.id === "string" && toolCall.id.length) { entry.id = toolCall.id; } if (typeof toolCall?.function?.name === "string" && toolCall.function.name.length) { entry.name = toolCall.function.name; } if (typeof toolCall?.function?.arguments === "string" && toolCall.function.arguments.length) { entry.arguments += toolCall.function.arguments; } roundToolCalls.set(idx, entry); } } const normalizedToolCalls: NormalizedToolCall[] = [...roundToolCalls.entries()] .sort((a, b) => a[0] - b[0]) .map(([_, call], index) => ({ id: call.id ?? `tool_call_${round}_${index}`, name: call.name ?? "unknown_tool", arguments: call.arguments || "{}", })); if (!normalizedToolCalls.length) { if (!streamedRoundText && danglingToolIntentRetries < MAX_DANGLING_TOOL_INTENT_RETRIES && looksLikeDanglingToolIntent(roundText)) { danglingToolIntentRetries += 1; appendDanglingToolIntentCorrection(conversation, roundText); continue; } const unstreamedText = getUnstreamedText(roundText, streamedRoundText); if (unstreamedText) { yield { type: "delta", text: unstreamedText }; } yield { type: "done", result: { text: roundText, usage: sawUsage ? usageAcc : undefined, raw: { streamed: true, responses: rawResponses, toolCallsUsed: totalToolCalls }, toolEvents, }, }; return; } totalToolCalls += normalizedToolCalls.length; const assistantToolCallMessage: any = { role: "assistant", tool_calls: normalizedToolCalls.map((call) => ({ id: call.id, type: "function", function: { name: call.name, arguments: call.arguments, }, })), }; if (roundText) { assistantToolCallMessage.content = roundText; } conversation.push(assistantToolCallMessage); for (const call of normalizedToolCalls) { const { event: initiatedEvent, execution } = prepareToolCallExecution(call); yield { type: "tool_call", event: initiatedEvent }; const { event, toolResult } = await executeToolCallAndBuildEvent(call, execution, params); toolEvents.push(event); yield { type: "tool_call", event }; conversation.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(toolResult), }); } } yield { type: "done", result: { text: "I reached the tool-call limit while gathering information. Please narrow the request and try again.", usage: sawUsage ? usageAcc : undefined, raw: { streamed: true, responses: rawResponses, toolCallsUsed: totalToolCalls, toolCallLimitReached: true }, toolEvents, }, }; }