mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00
🗄️ feat: Allow Skipping Transactions When Balance is Disabled (#9419)
* Disable transaction creation when balance is disabled * Add configuration to disable transactions creation * chore: remove comments --------- Co-authored-by: Danny Avila <danacordially@gmail.com>
This commit is contained in:
parent
0ecafcd38e
commit
e95e0052da
10 changed files with 544 additions and 7 deletions
|
@ -189,11 +189,15 @@ async function createAutoRefillTransaction(txData) {
|
||||||
* @param {txData} _txData - Transaction data.
|
* @param {txData} _txData - Transaction data.
|
||||||
*/
|
*/
|
||||||
async function createTransaction(_txData) {
|
async function createTransaction(_txData) {
|
||||||
const { balance, ...txData } = _txData;
|
const { balance, transactions, ...txData } = _txData;
|
||||||
if (txData.rawAmount != null && isNaN(txData.rawAmount)) {
|
if (txData.rawAmount != null && isNaN(txData.rawAmount)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (transactions?.enabled === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const transaction = new Transaction(txData);
|
const transaction = new Transaction(txData);
|
||||||
transaction.endpointTokenConfig = txData.endpointTokenConfig;
|
transaction.endpointTokenConfig = txData.endpointTokenConfig;
|
||||||
calculateTokenValue(transaction);
|
calculateTokenValue(transaction);
|
||||||
|
@ -222,7 +226,11 @@ async function createTransaction(_txData) {
|
||||||
* @param {txData} _txData - Transaction data.
|
* @param {txData} _txData - Transaction data.
|
||||||
*/
|
*/
|
||||||
async function createStructuredTransaction(_txData) {
|
async function createStructuredTransaction(_txData) {
|
||||||
const { balance, ...txData } = _txData;
|
const { balance, transactions, ...txData } = _txData;
|
||||||
|
if (transactions?.enabled === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const transaction = new Transaction({
|
const transaction = new Transaction({
|
||||||
...txData,
|
...txData,
|
||||||
endpointTokenConfig: txData.endpointTokenConfig,
|
endpointTokenConfig: txData.endpointTokenConfig,
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||||
const { spendTokens, spendStructuredTokens } = require('./spendTokens');
|
const { spendTokens, spendStructuredTokens } = require('./spendTokens');
|
||||||
|
|
||||||
const { getMultiplier, getCacheMultiplier } = require('./tx');
|
const { getMultiplier, getCacheMultiplier } = require('./tx');
|
||||||
const { createTransaction } = require('./Transaction');
|
const { createTransaction, createStructuredTransaction } = require('./Transaction');
|
||||||
const { Balance } = require('~/db/models');
|
const { Balance, Transaction } = require('~/db/models');
|
||||||
|
|
||||||
let mongoServer;
|
let mongoServer;
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
@ -380,3 +379,188 @@ describe('NaN Handling Tests', () => {
|
||||||
expect(balance.tokenCredits).toBe(initialBalance);
|
expect(balance.tokenCredits).toBe(initialBalance);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Transactions Config Tests', () => {
|
||||||
|
test('createTransaction should not save when transactions.enabled is false', async () => {
|
||||||
|
// Arrange
|
||||||
|
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 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await createTransaction(txData);
|
||||||
|
|
||||||
|
// Assert: No transaction should be created
|
||||||
|
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 () => {
|
||||||
|
// Arrange
|
||||||
|
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 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await createTransaction(txData);
|
||||||
|
|
||||||
|
// Assert: Transaction should be created
|
||||||
|
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 () => {
|
||||||
|
// Arrange
|
||||||
|
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 },
|
||||||
|
// No transactions config provided
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await createTransaction(txData);
|
||||||
|
|
||||||
|
// Assert: Transaction should be created (backward compatibility)
|
||||||
|
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 () => {
|
||||||
|
// Arrange
|
||||||
|
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 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await createTransaction(txData);
|
||||||
|
|
||||||
|
// Assert: Transaction should be created but balance unchanged
|
||||||
|
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 () => {
|
||||||
|
// Arrange
|
||||||
|
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 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await createStructuredTransaction(txData);
|
||||||
|
|
||||||
|
// Assert: No transaction should be created
|
||||||
|
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 () => {
|
||||||
|
// Arrange
|
||||||
|
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 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await createStructuredTransaction(txData);
|
||||||
|
|
||||||
|
// Assert: Transaction should be created but balance unchanged
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@ const {
|
||||||
checkAccess,
|
checkAccess,
|
||||||
resolveHeaders,
|
resolveHeaders,
|
||||||
getBalanceConfig,
|
getBalanceConfig,
|
||||||
|
getTransactionsConfig,
|
||||||
memoryInstructions,
|
memoryInstructions,
|
||||||
formatContentStrings,
|
formatContentStrings,
|
||||||
createMemoryProcessor,
|
createMemoryProcessor,
|
||||||
|
@ -623,11 +624,13 @@ class AgentClient extends BaseClient {
|
||||||
* @param {string} [params.model]
|
* @param {string} [params.model]
|
||||||
* @param {string} [params.context='message']
|
* @param {string} [params.context='message']
|
||||||
* @param {AppConfig['balance']} [params.balance]
|
* @param {AppConfig['balance']} [params.balance]
|
||||||
|
* @param {AppConfig['transactions']} [params.transactions]
|
||||||
* @param {UsageMetadata[]} [params.collectedUsage=this.collectedUsage]
|
* @param {UsageMetadata[]} [params.collectedUsage=this.collectedUsage]
|
||||||
*/
|
*/
|
||||||
async recordCollectedUsage({
|
async recordCollectedUsage({
|
||||||
model,
|
model,
|
||||||
balance,
|
balance,
|
||||||
|
transactions,
|
||||||
context = 'message',
|
context = 'message',
|
||||||
collectedUsage = this.collectedUsage,
|
collectedUsage = this.collectedUsage,
|
||||||
}) {
|
}) {
|
||||||
|
@ -653,6 +656,7 @@ class AgentClient extends BaseClient {
|
||||||
const txMetadata = {
|
const txMetadata = {
|
||||||
context,
|
context,
|
||||||
balance,
|
balance,
|
||||||
|
transactions,
|
||||||
conversationId: this.conversationId,
|
conversationId: this.conversationId,
|
||||||
user: this.user ?? this.options.req.user?.id,
|
user: this.user ?? this.options.req.user?.id,
|
||||||
endpointTokenConfig: this.options.endpointTokenConfig,
|
endpointTokenConfig: this.options.endpointTokenConfig,
|
||||||
|
@ -1051,7 +1055,12 @@ class AgentClient extends BaseClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
const balanceConfig = getBalanceConfig(appConfig);
|
const balanceConfig = getBalanceConfig(appConfig);
|
||||||
await this.recordCollectedUsage({ context: 'message', balance: balanceConfig });
|
const transactionsConfig = getTransactionsConfig(appConfig);
|
||||||
|
await this.recordCollectedUsage({
|
||||||
|
context: 'message',
|
||||||
|
balance: balanceConfig,
|
||||||
|
transactions: transactionsConfig,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'[api/server/controllers/agents/client.js #chatCompletion] Error recording collected usage',
|
'[api/server/controllers/agents/client.js #chatCompletion] Error recording collected usage',
|
||||||
|
@ -1245,11 +1254,13 @@ class AgentClient extends BaseClient {
|
||||||
});
|
});
|
||||||
|
|
||||||
const balanceConfig = getBalanceConfig(appConfig);
|
const balanceConfig = getBalanceConfig(appConfig);
|
||||||
|
const transactionsConfig = getTransactionsConfig(appConfig);
|
||||||
await this.recordCollectedUsage({
|
await this.recordCollectedUsage({
|
||||||
collectedUsage,
|
collectedUsage,
|
||||||
context: 'title',
|
context: 'title',
|
||||||
model: clientOptions.model,
|
model: clientOptions.model,
|
||||||
balance: balanceConfig,
|
balance: balanceConfig,
|
||||||
|
transactions: transactionsConfig,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
'[api/server/controllers/agents/client.js #titleConvo] Error recording collected usage',
|
'[api/server/controllers/agents/client.js #titleConvo] Error recording collected usage',
|
||||||
|
|
|
@ -237,6 +237,9 @@ describe('AgentClient - titleConvo', () => {
|
||||||
balance: {
|
balance: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
transactions: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ const AppService = async () => {
|
||||||
enabled: isEnabled(process.env.CHECK_BALANCE),
|
enabled: isEnabled(process.env.CHECK_BALANCE),
|
||||||
startBalance: startBalance ? parseInt(startBalance, 10) : undefined,
|
startBalance: startBalance ? parseInt(startBalance, 10) : undefined,
|
||||||
};
|
};
|
||||||
|
const transactions = config.transactions ?? configDefaults.transactions;
|
||||||
const imageOutputType = config?.imageOutputType ?? configDefaults.imageOutputType;
|
const imageOutputType = config?.imageOutputType ?? configDefaults.imageOutputType;
|
||||||
|
|
||||||
process.env.CDN_PROVIDER = fileStrategy;
|
process.env.CDN_PROVIDER = fileStrategy;
|
||||||
|
@ -84,6 +85,7 @@ const AppService = async () => {
|
||||||
memory,
|
memory,
|
||||||
speech,
|
speech,
|
||||||
balance,
|
balance,
|
||||||
|
transactions,
|
||||||
mcpConfig,
|
mcpConfig,
|
||||||
webSearch,
|
webSearch,
|
||||||
fileStrategy,
|
fileStrategy,
|
||||||
|
|
|
@ -121,6 +121,14 @@ registration:
|
||||||
# refillIntervalUnit: 'days'
|
# refillIntervalUnit: 'days'
|
||||||
# refillAmount: 10000
|
# refillAmount: 10000
|
||||||
|
|
||||||
|
# Example Transactions settings
|
||||||
|
# Controls whether to save transaction records to the database
|
||||||
|
# Default is true (enabled)
|
||||||
|
#transactions:
|
||||||
|
# enabled: false
|
||||||
|
# Note: If balance.enabled is true, transactions will always be enabled
|
||||||
|
# regardless of this setting to ensure balance tracking works correctly
|
||||||
|
|
||||||
# speech:
|
# speech:
|
||||||
# tts:
|
# tts:
|
||||||
# openai:
|
# openai:
|
||||||
|
|
285
packages/api/src/app/config.test.ts
Normal file
285
packages/api/src/app/config.test.ts
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
import { getTransactionsConfig, getBalanceConfig } from './config';
|
||||||
|
import { logger } from '@librechat/data-schemas';
|
||||||
|
import { FileSources } from 'librechat-data-provider';
|
||||||
|
import type { AppConfig } from '~/types';
|
||||||
|
import type { TCustomConfig } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
// Helper function to create a minimal AppConfig for testing
|
||||||
|
const createTestAppConfig = (overrides: Partial<AppConfig> = {}): AppConfig => {
|
||||||
|
const minimalConfig: TCustomConfig = {
|
||||||
|
version: '1.0.0',
|
||||||
|
cache: true,
|
||||||
|
interface: {
|
||||||
|
endpointsMenu: true,
|
||||||
|
},
|
||||||
|
registration: {
|
||||||
|
socialLogins: [],
|
||||||
|
},
|
||||||
|
endpoints: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: minimalConfig,
|
||||||
|
paths: {
|
||||||
|
uploads: '',
|
||||||
|
imageOutput: '',
|
||||||
|
publicPath: '',
|
||||||
|
},
|
||||||
|
fileStrategy: FileSources.local,
|
||||||
|
fileStrategies: {},
|
||||||
|
imageOutputType: 'png',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock('@librechat/data-schemas', () => ({
|
||||||
|
logger: {
|
||||||
|
warn: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/utils', () => ({
|
||||||
|
isEnabled: jest.fn((value) => value === 'true'),
|
||||||
|
normalizeEndpointName: jest.fn((name) => name),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('getTransactionsConfig', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
delete process.env.CHECK_BALANCE;
|
||||||
|
delete process.env.START_BALANCE;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when appConfig is not provided', () => {
|
||||||
|
it('should return default config with enabled: true', () => {
|
||||||
|
const result = getTransactionsConfig();
|
||||||
|
expect(result).toEqual({ enabled: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when appConfig is provided', () => {
|
||||||
|
it('should return transactions config when explicitly set to false', () => {
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
transactions: { enabled: false },
|
||||||
|
balance: { enabled: false },
|
||||||
|
});
|
||||||
|
const result = getTransactionsConfig(appConfig);
|
||||||
|
expect(result).toEqual({ enabled: false });
|
||||||
|
expect(logger.warn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return transactions config when explicitly set to true', () => {
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
transactions: { enabled: true },
|
||||||
|
balance: { enabled: false },
|
||||||
|
});
|
||||||
|
const result = getTransactionsConfig(appConfig);
|
||||||
|
expect(result).toEqual({ enabled: true });
|
||||||
|
expect(logger.warn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default config when transactions is not defined', () => {
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
balance: { enabled: false },
|
||||||
|
});
|
||||||
|
const result = getTransactionsConfig(appConfig);
|
||||||
|
expect(result).toEqual({ enabled: true });
|
||||||
|
expect(logger.warn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('balance and transactions interaction', () => {
|
||||||
|
it('should force transactions to be enabled when balance is enabled but transactions is disabled', () => {
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
transactions: { enabled: false },
|
||||||
|
balance: { enabled: true },
|
||||||
|
});
|
||||||
|
const result = getTransactionsConfig(appConfig);
|
||||||
|
expect(result).toEqual({ enabled: true });
|
||||||
|
expect(logger.warn).toHaveBeenCalledWith(
|
||||||
|
'Configuration warning: transactions.enabled=false is incompatible with balance.enabled=true. ' +
|
||||||
|
'Transactions will be enabled to ensure balance tracking works correctly.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not override transactions when balance is enabled and transactions is enabled', () => {
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
transactions: { enabled: true },
|
||||||
|
balance: { enabled: true },
|
||||||
|
});
|
||||||
|
const result = getTransactionsConfig(appConfig);
|
||||||
|
expect(result).toEqual({ enabled: true });
|
||||||
|
expect(logger.warn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow transactions to be disabled when balance is disabled', () => {
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
transactions: { enabled: false },
|
||||||
|
balance: { enabled: false },
|
||||||
|
});
|
||||||
|
const result = getTransactionsConfig(appConfig);
|
||||||
|
expect(result).toEqual({ enabled: false });
|
||||||
|
expect(logger.warn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default when balance is enabled but transactions is not defined', () => {
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
balance: { enabled: true },
|
||||||
|
});
|
||||||
|
const result = getTransactionsConfig(appConfig);
|
||||||
|
expect(result).toEqual({ enabled: true });
|
||||||
|
expect(logger.warn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with environment variables for balance', () => {
|
||||||
|
it('should force transactions enabled when CHECK_BALANCE env is true and transactions is false', () => {
|
||||||
|
process.env.CHECK_BALANCE = 'true';
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
transactions: { enabled: false },
|
||||||
|
});
|
||||||
|
const result = getTransactionsConfig(appConfig);
|
||||||
|
expect(result).toEqual({ enabled: true });
|
||||||
|
expect(logger.warn).toHaveBeenCalledWith(
|
||||||
|
'Configuration warning: transactions.enabled=false is incompatible with balance.enabled=true. ' +
|
||||||
|
'Transactions will be enabled to ensure balance tracking works correctly.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow transactions disabled when CHECK_BALANCE env is false', () => {
|
||||||
|
process.env.CHECK_BALANCE = 'false';
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
transactions: { enabled: false },
|
||||||
|
});
|
||||||
|
const result = getTransactionsConfig(appConfig);
|
||||||
|
expect(result).toEqual({ enabled: false });
|
||||||
|
expect(logger.warn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should handle empty appConfig object', () => {
|
||||||
|
const appConfig = createTestAppConfig();
|
||||||
|
const result = getTransactionsConfig(appConfig);
|
||||||
|
expect(result).toEqual({ enabled: true });
|
||||||
|
expect(logger.warn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle appConfig with null balance', () => {
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
transactions: { enabled: false },
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
balance: null as any,
|
||||||
|
});
|
||||||
|
const result = getTransactionsConfig(appConfig);
|
||||||
|
expect(result).toEqual({ enabled: false });
|
||||||
|
expect(logger.warn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle appConfig with undefined balance', () => {
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
transactions: { enabled: false },
|
||||||
|
balance: undefined,
|
||||||
|
});
|
||||||
|
const result = getTransactionsConfig(appConfig);
|
||||||
|
expect(result).toEqual({ enabled: false });
|
||||||
|
expect(logger.warn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle appConfig with balance enabled undefined', () => {
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
transactions: { enabled: false },
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
balance: { enabled: undefined as any },
|
||||||
|
});
|
||||||
|
const result = getTransactionsConfig(appConfig);
|
||||||
|
expect(result).toEqual({ enabled: false });
|
||||||
|
expect(logger.warn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBalanceConfig', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
delete process.env.CHECK_BALANCE;
|
||||||
|
delete process.env.START_BALANCE;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when appConfig is not provided', () => {
|
||||||
|
it('should return config based on environment variables', () => {
|
||||||
|
process.env.CHECK_BALANCE = 'true';
|
||||||
|
process.env.START_BALANCE = '1000';
|
||||||
|
const result = getBalanceConfig();
|
||||||
|
expect(result).toEqual({
|
||||||
|
enabled: true,
|
||||||
|
startBalance: 1000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty config when no env vars are set', () => {
|
||||||
|
const result = getBalanceConfig();
|
||||||
|
expect(result).toEqual({ enabled: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle CHECK_BALANCE true without START_BALANCE', () => {
|
||||||
|
process.env.CHECK_BALANCE = 'true';
|
||||||
|
const result = getBalanceConfig();
|
||||||
|
expect(result).toEqual({
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle START_BALANCE without CHECK_BALANCE', () => {
|
||||||
|
process.env.START_BALANCE = '5000';
|
||||||
|
const result = getBalanceConfig();
|
||||||
|
expect(result).toEqual({
|
||||||
|
enabled: false,
|
||||||
|
startBalance: 5000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when appConfig is provided', () => {
|
||||||
|
it('should merge appConfig balance with env config', () => {
|
||||||
|
process.env.CHECK_BALANCE = 'true';
|
||||||
|
process.env.START_BALANCE = '1000';
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
balance: {
|
||||||
|
enabled: false,
|
||||||
|
startBalance: 2000,
|
||||||
|
autoRefillEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = getBalanceConfig(appConfig);
|
||||||
|
expect(result).toEqual({
|
||||||
|
enabled: false,
|
||||||
|
startBalance: 2000,
|
||||||
|
autoRefillEnabled: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use env config when appConfig balance is not provided', () => {
|
||||||
|
process.env.CHECK_BALANCE = 'true';
|
||||||
|
process.env.START_BALANCE = '3000';
|
||||||
|
const appConfig = createTestAppConfig();
|
||||||
|
const result = getBalanceConfig(appConfig);
|
||||||
|
expect(result).toEqual({
|
||||||
|
enabled: true,
|
||||||
|
startBalance: 3000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle appConfig with null balance', () => {
|
||||||
|
process.env.CHECK_BALANCE = 'true';
|
||||||
|
const appConfig = createTestAppConfig({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
balance: null as any,
|
||||||
|
});
|
||||||
|
const result = getBalanceConfig(appConfig);
|
||||||
|
expect(result).toEqual({
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,7 +1,8 @@
|
||||||
import { EModelEndpoint, removeNullishValues } from 'librechat-data-provider';
|
import { EModelEndpoint, removeNullishValues } from 'librechat-data-provider';
|
||||||
import type { TCustomConfig, TEndpoint } from 'librechat-data-provider';
|
import type { TCustomConfig, TEndpoint, TTransactionsConfig } from 'librechat-data-provider';
|
||||||
import type { AppConfig } from '~/types';
|
import type { AppConfig } from '~/types';
|
||||||
import { isEnabled, normalizeEndpointName } from '~/utils';
|
import { isEnabled, normalizeEndpointName } from '~/utils';
|
||||||
|
import { logger } from '@librechat/data-schemas';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the balance configuration object
|
* Retrieves the balance configuration object
|
||||||
|
@ -20,6 +21,32 @@ export function getBalanceConfig(appConfig?: AppConfig): Partial<TCustomConfig['
|
||||||
return { ...config, ...(appConfig?.['balance'] ?? {}) };
|
return { ...config, ...(appConfig?.['balance'] ?? {}) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the transactions configuration object
|
||||||
|
* */
|
||||||
|
export function getTransactionsConfig(appConfig?: AppConfig): TTransactionsConfig {
|
||||||
|
const defaultConfig: TTransactionsConfig = { enabled: true };
|
||||||
|
|
||||||
|
if (!appConfig) {
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transactionsConfig = appConfig?.['transactions'] ?? defaultConfig;
|
||||||
|
const balanceConfig = getBalanceConfig(appConfig);
|
||||||
|
|
||||||
|
// If balance is enabled but transactions are disabled, force transactions to be enabled
|
||||||
|
// and log a warning
|
||||||
|
if (balanceConfig?.enabled && !transactionsConfig.enabled) {
|
||||||
|
logger.warn(
|
||||||
|
'Configuration warning: transactions.enabled=false is incompatible with balance.enabled=true. ' +
|
||||||
|
'Transactions will be enabled to ensure balance tracking works correctly.',
|
||||||
|
);
|
||||||
|
return { ...transactionsConfig, enabled: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactionsConfig;
|
||||||
|
}
|
||||||
|
|
||||||
export const getCustomEndpointConfig = ({
|
export const getCustomEndpointConfig = ({
|
||||||
endpoint,
|
endpoint,
|
||||||
appConfig,
|
appConfig,
|
||||||
|
|
|
@ -51,6 +51,8 @@ export interface AppConfig {
|
||||||
turnstileConfig?: TCustomConfig['turnstile'];
|
turnstileConfig?: TCustomConfig['turnstile'];
|
||||||
/** Balance configuration */
|
/** Balance configuration */
|
||||||
balance?: TCustomConfig['balance'];
|
balance?: TCustomConfig['balance'];
|
||||||
|
/** Transactions configuration */
|
||||||
|
transactions?: TCustomConfig['transactions'];
|
||||||
/** Speech configuration */
|
/** Speech configuration */
|
||||||
speech?: TCustomConfig['speech'];
|
speech?: TCustomConfig['speech'];
|
||||||
/** MCP server configuration */
|
/** MCP server configuration */
|
||||||
|
|
|
@ -577,6 +577,7 @@ export const interfaceSchema = z
|
||||||
|
|
||||||
export type TInterfaceConfig = z.infer<typeof interfaceSchema>;
|
export type TInterfaceConfig = z.infer<typeof interfaceSchema>;
|
||||||
export type TBalanceConfig = z.infer<typeof balanceSchema>;
|
export type TBalanceConfig = z.infer<typeof balanceSchema>;
|
||||||
|
export type TTransactionsConfig = z.infer<typeof transactionsSchema>;
|
||||||
|
|
||||||
export const turnstileOptionsSchema = z
|
export const turnstileOptionsSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -601,6 +602,7 @@ export type TStartupConfig = {
|
||||||
interface?: TInterfaceConfig;
|
interface?: TInterfaceConfig;
|
||||||
turnstile?: TTurnstileConfig;
|
turnstile?: TTurnstileConfig;
|
||||||
balance?: TBalanceConfig;
|
balance?: TBalanceConfig;
|
||||||
|
transactions?: TTransactionsConfig;
|
||||||
discordLoginEnabled: boolean;
|
discordLoginEnabled: boolean;
|
||||||
facebookLoginEnabled: boolean;
|
facebookLoginEnabled: boolean;
|
||||||
githubLoginEnabled: boolean;
|
githubLoginEnabled: boolean;
|
||||||
|
@ -768,6 +770,10 @@ export const balanceSchema = z.object({
|
||||||
refillAmount: z.number().optional().default(10000),
|
refillAmount: z.number().optional().default(10000),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const transactionsSchema = z.object({
|
||||||
|
enabled: z.boolean().optional().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
export const memorySchema = z.object({
|
export const memorySchema = z.object({
|
||||||
disabled: z.boolean().optional(),
|
disabled: z.boolean().optional(),
|
||||||
validKeys: z.array(z.string()).optional(),
|
validKeys: z.array(z.string()).optional(),
|
||||||
|
@ -821,6 +827,7 @@ export const configSchema = z.object({
|
||||||
})
|
})
|
||||||
.default({ socialLogins: defaultSocialLogins }),
|
.default({ socialLogins: defaultSocialLogins }),
|
||||||
balance: balanceSchema.optional(),
|
balance: balanceSchema.optional(),
|
||||||
|
transactions: transactionsSchema.optional(),
|
||||||
speech: z
|
speech: z
|
||||||
.object({
|
.object({
|
||||||
tts: ttsSchema.optional(),
|
tts: ttsSchema.optional(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue