SPB Git

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%
9.8 KB · 265 lines typescript
Raw Blame History
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