feat: add toggleable web search with UI control and provider-specific implementations

Implement web search toggle functionality across all AI providers (Claude, Gemini, Mistral, OpenAI) with conditional tool inclusion based on settings. Add globe button to chat input for toggling web search on/off. Change formatBinaryFiles visibility from public to protected in BaseAIClass and IAIClass interface. Extract tool configuration into private getTools() methods for each provider. Update tests to verify web search toggle behavior and formatBinaryFiles access level changes.
This commit is contained in:
Andrew Beal 2026-04-10 10:47:23 +01:00
parent 57fb5d3be3
commit ac1dcf7f83
13 changed files with 219 additions and 116 deletions

View file

@ -88,7 +88,7 @@ export abstract class BaseAIClass implements IAIClass {
public abstract streamRequest(conversation: Conversation): AsyncGenerator<IStreamChunk, void, unknown>;
public abstract formatBinaryFiles(attachments: Attachment[]): string;
protected abstract formatBinaryFiles(attachments: Attachment[]): string;
protected abstract parseStreamChunk(chunk: string): IStreamChunk;
protected abstract extractContents(conversationContent: ConversationContent[]): unknown;

View file

@ -63,14 +63,7 @@ export class Claude extends BaseAIClass {
const messages = this.addCacheControlToMessages(
await this.extractContents(conversation.contents));
const webSearchTool: WebSearchTool20250305 = {
type: "web_search_20250305",
name: "web_search",
max_uses: 5
};
const tools: ToolUnion[] = this.addCacheControlToTools([
webSearchTool, ...this.mapFunctionDefinitions(this.aiToolDefinitions)]);
const tools = this.getTools();
const requestBody = {
model: this.model(),
@ -288,7 +281,7 @@ export class Claude extends BaseAIClass {
}));
}
public formatBinaryFiles(attachments: Attachment[]): string {
protected formatBinaryFiles(attachments: Attachment[]): string {
const contentBlocks = attachments.flatMap(attachment => {
const fileID = attachment.getFileID(this.provider);
if (!fileID) {
@ -375,6 +368,20 @@ export class Claude extends BaseAIClass {
return cachedMessages;
}
private getTools(): ToolUnion[] {
if (this.settingsService.settings.enableWebSearch) {
const webSearchTool: WebSearchTool20250305 = {
type: "web_search_20250305",
name: "web_search",
max_uses: 5
};
return this.addCacheControlToTools([
webSearchTool, ...this.mapFunctionDefinitions(this.aiToolDefinitions)]);
}
return this.addCacheControlToTools(this.mapFunctionDefinitions(this.aiToolDefinitions));
}
private buildClaudeToolChoice(): { type: string } {
// If no tools defined, fall back to auto
if (this.aiToolDefinitions.length === 0) {

View file

@ -5,7 +5,7 @@ import type { Attachment } from "Conversations/Attachment";
import { Role } from "Enums/Role";
import { AIProvider, AIProviderURL } from "Enums/ApiProvider";
import { AIToolCall } from "AIClasses/AIToolCall";
import { fromString as aiToolFromString } from "Enums/AITool";
import { AITool, fromString as aiToolFromString } from "Enums/AITool";
import type { IAIToolDefinition } from "AIClasses/ToolDefinitions/IAIToolDefinition";
import type { ConversationContent } from "Conversations/ConversationContent";
import type { Candidate, Part, FunctionDeclaration } from "@google/genai";
@ -22,7 +22,6 @@ import { Copy, replaceCopy } from "Enums/Copy";
export class Gemini extends BaseAIClass {
private readonly REQUEST_WEB_SEARCH: string = "request_web_search";
private readonly SUPPORTED_MIMETYPES = [
// Common Text
@ -83,7 +82,7 @@ export class Gemini extends BaseAIClass {
public async* streamRequest(conversation: Conversation): AsyncGenerator<IStreamChunk, void, unknown> {
// next request should use web search only (gemini api doesn't support custom tooling and grounding at the same time)
const requestWebSearch = this.accumulatedFunctionName == this.REQUEST_WEB_SEARCH;
const requestWebSearch = this.accumulatedFunctionName == AITool.RequestWebSearch;
this.accumulatedFunctionName = null;
this.accumulatedFunctionArgs = {};
@ -96,18 +95,7 @@ export class Gemini extends BaseAIClass {
const contents = await this.extractContents(conversation.contents);
const tools = requestWebSearch ? { google_search: {} } :
{
functionDeclarations: [
{
name: "request_web_search",
description: `Use this function when you need to search the web for current
information, recent events, news, or facts that may have changed.
After calling this, you will be able to perform web searches.`,
},
...this.mapFunctionDefinitions(this.aiToolDefinitions),
]
}
const tools = requestWebSearch ? { google_search: {} } : this.getTools()
const requestBody: Record<string, unknown> = {
system_instruction: {
@ -326,7 +314,7 @@ export class Gemini extends BaseAIClass {
}));
}
public formatBinaryFiles(attachments: Attachment[]): string {
protected formatBinaryFiles(attachments: Attachment[]): string {
const parts: unknown[] = [];
for (const attachment of attachments) {
@ -361,6 +349,23 @@ export class Gemini extends BaseAIClass {
return JSON.stringify(parts);
}
private getTools(): { functionDeclarations: FunctionDeclaration[] } {
if (this.settingsService.settings.enableWebSearch) {
return {
functionDeclarations: [
{
name: AITool.RequestWebSearch,
description: `Use this function when you need to search the web for current
information, recent events, news, or facts that may have changed.
After calling this, you will be able to perform web searches.`,
},
...this.mapFunctionDefinitions(this.aiToolDefinitions),
]
};
}
return { functionDeclarations: this.mapFunctionDefinitions(this.aiToolDefinitions) };
}
private buildGeminiToolConfig(): { function_calling_config: { mode: string } } {
// If no tools defined, fall back to auto
if (this.aiToolDefinitions.length === 0) {

View file

@ -1,6 +1,5 @@
import type { IStreamChunk } from "Services/StreamingService";
import type { Conversation } from "Conversations/Conversation";
import type { Attachment } from "Conversations/Attachment";
import type { IAIToolDefinition } from "./ToolDefinitions/IAIToolDefinition";
import type { AIProvider } from "Enums/ApiProvider";
import type { AgentType } from "Enums/AgentType";
@ -17,7 +16,6 @@ export interface IAIClass {
set aiToolUsageMode(mode: AIToolUsageMode);
streamRequest(conversation: Conversation): AsyncGenerator<IStreamChunk, void, unknown>;
formatBinaryFiles(attachments: Attachment[]): string;
// Optional provider-level tool call handler. Called before AIToolService.
resolveToolCall?(toolCall: AIToolCall): Promise<AIToolResponse | null>;

View file

@ -46,24 +46,7 @@ export class Mistral extends BaseAIClass {
public async* streamRequest(conversation: Conversation): AsyncGenerator<IStreamChunk, void, unknown> {
const messages = await this.buildMessages(conversation);
const tools = [
{
type: "function" as const,
function: {
name: AITool.RequestWebSearch,
description: `Use this function when you need to search the web for current information, recent events, news, or facts that may have changed.`,
parameters: {
type: "object" as const,
properties: {
query: { type: "string", description: "The search query to look up on the web." }
},
required: ["query"]
}
}
},
...this.mapFunctionDefinitions(this.aiToolDefinitions)
];
const tools = this.getTools();
const requestBody: Record<string, unknown> = {
model: this.model(),
@ -357,7 +340,7 @@ export class Mistral extends BaseAIClass {
}));
}
public formatBinaryFiles(attachments: Attachment[]): string {
protected formatBinaryFiles(attachments: Attachment[]): string {
const contentParts: MistralContentPart[] = [];
const fileService = this.aiFileService as MistralFileService;
@ -420,6 +403,29 @@ export class Mistral extends BaseAIClass {
return JSON.stringify(contentParts);
}
private getTools(): MistralToolDefinition[] {
if (this.settingsService.settings.enableWebSearch) {
return [
{
type: "function" as const,
function: {
name: AITool.RequestWebSearch,
description: `Use this function when you need to search the web for current information, recent events, news, or facts that may have changed.`,
parameters: {
type: "object" as const,
properties: {
query: { type: "string", description: "The search query to look up on the web." }
},
required: ["query"]
}
}
},
...this.mapFunctionDefinitions(this.aiToolDefinitions)
];
}
return this.mapFunctionDefinitions(this.aiToolDefinitions);
}
private buildMistralToolChoice(): string {
if (this.aiToolDefinitions.length === 0) {
return "auto";

View file

@ -42,9 +42,7 @@ export class OpenAI extends BaseAIClass {
const input = await this.extractContents(conversation.contents);
const tools = [{
type: "web_search"
}, ...this.mapFunctionDefinitions(this.aiToolDefinitions)];
const tools = this.getTools();
const requestBody = {
model: this.model(),
@ -329,7 +327,7 @@ export class OpenAI extends BaseAIClass {
}));
}
public formatBinaryFiles(attachments: Attachment[]): string {
protected formatBinaryFiles(attachments: Attachment[]): string {
const contentBlocks: unknown[] = [];
for (const attachment of attachments) {
@ -367,6 +365,15 @@ export class OpenAI extends BaseAIClass {
}]);
}
private getTools(): (OpenAIToolTool | { type: string })[] {
if (this.settingsService.settings.enableWebSearch) {
return [{
type: "web_search"
}, ...this.mapFunctionDefinitions(this.aiToolDefinitions)];
}
return this.mapFunctionDefinitions(this.aiToolDefinitions);
}
private buildOpenAIToolChoice(): string {
// If no tools defined, fall back to auto
if (this.aiToolDefinitions.length === 0) {

View file

@ -21,6 +21,7 @@
import { Copy, replaceCopy } from "Enums/Copy";
import { HelpModal } from "Modals/HelpModal";
import type { IPrompt } from "AIPrompts/IPrompt";
import type { SettingsService } from "Services/SettingsService";
export let attachments: Attachment[] = [];
@ -32,6 +33,7 @@
export let onStop: () => void;
const inputService: InputService = Resolve<InputService>(Services.InputService);
const settingsService: SettingsService = Resolve<SettingsService>(Services.SettingsService);
const userInputService: UserInputService = Resolve<UserInputService>(Services.UserInputService);
const searchStateStore: SearchStateStore = Resolve<SearchStateStore>(Services.SearchStateStore);
const diffService: DiffService = Resolve<DiffService>(Services.DiffService);
@ -43,6 +45,7 @@
let inputDisplay: InputDisplay;
let textareaElement: HTMLDivElement;
let userInstructionButton: HTMLButtonElement;
let webSearchButton: HTMLButtonElement;
let submitButton: HTMLButtonElement;
let editModeButton: HTMLButtonElement;
let planningModeButton: HTMLButtonElement;
@ -177,6 +180,10 @@
setIcon(userInstructionButton, "user-round-pen");
}
$: if (webSearchButton) {
setIcon(webSearchButton, "globe");
}
$: userInstructionAreaActive, (() => {
tick().then(async () => {
userInstructionActive = (await aiPrompt.userInstruction()).trim() !== "";
@ -278,6 +285,11 @@
searchStateStore.resetSearch();
}
function toggleWebSearch() {
settingsService.settings.enableWebSearch = !settingsService.settings.enableWebSearch;
settingsService.saveSettings();
}
function toggleEditMode() {
if (planningModeActive) {
planningModeActive = false
@ -515,7 +527,15 @@
class:instruction-active={userInstructionActive}
bind:this={userInstructionButton}
on:click={toggleUserInstructionArea}
aria-label="User Instruction">
aria-label={Copy.ButtonUserInstruction}>
</button>
<button
id="web-search-button"
class:web-search-active={settingsService.settings.enableWebSearch}
bind:this={webSearchButton}
on:click={toggleWebSearch}
aria-label={settingsService.settings.enableWebSearch ? Copy.ButtonTurnOffWebSearch : Copy.ButtonTurnOnWebSearch}>
</button>
<div
@ -584,7 +604,7 @@
grid-column: 1;
display: grid;
grid-template-rows: auto auto auto auto var(--size-4-3) 1fr var(--size-4-3);
grid-template-columns: var(--size-4-3) auto var(--size-4-2) 1fr var(--size-4-2) auto var(--size-4-2) auto var(--size-4-2) auto var(--size-4-3);
grid-template-columns: var(--size-4-3) auto var(--size-4-2) auto var(--size-4-2) 1fr var(--size-4-2) auto var(--size-4-2) auto var(--size-4-2) auto var(--size-4-3);
border-radius: var(--radius-l);
background-color: var(--background-primary);
}
@ -596,27 +616,27 @@
#input-display-container {
grid-row: 1;
grid-column: 2 / 11;
grid-column: 2 / 13;
}
#input-attachments-container {
grid-row: 2;
grid-column: 2 / 11;
grid-column: 2 / 13;
}
#diff-controls-container {
grid-row: 3;
grid-column: 2 / 11;
grid-column: 2 / 13;
}
#input-search-results-container {
grid-row: 4;
grid-column: 2 / 11;
grid-column: 2 / 13;
}
#user-instruction-container {
grid-row: 4;
grid-column: 2 / 11;
grid-column: 2 / 13;
}
#user-instruction-button {
@ -635,9 +655,25 @@
box-shadow: 0px 0px 2px 1px var(--color-accent);
}
#input-field {
#web-search-button {
grid-row: 6;
grid-column: 4;
border-radius: var(--radius-l);
align-self: end;
transition-duration: 0.5s;
}
#web-search-button.web-search-active {
box-shadow: 0px 0px 2px 1px var(--color-accent);
}
:global(.is-mobile) #web-search-button {
max-height: 2rem;
}
#input-field {
grid-row: 6;
grid-column: 6;
height: 100%;
max-height: 30vh;
border-radius: var(--radius-m);
@ -698,7 +734,7 @@
#edit-mode-button {
grid-row: 6;
grid-column: 6;
grid-column: 8;
border-radius: var(--radius-l);
align-self: end;
transition-duration: 0.5s;
@ -714,7 +750,7 @@
#planning-mode-button {
grid-row: 6;
grid-column: 8;
grid-column: 10;
border-radius: var(--radius-l);
align-self: end;
transition-duration: 0.5s;
@ -730,7 +766,7 @@
#submit-button {
grid-row: 6;
grid-column: 10;
grid-column: 12;
border-radius: var(--radius-l);
padding-left: var(--size-4-5);
padding-right: var(--size-4-5);
@ -763,13 +799,17 @@
}
#input-container.stacked #input-field {
grid-column: 2 / 11;
grid-column: 2 / 13;
}
#input-container.stacked #user-instruction-button {
grid-row: 8;
}
#input-container.stacked #web-search-button {
grid-row: 8;
}
#input-container.stacked #edit-mode-button {
grid-row: 8;
}
@ -785,7 +825,7 @@
/* Narrow/mobile layout: input above, buttons below */
:global(.is-mobile) #input-container {
grid-template-rows: auto auto auto auto var(--size-4-3) 1fr var(--size-4-2) auto var(--size-4-3);
grid-template-columns: var(--size-4-3) auto 1fr auto var(--size-4-2) auto var(--size-4-2) auto var(--size-4-3);
grid-template-columns: var(--size-4-3) auto var(--size-4-2) auto 1fr auto var(--size-4-2) auto var(--size-4-2) auto var(--size-4-3);
}
:global(.is-mobile) #input-display-container,
@ -793,12 +833,12 @@
:global(.is-mobile) #diff-controls-container,
:global(.is-mobile) #input-search-results-container,
:global(.is-mobile) #user-instruction-container {
grid-column: 2 / 9;
grid-column: 2 / 11;
}
:global(.is-mobile) #input-field {
grid-row: 6;
grid-column: 2 / 9;
grid-column: 2 / 11;
}
:global(.is-mobile) #user-instruction-button {
@ -806,18 +846,23 @@
grid-column: 2;
}
:global(.is-mobile) #edit-mode-button {
:global(.is-mobile) #web-search-button {
grid-row: 8;
grid-column: 4;
}
:global(.is-mobile) #planning-mode-button {
:global(.is-mobile) #edit-mode-button {
grid-row: 8;
grid-column: 6;
}
:global(.is-mobile) #submit-button {
:global(.is-mobile) #planning-mode-button {
grid-row: 8;
grid-column: 8;
}
:global(.is-mobile) #submit-button {
grid-row: 8;
grid-column: 10;
}
</style>

View file

@ -107,6 +107,9 @@ export enum Copy {
ButtonTurnOnAgentMode = "Turn on Agent Mode",
ButtonTurnOffPlanningMode = "Turn off Planning Mode",
ButtonTurnOnPlanningMode = "Turn on Planning Mode",
ButtonTurnOffWebSearch = "Turn off Web Search",
ButtonTurnOnWebSearch = "Turn on Web Search",
ButtonUserInstruction = "User Instruction",
// Agent file message
AttachedFile = `The user has attached the file {fileName}. The contents of the file are included below.

View file

@ -21,7 +21,9 @@ const DEFAULT_SETTINGS: IVaultkeeperAISettings = {
snippetSizeLimit: 100,
enableMemories: false,
allowUpdatingMemories: true
allowUpdatingMemories: true,
enableWebSearch: true
}
export interface IVaultkeeperAISettings {
@ -43,6 +45,8 @@ export interface IVaultkeeperAISettings {
enableMemories: boolean;
allowUpdatingMemories: boolean;
enableWebSearch: boolean;
}
export class SettingsService {

View file

@ -1236,7 +1236,7 @@ describe('Claude', () => {
deleteFileID: vi.fn()
};
const result = claude.formatBinaryFiles([attachment as any]);
const result = (claude as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(2);
@ -1264,7 +1264,7 @@ describe('Claude', () => {
deleteFileID: vi.fn()
};
const result = claude.formatBinaryFiles([attachment as any]);
const result = (claude as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(2);
@ -1292,7 +1292,7 @@ describe('Claude', () => {
deleteFileID: vi.fn()
};
const result = claude.formatBinaryFiles([attachment as any]);
const result = (claude as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(2);
@ -1316,7 +1316,7 @@ describe('Claude', () => {
deleteFileID: vi.fn()
};
const result = claude.formatBinaryFiles([attachment as any]);
const result = (claude as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(2);
@ -1340,7 +1340,7 @@ describe('Claude', () => {
deleteFileID: vi.fn()
};
const result = claude.formatBinaryFiles([attachment as any]);
const result = (claude as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(2);
@ -1364,7 +1364,7 @@ describe('Claude', () => {
deleteFileID: vi.fn()
};
const result = claude.formatBinaryFiles([attachment as any]);
const result = (claude as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -1405,7 +1405,7 @@ describe('Claude', () => {
}
];
const result = claude.formatBinaryFiles(attachments as any);
const result = (claude as any).formatBinaryFiles(attachments as any);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(6);
@ -1472,7 +1472,7 @@ describe('Claude', () => {
}
];
const result = claude.formatBinaryFiles(attachments as any);
const result = (claude as any).formatBinaryFiles(attachments as any);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(5);
@ -1500,7 +1500,7 @@ describe('Claude', () => {
deleteFileID: vi.fn()
};
const result = claude.formatBinaryFiles([attachment as any]);
const result = (claude as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
// Should return empty array when no file ID is available
@ -1529,7 +1529,7 @@ describe('Claude', () => {
}
];
const result = claude.formatBinaryFiles(attachments as any);
const result = (claude as any).formatBinaryFiles(attachments as any);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(4);
@ -1542,7 +1542,7 @@ describe('Claude', () => {
it('should handle empty files array', () => {
const attachments: any[] = [];
const result = claude.formatBinaryFiles(attachments);
const result = (claude as any).formatBinaryFiles(attachments);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(0);
@ -1559,7 +1559,7 @@ describe('Claude', () => {
deleteFileID: vi.fn()
};
const result = claude.formatBinaryFiles([attachment as any]);
const result = (claude as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed[0].text).toBe(replaceCopy(Copy.AttachedFile, ["report (final) v2.pdf"]));
@ -1576,7 +1576,7 @@ describe('Claude', () => {
deleteFileID: vi.fn()
};
const result = claude.formatBinaryFiles([attachment as any]);
const result = (claude as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed[1]).toEqual({

View file

@ -337,7 +337,35 @@ describe('Gemini', () => {
});
describe('Web Search Toggle', () => {
it('should use custom tools by default', async () => {
it('should use custom tools by default (no request_web_search when disabled)', async () => {
mockSettingsService.settings.enableWebSearch = false;
const conversation = new Conversation();
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Test' }));
mockStreamingService.streamRequest.mockImplementation(async function* () {
yield { content: 'done', isComplete: true };
});
const generator = gemini.streamRequest(conversation);
for await (const chunk of generator) {}
const callArgs = mockStreamingService.streamRequest.mock.calls[0];
const requestBody = callArgs[1];
expect(requestBody.tools[0]).toHaveProperty('functionDeclarations');
expect(requestBody.tools[0].functionDeclarations).toBeInstanceOf(Array);
// Should NOT include request_web_search when web search is disabled
const hasWebSearchFunc = requestBody.tools[0].functionDeclarations.some(
(f: any) => f.name === 'request_web_search'
);
expect(hasWebSearchFunc).toBe(false);
});
it('should include request_web_search function when web search is enabled', async () => {
mockSettingsService.settings.enableWebSearch = true;
const conversation = new Conversation();
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Test' }));
@ -355,7 +383,6 @@ describe('Gemini', () => {
expect(requestBody.tools[0].functionDeclarations).toBeInstanceOf(Array);
expect(requestBody.tools[0].functionDeclarations.length).toBeGreaterThan(0);
// Should include request_web_search function
const hasWebSearchFunc = requestBody.tools[0].functionDeclarations.some(
(f: any) => f.name === 'request_web_search'
);
@ -1131,7 +1158,7 @@ describe('Gemini', () => {
deleteFileID: vi.fn()
};
const result = gemini.formatBinaryFiles([attachment as any]);
const result = (gemini as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(2);
@ -1157,7 +1184,7 @@ describe('Gemini', () => {
deleteFileID: vi.fn()
};
const result = gemini.formatBinaryFiles([attachment as any]);
const result = (gemini as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(2);
@ -1183,7 +1210,7 @@ describe('Gemini', () => {
deleteFileID: vi.fn()
};
const result = gemini.formatBinaryFiles([attachment as any]);
const result = (gemini as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(2);
@ -1206,7 +1233,7 @@ describe('Gemini', () => {
deleteFileID: vi.fn()
};
const result = gemini.formatBinaryFiles([attachment as any]);
const result = (gemini as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -1226,7 +1253,7 @@ describe('Gemini', () => {
deleteFileID: vi.fn()
};
const result = gemini.formatBinaryFiles([attachment as any]);
const result = (gemini as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -1266,7 +1293,7 @@ describe('Gemini', () => {
}
];
const result = gemini.formatBinaryFiles(attachments as any);
const result = (gemini as any).formatBinaryFiles(attachments as any);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(6);
@ -1330,7 +1357,7 @@ describe('Gemini', () => {
}
];
const result = gemini.formatBinaryFiles(attachments as any);
const result = (gemini as any).formatBinaryFiles(attachments as any);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(5);
@ -1366,7 +1393,7 @@ describe('Gemini', () => {
}
];
const result = gemini.formatBinaryFiles(attachments as any);
const result = (gemini as any).formatBinaryFiles(attachments as any);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(2); // Only successful upload
@ -1380,7 +1407,7 @@ describe('Gemini', () => {
});
it('should handle empty attachments array', () => {
const result = gemini.formatBinaryFiles([]);
const result = (gemini as any).formatBinaryFiles([]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(0);
@ -1397,7 +1424,7 @@ describe('Gemini', () => {
deleteFileID: vi.fn()
};
const result = gemini.formatBinaryFiles([attachment as any]);
const result = (gemini as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed[0].text).toBe(replaceCopy(Copy.AttachedFile, ["report (final) v2.pdf"]));
@ -1414,7 +1441,7 @@ describe('Gemini', () => {
deleteFileID: vi.fn()
};
const result = gemini.formatBinaryFiles([attachment as any]);
const result = (gemini as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed[1]).toEqual({

View file

@ -700,7 +700,7 @@ describe('Mistral', () => {
deleteFileID: vi.fn()
};
const result = mistral.formatBinaryFiles([attachment as any]);
const result = (mistral as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(2);
@ -725,7 +725,7 @@ describe('Mistral', () => {
deleteFileID: vi.fn()
};
const result = mistral.formatBinaryFiles([attachment as any]);
const result = (mistral as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(2);
@ -750,7 +750,7 @@ describe('Mistral', () => {
deleteFileID: vi.fn()
};
const result = mistral.formatBinaryFiles([attachment as any]);
const result = (mistral as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(2);
@ -771,7 +771,7 @@ describe('Mistral', () => {
deleteFileID: vi.fn()
};
const result = mistral.formatBinaryFiles([attachment as any]);
const result = (mistral as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -803,7 +803,7 @@ describe('Mistral', () => {
}
];
const result = mistral.formatBinaryFiles(attachments as any);
const result = (mistral as any).formatBinaryFiles(attachments as any);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(4);
@ -834,7 +834,7 @@ describe('Mistral', () => {
deleteFileID: vi.fn()
};
const result = mistral.formatBinaryFiles([attachment as any]);
const result = (mistral as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
// Should return empty array when no file ID is available
@ -844,7 +844,7 @@ describe('Mistral', () => {
it('should handle empty files array', () => {
const attachments: any[] = [];
const result = mistral.formatBinaryFiles(attachments);
const result = (mistral as any).formatBinaryFiles(attachments);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(0);

View file

@ -976,7 +976,9 @@ describe('OpenAI', () => {
);
});
it('should include name field in web_search tool', async () => {
it('should include web_search tool when web search is enabled', async () => {
mockSettingsService.settings.enableWebSearch = true;
const conversation = new Conversation();
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Test message' }));
@ -993,7 +995,6 @@ describe('OpenAI', () => {
expect(webSearchTool).toBeDefined();
expect(webSearchTool.type).toBe('web_search');
expect(webSearchTool.name).toBe(undefined);
});
});
@ -1009,7 +1010,7 @@ describe('OpenAI', () => {
deleteFileID: vi.fn()
};
const result = openai.formatBinaryFiles([attachment as any]);
const result = (openai as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -1036,7 +1037,7 @@ describe('OpenAI', () => {
deleteFileID: vi.fn()
};
const result = openai.formatBinaryFiles([attachment as any]);
const result = (openai as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -1063,7 +1064,7 @@ describe('OpenAI', () => {
deleteFileID: vi.fn()
};
const result = openai.formatBinaryFiles([attachment as any]);
const result = (openai as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -1089,7 +1090,7 @@ describe('OpenAI', () => {
deleteFileID: vi.fn()
};
const result = openai.formatBinaryFiles([attachment as any]);
const result = (openai as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -1115,7 +1116,7 @@ describe('OpenAI', () => {
deleteFileID: vi.fn()
};
const result = openai.formatBinaryFiles([attachment as any]);
const result = (openai as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -1137,7 +1138,7 @@ describe('OpenAI', () => {
deleteFileID: vi.fn()
};
const result = openai.formatBinaryFiles([attachment as any]);
const result = (openai as any).formatBinaryFiles([attachment as any]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -1176,7 +1177,7 @@ describe('OpenAI', () => {
}
];
const result = openai.formatBinaryFiles(attachments as any);
const result = (openai as any).formatBinaryFiles(attachments as any);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -1242,7 +1243,7 @@ describe('OpenAI', () => {
}
];
const result = openai.formatBinaryFiles(attachments as any);
const result = (openai as any).formatBinaryFiles(attachments as any);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -1286,7 +1287,7 @@ describe('OpenAI', () => {
}
];
const result = openai.formatBinaryFiles(attachments as any);
const result = (openai as any).formatBinaryFiles(attachments as any);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -1302,7 +1303,7 @@ describe('OpenAI', () => {
});
it('should handle empty attachments array', () => {
const result = openai.formatBinaryFiles([]);
const result = (openai as any).formatBinaryFiles([]);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
@ -1332,7 +1333,7 @@ describe('OpenAI', () => {
}
];
const result = openai.formatBinaryFiles(attachments as any);
const result = (openai as any).formatBinaryFiles(attachments as any);
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);