files cant't be seen by LLM, but everything else works.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
.env
|
||||
*.log
|
||||
data
|
||||
@@ -0,0 +1,20 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Copy source
|
||||
COPY src/ ./src/
|
||||
|
||||
# Create data directories
|
||||
RUN mkdir -p /app/data/uploads /app/data/chromadb
|
||||
|
||||
EXPOSE 3002
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3002
|
||||
|
||||
CMD ["node", "src/index.js"]
|
||||
+110
-2
@@ -6,7 +6,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const PORT = process.env.PORT || 3002;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
@@ -31,6 +31,42 @@ const knowledgeBases = new Map();
|
||||
const knowledgeFiles = new Map();
|
||||
const uploadedFiles = new Map();
|
||||
|
||||
// Token usage tracking
|
||||
const tokenUsageStats = new Map(); // conversationId -> aggregated token stats
|
||||
|
||||
// In-memory storage for token usage events
|
||||
const tokenUsageLog = [];
|
||||
|
||||
// Extract token usage from SSE response
|
||||
function extractTokenUsage(fullResponse) {
|
||||
try {
|
||||
// Parse the last SSE data block which contains usage info
|
||||
const lines = fullResponse.split('\n');
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i];
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6).trim();
|
||||
if (data === '[DONE]') continue;
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.usage) {
|
||||
return {
|
||||
promptTokens: parsed.usage.prompt_tokens || 0,
|
||||
completionTokens: parsed.usage.completion_tokens || 0,
|
||||
totalTokens: parsed.usage.total_tokens || 0,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Not valid JSON, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Global ChromaDB client (recreated per request when path changes)
|
||||
function getChromaClient(persistDir = './data/chromadb') {
|
||||
return new ChromaClient({
|
||||
@@ -78,13 +114,31 @@ app.post('/api/v1/chat/completions', async (req, res) => {
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let fullResponse = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
res.write(decoder.decode(value, { stream: true }));
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
fullResponse += chunk;
|
||||
res.write(chunk);
|
||||
}
|
||||
res.end();
|
||||
|
||||
// Parse token usage from the final SSE message
|
||||
const tokenUsage = extractTokenUsage(fullResponse);
|
||||
if (tokenUsage) {
|
||||
const usageEntry = {
|
||||
timestamp: Date.now(),
|
||||
conversationId: req.body.conversationId || 'unknown',
|
||||
model,
|
||||
promptTokens: tokenUsage.promptTokens,
|
||||
completionTokens: tokenUsage.completionTokens,
|
||||
totalTokens: tokenUsage.totalTokens,
|
||||
};
|
||||
tokenUsageLog.push(usageEntry);
|
||||
console.log(`Token usage: ${usageEntry.totalTokens} tokens (${usageEntry.promptTokens} prompt, ${usageEntry.completionTokens} completion) for model ${model}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Proxy error:', error);
|
||||
res.status(500).json({
|
||||
@@ -427,6 +481,60 @@ app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Token usage tracking endpoints
|
||||
app.get('/api/token-usage', (req, res) => {
|
||||
try {
|
||||
const { period = 'all', conversationId } = req.query;
|
||||
|
||||
let filtered = [...tokenUsageLog];
|
||||
|
||||
if (conversationId) {
|
||||
filtered = filtered.filter((e) => e.conversationId === conversationId);
|
||||
}
|
||||
|
||||
if (period === 'today') {
|
||||
const todayStart = new Date();
|
||||
todayStart.setHours(0, 0, 0, 0);
|
||||
filtered = filtered.filter((e) => e.timestamp >= todayStart.getTime());
|
||||
} else if (period === 'week') {
|
||||
const weekStart = new Date();
|
||||
weekStart.setDate(weekStart.getDate() - 7);
|
||||
filtered = filtered.filter((e) => e.timestamp >= weekStart.getTime());
|
||||
}
|
||||
|
||||
const promptTokens = filtered.reduce((sum, e) => sum + e.promptTokens, 0);
|
||||
const completionTokens = filtered.reduce((sum, e) => sum + e.completionTokens, 0);
|
||||
const totalTokens = filtered.reduce((sum, e) => sum + e.totalTokens, 0);
|
||||
|
||||
// Per-model breakdown
|
||||
const modelBreakdown = {};
|
||||
filtered.forEach((entry) => {
|
||||
if (!modelBreakdown[entry.model]) {
|
||||
modelBreakdown[entry.model] = { promptTokens: 0, completionTokens: 0, totalTokens: 0, requestCount: 0 };
|
||||
}
|
||||
modelBreakdown[entry.model].promptTokens += entry.promptTokens;
|
||||
modelBreakdown[entry.model].completionTokens += entry.completionTokens;
|
||||
modelBreakdown[entry.model].totalTokens += entry.totalTokens;
|
||||
modelBreakdown[entry.model].requestCount += 1;
|
||||
});
|
||||
|
||||
res.json({
|
||||
summary: { promptTokens, completionTokens, totalTokens, requestCount: filtered.length },
|
||||
modelBreakdown,
|
||||
history: filtered.slice(-100), // last 100 entries
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Token usage error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/token-usage', (req, res) => {
|
||||
tokenUsageLog.length = 0;
|
||||
tokenUsageStats.clear();
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`AIUI Backend running on port ${PORT}`);
|
||||
console.log(`Upload directory: ${uploadDir}`);
|
||||
|
||||
Reference in New Issue
Block a user