spb/vquant Public MIT
VibeQuant — AI-powered institutional-grade financial intelligence platform.
TypeScript 84.3%
Python 11.7%
JavaScript 1.6%
CSS 1.5%
HTML 0.7%
1/*2 * =============================================================================3 * VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 * File: server/services/claudeService.ts6 *7 * Author: Simon-Pierre Boucher8 * Contact: contact@spboucher.ai9 * Website: https://www.spboucher.ai10 * Demo: https://www.vquant.ai11 * License: MIT (see LICENSE)12 *13 * Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import axios from 'axios';18import { encode } from 'gpt-tokenizer';19import type { SearchResult } from '@shared/types';20import { AVAILABLE_TOOLS } from './claude/toolDefinitions';21import {22 estimateTokens,23 estimateMessagesTokens,24 truncateToolResults,25 summarizeHistory,26 calculateCost27} from './claude/tokenManagement';28import { buildSystemPrompt } from './claude/systemPrompt';2930// Using HTTP direct calls to Anthropic API (not SDK) as requested31const ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/messages';32const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;33const ANTHROPIC_VERSION = '2023-06-01';34const DEFAULT_MODEL = 'claude-fable-5';35const ADAPTIVE_THINKING_MODELS = ['claude-fable-5', 'claude-opus-4-6', 'claude-opus-4-7', 'claude-opus-4-8', 'claude-sonnet-4-6'];36function supportsAdaptiveThinking(model: string): boolean {37 return ADAPTIVE_THINKING_MODELS.some((m) => model.startsWith(m));38}3940interface ClaudeStreamEvent {41 type: string;42 message?: any;43 delta?: any;44 content_block?: any;45 index?: number;46}47export async function callClaudeStreaming(48 query: string,49 onChunk: (text: string) => void,50 messages: any[],51 model: string = DEFAULT_MODEL,52 onThinking?: (text: string) => void53): Promise<{ answer: string; toolCalls: any[]; usage: { inputTokens: number; outputTokens: number; cost: number } }> {5455 try {56 // Truncate large tool results first57 let processedMessages = truncateToolResults(messages, 5000);5859 // Check if messages exceed token limit (using 100K as safe threshold for 200K context)60 const estimatedTokens = estimateMessagesTokens(processedMessages);61 const TOKEN_LIMIT = 100000; // More conservative limit6263 if (estimatedTokens > TOKEN_LIMIT) {64 console.log(`Messages exceed token limit (${estimatedTokens} tokens). Summarizing old history...`);6566 // Keep only the last 5 messages and summarize the rest67 const recentMessages = processedMessages.slice(-5);68 const oldMessages = processedMessages.slice(0, -5);6970 if (oldMessages.length > 0) {71 const summary = await summarizeHistory(oldMessages, model);7273 // Create new message array with summary + recent messages74 processedMessages = [75 {76 role: 'user',77 content: `[Previous conversation summary]: ${summary}`78 },79 ...recentMessages80 ];8182 console.log(`History summarized. New token count: ${estimateMessagesTokens(processedMessages)}`);83 }84 }85 // Get current date and time86 const now = new Date();87 const currentDateTime = now.toLocaleString('en-US', {88 weekday: 'long',89 year: 'numeric',90 month: 'long',91 day: 'numeric',92 hour: '2-digit',93 minute: '2-digit',94 second: '2-digit',95 timeZone: 'America/New_York',96 timeZoneName: 'short'97 });98 const isoDate = now.toISOString().split('T')[0]; // YYYY-MM-DD format99100 const response = await axios.post(101 ANTHROPIC_API_URL,102 {103 model,104 max_tokens: 64000,105 temperature: 1,106 ...(supportsAdaptiveThinking(model) ? { thinking: { type: 'adaptive', display: 'summarized' } } : {}),107 stream: true,108 system: buildSystemPrompt(currentDateTime, isoDate),109 messages: processedMessages,110 tools: AVAILABLE_TOOLS,111 },112 {113 headers: {114 'Content-Type': 'application/json',115 'x-api-key': ANTHROPIC_API_KEY!,116 'anthropic-version': ANTHROPIC_VERSION,117 },118 responseType: 'stream',119 }120 );121122 let fullAnswer = '';123 let toolCalls: any[] = [];124 let buffer = '';125 let inputTokens = 0;126 let outputTokens = 0;127128 // Calculate cost: $3/million for input, $15/million for output129 const calculateCost = (input: number, output: number): number => {130 return (input * 3 / 1_000_000) + (output * 15 / 1_000_000);131 };132133 return new Promise((resolve, reject) => {134 response.data.on('data', (chunk: Buffer) => {135 buffer += chunk.toString();136 const lines = buffer.split('\n');137 buffer = lines.pop() || '';138139 for (const line of lines) {140 if (line.startsWith('data: ')) {141 const data = line.substring(6).trim();142 if (data === '[DONE]' || !data) continue;143144 try {145 const event: ClaudeStreamEvent = JSON.parse(data);146147 // Log ALL events for debugging148 if (event.type === 'message_start' || event.type === 'message_delta' || event.type === 'message_stop') {149 console.log(`[TOKEN DEBUG] Event type: ${event.type}`, JSON.stringify(event, null, 2).substring(0, 500));150 }151152 // Capture token usage from message_start event153 if (event.type === 'message_start') {154 if (event.message?.usage) {155 inputTokens = event.message.usage.input_tokens || 0;156 outputTokens = event.message.usage.output_tokens || 0;157 console.log(`[TOKEN DEBUG] message_start: in=${inputTokens}, out=${outputTokens}`);158 }159 }160161 // Update token usage from message_delta event162 if (event.type === 'message_delta') {163 console.log(`[TOKEN DEBUG] message_delta full event:`, JSON.stringify(event));164 if (event.delta?.usage) {165 const oldOutputTokens = outputTokens;166 outputTokens = event.delta.usage.output_tokens || outputTokens;167 console.log(`[TOKEN DEBUG] message_delta: output ${oldOutputTokens} → ${outputTokens}`);168 } else {169 console.log(`[TOKEN DEBUG] message_delta: NO usage in delta - event.delta =`, event.delta);170 }171 }172173 // Check message_stop for usage too174 if (event.type === 'message_stop') {175 console.log(`[TOKEN DEBUG] message_stop: in=${inputTokens}, out=${outputTokens}`);176 console.log(`[TOKEN DEBUG] fullAnswer length: ${fullAnswer.length} chars`);177178 // FIX: Count tokens locally since streaming API doesn't return accurate output_tokens179 try {180 const localTokenCount = encode(fullAnswer).length;181 console.log(`[TOKEN FIX] API reported: ${outputTokens}, Local count: ${localTokenCount}`);182183 // Use the maximum to ensure accuracy184 const finalOutputTokens = Math.max(outputTokens, localTokenCount);185 outputTokens = finalOutputTokens;186187 console.log(`[TOKEN FIX] Using corrected output tokens: ${finalOutputTokens}`);188 } catch (encodeError) {189 console.error(`[TOKEN FIX] Error counting tokens locally:`, encodeError);190 // Fallback to API value if encoding fails191 }192 }193194 if (event.type === 'content_block_start') {195 if (event.content_block?.type === 'tool_use') {196 toolCalls.push({197 id: event.content_block.id,198 name: event.content_block.name,199 input: {},200 });201 }202 }203204 if (event.type === 'content_block_delta') {205 if (event.delta?.type === 'text_delta') {206 const text = event.delta.text;207 fullAnswer += text;208 onChunk(text);209 }210 if (event.delta?.type === 'thinking_delta') {211 if (onThinking) onThinking(event.delta.thinking);212 }213 if (event.delta?.type === 'input_json_delta') {214 // Accumulate tool input215 const lastTool = toolCalls[toolCalls.length - 1];216 if (lastTool) {217 lastTool.partial_json = (lastTool.partial_json || '') + event.delta.partial_json;218 }219 }220 }221222 if (event.type === 'content_block_stop') {223 // Finalize tool input224 const lastTool = toolCalls[toolCalls.length - 1];225 if (lastTool && lastTool.partial_json) {226 lastTool.input = JSON.parse(lastTool.partial_json);227 delete lastTool.partial_json;228 }229 }230231 if (event.type === 'message_stop') {232 const cost = calculateCost(inputTokens, outputTokens);233 resolve({234 answer: fullAnswer,235 toolCalls,236 usage: { inputTokens, outputTokens, cost }237 });238 }239 } catch (e) {240 console.error('Error parsing SSE event:', e);241 }242 }243 }244 });245246 response.data.on('end', () => {247 const cost = calculateCost(inputTokens, outputTokens);248 resolve({249 answer: fullAnswer,250 toolCalls,251 usage: { inputTokens, outputTokens, cost }252 });253 });254255 response.data.on('error', (error: Error) => {256 console.error('Stream error:', error);257 reject(error);258 });259 });260 } catch (error) {261 console.error('Claude API error:', error);262 throw new Error('Failed to call Claude API: ' + (error as Error).message);263 }264}265