From dc89631d36b0a73dd473eaf0f5d3af4989f810da Mon Sep 17 00:00:00 2001 From: Anthony Wu Date: Wed, 27 Aug 2025 14:07:56 -0700 Subject: [PATCH 1/7] fix import error --- app/markdown-renderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/markdown-renderer.tsx b/app/markdown-renderer.tsx index 3130480..134d4b8 100644 --- a/app/markdown-renderer.tsx +++ b/app/markdown-renderer.tsx @@ -1,7 +1,7 @@ 'use client' import React, { useMemo, useCallback } from 'react' -import Streamdown from 'streamdown' +import { Streamdown } from 'streamdown' import { CitationTooltip } from './citation-tooltip-portal' import { SearchResult } from './types' From a13954b8e43fac9b600630a093c49b29d523e987 Mon Sep 17 00:00:00 2001 From: Anthony Wu Date: Wed, 27 Aug 2025 14:26:45 -0700 Subject: [PATCH 2/7] add providers and config examples --- .env.example | 16 +++++++++++++++- package.json | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 9fe9b8a..86b4f4c 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,21 @@ # Firecrawl API Key # Get your API key from: https://www.firecrawl.dev/ +# not required if FIRECRAWL_API_URL is self-hosted FIRECRAWL_API_KEY=fc-YOUR_API_KEY_HERE +# For self-hosting firecrawl via "docker compose" +# see: https://github.com/firecrawl/firecrawl/blob/main/SELF_HOST.md +# FIRECRAWL_API_URL=http://localhost:8001 + +# AI Provider: 'groq' or 'ollama' and providers to be supported in the future +AI_PROVIDER=groq + # Groq API Key # Get your API key from: https://console.groq.com/keys -GROQ_API_KEY=gsk_YOUR_GROQ_API_KEY_HERE \ No newline at end of file +GROQ_API_KEY=gsk_YOUR_GROQ_API_KEY_HERE +GROQ_MODEL=moonshotai/kimi-k2-instruct + +# ===== Example for ollama LLM where API is served from http://localhost:11434/api ===== +# AI_PROVIDER=ollama +# OLLAMA_HOST=http://localhost:11434 +# OLLAMA_MODEL=qwen3:14b diff --git a/package.json b/package.json index 0cb1903..e0396d8 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "lucide-react": "^0.511.0", "next": "15.3.2", "next-themes": "^0.4.6", + "ollama-ai-provider-v2": "^1.2.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-markdown": "^10.1.0", From 46d012b442f71604b08f17b994530089f2422d0c Mon Sep 17 00:00:00 2001 From: Anthony Wu Date: Wed, 27 Aug 2025 14:48:25 -0700 Subject: [PATCH 3/7] add firecrawl and ollama localhost support --- app/api/fireplexity/search/route.ts | 75 +++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/app/api/fireplexity/search/route.ts b/app/api/fireplexity/search/route.ts index e8fed93..5c65a37 100644 --- a/app/api/fireplexity/search/route.ts +++ b/app/api/fireplexity/search/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server' import { createGroq } from '@ai-sdk/groq' +import { createOllama } from 'ollama-ai-provider-v2' import { streamText, generateText, createUIMessageStream, createUIMessageStreamResponse, convertToModelMessages } from 'ai' import type { ModelMessage } from 'ai' import { detectCompanyTicker } from '@/lib/company-ticker-map' @@ -31,21 +32,40 @@ export async function POST(request: Request) { } // Use API key from request body if provided, otherwise fall back to environment variable - const firecrawlApiKey = body.firecrawlApiKey || process.env.FIRECRAWL_API_KEY - const groqApiKey = process.env.GROQ_API_KEY - - if (!firecrawlApiKey) { - return NextResponse.json({ error: 'Firecrawl API key not configured' }, { status: 500 }) - } - - if (!groqApiKey) { - return NextResponse.json({ error: 'Groq API key not configured' }, { status: 500 }) + const firecrawlApiHost = process.env.FIRECRAWL_API_HOST || "https://api.firecrawl.dev" + // Skip API key check for localhost/127.0.0.1 hosts (does not account for self-hosting at another machine) + const isFirecrawlLocalhost = firecrawlApiHost.includes('localhost') || firecrawlApiHost.includes('127.0.0.1') + const firecrawlApiKey = body.firecrawlApiKey || process.env.FIRECRAWL_API_KEY || 'fc-not-required-for-localhost' + if (!isFirecrawlLocalhost && !firecrawlApiKey) { + return NextResponse.json({ error: `Firecrawl API key required but not configured for ${firecrawlApiHost}` }, { status: 500 }) } - // Configure Groq with the OSS 120B model - const groq = createGroq({ - apiKey: groqApiKey - }) + // AI Provider selection + const aiProvider = process.env.AI_PROVIDER || 'groq' + + if (aiProvider === 'ollama') { + // https://ai-sdk.dev/providers/community-providers/ollama + const ollamaModel = process.env.OLLAMA_MODEL + const ollamaHost = process.env.OLLAMA_HOST || "http://localhost:11434" + const resolveHost = ollamaHost.startsWith('http') ? ollamaHost : `http://${ollamaHost}` + const resolveBaseURL = resolveHost.endsWith("/api") ? resolveHost : `${resolveHost}/api` + const ollamaInstance = createOllama({ + baseURL: resolveBaseURL + }) + console.log(`Ollama API URL: ${resolveBaseURL} / Model: ${ollamaModel}`) + const llm = ollamaInstance(ollamaModel) + console.log(llm) + const followUpLlm = ollamaInstance(ollamaModel) + } else { + const groqApiKey = process.env.GROQ_API_KEY + if (!groqApiKey) { + return NextResponse.json({ error: 'Groq API key not configured' }, { status: 500 }) + } + const groq = createGroq({ apiKey: groqApiKey }) + const groqModel = process.env.GROQ_MODEL || 'moonshotai/kimi-k2-instruct' + const llm = groq(groqModel) + const followUpLlm = groq(groqModel) + } // Always perform a fresh search for each query to ensure relevant results const isFollowUp = messages.length > 2 @@ -102,7 +122,7 @@ export async function POST(request: Request) { }) // Make direct API call to Firecrawl v2 search endpoint - const searchResponse = await fetch('https://api.firecrawl.dev/v2/search', { + const searchResponse = await fetch(`${firecrawlApiHost}`/v2/search', { method: 'POST', headers: { 'Authorization': `Bearer ${firecrawlApiKey}`, @@ -134,6 +154,18 @@ export async function POST(request: Request) { const imagesData = searchData.images || [] // Transform web sources metadata + const cleanWebResults = webResults.filter((item: WebResult) => { + try { + if (item.metadata?.statusCode && item.metadata.statusCode > 400) { + console.warn(`Skipping web search result for URL: ${item.url} due to HTTP status code: ${item.metadata.statusCode}`); + return false; // Skip this item + } + return true; // Keep this item if no status code or status code <= 400 + } catch (error) { + console.error(`Error checking status code for web search result URL: ${item.url}. Skipping item.`, error); + return false; // Skip on any error during metadata access + } + }); sources = webResults.map((item: any) => { return { url: item.url, @@ -169,7 +201,7 @@ export async function POST(request: Request) { url: item.url, title: item.title || 'Untitled', thumbnail: item.imageUrl, // Direct API returns 'imageUrl' field - source: item.url ? new URL(item.url).hostname : undefined, + source: item.url ? new URL(item.url).hostname : undefined, // new URL(item.url) can throw TypeError when status code 4xx 5xx width: item.imageWidth, height: item.imageHeight, position: item.position @@ -284,7 +316,7 @@ export async function POST(request: Request) { // Stream the text generation using Groq's Kimi K2 Instruct model const result = streamText({ - model: groq('moonshotai/kimi-k2-instruct'), + model: llm, messages: aiMessages, temperature: 0.7, maxRetries: 2 @@ -308,7 +340,7 @@ export async function POST(request: Request) { try { const followUpResponse = await generateText({ - model: groq('moonshotai/kimi-k2-instruct'), + model: followUpLlm, messages: [ { role: 'system', @@ -343,7 +375,7 @@ export async function POST(request: Request) { } } catch (error) { - + console.error("Error in Firecrawl POST request:", error); // Handle specific error types const errorMessage = error instanceof Error ? error.message : 'Unknown error' const statusCode = error && typeof error === 'object' && 'statusCode' in error @@ -356,11 +388,11 @@ export async function POST(request: Request) { const errorResponses: Record = { 401: { error: 'Invalid API key', - suggestion: 'Please check your Firecrawl API key is correct.' + suggestion: 'Please check your Firecrawl API key is correct if not using self-hosted Firecrawl API host.' }, 402: { error: 'Insufficient credits', - suggestion: 'You\'ve run out of Firecrawl credits. Please upgrade your plan.' + suggestion: 'You\'ve run out of Firecrawl credits. Please upgrade your plan. You can also self-host Firecrawl API.' }, 429: { error: 'Rate limit exceeded', @@ -393,6 +425,7 @@ export async function POST(request: Request) { return createUIMessageStreamResponse({ stream }) } catch (error) { + console.error("Error in search POST request:", error); const errorMessage = error instanceof Error ? error.message : 'Unknown error' const errorStack = error instanceof Error ? error.stack : '' return NextResponse.json( @@ -400,4 +433,4 @@ export async function POST(request: Request) { { status: 500 } ) } -} \ No newline at end of file +} From 0e72a3dfd6869de0930f8a0b037717859fab36d0 Mon Sep 17 00:00:00 2001 From: Anthony Wu Date: Wed, 27 Aug 2025 16:02:47 -0700 Subject: [PATCH 4/7] fix fc localhost --- .env.example | 1 + app/api/fireplexity/search/route.ts | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 86b4f4c..2850f2d 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,7 @@ FIRECRAWL_API_KEY=fc-YOUR_API_KEY_HERE # For self-hosting firecrawl via "docker compose" # see: https://github.com/firecrawl/firecrawl/blob/main/SELF_HOST.md # FIRECRAWL_API_URL=http://localhost:8001 +# FIRECRAWL_API_KEY=fc-not-required-for-self-host # AI Provider: 'groq' or 'ollama' and providers to be supported in the future AI_PROVIDER=groq diff --git a/app/api/fireplexity/search/route.ts b/app/api/fireplexity/search/route.ts index 5c65a37..087f85b 100644 --- a/app/api/fireplexity/search/route.ts +++ b/app/api/fireplexity/search/route.ts @@ -32,16 +32,20 @@ export async function POST(request: Request) { } // Use API key from request body if provided, otherwise fall back to environment variable - const firecrawlApiHost = process.env.FIRECRAWL_API_HOST || "https://api.firecrawl.dev" + const firecrawlApiHost = process.env.FIRECRAWL_API_URL || "https://api.firecrawl.dev" + const resolvedFirecrawlApiHost = firecrawlApiHost.startsWith('http') ? firecrawlApiHost : `http://${firecrawlApiHost}` // Skip API key check for localhost/127.0.0.1 hosts (does not account for self-hosting at another machine) const isFirecrawlLocalhost = firecrawlApiHost.includes('localhost') || firecrawlApiHost.includes('127.0.0.1') - const firecrawlApiKey = body.firecrawlApiKey || process.env.FIRECRAWL_API_KEY || 'fc-not-required-for-localhost' + const firecrawlApiKey = body.firecrawlApiKey || process.env.FIRECRAWL_API_KEY || 'fc-not-required-for-self-host' if (!isFirecrawlLocalhost && !firecrawlApiKey) { return NextResponse.json({ error: `Firecrawl API key required but not configured for ${firecrawlApiHost}` }, { status: 500 }) } // AI Provider selection const aiProvider = process.env.AI_PROVIDER || 'groq' + let providerInstance: any + let llm: any + let followUpLlm: any if (aiProvider === 'ollama') { // https://ai-sdk.dev/providers/community-providers/ollama @@ -49,22 +53,22 @@ export async function POST(request: Request) { const ollamaHost = process.env.OLLAMA_HOST || "http://localhost:11434" const resolveHost = ollamaHost.startsWith('http') ? ollamaHost : `http://${ollamaHost}` const resolveBaseURL = resolveHost.endsWith("/api") ? resolveHost : `${resolveHost}/api` - const ollamaInstance = createOllama({ + const providerInstance = createOllama({ baseURL: resolveBaseURL }) console.log(`Ollama API URL: ${resolveBaseURL} / Model: ${ollamaModel}`) - const llm = ollamaInstance(ollamaModel) + llm = providerInstance(ollamaModel) console.log(llm) - const followUpLlm = ollamaInstance(ollamaModel) + followUpLlm = providerInstance(ollamaModel) } else { const groqApiKey = process.env.GROQ_API_KEY if (!groqApiKey) { return NextResponse.json({ error: 'Groq API key not configured' }, { status: 500 }) } - const groq = createGroq({ apiKey: groqApiKey }) - const groqModel = process.env.GROQ_MODEL || 'moonshotai/kimi-k2-instruct' - const llm = groq(groqModel) - const followUpLlm = groq(groqModel) + providerInstance = createGroq({ apiKey: groqApiKey }) + groqModel = process.env.GROQ_MODEL || 'moonshotai/kimi-k2-instruct' + llm = groq(groqModel) + followUpLlm = groq(groqModel) } // Always perform a fresh search for each query to ensure relevant results @@ -122,7 +126,8 @@ export async function POST(request: Request) { }) // Make direct API call to Firecrawl v2 search endpoint - const searchResponse = await fetch(`${firecrawlApiHost}`/v2/search', { + console.log(`Requesting Firecrawl API at ${resolvedFirecrawlApiHost}`) + const searchResponse = await fetch(`${resolvedFirecrawlApiHost}/v2/search`, { method: 'POST', headers: { 'Authorization': `Bearer ${firecrawlApiKey}`, @@ -154,7 +159,7 @@ export async function POST(request: Request) { const imagesData = searchData.images || [] // Transform web sources metadata - const cleanWebResults = webResults.filter((item: WebResult) => { + const cleanWebResults = webResults.filter((item: any) => { try { if (item.metadata?.statusCode && item.metadata.statusCode > 400) { console.warn(`Skipping web search result for URL: ${item.url} due to HTTP status code: ${item.metadata.statusCode}`); From b2d56851b7a8a7065f3b140d9ff476e177c8e4fa Mon Sep 17 00:00:00 2001 From: Anthony Wu Date: Wed, 27 Aug 2025 16:12:11 -0700 Subject: [PATCH 5/7] scope fixes --- app/api/fireplexity/search/route.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/app/api/fireplexity/search/route.ts b/app/api/fireplexity/search/route.ts index 087f85b..9e8adc8 100644 --- a/app/api/fireplexity/search/route.ts +++ b/app/api/fireplexity/search/route.ts @@ -44,33 +44,30 @@ export async function POST(request: Request) { // AI Provider selection const aiProvider = process.env.AI_PROVIDER || 'groq' let providerInstance: any - let llm: any - let followUpLlm: any + let providerModel: string if (aiProvider === 'ollama') { // https://ai-sdk.dev/providers/community-providers/ollama - const ollamaModel = process.env.OLLAMA_MODEL const ollamaHost = process.env.OLLAMA_HOST || "http://localhost:11434" - const resolveHost = ollamaHost.startsWith('http') ? ollamaHost : `http://${ollamaHost}` - const resolveBaseURL = resolveHost.endsWith("/api") ? resolveHost : `${resolveHost}/api` - const providerInstance = createOllama({ - baseURL: resolveBaseURL - }) - console.log(`Ollama API URL: ${resolveBaseURL} / Model: ${ollamaModel}`) - llm = providerInstance(ollamaModel) - console.log(llm) - followUpLlm = providerInstance(ollamaModel) + const resolvedOllamaHost = ollamaHost.startsWith('http') ? ollamaHost : `http://${ollamaHost}` + const resolvedOllamaApiUrl = resolvedOllamaHost.endsWith("/api") ? resolvedOllamaHost : `${resolvedOllamaHost}/api` + providerInstance = createOllama({ baseURL: resolvedOllamaApiUrl }) + providerModel = process.env.OLLAMA_MODEL + console.log(`Ollama API URL: ${resolvedOllamaApiUrl} / Model: ${providerModel}`) } else { const groqApiKey = process.env.GROQ_API_KEY if (!groqApiKey) { return NextResponse.json({ error: 'Groq API key not configured' }, { status: 500 }) } providerInstance = createGroq({ apiKey: groqApiKey }) - groqModel = process.env.GROQ_MODEL || 'moonshotai/kimi-k2-instruct' - llm = groq(groqModel) - followUpLlm = groq(groqModel) + providerModel = process.env.GROQ_MODEL || 'moonshotai/kimi-k2-instruct' + console.log(`Groq Model: ${providerModel}`) } + const llm = providerInstance(providerModel) + console.log(llm) + const followUpLlm = providerInstance(providerModel) + // Always perform a fresh search for each query to ensure relevant results const isFollowUp = messages.length > 2 From 893d69edca76243697f89bf34c4a507a29889a77 Mon Sep 17 00:00:00 2001 From: Anthony Wu Date: Wed, 27 Aug 2025 23:15:13 -0700 Subject: [PATCH 6/7] revert unintentional edit by agent --- app/markdown-renderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/markdown-renderer.tsx b/app/markdown-renderer.tsx index 134d4b8..3130480 100644 --- a/app/markdown-renderer.tsx +++ b/app/markdown-renderer.tsx @@ -1,7 +1,7 @@ 'use client' import React, { useMemo, useCallback } from 'react' -import { Streamdown } from 'streamdown' +import Streamdown from 'streamdown' import { CitationTooltip } from './citation-tooltip-portal' import { SearchResult } from './types' From 8bba06c714ca8fdf74c9858fbe955e403316525a Mon Sep 17 00:00:00 2001 From: Anthony Wu Date: Thu, 28 Aug 2025 00:31:06 -0700 Subject: [PATCH 7/7] fix build issues --- app/api/fireplexity/search/route.ts | 4 ++-- app/markdown-renderer.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/api/fireplexity/search/route.ts b/app/api/fireplexity/search/route.ts index 9e8adc8..3d7d2de 100644 --- a/app/api/fireplexity/search/route.ts +++ b/app/api/fireplexity/search/route.ts @@ -44,7 +44,7 @@ export async function POST(request: Request) { // AI Provider selection const aiProvider = process.env.AI_PROVIDER || 'groq' let providerInstance: any - let providerModel: string + let providerModel: string | undefined if (aiProvider === 'ollama') { // https://ai-sdk.dev/providers/community-providers/ollama @@ -52,7 +52,7 @@ export async function POST(request: Request) { const resolvedOllamaHost = ollamaHost.startsWith('http') ? ollamaHost : `http://${ollamaHost}` const resolvedOllamaApiUrl = resolvedOllamaHost.endsWith("/api") ? resolvedOllamaHost : `${resolvedOllamaHost}/api` providerInstance = createOllama({ baseURL: resolvedOllamaApiUrl }) - providerModel = process.env.OLLAMA_MODEL + providerModel = process.env.OLLAMA_MODEL || 'qwen3:14b' console.log(`Ollama API URL: ${resolvedOllamaApiUrl} / Model: ${providerModel}`) } else { const groqApiKey = process.env.GROQ_API_KEY diff --git a/app/markdown-renderer.tsx b/app/markdown-renderer.tsx index 3130480..961616d 100644 --- a/app/markdown-renderer.tsx +++ b/app/markdown-renderer.tsx @@ -1,7 +1,7 @@ 'use client' import React, { useMemo, useCallback } from 'react' -import Streamdown from 'streamdown' +import { Streamdown } from 'streamdown' import { CitationTooltip } from './citation-tooltip-portal' import { SearchResult } from './types' @@ -105,4 +105,4 @@ export function MarkdownRenderer({ content, sources }: MarkdownRendererProps) { {sources && sources.length > 0 && } ) -} \ No newline at end of file +}