| | const mongoose = require('mongoose'); |
| | const { MongoMemoryServer } = require('mongodb-memory-server'); |
| | const { spendTokens, spendStructuredTokens } = require('./spendTokens'); |
| | const { getMultiplier, getCacheMultiplier } = require('./tx'); |
| | const { createTransaction, createStructuredTransaction } = require('./Transaction'); |
| | const { Balance, Transaction } = require('~/db/models'); |
| |
|
| | let mongoServer; |
| | beforeAll(async () => { |
| | mongoServer = await MongoMemoryServer.create(); |
| | const mongoUri = mongoServer.getUri(); |
| | await mongoose.connect(mongoUri); |
| | }); |
| |
|
| | afterAll(async () => { |
| | await mongoose.disconnect(); |
| | await mongoServer.stop(); |
| | }); |
| |
|
| | beforeEach(async () => { |
| | await mongoose.connection.dropDatabase(); |
| | }); |
| |
|
| | describe('Regular Token Spending Tests', () => { |
| | test('Balance should decrease when spending tokens with spendTokens', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 10000000; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'gpt-3.5-turbo'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-conversation-id', |
| | model, |
| | context: 'test', |
| | endpointTokenConfig: null, |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage = { |
| | promptTokens: 100, |
| | completionTokens: 50, |
| | }; |
| |
|
| | |
| | await spendTokens(txData, tokenUsage); |
| |
|
| | |
| | const updatedBalance = await Balance.findOne({ user: userId }); |
| | const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); |
| | const completionMultiplier = getMultiplier({ model, tokenType: 'completion' }); |
| | const expectedTotalCost = 100 * promptMultiplier + 50 * completionMultiplier; |
| | const expectedBalance = initialBalance - expectedTotalCost; |
| |
|
| | expect(updatedBalance.tokenCredits).toBeCloseTo(expectedBalance, 0); |
| | }); |
| |
|
| | test('spendTokens should handle zero completion tokens', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 10000000; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'gpt-3.5-turbo'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-conversation-id', |
| | model, |
| | context: 'test', |
| | endpointTokenConfig: null, |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage = { |
| | promptTokens: 100, |
| | completionTokens: 0, |
| | }; |
| |
|
| | |
| | await spendTokens(txData, tokenUsage); |
| |
|
| | |
| | const updatedBalance = await Balance.findOne({ user: userId }); |
| | const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); |
| | const expectedCost = 100 * promptMultiplier; |
| | expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); |
| | }); |
| |
|
| | test('spendTokens should handle undefined token counts', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 10000000; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'gpt-3.5-turbo'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-conversation-id', |
| | model, |
| | context: 'test', |
| | endpointTokenConfig: null, |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage = {}; |
| |
|
| | |
| | const result = await spendTokens(txData, tokenUsage); |
| |
|
| | |
| | expect(result).toBeUndefined(); |
| | }); |
| |
|
| | test('spendTokens should handle only prompt tokens', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 10000000; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'gpt-3.5-turbo'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-conversation-id', |
| | model, |
| | context: 'test', |
| | endpointTokenConfig: null, |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage = { promptTokens: 100 }; |
| |
|
| | |
| | await spendTokens(txData, tokenUsage); |
| |
|
| | |
| | const updatedBalance = await Balance.findOne({ user: userId }); |
| | const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); |
| | const expectedCost = 100 * promptMultiplier; |
| | expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); |
| | }); |
| |
|
| | test('spendTokens should not update balance when balance feature is disabled', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 10000000; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'gpt-3.5-turbo'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-conversation-id', |
| | model, |
| | context: 'test', |
| | endpointTokenConfig: null, |
| | balance: { enabled: false }, |
| | }; |
| |
|
| | const tokenUsage = { |
| | promptTokens: 100, |
| | completionTokens: 50, |
| | }; |
| |
|
| | |
| | await spendTokens(txData, tokenUsage); |
| |
|
| | |
| | const updatedBalance = await Balance.findOne({ user: userId }); |
| | expect(updatedBalance.tokenCredits).toBe(initialBalance); |
| | }); |
| | }); |
| |
|
| | describe('Structured Token Spending Tests', () => { |
| | test('Balance should decrease and rawAmount should be set when spending a large number of structured tokens', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 17613154.55; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'claude-3-5-sonnet'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'c23a18da-706c-470a-ac28-ec87ed065199', |
| | model, |
| | context: 'message', |
| | endpointTokenConfig: null, |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage = { |
| | promptTokens: { |
| | input: 11, |
| | write: 140522, |
| | read: 0, |
| | }, |
| | completionTokens: 5, |
| | }; |
| |
|
| | const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); |
| | const completionMultiplier = getMultiplier({ model, tokenType: 'completion' }); |
| | const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); |
| | const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); |
| |
|
| | |
| | const result = await spendStructuredTokens(txData, tokenUsage); |
| |
|
| | |
| | const expectedPromptCost = |
| | tokenUsage.promptTokens.input * promptMultiplier + |
| | tokenUsage.promptTokens.write * writeMultiplier + |
| | tokenUsage.promptTokens.read * readMultiplier; |
| | const expectedCompletionCost = tokenUsage.completionTokens * completionMultiplier; |
| | const expectedTotalCost = expectedPromptCost + expectedCompletionCost; |
| | const expectedBalance = initialBalance - expectedTotalCost; |
| |
|
| | |
| | expect(result.completion.balance).toBeLessThan(initialBalance); |
| | const allowedDifference = 100; |
| | expect(Math.abs(result.completion.balance - expectedBalance)).toBeLessThan(allowedDifference); |
| | const balanceDecrease = initialBalance - result.completion.balance; |
| | expect(balanceDecrease).toBeCloseTo(expectedTotalCost, 0); |
| |
|
| | const expectedPromptTokenValue = -expectedPromptCost; |
| | const expectedCompletionTokenValue = -expectedCompletionCost; |
| | expect(result.prompt.prompt).toBeCloseTo(expectedPromptTokenValue, 1); |
| | expect(result.completion.completion).toBe(expectedCompletionTokenValue); |
| | }); |
| |
|
| | test('should handle zero completion tokens in structured spending', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 17613154.55; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'claude-3-5-sonnet'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-convo', |
| | model, |
| | context: 'message', |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage = { |
| | promptTokens: { |
| | input: 10, |
| | write: 100, |
| | read: 5, |
| | }, |
| | completionTokens: 0, |
| | }; |
| |
|
| | |
| | const result = await spendStructuredTokens(txData, tokenUsage); |
| |
|
| | |
| | expect(result.prompt).toBeDefined(); |
| | expect(result.completion).toBeUndefined(); |
| | expect(result.prompt.prompt).toBeLessThan(0); |
| | }); |
| |
|
| | test('should handle only prompt tokens in structured spending', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 17613154.55; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'claude-3-5-sonnet'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-convo', |
| | model, |
| | context: 'message', |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage = { |
| | promptTokens: { |
| | input: 10, |
| | write: 100, |
| | read: 5, |
| | }, |
| | }; |
| |
|
| | |
| | const result = await spendStructuredTokens(txData, tokenUsage); |
| |
|
| | |
| | expect(result.prompt).toBeDefined(); |
| | expect(result.completion).toBeUndefined(); |
| | expect(result.prompt.prompt).toBeLessThan(0); |
| | }); |
| |
|
| | test('should handle undefined token counts in structured spending', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 17613154.55; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'claude-3-5-sonnet'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-convo', |
| | model, |
| | context: 'message', |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage = {}; |
| |
|
| | |
| | const result = await spendStructuredTokens(txData, tokenUsage); |
| |
|
| | |
| | expect(result).toEqual({ |
| | prompt: undefined, |
| | completion: undefined, |
| | }); |
| | }); |
| |
|
| | test('should handle incomplete context for completion tokens', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 17613154.55; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'claude-3-5-sonnet'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-convo', |
| | model, |
| | context: 'incomplete', |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | const tokenUsage = { |
| | promptTokens: { |
| | input: 10, |
| | write: 100, |
| | read: 5, |
| | }, |
| | completionTokens: 50, |
| | }; |
| |
|
| | |
| | const result = await spendStructuredTokens(txData, tokenUsage); |
| |
|
| | |
| | |
| | expect(result.completion.completion).toBeCloseTo(-50 * 15 * 1.15, 0); |
| | }); |
| | }); |
| |
|
| | describe('NaN Handling Tests', () => { |
| | test('should skip transaction creation when rawAmount is NaN', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 10000000; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'gpt-3.5-turbo'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-conversation-id', |
| | model, |
| | context: 'test', |
| | endpointTokenConfig: null, |
| | rawAmount: NaN, |
| | tokenType: 'prompt', |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | |
| | const result = await createTransaction(txData); |
| |
|
| | |
| | expect(result).toBeUndefined(); |
| | const balance = await Balance.findOne({ user: userId }); |
| | expect(balance.tokenCredits).toBe(initialBalance); |
| | }); |
| | }); |
| |
|
| | describe('Transactions Config Tests', () => { |
| | test('createTransaction should not save when transactions.enabled is false', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 10000000; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'gpt-3.5-turbo'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-conversation-id', |
| | model, |
| | context: 'test', |
| | endpointTokenConfig: null, |
| | rawAmount: -100, |
| | tokenType: 'prompt', |
| | transactions: { enabled: false }, |
| | }; |
| |
|
| | |
| | const result = await createTransaction(txData); |
| |
|
| | |
| | expect(result).toBeUndefined(); |
| | const transactions = await Transaction.find({ user: userId }); |
| | expect(transactions).toHaveLength(0); |
| | const balance = await Balance.findOne({ user: userId }); |
| | expect(balance.tokenCredits).toBe(initialBalance); |
| | }); |
| |
|
| | test('createTransaction should save when transactions.enabled is true', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 10000000; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'gpt-3.5-turbo'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-conversation-id', |
| | model, |
| | context: 'test', |
| | endpointTokenConfig: null, |
| | rawAmount: -100, |
| | tokenType: 'prompt', |
| | transactions: { enabled: true }, |
| | balance: { enabled: true }, |
| | }; |
| |
|
| | |
| | const result = await createTransaction(txData); |
| |
|
| | |
| | expect(result).toBeDefined(); |
| | expect(result.balance).toBeLessThan(initialBalance); |
| | const transactions = await Transaction.find({ user: userId }); |
| | expect(transactions).toHaveLength(1); |
| | expect(transactions[0].rawAmount).toBe(-100); |
| | }); |
| |
|
| | test('createTransaction should save when balance.enabled is true even if transactions config is missing', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 10000000; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'gpt-3.5-turbo'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-conversation-id', |
| | model, |
| | context: 'test', |
| | endpointTokenConfig: null, |
| | rawAmount: -100, |
| | tokenType: 'prompt', |
| | balance: { enabled: true }, |
| | |
| | }; |
| |
|
| | |
| | const result = await createTransaction(txData); |
| |
|
| | |
| | expect(result).toBeDefined(); |
| | expect(result.balance).toBeLessThan(initialBalance); |
| | const transactions = await Transaction.find({ user: userId }); |
| | expect(transactions).toHaveLength(1); |
| | }); |
| |
|
| | test('createTransaction should save transaction but not update balance when balance is disabled but transactions enabled', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 10000000; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'gpt-3.5-turbo'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-conversation-id', |
| | model, |
| | context: 'test', |
| | endpointTokenConfig: null, |
| | rawAmount: -100, |
| | tokenType: 'prompt', |
| | transactions: { enabled: true }, |
| | balance: { enabled: false }, |
| | }; |
| |
|
| | |
| | const result = await createTransaction(txData); |
| |
|
| | |
| | expect(result).toBeUndefined(); |
| | const transactions = await Transaction.find({ user: userId }); |
| | expect(transactions).toHaveLength(1); |
| | expect(transactions[0].rawAmount).toBe(-100); |
| | const balance = await Balance.findOne({ user: userId }); |
| | expect(balance.tokenCredits).toBe(initialBalance); |
| | }); |
| |
|
| | test('createStructuredTransaction should not save when transactions.enabled is false', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 10000000; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'claude-3-5-sonnet'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-conversation-id', |
| | model, |
| | context: 'message', |
| | tokenType: 'prompt', |
| | inputTokens: -10, |
| | writeTokens: -100, |
| | readTokens: -5, |
| | transactions: { enabled: false }, |
| | }; |
| |
|
| | |
| | const result = await createStructuredTransaction(txData); |
| |
|
| | |
| | expect(result).toBeUndefined(); |
| | const transactions = await Transaction.find({ user: userId }); |
| | expect(transactions).toHaveLength(0); |
| | const balance = await Balance.findOne({ user: userId }); |
| | expect(balance.tokenCredits).toBe(initialBalance); |
| | }); |
| |
|
| | test('createStructuredTransaction should save transaction but not update balance when balance is disabled but transactions enabled', async () => { |
| | |
| | const userId = new mongoose.Types.ObjectId(); |
| | const initialBalance = 10000000; |
| | await Balance.create({ user: userId, tokenCredits: initialBalance }); |
| |
|
| | const model = 'claude-3-5-sonnet'; |
| | const txData = { |
| | user: userId, |
| | conversationId: 'test-conversation-id', |
| | model, |
| | context: 'message', |
| | tokenType: 'prompt', |
| | inputTokens: -10, |
| | writeTokens: -100, |
| | readTokens: -5, |
| | transactions: { enabled: true }, |
| | balance: { enabled: false }, |
| | }; |
| |
|
| | |
| | const result = await createStructuredTransaction(txData); |
| |
|
| | |
| | expect(result).toBeUndefined(); |
| | const transactions = await Transaction.find({ user: userId }); |
| | expect(transactions).toHaveLength(1); |
| | expect(transactions[0].inputTokens).toBe(-10); |
| | expect(transactions[0].writeTokens).toBe(-100); |
| | expect(transactions[0].readTokens).toBe(-5); |
| | const balance = await Balance.findOne({ user: userId }); |
| | expect(balance.tokenCredits).toBe(initialBalance); |
| | }); |
| | }); |
| |
|