🩹 fix: Move Token-Config Resolution to TS; Key Live Usage by Created Convo

- extract the token-config resolution (override gathering + cache lookup +
  buildTokenConfigMap) into resolveTokenConfigMap in packages/api, leaving
  the /api controller a thin request-scoped wrapper (CLAUDE.md TS rule)
- getConvoKey prefers the user message's real conversationId once the
  `created` event stamps it, so a new chat's first-response live gauge and
  totals land under the id TokenUsage subscribes to instead of NEW_CONVO
This commit is contained in:
Danny Avila 2026-06-13 13:52:30 -04:00
parent df37d1f0ec
commit a5f9ae3351
5 changed files with 145 additions and 33 deletions

View file

@ -1,47 +1,20 @@
const { logger } = require('@librechat/data-schemas');
const { EModelEndpoint, normalizeEndpointName } = require('librechat-data-provider');
const { buildTokenConfigMap, getTokenConfigKey, tokenConfigCache } = require('@librechat/api');
const { resolveTokenConfigMap } = require('@librechat/api');
const { getModelsConfig } = require('~/server/controllers/ModelController');
const { getValueKey, getMultiplier, getCacheMultiplier } = require('~/models');
/**
* Returns server-resolved context windows (and pricing when
* `interface.contextCost` is enabled) for every configured model.
* `interface.contextCost` is enabled) for every configured model. Resolution
* lives in `@librechat/api`; this controller only supplies request-scoped deps.
* @param {ServerRequest} req
* @param {ServerResponse} res
*/
async function tokenConfigController(req, res) {
try {
const appConfig = req.config;
const includePricing = appConfig?.interfaceConfig?.contextCost === true;
const modelsConfig = await getModelsConfig(req);
/** @type {Record<string, import('@librechat/api').EndpointTokenConfig | undefined>} */
const endpointTokenConfigs = {};
const customEndpoints = appConfig?.endpoints?.[EModelEndpoint.custom] ?? [];
const cache = tokenConfigCache();
for (const endpointConfig of customEndpoints) {
/** Models config and the token-config cache key by the normalized name */
const name = normalizeEndpointName(endpointConfig?.name);
if (!name) {
continue;
}
if (endpointConfig.tokenConfig != null) {
endpointTokenConfigs[name] = endpointConfig.tokenConfig;
continue;
}
/** Model fetches and chat initialization both store under this key
* user-scoped whenever the fetched config can be user-specific, so a
* plain-name fallback would risk serving another user's entry */
const tokenKey = getTokenConfigKey(endpointConfig, name, req.user.id);
const cached = await cache.get(tokenKey);
if (cached) {
endpointTokenConfigs[name] = cached;
}
}
const tokenConfigMap = buildTokenConfigMap(
{ modelsConfig, endpointTokenConfigs, includePricing },
const tokenConfigMap = await resolveTokenConfigMap(
{ appConfig: req.config, modelsConfig, userId: req.user.id },
{ getValueKey, getMultiplier, getCacheMultiplier },
);
res.json(tokenConfigMap);

View file

@ -28,7 +28,7 @@ import {
const FLUSH_INTERVAL_MS = 250;
interface UsageSubmissionLike {
userMessage?: Pick<TMessage, 'messageId'> | null;
userMessage?: Pick<TMessage, 'messageId' | 'conversationId'> | null;
conversation?: Partial<Pick<TConversation, 'conversationId' | 'endpoint' | 'model'>> | null;
}
@ -54,6 +54,13 @@ export interface UsageHandlers {
}
function getConvoKey(submission: UsageSubmissionLike): string {
/** On a new chat's first turn the `created` event stamps the real id onto
* the user message while the submission's conversation is still `new`;
* prefer the real id so live writes land where TokenUsage is subscribed */
const fromUserMessage = submission.userMessage?.conversationId;
if (fromUserMessage != null && fromUserMessage !== Constants.NEW_CONVO) {
return fromUserMessage;
}
return submission.conversation?.conversationId ?? Constants.NEW_CONVO;
}

View file

@ -6,3 +6,4 @@ export * from './google';
export * from './models';
export * from './openai';
export * from './pricing';
export * from './tokenConfig';

View file

@ -0,0 +1,79 @@
import { EModelEndpoint } from 'librechat-data-provider';
import type { AppConfig } from '@librechat/data-schemas';
import type { TokenomicsDeps } from './pricing';
const mockCacheGet = jest.fn();
jest.mock('~/cache', () => ({
tokenConfigCache: jest.fn(() => ({ get: (key: string) => mockCacheGet(key) })),
}));
import { resolveTokenConfigMap } from './tokenConfig';
const deps: TokenomicsDeps = {
getValueKey: () => 'value-key',
getMultiplier: ({ tokenType }) => (tokenType === 'prompt' ? 5 : 15),
getCacheMultiplier: () => null,
};
function appConfigWith(custom: unknown[], contextCost = true): AppConfig {
return {
interfaceConfig: { contextCost },
endpoints: { [EModelEndpoint.custom]: custom },
} as unknown as AppConfig;
}
describe('resolveTokenConfigMap', () => {
beforeEach(() => mockCacheGet.mockReset().mockResolvedValue(null));
it('uses a static tokenConfig override without consulting the cache', async () => {
const appConfig = appConfigWith([
{
name: 'MyProxy',
tokenConfig: { 'custom-model': { prompt: 1.5, completion: 4.5, context: 32000 } },
},
]);
const map = await resolveTokenConfigMap(
{ appConfig, modelsConfig: { MyProxy: ['custom-model'] }, userId: 'user-1' },
deps,
);
expect(map.MyProxy['custom-model']).toEqual({ context: 32000, prompt: 1.5, completion: 4.5 });
expect(mockCacheGet).not.toHaveBeenCalled();
});
it('falls back to the cached fetched config when no static override exists', async () => {
mockCacheGet.mockResolvedValue({ 'custom-model': { prompt: 2, completion: 6, context: 8000 } });
const appConfig = appConfigWith([{ name: 'MyProxy', models: { fetch: true } }]);
const map = await resolveTokenConfigMap(
{ appConfig, modelsConfig: { MyProxy: ['custom-model'] }, userId: 'user-1' },
deps,
);
expect(mockCacheGet).toHaveBeenCalled();
expect(map.MyProxy['custom-model'].context).toBe(8000);
expect(map.MyProxy['custom-model'].prompt).toBe(2);
});
it('omits pricing when contextCost is disabled', async () => {
const appConfig = appConfigWith(
[
{
name: 'MyProxy',
tokenConfig: { 'custom-model': { prompt: 1.5, completion: 4.5, context: 32000 } },
},
],
false,
);
const map = await resolveTokenConfigMap(
{ appConfig, modelsConfig: { MyProxy: ['custom-model'] }, userId: 'user-1' },
deps,
);
expect(map.MyProxy['custom-model'].context).toBe(32000);
expect(map.MyProxy['custom-model'].prompt).toBeUndefined();
expect(map.MyProxy['custom-model'].completion).toBeUndefined();
});
});

View file

@ -0,0 +1,52 @@
import { EModelEndpoint, normalizeEndpointName } from 'librechat-data-provider';
import type { TModelsConfig, TTokenConfigMap, TEndpoint } from 'librechat-data-provider';
import type { AppConfig } from '@librechat/data-schemas';
import type { EndpointTokenConfig } from '~/types';
import { buildTokenConfigMap, type TokenomicsDeps } from '~/endpoints/pricing';
import { getTokenConfigKey } from '~/endpoints/custom/initialize';
import { tokenConfigCache } from '~/cache';
export interface ResolveTokenConfigParams {
appConfig?: AppConfig;
/** endpoint → model list, from the resolved models config */
modelsConfig: TModelsConfig;
userId: string;
}
/**
* Server-side token-config resolution for the `/endpoints/token-config`
* route. Gathers each custom endpoint's override a static yaml `tokenConfig`
* or the cached fetched config then resolves context windows (and pricing,
* when `interface.contextCost` is enabled) via {@link buildTokenConfigMap}.
* Lives in TypeScript so the `/api` controller stays a thin wrapper.
*/
export async function resolveTokenConfigMap(
{ appConfig, modelsConfig, userId }: ResolveTokenConfigParams,
deps: TokenomicsDeps,
): Promise<TTokenConfigMap> {
const includePricing = appConfig?.interfaceConfig?.contextCost === true;
const customEndpoints = (appConfig?.endpoints?.[EModelEndpoint.custom] ?? []) as TEndpoint[];
const cache = tokenConfigCache();
const endpointTokenConfigs: Record<string, EndpointTokenConfig | undefined> = {};
for (const endpointConfig of customEndpoints) {
/** Models config and the token-config cache key by the normalized name */
const name = normalizeEndpointName(endpointConfig?.name);
if (!name) {
continue;
}
if (endpointConfig.tokenConfig != null) {
endpointTokenConfigs[name] = endpointConfig.tokenConfig as EndpointTokenConfig;
continue;
}
/** Model fetches and chat initialization both store under this key
* user-scoped whenever the fetched config can be user-specific */
const tokenKey = getTokenConfigKey(endpointConfig, name, userId);
const cached = (await cache.get(tokenKey)) as EndpointTokenConfig | undefined;
if (cached) {
endpointTokenConfigs[name] = cached;
}
}
return buildTokenConfigMap({ modelsConfig, endpointTokenConfigs, includePricing }, deps);
}