| | const mongoose = require('mongoose'); |
| | const { MongoMemoryServer } = require('mongodb-memory-server'); |
| | const { spendTokens, spendStructuredTokens } = require('./spendTokens'); |
| | const { createTransaction, createAutoRefillTransaction } = require('./Transaction'); |
| |
|
| | require('~/db/models'); |
| |
|
| | jest.mock('~/config', () => ({ |
| | logger: { |
| | debug: jest.fn(), |
| | error: jest.fn(), |
| | }, |
| | })); |
| |
|
| | describe('spendTokens', () => { |
| | let mongoServer; |
| | let userId; |
| | let Transaction; |
| | let Balance; |
| |
|
| | beforeAll(async () => { |
| | mongoServer = await MongoMemoryServer.create(); |
| | await mongoose.connect(mongoServer.getUri()); |
| |
|
| | Transaction = mongoose.model('Transaction'); |
| | Balance = mongoose.model('Balance'); |
| | }); |
| |
|
| | afterAll(async () => { |
| | await mongoose.disconnect(); |
| | await mongoServer.stop(); |
| | }); |
| |
|
| | beforeEach(async () => { |
| | |
| | await Transaction.deleteMany({}); |
| | await Balance.deleteMany({}); |
| |
|
| | |
| | userId = new mongoose.Types.ObjectId(); |
| |
|
| | |
| | }); |
| |
|
| | it('should create transactions for both prompt and completion tokens', async () => { |
| | |
| | await Balance.create({ |
| | user: userId, |
| | tokenCredits: 10000, |
| | }); |
| |
|
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-convo', |
| | model: 'gpt-3.5-turbo', |
| | context: 'test', |
| | balance: { enabled: true }, |
| | }; |
| | const tokenUsage = { |
| | promptTokens: 100, |
| | completionTokens: 50, |
| | }; |
| |
|
| | await spendTokens(txData, tokenUsage); |
| |
|
| | |
| | const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); |
| | expect(transactions).toHaveLength(2); |
| |
|
| | |
| | expect(transactions[0].tokenType).toBe('completion'); |
| | expect(transactions[0].rawAmount).toBe(-50); |
| |
|
| | |
| | expect(transactions[1].tokenType).toBe('prompt'); |
| | expect(transactions[1].rawAmount).toBe(-100); |
| |
|
| | |
| | const balance = await Balance.findOne({ user: userId }); |
| | expect(balance).toBeDefined(); |
| | expect(balance.tokenCredits).toBeLessThan(10000); |
| | }); |
| |
|
| | it('should handle zero completion tokens', async () => { |
| | |
| | await Balance.create({ |
| | user: userId, |
| | tokenCredits: 10000, |
| | }); |
| |
|
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-convo', |
| | model: 'gpt-3.5-turbo', |
| | context: 'test', |
| | balance: { enabled: true }, |
| | }; |
| | const tokenUsage = { |
| | promptTokens: 100, |
| | completionTokens: 0, |
| | }; |
| |
|
| | await spendTokens(txData, tokenUsage); |
| |
|
| | |
| | const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); |
| | expect(transactions).toHaveLength(2); |
| |
|
| | |
| | expect(transactions[0].tokenType).toBe('completion'); |
| | |
| | |
| | expect(Math.abs(transactions[0].rawAmount)).toBe(0); |
| |
|
| | |
| | expect(transactions[1].tokenType).toBe('prompt'); |
| | expect(transactions[1].rawAmount).toBe(-100); |
| | }); |
| |
|
| | it('should handle undefined token counts', async () => { |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-convo', |
| | model: 'gpt-3.5-turbo', |
| | context: 'test', |
| | balance: { enabled: true }, |
| | }; |
| | const tokenUsage = {}; |
| |
|
| | await spendTokens(txData, tokenUsage); |
| |
|
| | |
| | const transactions = await Transaction.find({ user: userId }); |
| | expect(transactions).toHaveLength(0); |
| | }); |
| |
|
| | it('should not update balance when the balance feature is disabled', async () => { |
| | |
| | |
| | await Balance.create({ |
| | user: userId, |
| | tokenCredits: 10000, |
| | }); |
| |
|
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-convo', |
| | model: 'gpt-3.5-turbo', |
| | context: 'test', |
| | balance: { enabled: false }, |
| | }; |
| | const tokenUsage = { |
| | promptTokens: 100, |
| | completionTokens: 50, |
| | }; |
| |
|
| | await spendTokens(txData, tokenUsage); |
| |
|
| | |
| | const transactions = await Transaction.find({ user: userId }); |
| | expect(transactions).toHaveLength(2); |
| |
|
| | |
| | const balance = await Balance.findOne({ user: userId }); |
| | expect(balance.tokenCredits).toBe(10000); |
| | }); |
| |
|
| | it('should not allow balance to go below zero when spending tokens', async () => { |
| | |
| | await Balance.create({ |
| | user: userId, |
| | tokenCredits: 5000, |
| | }); |
| |
|
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-convo', |
| | model: 'gpt-4', |
| | context: 'test', |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | |
| | const tokenUsage = { |
| | promptTokens: 1000, |
| | completionTokens: 500, |
| | }; |
| |
|
| | await spendTokens(txData, tokenUsage); |
| |
|
| | |
| | const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); |
| | expect(transactions).toHaveLength(2); |
| |
|
| | |
| | const balance = await Balance.findOne({ user: userId }); |
| | expect(balance).toBeDefined(); |
| | expect(balance.tokenCredits).toBe(0); |
| |
|
| | |
| | const transactionResults = await Promise.all( |
| | transactions.map((t) => |
| | createTransaction({ |
| | ...txData, |
| | tokenType: t.tokenType, |
| | rawAmount: t.rawAmount, |
| | }), |
| | ), |
| | ); |
| |
|
| | |
| | expect(transactionResults[1]).toEqual( |
| | expect.objectContaining({ |
| | balance: 0, |
| | }), |
| | ); |
| | }); |
| |
|
| | it('should handle multiple transactions in sequence with low balance and not increase balance', async () => { |
| | |
| | |
| | |
| | await Balance.create({ |
| | user: userId, |
| | tokenCredits: 100, |
| | }); |
| |
|
| | |
| | const txData1 = { |
| | user: userId, |
| | conversationId: 'test-convo-1', |
| | model: 'gpt-4', |
| | context: 'test', |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage1 = { |
| | promptTokens: 100, |
| | completionTokens: 50, |
| | }; |
| |
|
| | await spendTokens(txData1, tokenUsage1); |
| |
|
| | |
| | let balance = await Balance.findOne({ user: userId }); |
| | expect(balance.tokenCredits).toBe(0); |
| |
|
| | |
| | const txData2 = { |
| | user: userId, |
| | conversationId: 'test-convo-2', |
| | model: 'gpt-4', |
| | context: 'test', |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage2 = { |
| | promptTokens: 200, |
| | completionTokens: 100, |
| | }; |
| |
|
| | await spendTokens(txData2, tokenUsage2); |
| |
|
| | |
| | balance = await Balance.findOne({ user: userId }); |
| | expect(balance.tokenCredits).toBe(0); |
| |
|
| | |
| | const transactions = await Transaction.find({ user: userId }); |
| | expect(transactions).toHaveLength(4); |
| |
|
| | |
| | const transactionDetails = await Transaction.find({ user: userId }).sort({ createdAt: 1 }); |
| |
|
| | |
| | console.log('Transaction details:'); |
| | transactionDetails.forEach((tx, i) => { |
| | console.log(`Transaction ${i + 1}:`, { |
| | tokenType: tx.tokenType, |
| | rawAmount: tx.rawAmount, |
| | tokenValue: tx.tokenValue, |
| | model: tx.model, |
| | }); |
| | }); |
| |
|
| | |
| | |
| | const directResult = await createTransaction({ |
| | user: userId, |
| | conversationId: 'test-convo-3', |
| | model: 'gpt-4', |
| | tokenType: 'completion', |
| | rawAmount: -100, |
| | context: 'test', |
| | balance: { enabled: true }, |
| | }); |
| |
|
| | console.log('Direct Transaction.create result:', directResult); |
| |
|
| | |
| | expect(directResult.completion).not.toBeGreaterThan(0); |
| | }); |
| |
|
| | it('should ensure tokenValue is always negative for spending tokens', async () => { |
| | |
| | await Balance.create({ |
| | user: userId, |
| | tokenCredits: 10000, |
| | }); |
| |
|
| | |
| | const models = ['gpt-3.5-turbo', 'gpt-4', 'claude-3-5-sonnet']; |
| |
|
| | for (const model of models) { |
| | const txData = { |
| | user: userId, |
| | conversationId: `test-convo-${model}`, |
| | model, |
| | context: 'test', |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage = { |
| | promptTokens: 100, |
| | completionTokens: 50, |
| | }; |
| |
|
| | await spendTokens(txData, tokenUsage); |
| |
|
| | |
| | const transactions = await Transaction.find({ |
| | user: userId, |
| | model, |
| | }); |
| |
|
| | |
| | transactions.forEach((tx) => { |
| | console.log(`Model ${model}, Type ${tx.tokenType}: tokenValue = ${tx.tokenValue}`); |
| | expect(tx.tokenValue).toBeLessThan(0); |
| | }); |
| | } |
| | }); |
| |
|
| | it('should handle structured transactions in sequence with low balance', async () => { |
| | |
| | await Balance.create({ |
| | user: userId, |
| | tokenCredits: 100, |
| | }); |
| |
|
| | |
| | const txData1 = { |
| | user: userId, |
| | conversationId: 'test-convo-1', |
| | model: 'claude-3-5-sonnet', |
| | context: 'test', |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage1 = { |
| | promptTokens: { |
| | input: 10, |
| | write: 100, |
| | read: 5, |
| | }, |
| | completionTokens: 50, |
| | }; |
| |
|
| | await spendStructuredTokens(txData1, tokenUsage1); |
| |
|
| | |
| | let balance = await Balance.findOne({ user: userId }); |
| | expect(balance.tokenCredits).toBe(0); |
| |
|
| | |
| | const txData2 = { |
| | user: userId, |
| | conversationId: 'test-convo-2', |
| | model: 'claude-3-5-sonnet', |
| | context: 'test', |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage2 = { |
| | promptTokens: { |
| | input: 20, |
| | write: 200, |
| | read: 10, |
| | }, |
| | completionTokens: 100, |
| | }; |
| |
|
| | await spendStructuredTokens(txData2, tokenUsage2); |
| |
|
| | |
| | balance = await Balance.findOne({ user: userId }); |
| | expect(balance.tokenCredits).toBe(0); |
| |
|
| | |
| | const transactions = await Transaction.find({ user: userId }); |
| | expect(transactions).toHaveLength(4); |
| |
|
| | |
| | const transactionDetails = await Transaction.find({ user: userId }).sort({ createdAt: 1 }); |
| |
|
| | |
| | console.log('Structured transaction details:'); |
| | transactionDetails.forEach((tx, i) => { |
| | console.log(`Transaction ${i + 1}:`, { |
| | tokenType: tx.tokenType, |
| | rawAmount: tx.rawAmount, |
| | tokenValue: tx.tokenValue, |
| | inputTokens: tx.inputTokens, |
| | writeTokens: tx.writeTokens, |
| | readTokens: tx.readTokens, |
| | model: tx.model, |
| | }); |
| | }); |
| | }); |
| |
|
| | it('should not allow balance to go below zero when spending structured tokens', async () => { |
| | |
| | await Balance.create({ |
| | user: userId, |
| | tokenCredits: 5000, |
| | }); |
| |
|
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-convo', |
| | model: 'claude-3-5-sonnet', |
| | context: 'test', |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | |
| | const tokenUsage = { |
| | promptTokens: { |
| | input: 100, |
| | write: 1000, |
| | read: 50, |
| | }, |
| | completionTokens: 500, |
| | }; |
| |
|
| | const result = await spendStructuredTokens(txData, tokenUsage); |
| |
|
| | |
| | const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); |
| | expect(transactions).toHaveLength(2); |
| |
|
| | |
| | const balance = await Balance.findOne({ user: userId }); |
| | expect(balance).toBeDefined(); |
| | expect(balance.tokenCredits).toBe(0); |
| |
|
| | |
| | expect(result).toEqual({ |
| | prompt: expect.objectContaining({ |
| | user: userId.toString(), |
| | balance: expect.any(Number), |
| | }), |
| | completion: expect.objectContaining({ |
| | user: userId.toString(), |
| | balance: 0, |
| | }), |
| | }); |
| | }); |
| |
|
| | it('should handle multiple concurrent transactions correctly with a high balance', async () => { |
| | |
| | const initialBalance = 10000000; |
| | await Balance.create({ |
| | user: userId, |
| | tokenCredits: initialBalance, |
| | }); |
| |
|
| | |
| | const conversationId = 'test-concurrent-convo'; |
| | const context = 'message'; |
| | const model = 'gpt-4'; |
| |
|
| | const amount = 50; |
| | |
| | const collectedUsage = Array.from({ length: amount }, (_, i) => ({ |
| | model, |
| | input_tokens: 100 + i * 10, |
| | output_tokens: 50 + i * 5, |
| | input_token_details: { |
| | cache_creation: i % 2 === 0 ? 20 : 0, |
| | cache_read: i % 3 === 0 ? 10 : 0, |
| | }, |
| | })); |
| |
|
| | |
| | const promises = []; |
| | let expectedTotalSpend = 0; |
| |
|
| | for (let i = 0; i < collectedUsage.length; i++) { |
| | const usage = collectedUsage[i]; |
| | if (!usage) { |
| | continue; |
| | } |
| |
|
| | const cache_creation = Number(usage.input_token_details?.cache_creation) || 0; |
| | const cache_read = Number(usage.input_token_details?.cache_read) || 0; |
| |
|
| | const txMetadata = { |
| | context, |
| | conversationId, |
| | user: userId, |
| | model: usage.model, |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | |
| | const promptTokens = usage.input_tokens; |
| | const completionTokens = usage.output_tokens; |
| |
|
| | |
| | if (cache_creation === 0 && cache_read === 0) { |
| | |
| | |
| | expectedTotalSpend += promptTokens * 30; |
| | expectedTotalSpend += completionTokens * 60; |
| |
|
| | promises.push( |
| | spendTokens(txMetadata, { |
| | promptTokens, |
| | completionTokens, |
| | }), |
| | ); |
| | } else { |
| | |
| | |
| | |
| | expectedTotalSpend += promptTokens * 30; |
| | |
| | expectedTotalSpend += cache_creation * 30; |
| | expectedTotalSpend += cache_read * 30; |
| | expectedTotalSpend += completionTokens * 60; |
| |
|
| | promises.push( |
| | spendStructuredTokens(txMetadata, { |
| | promptTokens: { |
| | input: promptTokens, |
| | write: cache_creation, |
| | read: cache_read, |
| | }, |
| | completionTokens, |
| | }), |
| | ); |
| | } |
| | } |
| |
|
| | |
| | await Promise.all(promises); |
| |
|
| | |
| | const finalBalance = await Balance.findOne({ user: userId }); |
| | expect(finalBalance).toBeDefined(); |
| |
|
| | |
| | const expectedFinalBalance = initialBalance - expectedTotalSpend; |
| |
|
| | console.log('Initial balance:', initialBalance); |
| | console.log('Expected total spend:', expectedTotalSpend); |
| | console.log('Expected final balance:', expectedFinalBalance); |
| | console.log('Actual final balance:', finalBalance.tokenCredits); |
| |
|
| | |
| | expect(finalBalance.tokenCredits).toBeCloseTo(expectedFinalBalance, 0); |
| |
|
| | |
| | const transactions = await Transaction.find({ |
| | user: userId, |
| | conversationId, |
| | }); |
| |
|
| | |
| | |
| | expect(transactions.length).toBeGreaterThanOrEqual(collectedUsage.length); |
| |
|
| | |
| | console.log('Transaction summary:'); |
| | let totalTokenValue = 0; |
| | transactions.forEach((tx) => { |
| | console.log(`${tx.tokenType}: rawAmount=${tx.rawAmount}, tokenValue=${tx.tokenValue}`); |
| | totalTokenValue += tx.tokenValue; |
| | }); |
| | console.log('Total token value from transactions:', totalTokenValue); |
| |
|
| | |
| | |
| | |
| | const actualSpend = initialBalance - finalBalance.tokenCredits; |
| | console.log('Actual spend:', actualSpend); |
| |
|
| | |
| | |
| | expect(finalBalance.tokenCredits).toBeLessThan(initialBalance); |
| | |
| | expect(Math.abs(totalTokenValue)).toBeCloseTo(actualSpend, -3); |
| | }); |
| |
|
| | |
| | it('should handle multiple concurrent balance increases correctly', async () => { |
| | |
| | const initialBalance = 0; |
| | await Balance.create({ |
| | user: userId, |
| | tokenCredits: initialBalance, |
| | }); |
| |
|
| | const numberOfRefills = 25; |
| | const refillAmount = 1000; |
| |
|
| | const promises = []; |
| | for (let i = 0; i < numberOfRefills; i++) { |
| | promises.push( |
| | createAutoRefillTransaction({ |
| | user: userId, |
| | tokenType: 'credits', |
| | context: 'concurrent-refill-test', |
| | rawAmount: refillAmount, |
| | balance: { enabled: true }, |
| | }), |
| | ); |
| | } |
| |
|
| | |
| | const results = await Promise.all(promises); |
| |
|
| | |
| | const finalBalance = await Balance.findOne({ user: userId }); |
| | expect(finalBalance).toBeDefined(); |
| |
|
| | |
| | const expectedFinalBalance = initialBalance + numberOfRefills * refillAmount; |
| |
|
| | console.log('Initial balance (Increase Test):', initialBalance); |
| | console.log(`Performed ${numberOfRefills} refills of ${refillAmount} each.`); |
| | console.log('Expected final balance (Increase Test):', expectedFinalBalance); |
| | console.log('Actual final balance (Increase Test):', finalBalance.tokenCredits); |
| |
|
| | |
| | expect(finalBalance.tokenCredits).toBeCloseTo(expectedFinalBalance, 0); |
| |
|
| | |
| | const transactions = await Transaction.find({ |
| | user: userId, |
| | context: 'concurrent-refill-test', |
| | }); |
| |
|
| | |
| | expect(transactions.length).toBe(numberOfRefills); |
| |
|
| | |
| | const totalIncrementReported = results.reduce((sum, result) => { |
| | |
| | |
| | |
| | |
| | return sum + (result?.transaction?.rawAmount || 0); |
| | }, 0); |
| | console.log('Total increment reported by results:', totalIncrementReported); |
| | expect(totalIncrementReported).toBe(expectedFinalBalance - initialBalance); |
| |
|
| | |
| | let totalTokenValueFromDb = 0; |
| | transactions.forEach((tx) => { |
| | |
| | |
| | |
| | totalTokenValueFromDb += tx.rawAmount; |
| | }); |
| | console.log('Total rawAmount from DB transactions:', totalTokenValueFromDb); |
| | expect(totalTokenValueFromDb).toBeCloseTo(expectedFinalBalance - initialBalance, 0); |
| | }); |
| |
|
| | it('should create structured transactions for both prompt and completion tokens', async () => { |
| | |
| | await Balance.create({ |
| | user: userId, |
| | tokenCredits: 10000, |
| | }); |
| |
|
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-convo', |
| | model: 'claude-3-5-sonnet', |
| | context: 'test', |
| | balance: { enabled: true }, |
| | }; |
| | const tokenUsage = { |
| | promptTokens: { |
| | input: 10, |
| | write: 100, |
| | read: 5, |
| | }, |
| | completionTokens: 50, |
| | }; |
| |
|
| | const result = await spendStructuredTokens(txData, tokenUsage); |
| |
|
| | |
| | const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); |
| | expect(transactions).toHaveLength(2); |
| |
|
| | |
| | expect(transactions[0].tokenType).toBe('completion'); |
| | expect(transactions[0].rawAmount).toBe(-50); |
| |
|
| | |
| | expect(transactions[1].tokenType).toBe('prompt'); |
| | expect(transactions[1].inputTokens).toBe(-10); |
| | expect(transactions[1].writeTokens).toBe(-100); |
| | expect(transactions[1].readTokens).toBe(-5); |
| |
|
| | |
| | expect(result).toEqual({ |
| | prompt: expect.objectContaining({ |
| | user: userId.toString(), |
| | prompt: expect.any(Number), |
| | }), |
| | completion: expect.objectContaining({ |
| | user: userId.toString(), |
| | completion: expect.any(Number), |
| | }), |
| | }); |
| |
|
| | |
| | const balance = await Balance.findOne({ user: userId }); |
| | expect(balance).toBeDefined(); |
| | expect(balance.tokenCredits).toBeLessThan(10000); |
| | }); |
| | }); |
| |
|