From 93e34d086f841c3c1c52f345da95b8992682e456 Mon Sep 17 00:00:00 2001 From: Agent Date: Sun, 24 May 2026 21:59:38 +0000 Subject: [PATCH 1/2] Augment system prompt with date and user location (default SF) --- .../Sybil/Sources/Sybil/SybilAPIClient.swift | 1 + server/src/llm/chat-tools.ts | 31 +++++++------ server/src/llm/message-content.ts | 25 ++++++++++- server/src/llm/multiplexer.ts | 5 ++- server/src/llm/streaming.ts | 5 ++- server/src/llm/types.ts | 1 + server/src/routes.ts | 43 ++++++++++++++++++- server/tests/message-content.test.ts | 26 +++++++++++ tui/src/api.ts | 1 + web/src/lib/api.ts | 2 + 10 files changed, 121 insertions(+), 19 deletions(-) create mode 100644 server/tests/message-content.test.ts diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilAPIClient.swift b/ios/Packages/Sybil/Sources/Sybil/SybilAPIClient.swift index fdc4eb5..3a8135b 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilAPIClient.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilAPIClient.swift @@ -631,6 +631,7 @@ struct CompletionStreamRequest: Codable, Sendable { var provider: Provider var model: String var messages: [CompletionRequestMessage] + var userLocation: String? = nil } private struct ChatCreateBody: Encodable { diff --git a/server/src/llm/chat-tools.ts b/server/src/llm/chat-tools.ts index 0d32bbd..f58fd4b 100644 --- a/server/src/llm/chat-tools.ts +++ b/server/src/llm/chat-tools.ts @@ -9,7 +9,11 @@ import { z } from "zod"; import { env } from "../env.js"; import { exaClient } from "../search/exa.js"; import { searchSearxng } from "../search/searxng.js"; -import { buildOpenAIConversationMessage, buildOpenAIResponsesInputMessage } from "./message-content.js"; +import { + buildOpenAIConversationMessage, + buildOpenAIResponsesInputMessage, + buildSystemPromptAugmentationMessage, +} from "./message-content.js"; import type { ChatMessage } from "./types.js"; const MAX_TOOL_ROUNDS = env.CHAT_MAX_TOOL_ROUNDS; @@ -239,6 +243,7 @@ type ToolAwareCompletionParams = { client: OpenAI; model: string; messages: ChatMessage[]; + userLocation?: string; temperature?: number; maxTokens?: number; onToolEvent?: (event: ToolExecutionEvent) => void | Promise; @@ -379,20 +384,20 @@ function extractHtmlTitle(html: string) { ); } -function normalizeIncomingMessages(messages: ChatMessage[]) { +function normalizeIncomingMessages(messages: ChatMessage[], userLocation?: string) { const normalized = messages.map((message) => buildOpenAIConversationMessage(message)); - return [{ role: "system", content: CHAT_TOOL_SYSTEM_PROMPT }, ...normalized]; + return [{ role: "system", content: CHAT_TOOL_SYSTEM_PROMPT }, buildSystemPromptAugmentationMessage(userLocation), ...normalized]; } -function normalizePlainIncomingMessages(messages: ChatMessage[]) { - return messages.map((message) => buildOpenAIConversationMessage(message)); +function normalizePlainIncomingMessages(messages: ChatMessage[], userLocation?: string) { + return [buildSystemPromptAugmentationMessage(userLocation), ...messages.map((message) => buildOpenAIConversationMessage(message))]; } -function normalizeIncomingResponsesInput(messages: ChatMessage[]) { +function normalizeIncomingResponsesInput(messages: ChatMessage[], userLocation?: string) { const normalized = messages.map((message) => buildOpenAIResponsesInputMessage(message)); - return [{ role: "system", content: CHAT_TOOL_SYSTEM_PROMPT }, ...normalized]; + return [{ role: "system", content: CHAT_TOOL_SYSTEM_PROMPT }, buildSystemPromptAugmentationMessage(userLocation), ...normalized]; } async function runExaWebSearchTool(args: WebSearchArgs): Promise { @@ -957,7 +962,7 @@ async function executeToolCallAndBuildEvent( } export async function runToolAwareOpenAIChat(params: ToolAwareCompletionParams): Promise { - const input: any[] = normalizeIncomingResponsesInput(params.messages); + const input: any[] = normalizeIncomingResponsesInput(params.messages, params.userLocation); const rawResponses: unknown[] = []; const toolEvents: ToolExecutionEvent[] = []; const usageAcc: Required = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; @@ -1026,7 +1031,7 @@ export async function runToolAwareOpenAIChat(params: ToolAwareCompletionParams): } export async function runToolAwareChatCompletions(params: ToolAwareCompletionParams): Promise { - const conversation: any[] = normalizeIncomingMessages(params.messages); + const conversation: any[] = normalizeIncomingMessages(params.messages, params.userLocation); const rawResponses: unknown[] = []; const toolEvents: ToolExecutionEvent[] = []; const usageAcc: Required = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; @@ -1114,7 +1119,7 @@ export async function runToolAwareChatCompletions(params: ToolAwareCompletionPar export async function runPlainChatCompletions(params: ToolAwareCompletionParams): Promise { const completion = await params.client.chat.completions.create({ model: params.model, - messages: normalizePlainIncomingMessages(params.messages), + messages: normalizePlainIncomingMessages(params.messages, params.userLocation), temperature: params.temperature, max_tokens: params.maxTokens, } as any); @@ -1134,7 +1139,7 @@ export async function runPlainChatCompletions(params: ToolAwareCompletionParams) export async function* runToolAwareOpenAIChatStream( params: ToolAwareCompletionParams ): AsyncGenerator { - const input: any[] = normalizeIncomingResponsesInput(params.messages); + const input: any[] = normalizeIncomingResponsesInput(params.messages, params.userLocation); const rawResponses: unknown[] = []; const toolEvents: ToolExecutionEvent[] = []; const usageAcc: Required = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; @@ -1260,7 +1265,7 @@ export async function* runToolAwareOpenAIChatStream( export async function* runToolAwareChatCompletionsStream( params: ToolAwareCompletionParams ): AsyncGenerator { - const conversation: any[] = normalizeIncomingMessages(params.messages); + const conversation: any[] = normalizeIncomingMessages(params.messages, params.userLocation); const rawResponses: unknown[] = []; const toolEvents: ToolExecutionEvent[] = []; const usageAcc: Required = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; @@ -1403,7 +1408,7 @@ export async function* runPlainChatCompletionsStream( const stream = await params.client.chat.completions.create({ model: params.model, - messages: normalizePlainIncomingMessages(params.messages), + messages: normalizePlainIncomingMessages(params.messages, params.userLocation), temperature: params.temperature, max_tokens: params.maxTokens, stream: true, diff --git a/server/src/llm/message-content.ts b/server/src/llm/message-content.ts index 8b3434c..d399b29 100644 --- a/server/src/llm/message-content.ts +++ b/server/src/llm/message-content.ts @@ -1,5 +1,19 @@ import type { ChatAttachment, ChatImageAttachment, ChatMessage, ChatTextAttachment } from "./types.js"; +const DEFAULT_USER_LOCATION = "San Francisco, CA"; + +function currentDateString(now = new Date()) { + return now.toISOString().slice(0, 10); +} + +function resolveUserLocation(userLocation?: string) { + return userLocation?.trim() || process.env.SYBIL_USER_LOCATION?.trim() || DEFAULT_USER_LOCATION; +} + +export function buildSystemPromptAugmentation(userLocation?: string, now = new Date()) { + return `Current date: ${currentDateString(now)}.\nUser location: ${resolveUserLocation(userLocation)}.`; +} + function escapeAttribute(value: string) { return value.replace(/"/g, """); } @@ -198,11 +212,18 @@ export function buildOpenAIResponsesInputMessage(message: ChatMessage) { }; } +export function buildSystemPromptAugmentationMessage(userLocation?: string) { + return { + role: "system", + content: buildSystemPromptAugmentation(userLocation), + }; +} + const ANTHROPIC_NO_SERVER_TOOLS_PROMPT = "This Anthropic backend path does not have server-managed tool calls. Do not claim to run shell commands, Codex tasks, web searches, or fetch URLs. If the user asks for tool execution, explain that they should switch to OpenAI or xAI in this app for tool-enabled chat."; -export function getAnthropicSystemPrompt(messages: ChatMessage[]) { - return [ANTHROPIC_NO_SERVER_TOOLS_PROMPT, messages.find((message) => message.role === "system")?.content] +export function getAnthropicSystemPrompt(messages: ChatMessage[], userLocation?: string) { + return [ANTHROPIC_NO_SERVER_TOOLS_PROMPT, buildSystemPromptAugmentation(userLocation), messages.find((message) => message.role === "system")?.content] .filter(Boolean) .join("\n\n"); } diff --git a/server/src/llm/multiplexer.ts b/server/src/llm/multiplexer.ts index e2b39ed..f852f13 100644 --- a/server/src/llm/multiplexer.ts +++ b/server/src/llm/multiplexer.ts @@ -54,6 +54,7 @@ export async function runMultiplex(req: MultiplexRequest): Promise message.role !== "system").map((message) => buildAnthropicConversationMessage(message)); const r = await client.messages.create({ diff --git a/server/src/llm/streaming.ts b/server/src/llm/streaming.ts index cbafb19..32ea834 100644 --- a/server/src/llm/streaming.ts +++ b/server/src/llm/streaming.ts @@ -82,6 +82,7 @@ export async function* runMultiplexStream(req: MultiplexRequest): AsyncGenerator client, model: req.model, messages: req.messages, + userLocation: req.userLocation, temperature: req.temperature, maxTokens: req.maxTokens, logContext: { @@ -95,6 +96,7 @@ export async function* runMultiplexStream(req: MultiplexRequest): AsyncGenerator client, model: req.model, messages: req.messages, + userLocation: req.userLocation, temperature: req.temperature, maxTokens: req.maxTokens, logContext: { @@ -107,6 +109,7 @@ export async function* runMultiplexStream(req: MultiplexRequest): AsyncGenerator client, model: req.model, messages: req.messages, + userLocation: req.userLocation, temperature: req.temperature, maxTokens: req.maxTokens, logContext: { @@ -146,7 +149,7 @@ export async function* runMultiplexStream(req: MultiplexRequest): AsyncGenerator } else if (req.provider === "anthropic") { const client = anthropicClient(); - const system = getAnthropicSystemPrompt(req.messages); + const system = getAnthropicSystemPrompt(req.messages, req.userLocation); const msgs = req.messages.filter((message) => message.role !== "system").map((message) => buildAnthropicConversationMessage(message)); const stream = await client.messages.create({ diff --git a/server/src/llm/types.ts b/server/src/llm/types.ts index 618fdf4..427f053 100644 --- a/server/src/llm/types.ts +++ b/server/src/llm/types.ts @@ -36,6 +36,7 @@ export type MultiplexRequest = { provider: Provider; model: string; messages: ChatMessage[]; + userLocation?: string; temperature?: number; maxTokens?: number; }; diff --git a/server/src/routes.ts b/server/src/routes.ts index 4a58109..ddecbef 100644 --- a/server/src/routes.ts +++ b/server/src/routes.ts @@ -47,6 +47,43 @@ function isToolCallLogMessage(message: { role: string; metadata: unknown }) { return message.role === "tool" && isToolCallLogMetadata(message.metadata); } +function getHeaderString(req: FastifyRequest, name: string) { + const value = req.headers[name.toLowerCase()]; + if (Array.isArray(value)) return value.find((item) => item.trim()); + return typeof value === "string" && value.trim() ? value : undefined; +} + +function decodeHeaderPart(value: string | undefined) { + if (!value) return undefined; + const trimmed = value.trim(); + if (!trimmed) return undefined; + try { + return decodeURIComponent(trimmed); + } catch { + return trimmed; + } +} + +function inferRequestUserLocation(req: FastifyRequest) { + const explicit = decodeHeaderPart(getHeaderString(req, "x-user-location")); + if (explicit) return explicit; + + const vercelCity = decodeHeaderPart(getHeaderString(req, "x-vercel-ip-city")); + const vercelRegion = decodeHeaderPart(getHeaderString(req, "x-vercel-ip-country-region")); + const vercelCountry = decodeHeaderPart(getHeaderString(req, "x-vercel-ip-country")); + const vercelLocation = [vercelCity, vercelRegion, vercelCountry].filter(Boolean).join(", "); + if (vercelLocation) return vercelLocation; + + const cfCity = decodeHeaderPart(getHeaderString(req, "cf-ipcity")); + const cfRegion = decodeHeaderPart(getHeaderString(req, "cf-region")); + const cfCountry = decodeHeaderPart(getHeaderString(req, "cf-ipcountry")); + return [cfCity, cfRegion, cfCountry].filter(Boolean).join(", ") || undefined; +} + +function withRequestUserLocation(body: T, req: FastifyRequest): T { + return body.userLocation ? body : { ...body, userLocation: inferRequestUserLocation(req) }; +} + async function storeNonAssistantMessages(chatId: string, messages: IncomingChatMessage[]) { const incoming = messages.filter((m) => m.role !== "assistant"); if (!incoming.length) return; @@ -131,6 +168,7 @@ const CompletionStreamBody = z provider: ProviderSchema, model: z.string().min(1), messages: z.array(CompletionMessageSchema), + userLocation: z.string().trim().min(1).max(200).optional(), temperature: z.number().min(0).max(2).optional(), maxTokens: z.number().int().positive().optional(), }) @@ -1085,13 +1123,14 @@ export async function registerRoutes(app: FastifyInstance) { provider: ProviderSchema, model: z.string().min(1), messages: z.array(CompletionMessageSchema), + userLocation: z.string().trim().min(1).max(200).optional(), temperature: z.number().min(0).max(2).optional(), maxTokens: z.number().int().positive().optional(), }); const parsed = Body.safeParse(req.body); if (!parsed.success) return app.httpErrors.badRequest(parsed.error.message); - const body = parsed.data; + const body = withRequestUserLocation(parsed.data, req); // ensure chat exists if provided if (body.chatId) { @@ -1118,7 +1157,7 @@ export async function registerRoutes(app: FastifyInstance) { const parsed = CompletionStreamBody.safeParse(req.body); if (!parsed.success) return app.httpErrors.badRequest(parsed.error.message); - const body = parsed.data; + const body = withRequestUserLocation(parsed.data, req); // ensure chat exists if provided if (body.chatId) { diff --git a/server/tests/message-content.test.ts b/server/tests/message-content.test.ts new file mode 100644 index 0000000..3f38080 --- /dev/null +++ b/server/tests/message-content.test.ts @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { buildSystemPromptAugmentation, getAnthropicSystemPrompt } from "../src/llm/message-content.js"; + +test("system prompt augmentation includes date and default location", () => { + const prompt = buildSystemPromptAugmentation(undefined, new Date("2026-05-24T15:30:00Z")); + + assert.equal(prompt, "Current date: 2026-05-24.\nUser location: San Francisco, CA."); +}); + +test("system prompt augmentation uses provided user location", () => { + const prompt = buildSystemPromptAugmentation("New York, NY", new Date("2026-05-24T15:30:00Z")); + + assert.equal(prompt, "Current date: 2026-05-24.\nUser location: New York, NY."); +}); + +test("Anthropic system prompt includes runtime context with existing system messages", () => { + const prompt = getAnthropicSystemPrompt( + [{ role: "system", content: "Use concise answers." }], + "Los Angeles, CA" + ); + + assert.match(prompt, /Current date: \d{4}-\d{2}-\d{2}\./); + assert.match(prompt, /User location: Los Angeles, CA\./); + assert.match(prompt, /Use concise answers\./); +}); diff --git a/tui/src/api.ts b/tui/src/api.ts index 2a81506..be66200 100644 --- a/tui/src/api.ts +++ b/tui/src/api.ts @@ -100,6 +100,7 @@ export class SybilApiClient { provider: Provider; model: string; messages: CompletionRequestMessage[]; + userLocation?: string; }, handlers: CompletionStreamHandlers, options?: { signal?: AbortSignal } diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 5a4b9f1..3a1b00d 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -569,6 +569,7 @@ export async function runCompletion(body: { provider: Provider; model: string; messages: CompletionRequestMessage[]; + userLocation?: string; }) { return api("/v1/chat-completions", { method: "POST", @@ -583,6 +584,7 @@ export async function runCompletionStream( provider: Provider; model: string; messages: CompletionRequestMessage[]; + userLocation?: string; }, handlers: CompletionStreamHandlers, options?: { signal?: AbortSignal } -- 2.49.1 From f3bb8503aa870c7b05ded9549bd4c0551f818b85 Mon Sep 17 00:00:00 2001 From: Agent Date: Sun, 24 May 2026 22:04:05 +0000 Subject: [PATCH 2/2] Add per-chat settings UI in web app for additional system prompt and tool checkboxes --- .../migration.sql | 3 + server/prisma/schema.prisma | 3 + server/src/llm/chat-tools.ts | 88 ++++++++++++++++--- server/src/llm/multiplexer.ts | 13 +-- server/src/llm/streaming.ts | 8 +- server/src/llm/types.ts | 2 + server/src/routes.ts | 84 ++++++++++++++++-- web/src/App.tsx | 81 ++++++++++++++++- web/src/lib/api.ts | 28 ++++++ 9 files changed, 282 insertions(+), 28 deletions(-) create mode 100644 server/prisma/migrations/20260524000000_add_chat_settings/migration.sql diff --git a/server/prisma/migrations/20260524000000_add_chat_settings/migration.sql b/server/prisma/migrations/20260524000000_add_chat_settings/migration.sql new file mode 100644 index 0000000..d6e7b74 --- /dev/null +++ b/server/prisma/migrations/20260524000000_add_chat_settings/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Chat" ADD COLUMN "additionalSystemPrompt" TEXT; +ALTER TABLE "Chat" ADD COLUMN "enabledTools" JSONB; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index a9d3dd3..2103b92 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -51,6 +51,9 @@ model Chat { lastUsedProvider Provider? lastUsedModel String? + additionalSystemPrompt String? + enabledTools Json? + user User? @relation(fields: [userId], references: [id]) userId String? diff --git a/server/src/llm/chat-tools.ts b/server/src/llm/chat-tools.ts index f58fd4b..29ed23a 100644 --- a/server/src/llm/chat-tools.ts +++ b/server/src/llm/chat-tools.ts @@ -192,7 +192,43 @@ const CHAT_TOOLS: any[] = [ ...(env.CHAT_SHELL_TOOL_ENABLED ? [SHELL_EXEC_TOOL] : []), ]; -const RESPONSES_CHAT_TOOLS: any[] = CHAT_TOOLS.map((tool) => { +function getToolName(tool: any) { + return typeof tool?.function?.name === "string" ? tool.function.name : null; +} + +export function getAvailableChatTools() { + return CHAT_TOOLS.map((tool) => { + const name = getToolName(tool); + if (!name) return null; + return { + name, + description: typeof tool?.function?.description === "string" ? tool.function.description : "", + }; + }).filter((tool): tool is { name: string; description: string } => tool !== null); +} + +export function normalizeEnabledChatTools(value: unknown) { + if (!Array.isArray(value)) return getAvailableChatTools().map((tool) => tool.name); + const available = new Set(getAvailableChatTools().map((tool) => tool.name)); + return [...new Set(value.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean))].filter((name) => + available.has(name) + ); +} + +function getEnabledToolSet(params: Pick) { + return new Set(normalizeEnabledChatTools(params.enabledTools)); +} + +function getEnabledChatTools(params: Pick) { + const enabled = getEnabledToolSet(params); + return CHAT_TOOLS.filter((tool) => { + const name = getToolName(tool); + return name ? enabled.has(name) : false; + }); +} + +function toResponsesChatTools(tools: any[]) { + return tools.map((tool) => { if (tool?.type !== "function") return tool; return { type: "function", @@ -201,7 +237,8 @@ const RESPONSES_CHAT_TOOLS: any[] = CHAT_TOOLS.map((tool) => { parameters: tool.function.parameters, strict: false, }; -}); + }); +} export const CHAT_TOOL_SYSTEM_PROMPT = "You can use tools to gather up-to-date web information when needed. " + @@ -243,6 +280,7 @@ type ToolAwareCompletionParams = { client: OpenAI; model: string; messages: ChatMessage[]; + enabledTools?: string[]; userLocation?: string; temperature?: number; maxTokens?: number; @@ -384,20 +422,38 @@ function extractHtmlTitle(html: string) { ); } -function normalizeIncomingMessages(messages: ChatMessage[], userLocation?: string) { +function buildChatToolSystemPrompt(params: Pick) { + const enabled = getEnabledToolSet(params); + return ( + "You can use tools to gather up-to-date web information when needed. " + + (enabled.has("web_search") ? "Use web_search for discovery and recent facts. " : "") + + (enabled.has("fetch_url") ? "Use fetch_url to read the full content of a specific page. " : "") + + "Prefer tools when the user asks for current events, verification, sources, or details you do not already have. " + + "When you decide tool use is needed, call the tool immediately in the same response; do not say you are running a tool unless you actually call it. " + + (enabled.has("codex_exec") + ? "Use codex_exec when a request needs substantial coding work, repository inspection, shell commands, tests, debugging, or another complex task suited to a persistent Codex workspace. Provide codex_exec a complete prompt with the goal, constraints, assumptions, and expected report-back format. Never ask codex_exec to wait for user input or run interactive commands. " + : "") + + (enabled.has("shell_exec") + ? "Use shell_exec for direct non-interactive command-line work on the remote devbox, including quick Python programs, calculations, file inspection, running tests, and small scripts. " + : "") + + "Do not fabricate tool outputs; reason only from provided tool results." + ); +} + +function normalizeIncomingMessages(messages: ChatMessage[], userLocation?: string, params: Pick = {}) { const normalized = messages.map((message) => buildOpenAIConversationMessage(message)); - return [{ role: "system", content: CHAT_TOOL_SYSTEM_PROMPT }, buildSystemPromptAugmentationMessage(userLocation), ...normalized]; + return [{ role: "system", content: buildChatToolSystemPrompt(params) }, buildSystemPromptAugmentationMessage(userLocation), ...normalized]; } function normalizePlainIncomingMessages(messages: ChatMessage[], userLocation?: string) { return [buildSystemPromptAugmentationMessage(userLocation), ...messages.map((message) => buildOpenAIConversationMessage(message))]; } -function normalizeIncomingResponsesInput(messages: ChatMessage[], userLocation?: string) { +function normalizeIncomingResponsesInput(messages: ChatMessage[], userLocation?: string, params: Pick = {}) { const normalized = messages.map((message) => buildOpenAIResponsesInputMessage(message)); - return [{ role: "system", content: CHAT_TOOL_SYSTEM_PROMPT }, buildSystemPromptAugmentationMessage(userLocation), ...normalized]; + return [{ role: "system", content: buildChatToolSystemPrompt(params) }, buildSystemPromptAugmentationMessage(userLocation), ...normalized]; } async function runExaWebSearchTool(args: WebSearchArgs): Promise { @@ -962,7 +1018,8 @@ async function executeToolCallAndBuildEvent( } export async function runToolAwareOpenAIChat(params: ToolAwareCompletionParams): Promise { - const input: any[] = normalizeIncomingResponsesInput(params.messages, params.userLocation); + const enabledTools = getEnabledChatTools(params); + const input: any[] = normalizeIncomingResponsesInput(params.messages, params.userLocation, params); const rawResponses: unknown[] = []; const toolEvents: ToolExecutionEvent[] = []; const usageAcc: Required = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; @@ -976,7 +1033,7 @@ export async function runToolAwareOpenAIChat(params: ToolAwareCompletionParams): input, temperature: params.temperature, max_output_tokens: params.maxTokens, - tools: RESPONSES_CHAT_TOOLS, + tools: toResponsesChatTools(enabledTools), tool_choice: "auto", parallel_tool_calls: true, // Tool loops pass response output items back as input; reasoning items need persistence. @@ -1031,7 +1088,8 @@ export async function runToolAwareOpenAIChat(params: ToolAwareCompletionParams): } export async function runToolAwareChatCompletions(params: ToolAwareCompletionParams): Promise { - const conversation: any[] = normalizeIncomingMessages(params.messages, params.userLocation); + const enabledTools = getEnabledChatTools(params); + const conversation: any[] = normalizeIncomingMessages(params.messages, params.userLocation, params); const rawResponses: unknown[] = []; const toolEvents: ToolExecutionEvent[] = []; const usageAcc: Required = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; @@ -1045,7 +1103,7 @@ export async function runToolAwareChatCompletions(params: ToolAwareCompletionPar messages: conversation, temperature: params.temperature, max_tokens: params.maxTokens, - tools: CHAT_TOOLS, + tools: enabledTools, tool_choice: "auto", } as any); rawResponses.push(completion); @@ -1139,7 +1197,8 @@ export async function runPlainChatCompletions(params: ToolAwareCompletionParams) export async function* runToolAwareOpenAIChatStream( params: ToolAwareCompletionParams ): AsyncGenerator { - const input: any[] = normalizeIncomingResponsesInput(params.messages, params.userLocation); + const enabledTools = getEnabledChatTools(params); + const input: any[] = normalizeIncomingResponsesInput(params.messages, params.userLocation, params); const rawResponses: unknown[] = []; const toolEvents: ToolExecutionEvent[] = []; const usageAcc: Required = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; @@ -1153,7 +1212,7 @@ export async function* runToolAwareOpenAIChatStream( input, temperature: params.temperature, max_output_tokens: params.maxTokens, - tools: RESPONSES_CHAT_TOOLS, + tools: toResponsesChatTools(enabledTools), tool_choice: "auto", parallel_tool_calls: true, // Tool loops pass response output items back as input; reasoning items need persistence. @@ -1265,7 +1324,8 @@ export async function* runToolAwareOpenAIChatStream( export async function* runToolAwareChatCompletionsStream( params: ToolAwareCompletionParams ): AsyncGenerator { - const conversation: any[] = normalizeIncomingMessages(params.messages, params.userLocation); + const enabledTools = getEnabledChatTools(params); + const conversation: any[] = normalizeIncomingMessages(params.messages, params.userLocation, params); const rawResponses: unknown[] = []; const toolEvents: ToolExecutionEvent[] = []; const usageAcc: Required = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; @@ -1279,7 +1339,7 @@ export async function* runToolAwareChatCompletionsStream( messages: conversation, temperature: params.temperature, max_tokens: params.maxTokens, - tools: CHAT_TOOLS, + tools: enabledTools, tool_choice: "auto", stream: true, stream_options: { include_usage: true }, diff --git a/server/src/llm/multiplexer.ts b/server/src/llm/multiplexer.ts index f852f13..1bacdb2 100644 --- a/server/src/llm/multiplexer.ts +++ b/server/src/llm/multiplexer.ts @@ -1,7 +1,7 @@ import { performance } from "node:perf_hooks"; import { prisma } from "../db.js"; import { anthropicClient, hermesAgentClient, openaiClient, xaiClient } from "./providers.js"; -import { buildToolLogMessageData, runPlainChatCompletions, runToolAwareChatCompletions, runToolAwareOpenAIChat } from "./chat-tools.js"; +import { buildToolLogMessageData, normalizeEnabledChatTools, runPlainChatCompletions, runToolAwareChatCompletions, runToolAwareOpenAIChat } from "./chat-tools.js"; import { buildAnthropicConversationMessage, getAnthropicSystemPrompt } from "./message-content.js"; import { toPrismaProvider } from "./provider-ids.js"; import type { MultiplexRequest, MultiplexResponse, Provider } from "./types.js"; @@ -47,13 +47,15 @@ export async function runMultiplex(req: MultiplexRequest): Promise[] = []; + const enabledTools = normalizeEnabledChatTools(req.enabledTools); - if (req.provider === "openai") { + if (req.provider === "openai" && enabledTools.length > 0) { const client = openaiClient(); const r = await runToolAwareOpenAIChat({ client, model: req.model, messages: req.messages, + enabledTools, userLocation: req.userLocation, temperature: req.temperature, maxTokens: req.maxTokens, @@ -67,12 +69,13 @@ export async function runMultiplex(req: MultiplexRequest): Promise buildToolLogMessageData(call.chatId, event)); - } else if (req.provider === "xai") { + } else if (req.provider === "xai" && enabledTools.length > 0) { const client = xaiClient(); const r = await runToolAwareChatCompletions({ client, model: req.model, messages: req.messages, + enabledTools, userLocation: req.userLocation, temperature: req.temperature, maxTokens: req.maxTokens, @@ -86,8 +89,8 @@ export async function runMultiplex(req: MultiplexRequest): Promise buildToolLogMessageData(call.chatId, event)); - } else if (req.provider === "hermes-agent") { - const client = hermesAgentClient(); + } else if (req.provider === "openai" || req.provider === "xai" || req.provider === "hermes-agent") { + const client = req.provider === "openai" ? openaiClient() : req.provider === "xai" ? xaiClient() : hermesAgentClient(); const r = await runPlainChatCompletions({ client, model: req.model, diff --git a/server/src/llm/streaming.ts b/server/src/llm/streaming.ts index 32ea834..4457a63 100644 --- a/server/src/llm/streaming.ts +++ b/server/src/llm/streaming.ts @@ -3,6 +3,7 @@ import { prisma } from "../db.js"; import { anthropicClient, hermesAgentClient, openaiClient, xaiClient } from "./providers.js"; import { buildToolLogMessageData, + normalizeEnabledChatTools, runPlainChatCompletionsStream, runToolAwareChatCompletionsStream, runToolAwareOpenAIChatStream, @@ -76,12 +77,14 @@ export async function* runMultiplexStream(req: MultiplexRequest): AsyncGenerator try { if (req.provider === "openai" || req.provider === "xai" || req.provider === "hermes-agent") { const client = req.provider === "openai" ? openaiClient() : req.provider === "xai" ? xaiClient() : hermesAgentClient(); + const enabledTools = normalizeEnabledChatTools(req.enabledTools); const streamEvents = - req.provider === "openai" + req.provider === "openai" && enabledTools.length > 0 ? runToolAwareOpenAIChatStream({ client, model: req.model, messages: req.messages, + enabledTools, userLocation: req.userLocation, temperature: req.temperature, maxTokens: req.maxTokens, @@ -91,7 +94,7 @@ export async function* runMultiplexStream(req: MultiplexRequest): AsyncGenerator chatId: chatId ?? undefined, }, }) - : req.provider === "hermes-agent" + : req.provider === "hermes-agent" || enabledTools.length === 0 ? runPlainChatCompletionsStream({ client, model: req.model, @@ -109,6 +112,7 @@ export async function* runMultiplexStream(req: MultiplexRequest): AsyncGenerator client, model: req.model, messages: req.messages, + enabledTools, userLocation: req.userLocation, temperature: req.temperature, maxTokens: req.maxTokens, diff --git a/server/src/llm/types.ts b/server/src/llm/types.ts index 427f053..b4cf6d4 100644 --- a/server/src/llm/types.ts +++ b/server/src/llm/types.ts @@ -36,6 +36,8 @@ export type MultiplexRequest = { provider: Provider; model: string; messages: ChatMessage[]; + additionalSystemPrompt?: string; + enabledTools?: string[]; userLocation?: string; temperature?: number; maxTokens?: number; diff --git a/server/src/routes.ts b/server/src/routes.ts index ddecbef..1f4ace4 100644 --- a/server/src/routes.ts +++ b/server/src/routes.ts @@ -8,6 +8,7 @@ import { env } from "./env.js"; import { buildComparableAttachments } from "./llm/message-content.js"; import { runMultiplex } from "./llm/multiplexer.js"; import { runMultiplexStream, type StreamEvent } from "./llm/streaming.js"; +import { getAvailableChatTools, normalizeEnabledChatTools } from "./llm/chat-tools.js"; import { getModelCatalogSnapshot } from "./llm/model-catalog.js"; import { openaiClient } from "./llm/providers.js"; import { serializeProviderFields, toPrismaProvider } from "./llm/provider-ids.js"; @@ -15,6 +16,8 @@ import { exaClient } from "./search/exa.js"; import type { ChatAttachment } from "./llm/types.js"; const ProviderSchema = z.enum(["openai", "anthropic", "xai", "hermes-agent"]); +const MAX_ADDITIONAL_SYSTEM_PROMPT_CHARS = 12_000; +const EnabledToolsSchema = z.array(z.string().trim().min(1).max(80)).max(20).transform((value) => normalizeEnabledChatTools(value)); type IncomingChatMessage = { role: "system" | "user" | "assistant" | "tool"; @@ -168,6 +171,8 @@ const CompletionStreamBody = z provider: ProviderSchema, model: z.string().min(1), messages: z.array(CompletionMessageSchema), + additionalSystemPrompt: z.string().max(MAX_ADDITIONAL_SYSTEM_PROMPT_CHARS).optional(), + enabledTools: EnabledToolsSchema.optional(), userLocation: z.string().trim().min(1).max(200).optional(), temperature: z.number().min(0).max(2).optional(), maxTokens: z.number().int().positive().optional(), @@ -193,6 +198,41 @@ function mergeAttachmentsIntoMetadata(metadata: unknown, attachments?: ChatAttac }; } +function normalizeAdditionalSystemPrompt(value: string | null | undefined) { + const trimmed = value?.trim(); + return trimmed || null; +} + +function prependAdditionalSystemPrompt(body: T): T { + const additionalSystemPrompt = normalizeAdditionalSystemPrompt(body.additionalSystemPrompt); + if (!additionalSystemPrompt) return { ...body, additionalSystemPrompt: undefined }; + return { + ...body, + additionalSystemPrompt, + messages: [{ role: "system", content: additionalSystemPrompt }, ...body.messages], + }; +} + +async function applyStoredChatSettings( + body: T +) { + if (!body.chatId || (body.additionalSystemPrompt !== undefined && body.enabledTools !== undefined)) { + return prependAdditionalSystemPrompt(body); + } + + const chat = await prisma.chat.findUnique({ + where: { id: body.chatId }, + select: { additionalSystemPrompt: true, enabledTools: true }, + }); + if (!chat) return prependAdditionalSystemPrompt(body); + + return prependAdditionalSystemPrompt({ + ...body, + additionalSystemPrompt: body.additionalSystemPrompt ?? chat.additionalSystemPrompt ?? undefined, + enabledTools: body.enabledTools ?? normalizeEnabledChatTools(chat.enabledTools), + }); +} + const SearchRunBody = z.object({ query: z.string().trim().min(1).optional(), title: z.string().trim().min(1).optional(), @@ -382,6 +422,8 @@ async function listWorkspaceItems() { initiatedModel: true, lastUsedProvider: true, lastUsedModel: true, + additionalSystemPrompt: true, + enabledTools: true, }, }), prisma.search.findMany({ @@ -641,6 +683,11 @@ export async function registerRoutes(app: FastifyInstance) { return { providers: getModelCatalogSnapshot() }; }); + app.get("/v1/chat-tools", async (req) => { + requireAdmin(req); + return { tools: getAvailableChatTools() }; + }); + app.get("/v1/active-runs", async (req) => { requireAdmin(req); return { @@ -668,6 +715,8 @@ export async function registerRoutes(app: FastifyInstance) { initiatedModel: true, lastUsedProvider: true, lastUsedModel: true, + additionalSystemPrompt: true, + enabledTools: true, }, }); return { chats: chats.map((chat) => serializeProviderFields(chat)) }; @@ -680,6 +729,8 @@ export async function registerRoutes(app: FastifyInstance) { title: z.string().optional(), provider: ProviderSchema.optional(), model: z.string().trim().min(1).optional(), + additionalSystemPrompt: z.string().max(MAX_ADDITIONAL_SYSTEM_PROMPT_CHARS).optional(), + enabledTools: EnabledToolsSchema.optional(), messages: z.array(CompletionMessageSchema).optional(), }) .superRefine((value, ctx) => { @@ -708,6 +759,8 @@ export async function registerRoutes(app: FastifyInstance) { initiatedModel: body.model, lastUsedProvider: body.provider ? (toPrismaProvider(body.provider) as any) : undefined, lastUsedModel: body.model, + additionalSystemPrompt: normalizeAdditionalSystemPrompt(body.additionalSystemPrompt), + enabledTools: body.enabledTools as any, messages: body.messages?.length ? { create: body.messages.map((message) => ({ @@ -728,6 +781,8 @@ export async function registerRoutes(app: FastifyInstance) { initiatedModel: true, lastUsedProvider: true, lastUsedModel: true, + additionalSystemPrompt: true, + enabledTools: true, }, }); return { chat: serializeProviderFields(chat) }; @@ -736,13 +791,22 @@ export async function registerRoutes(app: FastifyInstance) { app.patch("/v1/chats/:chatId", async (req) => { requireAdmin(req); const Params = z.object({ chatId: z.string() }); - const Body = z.object({ title: z.string().trim().min(1) }); + const Body = z.object({ + title: z.string().trim().min(1).optional(), + additionalSystemPrompt: z.string().max(MAX_ADDITIONAL_SYSTEM_PROMPT_CHARS).nullable().optional(), + enabledTools: EnabledToolsSchema.optional(), + }); const { chatId } = Params.parse(req.params); const body = Body.parse(req.body ?? {}); + const data: Record = {}; + if (body.title !== undefined) data.title = body.title; + if (body.additionalSystemPrompt !== undefined) data.additionalSystemPrompt = normalizeAdditionalSystemPrompt(body.additionalSystemPrompt); + if (body.enabledTools !== undefined) data.enabledTools = body.enabledTools; + const updated = await prisma.chat.updateMany({ where: { id: chatId }, - data: { title: body.title }, + data: data as any, }); if (updated.count === 0) return app.httpErrors.notFound("chat not found"); @@ -758,6 +822,8 @@ export async function registerRoutes(app: FastifyInstance) { initiatedModel: true, lastUsedProvider: true, lastUsedModel: true, + additionalSystemPrompt: true, + enabledTools: true, }, }); if (!chat) return app.httpErrors.notFound("chat not found"); @@ -783,6 +849,8 @@ export async function registerRoutes(app: FastifyInstance) { initiatedModel: true, lastUsedProvider: true, lastUsedModel: true, + additionalSystemPrompt: true, + enabledTools: true, }, }); if (!existing) return app.httpErrors.notFound("chat not found"); @@ -804,6 +872,8 @@ export async function registerRoutes(app: FastifyInstance) { initiatedModel: true, lastUsedProvider: true, lastUsedModel: true, + additionalSystemPrompt: true, + enabledTools: true, }, }); @@ -924,6 +994,8 @@ export async function registerRoutes(app: FastifyInstance) { initiatedModel: true, lastUsedProvider: true, lastUsedModel: true, + additionalSystemPrompt: true, + enabledTools: true, }, }); @@ -1123,6 +1195,8 @@ export async function registerRoutes(app: FastifyInstance) { provider: ProviderSchema, model: z.string().min(1), messages: z.array(CompletionMessageSchema), + additionalSystemPrompt: z.string().max(MAX_ADDITIONAL_SYSTEM_PROMPT_CHARS).optional(), + enabledTools: EnabledToolsSchema.optional(), userLocation: z.string().trim().min(1).max(200).optional(), temperature: z.number().min(0).max(2).optional(), maxTokens: z.number().int().positive().optional(), @@ -1143,7 +1217,7 @@ export async function registerRoutes(app: FastifyInstance) { await storeNonAssistantMessages(body.chatId, body.messages); } - const result = await runMultiplex(body); + const result = await runMultiplex(await applyStoredChatSettings(body)); return { chatId: body.chatId ?? null, @@ -1174,14 +1248,14 @@ export async function registerRoutes(app: FastifyInstance) { if (activeChatStreams.has(body.chatId)) { return app.httpErrors.conflict("chat completion already running"); } - const stream = startActiveChatStream(body.chatId, body); + const stream = startActiveChatStream(body.chatId, await applyStoredChatSettings(body)); return streamActiveRun(req, reply, stream); } reply.raw.writeHead(200, buildSseHeaders(typeof req.headers.origin === "string" ? req.headers.origin : undefined)); reply.raw.flushHeaders(); - for await (const ev of runMultiplexStream(body)) { + for await (const ev of runMultiplexStream(await applyStoredChatSettings(body))) { writeSseEvent(reply, mapChatStreamEvent(ev)); } diff --git a/web/src/App.tsx b/web/src/App.tsx index 3caed39..2f24613 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,5 +1,20 @@ import { useEffect, useMemo, useRef, useState } from "preact/hooks"; -import { Check, ChevronDown, Globe2, LoaderCircle, Menu, MessageSquare, Paperclip, Plus, Rabbit, Search, SendHorizontal, Trash2, X } from "lucide-preact"; +import { + Check, + ChevronDown, + Globe2, + LoaderCircle, + Menu, + MessageSquare, + Paperclip, + Plus, + Rabbit, + Search, + SendHorizontal, + Settings2, + Trash2, + X, +} from "lucide-preact"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Separator } from "@/components/ui/separator"; @@ -18,12 +33,14 @@ import { attachSearchStream, getActiveRuns, getChat, + listChatTools, listModels, getSearch, listWorkspaceItems, runCompletionStream, runSearchStream, suggestChatTitle, + updateChatSettings, getMessageAttachments, type ChatAttachment, type ActiveRunsResponse, @@ -31,6 +48,7 @@ import { type Provider, type ChatDetail, type ChatSummary, + type ChatToolInfo, type CompletionRequestMessage, type Message, type SearchDetail, @@ -371,6 +389,30 @@ function getProviderLabel(provider: Provider | null | undefined) { return ""; } +function getToolLabel(name: string) { + if (name === "web_search") return "Web search"; + if (name === "fetch_url") return "Fetch URL"; + if (name === "codex_exec") return "Codex"; + if (name === "shell_exec") return "Shell"; + return name + .split("_") + .filter(Boolean) + .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)) + .join(" "); +} + +function getDefaultEnabledTools(availableTools: ChatToolInfo[]) { + return availableTools.map((tool) => tool.name); +} + +function normalizeEnabledTools(value: unknown, availableTools: ChatToolInfo[]) { + const available = new Set(availableTools.map((tool) => tool.name)); + if (!Array.isArray(value)) return getDefaultEnabledTools(availableTools); + return [...new Set(value.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean))].filter((name) => + available.has(name) + ); +} + function getChatModelSelection(chat: Pick | Pick | null) { if (!chat?.lastUsedProvider || !chat.lastUsedModel?.trim()) return null; return { @@ -730,6 +772,7 @@ export default function App() { const [isComposerDropActive, setIsComposerDropActive] = useState(false); const [provider, setProvider] = useState("openai"); const [modelCatalog, setModelCatalog] = useState(EMPTY_MODEL_CATALOG); + const [availableChatTools, setAvailableChatTools] = useState([]); const [providerModelPreferences, setProviderModelPreferences] = useState(() => loadStoredModelPreferences()); const [model, setModel] = useState(() => { const stored = loadStoredModelPreferences(); @@ -752,6 +795,9 @@ export default function App() { const [isConvertingQuickQuestion, setIsConvertingQuickQuestion] = useState(false); const [quickQuestionError, setQuickQuestionError] = useState(null); const [error, setError] = useState(null); + const [isChatSettingsOpen, setIsChatSettingsOpen] = useState(false); + const [additionalSystemPrompt, setAdditionalSystemPrompt] = useState(""); + const [enabledTools, setEnabledTools] = useState([]); const [transcriptTailSpacerHeight, setTranscriptTailSpacerHeight] = useState(TRANSCRIPT_BOTTOM_GAP); const transcriptContainerRef = useRef(null); const transcriptEndRef = useRef(null); @@ -876,6 +922,9 @@ export default function App() { searchRunCountersRef.current.clear(); setComposer(""); setPendingAttachments([]); + setIsChatSettingsOpen(false); + setAdditionalSystemPrompt(""); + setEnabledTools([]); setIsQuickQuestionOpen(false); setQuickPrompt(""); setQuickSubmittedPrompt(null); @@ -940,6 +989,21 @@ export default function App() { } }; + const refreshChatTools = async () => { + try { + const tools = await listChatTools(); + setAvailableChatTools(tools); + setEnabledTools((current) => normalizeEnabledTools(current.length ? current : null, tools)); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (message.includes("bearer token")) { + handleAuthFailure(message); + } else { + setError(message); + } + } + }; + const refreshActiveRuns = async () => { try { const data = await getActiveRuns(); @@ -992,7 +1056,7 @@ export default function App() { if (!isAuthenticated) return; const preferredSelection = initialRouteSelectionRef.current; initialRouteSelectionRef.current = null; - void Promise.all([refreshCollections(preferredSelection ?? undefined), refreshModels(), refreshActiveRuns()]); + void Promise.all([refreshCollections(preferredSelection ?? undefined), refreshModels(), refreshChatTools(), refreshActiveRuns()]); }, [isAuthenticated]); useEffect(() => { @@ -1254,6 +1318,19 @@ export default function App() { setModel(nextSelection.model); }, [draftKind, selectedChat, selectedChatSummary, selectedItem]); + useEffect(() => { + if (draftKind === "chat") { + setAdditionalSystemPrompt(""); + setEnabledTools(getDefaultEnabledTools(availableChatTools)); + return; + } + if (selectedItem?.kind !== "chat") return; + const chat = selectedChat?.id === selectedItem.id ? selectedChat : selectedChatSummary; + if (!chat) return; + setAdditionalSystemPrompt(chat.additionalSystemPrompt ?? ""); + setEnabledTools(normalizeEnabledTools(chat.enabledTools, availableChatTools)); + }, [availableChatTools, draftKind, selectedChat, selectedChatSummary, selectedItem]); + const selectedTitle = useMemo(() => { if (draftKind === "chat") return "New chat"; if (draftKind === "search") return "New search"; diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 3a1b00d..e6507a6 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -7,6 +7,8 @@ export type ChatSummary = { initiatedModel: string | null; lastUsedProvider: Provider | null; lastUsedModel: string | null; + additionalSystemPrompt: string | null; + enabledTools: string[] | null; }; export type SearchSummary = { @@ -58,6 +60,8 @@ export type ChatDetail = { initiatedModel: string | null; lastUsedProvider: Provider | null; lastUsedModel: string | null; + additionalSystemPrompt: string | null; + enabledTools: string[] | null; messages: Message[]; }; @@ -149,6 +153,11 @@ export type ModelCatalogResponse = { providers: Partial>; }; +export type ChatToolInfo = { + name: string; + description: string; +}; + export type ActiveRunsResponse = { chats: string[]; searches: string[]; @@ -174,6 +183,8 @@ type CreateChatRequest = { title?: string; provider?: Provider; model?: string; + additionalSystemPrompt?: string; + enabledTools?: string[]; messages?: CompletionRequestMessage[]; }; @@ -237,6 +248,11 @@ export async function listModels() { return api("/v1/models"); } +export async function listChatTools() { + const data = await api<{ tools: ChatToolInfo[] }>("/v1/chat-tools"); + return data.tools; +} + export async function getActiveRuns() { return api("/v1/active-runs"); } @@ -263,6 +279,14 @@ export async function updateChatTitle(chatId: string, title: string) { return data.chat; } +export async function updateChatSettings(chatId: string, body: { additionalSystemPrompt?: string | null; enabledTools?: string[] }) { + const data = await api<{ chat: ChatSummary }>(`/v1/chats/${chatId}`, { + method: "PATCH", + body: JSON.stringify(body), + }); + return data.chat; +} + export async function suggestChatTitle(body: { chatId: string; content: string }) { const data = await api<{ chat: ChatSummary }>("/v1/chats/title/suggest", { method: "POST", @@ -569,6 +593,8 @@ export async function runCompletion(body: { provider: Provider; model: string; messages: CompletionRequestMessage[]; + additionalSystemPrompt?: string; + enabledTools?: string[]; userLocation?: string; }) { return api("/v1/chat-completions", { @@ -584,6 +610,8 @@ export async function runCompletionStream( provider: Provider; model: string; messages: CompletionRequestMessage[]; + additionalSystemPrompt?: string; + enabledTools?: string[]; userLocation?: string; }, handlers: CompletionStreamHandlers, -- 2.49.1