diff --git a/src/services/openRouterService.js b/src/services/openRouterService.js index df0db4e..5a5331d 100644 --- a/src/services/openRouterService.js +++ b/src/services/openRouterService.js @@ -206,30 +206,33 @@ export async function rephraseQuestion({ question, previousClarification, origin } export async function summarizeFinalAnswer({ openrouter, text, question }) { - const prompt = ` + const apiKey = process.env.OPENROUTER_API_KEY; + + // Step 1: Get summary, sources, and suggested searches + const summaryPrompt = ` You are a search result analyst. Today is the date of ${new Date().toLocaleDateString()}. Based on the following search results for the query "${question}", - Summarize the search results to answer the original query. Use Emoji and HTML. Tags allowed: , , ,
, 
    ,
  • , ,


    - Also provide the most relevant sources. Answer in the language of the question. You may suggest followup searched to the user. + Summarize the search results to answer the original query in a detailed manner. + Also provide the most relevant sources. Answer in the language of the question. You may suggest 2 followup searches to the user. `; - const params = { + const summaryParams = { model: 'openai/gpt-oss-120b:nitro', messages: [ - { role: 'system', content: prompt }, + { role: 'system', content: summaryPrompt }, { role: 'user', content: text }, ], reasoning: { effort: 'low' }, response_format: { type: 'json_schema', json_schema: { - name: 'response', + name: 'summary', strict: true, schema: { type: 'object', - required: ['fullAnswerHTMLSnippet', 'mostRelevantSources', 'suggestedSearches'], + required: ['summary', 'mostRelevantSources', 'suggestedSearches'], properties: { - fullAnswerHTMLSnippet: { type: 'string' }, + summary: { type: 'string' }, mostRelevantSources: { type: 'array', items: { type: 'string' } }, suggestedSearches: { type: 'array', items: { type: 'string' } }, }, @@ -239,19 +242,88 @@ export async function summarizeFinalAnswer({ openrouter, text, question }) { stream: false, }; - // Using direct fetch API instead of OpenRouter SDK - const apiKey = process.env.OPENROUTER_API_KEY; - const fetchResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', { + const summaryFetchResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, - body: JSON.stringify(params), + body: JSON.stringify(summaryParams), }); - const response = await fetchResponse.json(); + const summaryResponse = await summaryFetchResponse.json(); + await logOpenRouterCall('summarizeFinalAnswer-step1', text, summaryParams, summaryResponse); + const summaryData = parseResponse(summaryResponse); - await logOpenRouterCall('summarizeFinalAnswer', text, params, response); + // Step 2: Enrich the summary with HTML tags and emojis + const enrichmentPrompt = ` + You are a content formatter. Take the following summary and enrich it with emojis and HTML tags. + Allowed tags: , , ,
    , 
      ,
    • , ,

      ,

      ,
      + Make it visually appealing and easy to read. Keep the same language as the original summary. + Summary to enrich: + ${summaryData.data.summary} + `; + + const enrichmentParams = { + model: 'kwaipilot/kat-coder-pro-v2', + messages: [ + { role: 'system', content: enrichmentPrompt }, + { role: 'user', content: 'Please enrich the summary with HTML tags and emojis.' }, + ], + reasoning: { effort: 'low' }, + response_format: { + type: 'json_schema', + json_schema: { + name: 'enrichedSummary', + strict: true, + schema: { + type: 'object', + required: ['fullAnswerHTMLSnippet'], + properties: { + fullAnswerHTMLSnippet: { type: 'string' }, + }, + }, + }, + }, + stream: false, + }; + + const enrichmentFetchResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(enrichmentParams), + }); + const enrichmentResponse = await enrichmentFetchResponse.json(); + await logOpenRouterCall('summarizeFinalAnswer-step2', summaryData.data.summary, enrichmentParams, enrichmentResponse); + const enrichmentData = parseResponse(enrichmentResponse); - return parseResponse(response); + // Combine results with separate cost breakdowns + return { + totalCost: summaryData.cost + enrichmentData.cost, + totalPromptTokens: summaryData.prompt_tokens + enrichmentData.prompt_tokens, + totalCompletionTokens: summaryData.completion_tokens + enrichmentData.completion_tokens, + steps: [ + { + name: 'summary', + model: 'openai/gpt-oss-120b:nitro', + cost: summaryData.cost, + promptTokens: summaryData.prompt_tokens, + completionTokens: summaryData.completion_tokens, + }, + { + name: 'enrichment', + model: 'kwaipilot/kat-coder-pro-v2', + cost: enrichmentData.cost, + promptTokens: enrichmentData.prompt_tokens, + completionTokens: enrichmentData.completion_tokens, + }, + ], + data: { + fullAnswerHTMLSnippet: enrichmentData.data.fullAnswerHTMLSnippet, + mostRelevantSources: summaryData.data.mostRelevantSources, + suggestedSearches: summaryData.data.suggestedSearches, + }, + }; } diff --git a/src/services/searchService.js b/src/services/searchService.js index 1bac882..9b62b1c 100644 --- a/src/services/searchService.js +++ b/src/services/searchService.js @@ -1,6 +1,6 @@ import { extractContent } from './extractContent.js'; import { logExtraction } from './extractionLogger.js'; -import { summarizeFinalAnswer, summarizeSources, rephraseQuestion } from './openRouterService.js'; +import { summarizeFinalAnswer, summarizeSources, summarizeDetail, rephraseQuestion } from './openRouterService.js'; import { formatSummarySources } from './searchFormatter.js'; import { get_encoding } from 'tiktoken'; @@ -58,11 +58,22 @@ async function fetchDetailedContents({ exa, question, sources, broadcast }) { const content = await exa.getContents([source.url], EXA_CONTENT_OPTIONS(question)); - console.log(content.results.highlights.join('\n')); + console.log(content.results[0].highlights); + return { url: source.url, content , cost: content.costDollars.total }; - const summary = await summarizeDetail({text: content.results.highlights.join('\n'), url: source.url, question}) + /*const summary = await summarizeDetail({text: content.results, url: source.url, question}) - return { url: source.url, content:summary, cost: content.costDollars.total }; + const cost2 = { + type:'openrouter_detail_summary', + amount: summary.cost, + prompt_tokens: summary.prompt_tokens, + completion_tokens: summary.completion_tokens, + model: 'openai/gpt-oss-120b:nitro' + }; + + const summaryIsLonger = JSON.stringify(summary.data).length > JSON.stringify(content.results).length; + + return { url: source.url, content: summaryIsLonger ? content.results : source.summary, cost: content.costDollars.total, cost2 };*/ } catch (error) { broadcast(`⚠️ Could not fetch content for ${source.url}: ${error.message}`, 'warning'); return { url: source.url, content: null }; @@ -256,6 +267,9 @@ export function createSearchService({ exa, openrouter, broadcast }) { completion_tokens: outputTokens }); } + //if (detailed.cost2) { + // cost.push(detailed.cost2); + //} } broadcast('📊 Generating final summary...', 'info'); @@ -268,13 +282,28 @@ export function createSearchService({ exa, openrouter, broadcast }) { question, }); finalSummary = finalSummaryResult.data; - cost.push({ - type:'openrouter_final_summary', - amount: finalSummaryResult.cost, - prompt_tokens: finalSummaryResult.prompt_tokens, - completion_tokens: finalSummaryResult.completion_tokens, - model: 'openai/gpt-oss-120b:nitro' - }); + + // Push each step separately for detailed cost tracking + if (finalSummaryResult.steps && Array.isArray(finalSummaryResult.steps)) { + for (const step of finalSummaryResult.steps) { + cost.push({ + type: `openrouter_${step.name}`, + amount: step.cost, + prompt_tokens: step.promptTokens, + completion_tokens: step.completionTokens, + model: step.model, + }); + } + } else { + // Fallback for backward compatibility + cost.push({ + type: 'openrouter_final_summary', + amount: finalSummaryResult.totalCost, + prompt_tokens: finalSummaryResult.totalPromptTokens, + completion_tokens: finalSummaryResult.totalCompletionTokens, + model: 'openai/gpt-oss-120b:nitro', + }); + } } catch (error) { throw new SearchServiceError('Failed to generate final summary', 500, error); }