🗄️ 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:
Sebastien Bruel 2025-09-06 00:21:02 +09:00 committed by GitHub
parent 0ecafcd38e
commit e95e0052da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 544 additions and 7 deletions

View file

@ -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,

View file

@ -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);
});
});

View file

@ -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',

View file

@ -237,6 +237,9 @@ describe('AgentClient - titleConvo', () => {
balance: { balance: {
enabled: false, enabled: false,
}, },
transactions: {
enabled: true,
},
}); });
}); });

View file

@ -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,

View file

@ -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:

View 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,
});
});
});
});

View file

@ -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,

View file

@ -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 */

View file

@ -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(),