mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +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.
|
||||
*/
|
||||
async function createTransaction(_txData) {
|
||||
const { balance, ...txData } = _txData;
|
||||
const { balance, transactions, ...txData } = _txData;
|
||||
if (txData.rawAmount != null && isNaN(txData.rawAmount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transactions?.enabled === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transaction = new Transaction(txData);
|
||||
transaction.endpointTokenConfig = txData.endpointTokenConfig;
|
||||
calculateTokenValue(transaction);
|
||||
|
@ -222,7 +226,11 @@ async function createTransaction(_txData) {
|
|||
* @param {txData} _txData - Transaction data.
|
||||
*/
|
||||
async function createStructuredTransaction(_txData) {
|
||||
const { balance, ...txData } = _txData;
|
||||
const { balance, transactions, ...txData } = _txData;
|
||||
if (transactions?.enabled === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transaction = new Transaction({
|
||||
...txData,
|
||||
endpointTokenConfig: txData.endpointTokenConfig,
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||
const { spendTokens, spendStructuredTokens } = require('./spendTokens');
|
||||
|
||||
const { getMultiplier, getCacheMultiplier } = require('./tx');
|
||||
const { createTransaction } = require('./Transaction');
|
||||
const { Balance } = require('~/db/models');
|
||||
const { createTransaction, createStructuredTransaction } = require('./Transaction');
|
||||
const { Balance, Transaction } = require('~/db/models');
|
||||
|
||||
let mongoServer;
|
||||
beforeAll(async () => {
|
||||
|
@ -380,3 +379,188 @@ describe('NaN Handling Tests', () => {
|
|||
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,
|
||||
resolveHeaders,
|
||||
getBalanceConfig,
|
||||
getTransactionsConfig,
|
||||
memoryInstructions,
|
||||
formatContentStrings,
|
||||
createMemoryProcessor,
|
||||
|
@ -623,11 +624,13 @@ class AgentClient extends BaseClient {
|
|||
* @param {string} [params.model]
|
||||
* @param {string} [params.context='message']
|
||||
* @param {AppConfig['balance']} [params.balance]
|
||||
* @param {AppConfig['transactions']} [params.transactions]
|
||||
* @param {UsageMetadata[]} [params.collectedUsage=this.collectedUsage]
|
||||
*/
|
||||
async recordCollectedUsage({
|
||||
model,
|
||||
balance,
|
||||
transactions,
|
||||
context = 'message',
|
||||
collectedUsage = this.collectedUsage,
|
||||
}) {
|
||||
|
@ -653,6 +656,7 @@ class AgentClient extends BaseClient {
|
|||
const txMetadata = {
|
||||
context,
|
||||
balance,
|
||||
transactions,
|
||||
conversationId: this.conversationId,
|
||||
user: this.user ?? this.options.req.user?.id,
|
||||
endpointTokenConfig: this.options.endpointTokenConfig,
|
||||
|
@ -1051,7 +1055,12 @@ class AgentClient extends BaseClient {
|
|||
}
|
||||
|
||||
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) {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #chatCompletion] Error recording collected usage',
|
||||
|
@ -1245,11 +1254,13 @@ class AgentClient extends BaseClient {
|
|||
});
|
||||
|
||||
const balanceConfig = getBalanceConfig(appConfig);
|
||||
const transactionsConfig = getTransactionsConfig(appConfig);
|
||||
await this.recordCollectedUsage({
|
||||
collectedUsage,
|
||||
context: 'title',
|
||||
model: clientOptions.model,
|
||||
balance: balanceConfig,
|
||||
transactions: transactionsConfig,
|
||||
}).catch((err) => {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #titleConvo] Error recording collected usage',
|
||||
|
|
|
@ -237,6 +237,9 @@ describe('AgentClient - titleConvo', () => {
|
|||
balance: {
|
||||
enabled: false,
|
||||
},
|
||||
transactions: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ const AppService = async () => {
|
|||
enabled: isEnabled(process.env.CHECK_BALANCE),
|
||||
startBalance: startBalance ? parseInt(startBalance, 10) : undefined,
|
||||
};
|
||||
const transactions = config.transactions ?? configDefaults.transactions;
|
||||
const imageOutputType = config?.imageOutputType ?? configDefaults.imageOutputType;
|
||||
|
||||
process.env.CDN_PROVIDER = fileStrategy;
|
||||
|
@ -84,6 +85,7 @@ const AppService = async () => {
|
|||
memory,
|
||||
speech,
|
||||
balance,
|
||||
transactions,
|
||||
mcpConfig,
|
||||
webSearch,
|
||||
fileStrategy,
|
||||
|
|
|
@ -121,6 +121,14 @@ registration:
|
|||
# refillIntervalUnit: 'days'
|
||||
# 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:
|
||||
# tts:
|
||||
# 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 type { TCustomConfig, TEndpoint } from 'librechat-data-provider';
|
||||
import type { TCustomConfig, TEndpoint, TTransactionsConfig } from 'librechat-data-provider';
|
||||
import type { AppConfig } from '~/types';
|
||||
import { isEnabled, normalizeEndpointName } from '~/utils';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
|
||||
/**
|
||||
* Retrieves the balance configuration object
|
||||
|
@ -20,6 +21,32 @@ export function getBalanceConfig(appConfig?: AppConfig): Partial<TCustomConfig['
|
|||
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 = ({
|
||||
endpoint,
|
||||
appConfig,
|
||||
|
|
|
@ -51,6 +51,8 @@ export interface AppConfig {
|
|||
turnstileConfig?: TCustomConfig['turnstile'];
|
||||
/** Balance configuration */
|
||||
balance?: TCustomConfig['balance'];
|
||||
/** Transactions configuration */
|
||||
transactions?: TCustomConfig['transactions'];
|
||||
/** Speech configuration */
|
||||
speech?: TCustomConfig['speech'];
|
||||
/** MCP server configuration */
|
||||
|
|
|
@ -577,6 +577,7 @@ export const interfaceSchema = z
|
|||
|
||||
export type TInterfaceConfig = z.infer<typeof interfaceSchema>;
|
||||
export type TBalanceConfig = z.infer<typeof balanceSchema>;
|
||||
export type TTransactionsConfig = z.infer<typeof transactionsSchema>;
|
||||
|
||||
export const turnstileOptionsSchema = z
|
||||
.object({
|
||||
|
@ -601,6 +602,7 @@ export type TStartupConfig = {
|
|||
interface?: TInterfaceConfig;
|
||||
turnstile?: TTurnstileConfig;
|
||||
balance?: TBalanceConfig;
|
||||
transactions?: TTransactionsConfig;
|
||||
discordLoginEnabled: boolean;
|
||||
facebookLoginEnabled: boolean;
|
||||
githubLoginEnabled: boolean;
|
||||
|
@ -768,6 +770,10 @@ export const balanceSchema = z.object({
|
|||
refillAmount: z.number().optional().default(10000),
|
||||
});
|
||||
|
||||
export const transactionsSchema = z.object({
|
||||
enabled: z.boolean().optional().default(true),
|
||||
});
|
||||
|
||||
export const memorySchema = z.object({
|
||||
disabled: z.boolean().optional(),
|
||||
validKeys: z.array(z.string()).optional(),
|
||||
|
@ -821,6 +827,7 @@ export const configSchema = z.object({
|
|||
})
|
||||
.default({ socialLogins: defaultSocialLogins }),
|
||||
balance: balanceSchema.optional(),
|
||||
transactions: transactionsSchema.optional(),
|
||||
speech: z
|
||||
.object({
|
||||
tts: ttsSchema.optional(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue