ollama_web_search
Search the web to retrieve current information and reduce model hallucinations by augmenting queries with real-time data.
Instructions
Perform a web search using Ollama's web search API. Augments models with latest information to reduce hallucinations. Requires OLLAMA_API_KEY environment variable.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | The search query string | |
| max_results | No | Maximum number of results to return (1-10, default 5) | |
| format | No | json |
Implementation Reference
- src/tools/web-search.ts:13-59 (handler)The main webSearch function that performs the actual web search by making an HTTP POST request to Ollama's web search API at https://ollama.com/api/web_search. Requires OLLAMA_API_KEY. Uses retryWithBackoff with configurable retries and fetchWithTimeout for reliability. Returns formatted response (JSON or markdown).
export async function webSearch( ollama: Ollama, query: string, maxResults: number, format: ResponseFormat ): Promise<string> { // Web search requires direct API call as it's not in the SDK const apiKey = process.env.OLLAMA_API_KEY; if (!apiKey) { throw new Error( 'OLLAMA_API_KEY environment variable is required for web search' ); } return retryWithBackoff( async () => { const response = await fetchWithTimeout( 'https://ollama.com/api/web_search', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ query, max_results: maxResults, }), }, WEB_API_TIMEOUT ); if (!response.ok) { const retryAfter = response.headers.get('retry-after') ?? undefined; throw new HttpError( `Web search failed: ${response.status} ${response.statusText}`, response.status, retryAfter ); } const data = await response.json(); return formatResponse(JSON.stringify(data), format); }, WEB_API_RETRY_CONFIG ); } - src/tools/web-search.ts:61-89 (handler)The toolDefinition export for 'ollama_web_search' containing the handler function. The handler validates args using WebSearchInputSchema then delegates to the webSearch function.
export const toolDefinition: ToolDefinition = { name: 'ollama_web_search', description: 'Perform a web search using Ollama\'s web search API. Augments models with latest information to reduce hallucinations. Requires OLLAMA_API_KEY environment variable.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The search query string', }, max_results: { type: 'number', description: 'Maximum number of results to return (1-10, default 5)', default: 5, }, format: { type: 'string', enum: ['json', 'markdown'], default: 'json', }, }, required: ['query'], }, handler: async (ollama: Ollama, args: Record<string, unknown>, format: ResponseFormat) => { const validated = WebSearchInputSchema.parse(args); return webSearch(ollama, validated.query, validated.max_results, format); }, }; - src/schemas.ts:208-214 (schema)Zod schema WebSearchInputSchema for the ollama_web_search tool. Validates query (string, min 1), max_results (number 1-10, default 5), and format (json/markdown, default json).
* Schema for ollama_web_search tool */ export const WebSearchInputSchema = z.object({ query: z.string().min(1), max_results: z.number().int().min(1).max(10).default(5), format: ResponseFormatSchema.default('json'), }); - src/autoloader.ts:31-57 (registration)The autoloader mechanism that discovers all tools. It imports modules from the tools/ directory and collects any exported toolDefinition. This is how 'ollama_web_search' gets registered as a discoverable tool.
export async function discoverTools(): Promise<ToolDefinition[]> { const toolsDir = join(__dirname, 'tools'); const files = await readdir(toolsDir); // Filter for .js files (production) or .ts files (development) // Exclude test files and declaration files const toolFiles = files.filter( (file) => (file.endsWith('.js') || file.endsWith('.ts')) && !file.includes('.test.') && !file.endsWith('.d.ts') ); const tools: ToolDefinition[] = []; for (const file of toolFiles) { const toolPath = join(toolsDir, file); const module = await import(toolPath); // Check if module exports tool metadata if (module.toolDefinition) { tools.push(module.toolDefinition); } } return tools; } - src/utils/retry.ts:60-186 (helper)fetchWithTimeout and retryWithBackoff helpers used by the webSearch function. fetchWithTimeout wraps fetch with AbortController-based timeout. retryWithBackoff implements exponential backoff with jitter for retrying transient errors (429, 500, 502, 503, 504).
export async function fetchWithTimeout( url: string, options?: RequestInit, timeout: number = 30000 ): Promise<Response> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, signal: controller.signal, }); return response; } catch (error: unknown) { if (error instanceof Error && error.name === 'AbortError') { throw new Error(`Request to ${url} timed out after ${timeout}ms`); } throw error; } finally { // Always clear timeout to prevent memory leaks and race conditions. // If fetch completes exactly at timeout boundary, clearTimeout ensures // the timeout callback doesn't execute after we've already returned. clearTimeout(timeoutId); } } /** * Parse Retry-After header value to milliseconds * Supports both delay-seconds and HTTP-date formats * @param retryAfter - Retry-After header value * @returns Delay in milliseconds, or null if invalid */ function parseRetryAfter(retryAfter: string | undefined): number | null { if (!retryAfter) { return null; } // Try parsing as seconds (integer) const seconds = parseInt(retryAfter, 10); if (!isNaN(seconds) && seconds >= 0) { return seconds * 1000; } // Try parsing as HTTP-date const date = new Date(retryAfter); if (!isNaN(date.getTime())) { const delay = date.getTime() - Date.now(); // Only use if it's a future date return delay > 0 ? delay : 0; } return null; } /** * Retry a function with exponential backoff on rate limit errors * * Uses exponential backoff with full jitter to prevent thundering herd: * - Attempt 0: 0-1 seconds (random in range [0, 1s]) * - Attempt 1: 0-2 seconds (random in range [0, 2s]) * - Attempt 2: 0-4 seconds (random in range [0, 4s]) * - And so on... * * Retry attempts are logged to console.debug for debugging and telemetry purposes, * including attempt number, delay, and error message. * * @param fn - The function to retry * @param options - Retry options (maxRetries: number of retry attempts after initial call) * @returns The result of the function * @throws The last error if max retries exceeded or non-retryable error */ export async function retryWithBackoff<T>( fn: () => Promise<T>, options: RetryOptions = {} ): Promise<T> { const { maxRetries = 3, initialDelay = 1000, maxDelay = 10000 } = options; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { // Only retry on transient errors (429, 500, 502, 503, 504) // Throw immediately for any other error type if (!isRetryableError(error)) { throw error; } // Throw if we've exhausted all retry attempts if (attempt === maxRetries) { throw error; } // Check if error has Retry-After header let delay: number; const retryAfterDelay = error instanceof HttpError ? parseRetryAfter(error.retryAfter) : null; if (retryAfterDelay !== null) { // Use Retry-After header value, capped at maxDelay delay = Math.min(retryAfterDelay, maxDelay); } else { // Calculate delay with exponential backoff and full jitter, capped at maxDelay const exponentialDelay = Math.min( initialDelay * Math.pow(2, attempt), maxDelay ); // Full jitter: random value between 0 and exponentialDelay delay = Math.random() * exponentialDelay; } // Log retry attempt for debugging/telemetry console.debug( `Retry attempt ${attempt + 1}/${maxRetries} after ${Math.round(delay)}ms delay`, { delay: Math.round(delay), error: error instanceof Error ? error.message : String(error) } ); await sleep(delay); } } // This line is unreachable due to the throw statements above, // but TypeScript requires it for exhaustiveness checking throw new Error('Unexpected: retry loop completed without return or throw'); }