mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
Merge remote-tracking branch 'origin/dev-staging' into feat/context-window-ui
# Conflicts: # api/server/controllers/assistants/chatV1.js # api/server/controllers/assistants/chatV2.js # api/utils/tokens.spec.js # packages/api/src/agents/usage.ts # packages/api/src/types/tokens.ts # packages/api/src/utils/tokens.ts # packages/data-schemas/src/methods/tx.spec.ts # packages/data-schemas/src/methods/tx.ts
This commit is contained in:
commit
dca258f36a
825 changed files with 73717 additions and 24974 deletions
|
|
@ -1,13 +1,13 @@
|
|||
/** Note: No hard-coded values should be used in this file. */
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const {
|
||||
EModelEndpoint,
|
||||
maxTokensMap,
|
||||
matchModelName,
|
||||
getModelMaxTokens,
|
||||
maxOutputTokensMap,
|
||||
findMatchingPattern,
|
||||
} = require('librechat-data-provider');
|
||||
const { processModelData } = require('@librechat/api');
|
||||
processModelData,
|
||||
} = require('@librechat/api');
|
||||
|
||||
describe('getModelMaxTokens', () => {
|
||||
test('should return correct tokens for exact match', () => {
|
||||
|
|
@ -200,6 +200,39 @@ describe('getModelMaxTokens', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('should return correct tokens for gpt-5.3 matches', () => {
|
||||
expect(getModelMaxTokens('gpt-5.3')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5.3']);
|
||||
expect(getModelMaxTokens('gpt-5.3-codex')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5.3']);
|
||||
expect(getModelMaxTokens('openai/gpt-5.3')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5.3'],
|
||||
);
|
||||
expect(getModelMaxTokens('gpt-5.3-2025-03-01')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5.3'],
|
||||
);
|
||||
expect(getModelMaxTokens('gpt-5.3-preview')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5.3'],
|
||||
);
|
||||
});
|
||||
|
||||
test('should return correct tokens for gpt-5.4 matches', () => {
|
||||
expect(getModelMaxTokens('gpt-5.4')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5.4']);
|
||||
expect(getModelMaxTokens('gpt-5.4-thinking')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5.4'],
|
||||
);
|
||||
expect(getModelMaxTokens('openai/gpt-5.4')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5.4'],
|
||||
);
|
||||
});
|
||||
|
||||
test('should return correct tokens for gpt-5.4-pro matches', () => {
|
||||
expect(getModelMaxTokens('gpt-5.4-pro')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5.4-pro'],
|
||||
);
|
||||
expect(getModelMaxTokens('openai/gpt-5.4-pro')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5.4-pro'],
|
||||
);
|
||||
});
|
||||
|
||||
test('should return correct tokens for Anthropic models', () => {
|
||||
const models = [
|
||||
'claude-2.1',
|
||||
|
|
@ -237,16 +270,6 @@ describe('getModelMaxTokens', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Tests for Google models
|
||||
test('should return correct tokens for exact match - Google models', () => {
|
||||
expect(getModelMaxTokens('text-bison-32k', EModelEndpoint.google)).toBe(
|
||||
maxTokensMap[EModelEndpoint.google]['text-bison-32k'],
|
||||
);
|
||||
expect(getModelMaxTokens('codechat-bison-32k', EModelEndpoint.google)).toBe(
|
||||
maxTokensMap[EModelEndpoint.google]['codechat-bison-32k'],
|
||||
);
|
||||
});
|
||||
|
||||
test('should return undefined for no match - Google models', () => {
|
||||
expect(getModelMaxTokens('unknown-google-model', EModelEndpoint.google)).toBeUndefined();
|
||||
});
|
||||
|
|
@ -279,6 +302,12 @@ describe('getModelMaxTokens', () => {
|
|||
expect(getModelMaxTokens('gemini-3', EModelEndpoint.google)).toBe(
|
||||
maxTokensMap[EModelEndpoint.google]['gemini-3'],
|
||||
);
|
||||
expect(getModelMaxTokens('gemini-3.1-pro-preview', EModelEndpoint.google)).toBe(
|
||||
maxTokensMap[EModelEndpoint.google]['gemini-3.1'],
|
||||
);
|
||||
expect(getModelMaxTokens('gemini-3.1-pro-preview-customtools', EModelEndpoint.google)).toBe(
|
||||
maxTokensMap[EModelEndpoint.google]['gemini-3.1'],
|
||||
);
|
||||
expect(getModelMaxTokens('gemini-2.5-pro', EModelEndpoint.google)).toBe(
|
||||
maxTokensMap[EModelEndpoint.google]['gemini-2.5-pro'],
|
||||
);
|
||||
|
|
@ -297,12 +326,6 @@ describe('getModelMaxTokens', () => {
|
|||
expect(getModelMaxTokens('gemini-pro', EModelEndpoint.google)).toBe(
|
||||
maxTokensMap[EModelEndpoint.google]['gemini'],
|
||||
);
|
||||
expect(getModelMaxTokens('code-', EModelEndpoint.google)).toBe(
|
||||
maxTokensMap[EModelEndpoint.google]['code-'],
|
||||
);
|
||||
expect(getModelMaxTokens('chat-', EModelEndpoint.google)).toBe(
|
||||
maxTokensMap[EModelEndpoint.google]['chat-'],
|
||||
);
|
||||
});
|
||||
|
||||
test('should return correct tokens for partial match - Cohere models', () => {
|
||||
|
|
@ -485,8 +508,20 @@ describe('getModelMaxTokens', () => {
|
|||
});
|
||||
|
||||
test('should return correct max output tokens for GPT-5 models', () => {
|
||||
const { getModelMaxOutputTokens } = require('librechat-data-provider');
|
||||
['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro'].forEach((model) => {
|
||||
const { getModelMaxOutputTokens } = require('@librechat/api');
|
||||
const gpt5Models = [
|
||||
'gpt-5',
|
||||
'gpt-5.1',
|
||||
'gpt-5.2',
|
||||
'gpt-5.3',
|
||||
'gpt-5.4',
|
||||
'gpt-5.4-pro',
|
||||
'gpt-5-mini',
|
||||
'gpt-5-nano',
|
||||
'gpt-5-pro',
|
||||
'gpt-5.2-pro',
|
||||
];
|
||||
for (const model of gpt5Models) {
|
||||
expect(getModelMaxOutputTokens(model)).toBe(maxOutputTokensMap[EModelEndpoint.openAI][model]);
|
||||
expect(getModelMaxOutputTokens(model, EModelEndpoint.openAI)).toBe(
|
||||
maxOutputTokensMap[EModelEndpoint.openAI][model],
|
||||
|
|
@ -494,7 +529,7 @@ describe('getModelMaxTokens', () => {
|
|||
expect(getModelMaxOutputTokens(model, EModelEndpoint.azureOpenAI)).toBe(
|
||||
maxOutputTokensMap[EModelEndpoint.azureOpenAI][model],
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should return correct max output tokens for GPT-OSS models', () => {
|
||||
|
|
@ -511,6 +546,184 @@ describe('getModelMaxTokens', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('findMatchingPattern - longest match wins', () => {
|
||||
test('should prefer longer matching key over shorter cross-provider pattern', () => {
|
||||
const result = findMatchingPattern(
|
||||
'gpt-5.2-chat-2025-12-11',
|
||||
maxTokensMap[EModelEndpoint.openAI],
|
||||
);
|
||||
expect(result).toBe('gpt-5.2');
|
||||
});
|
||||
|
||||
test('should match gpt-5.2 tokens for date-suffixed chat variant', () => {
|
||||
expect(getModelMaxTokens('gpt-5.2-chat-2025-12-11')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5.2'],
|
||||
);
|
||||
});
|
||||
|
||||
test('should match gpt-5.2-pro over shorter patterns', () => {
|
||||
expect(getModelMaxTokens('gpt-5.2-pro-chat-2025-12-11')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5.2-pro'],
|
||||
);
|
||||
});
|
||||
|
||||
test('should match gpt-5-mini over gpt-5 for mini variants', () => {
|
||||
expect(getModelMaxTokens('gpt-5-mini-chat-2025-01-01')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5-mini'],
|
||||
);
|
||||
});
|
||||
|
||||
test('should prefer gpt-4-1106 over gpt-4 for versioned model names', () => {
|
||||
const result = findMatchingPattern('gpt-4-1106-preview', maxTokensMap[EModelEndpoint.openAI]);
|
||||
expect(result).toBe('gpt-4-1106');
|
||||
});
|
||||
|
||||
test('should prefer gpt-4-32k-0613 over gpt-4-32k for exact versioned names', () => {
|
||||
const result = findMatchingPattern('gpt-4-32k-0613', maxTokensMap[EModelEndpoint.openAI]);
|
||||
expect(result).toBe('gpt-4-32k-0613');
|
||||
});
|
||||
|
||||
test('should prefer claude-3-5-sonnet over claude-3', () => {
|
||||
const result = findMatchingPattern(
|
||||
'claude-3-5-sonnet-20241022',
|
||||
maxTokensMap[EModelEndpoint.anthropic],
|
||||
);
|
||||
expect(result).toBe('claude-3-5-sonnet');
|
||||
});
|
||||
|
||||
test('should prefer gemini-2.0-flash-lite over gemini-2.0-flash', () => {
|
||||
const result = findMatchingPattern(
|
||||
'gemini-2.0-flash-lite-preview',
|
||||
maxTokensMap[EModelEndpoint.google],
|
||||
);
|
||||
expect(result).toBe('gemini-2.0-flash-lite');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findMatchingPattern - bestLength selection', () => {
|
||||
test('should return the longest matching key when multiple keys match', () => {
|
||||
const tokensMap = { short: 100, 'short-med': 200, 'short-med-long': 300 };
|
||||
expect(findMatchingPattern('short-med-long-extra', tokensMap)).toBe('short-med-long');
|
||||
});
|
||||
|
||||
test('should return the longest match regardless of key insertion order', () => {
|
||||
const tokensMap = { 'a-b-c': 300, a: 100, 'a-b': 200 };
|
||||
expect(findMatchingPattern('a-b-c-d', tokensMap)).toBe('a-b-c');
|
||||
});
|
||||
|
||||
test('should return null when no key matches', () => {
|
||||
const tokensMap = { alpha: 100, beta: 200 };
|
||||
expect(findMatchingPattern('gamma-delta', tokensMap)).toBeNull();
|
||||
});
|
||||
|
||||
test('should return the single matching key when only one matches', () => {
|
||||
const tokensMap = { alpha: 100, beta: 200, gamma: 300 };
|
||||
expect(findMatchingPattern('beta-extended', tokensMap)).toBe('beta');
|
||||
});
|
||||
|
||||
test('should match case-insensitively against model name', () => {
|
||||
const tokensMap = { 'gpt-5': 400000 };
|
||||
expect(findMatchingPattern('GPT-5-turbo', tokensMap)).toBe('gpt-5');
|
||||
});
|
||||
|
||||
test('should select the longest key among overlapping substring matches', () => {
|
||||
const tokensMap = { 'gpt-': 100, 'gpt-5': 200, 'gpt-5.2': 300, 'gpt-5.2-pro': 400 };
|
||||
expect(findMatchingPattern('gpt-5.2-pro-2025-01-01', tokensMap)).toBe('gpt-5.2-pro');
|
||||
expect(findMatchingPattern('gpt-5.2-chat-2025-01-01', tokensMap)).toBe('gpt-5.2');
|
||||
expect(findMatchingPattern('gpt-5.1-preview', tokensMap)).toBe('gpt-5');
|
||||
expect(findMatchingPattern('gpt-unknown', tokensMap)).toBe('gpt-');
|
||||
});
|
||||
|
||||
test('should not be confused by a short key that appears later in the model name', () => {
|
||||
const tokensMap = { 'model-v2': 200, v2: 100 };
|
||||
expect(findMatchingPattern('model-v2-extended', tokensMap)).toBe('model-v2');
|
||||
});
|
||||
|
||||
test('should handle exact-length match as the best match', () => {
|
||||
const tokensMap = { 'exact-model': 500, exact: 100 };
|
||||
expect(findMatchingPattern('exact-model', tokensMap)).toBe('exact-model');
|
||||
});
|
||||
|
||||
test('should return null for empty model name', () => {
|
||||
expect(findMatchingPattern('', { 'gpt-5': 400000 })).toBeNull();
|
||||
});
|
||||
|
||||
test('should prefer last-defined key on same-length ties', () => {
|
||||
const tokensMap = { 'aa-bb': 100, 'cc-dd': 200 };
|
||||
// model name contains both 5-char keys; last-defined wins in reverse iteration
|
||||
expect(findMatchingPattern('aa-bb-cc-dd', tokensMap)).toBe('cc-dd');
|
||||
});
|
||||
|
||||
test('longest match beats short cross-provider pattern even when both present', () => {
|
||||
const tokensMap = { 'gpt-5.2': 400000, 'chat-': 8187 };
|
||||
expect(findMatchingPattern('gpt-5.2-chat-2025-12-11', tokensMap)).toBe('gpt-5.2');
|
||||
});
|
||||
|
||||
test('should match case-insensitively against keys', () => {
|
||||
const tokensMap = { 'GPT-5': 400000 };
|
||||
expect(findMatchingPattern('gpt-5-turbo', tokensMap)).toBe('GPT-5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findMatchingPattern - iteration performance', () => {
|
||||
let includesSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
includesSpy = jest.spyOn(String.prototype, 'includes');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
includesSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('exact match early-exits with minimal includes() checks', () => {
|
||||
const openAIMap = maxTokensMap[EModelEndpoint.openAI];
|
||||
const keys = Object.keys(openAIMap);
|
||||
const lastKey = keys[keys.length - 1];
|
||||
includesSpy.mockClear();
|
||||
const result = findMatchingPattern(lastKey, openAIMap);
|
||||
const exactCalls = includesSpy.mock.calls.length;
|
||||
|
||||
expect(result).toBe(lastKey);
|
||||
expect(exactCalls).toBe(1);
|
||||
});
|
||||
|
||||
test('bestLength check skips includes() for shorter keys after a long match', () => {
|
||||
const openAIMap = maxTokensMap[EModelEndpoint.openAI];
|
||||
includesSpy.mockClear();
|
||||
findMatchingPattern('gpt-3.5-turbo-0301-test', openAIMap);
|
||||
const longKeyCalls = includesSpy.mock.calls.length;
|
||||
|
||||
includesSpy.mockClear();
|
||||
findMatchingPattern('gpt-5.3-chat-latest', openAIMap);
|
||||
const shortKeyCalls = includesSpy.mock.calls.length;
|
||||
|
||||
// gpt-3.5-turbo-0301 (20 chars) matches early, then bestLength prunes most keys
|
||||
// gpt-5.3 (7 chars) is short, so fewer keys are pruned by the length check
|
||||
expect(longKeyCalls).toBeLessThan(shortKeyCalls);
|
||||
});
|
||||
|
||||
test('last-defined keys are checked first in reverse iteration', () => {
|
||||
const tokensMap = { first: 100, second: 200, third: 300 };
|
||||
includesSpy.mockClear();
|
||||
const result = findMatchingPattern('third', tokensMap);
|
||||
const calls = includesSpy.mock.calls.length;
|
||||
|
||||
// 'third' is last key, found on first reverse check, exact match exits immediately
|
||||
expect(result).toBe('third');
|
||||
expect(calls).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deprecated PaLM2/Codey model removal', () => {
|
||||
test('deprecated PaLM2/Codey models no longer have token entries', () => {
|
||||
expect(getModelMaxTokens('text-bison-32k', EModelEndpoint.google)).toBeUndefined();
|
||||
expect(getModelMaxTokens('codechat-bison-32k', EModelEndpoint.google)).toBeUndefined();
|
||||
expect(getModelMaxTokens('code-bison', EModelEndpoint.google)).toBeUndefined();
|
||||
expect(getModelMaxTokens('chat-bison', EModelEndpoint.google)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchModelName', () => {
|
||||
it('should return the exact model name if it exists in maxTokensMap', () => {
|
||||
expect(matchModelName('gpt-4-32k-0613')).toBe('gpt-4-32k-0613');
|
||||
|
|
@ -606,10 +819,16 @@ describe('matchModelName', () => {
|
|||
expect(matchModelName('gpt-5-pro-2025-01-30-0130')).toBe('gpt-5-pro');
|
||||
});
|
||||
|
||||
// Tests for Google models
|
||||
it('should return the exact model name if it exists in maxTokensMap - Google models', () => {
|
||||
expect(matchModelName('text-bison-32k', EModelEndpoint.google)).toBe('text-bison-32k');
|
||||
expect(matchModelName('codechat-bison-32k', EModelEndpoint.google)).toBe('codechat-bison-32k');
|
||||
it('should return the closest matching key for gpt-5.3 matches', () => {
|
||||
expect(matchModelName('openai/gpt-5.3')).toBe('gpt-5.3');
|
||||
expect(matchModelName('gpt-5.3-codex')).toBe('gpt-5.3');
|
||||
expect(matchModelName('gpt-5.3-2025-03-01')).toBe('gpt-5.3');
|
||||
});
|
||||
|
||||
it('should return the closest matching key for gpt-5.4 matches', () => {
|
||||
expect(matchModelName('openai/gpt-5.4')).toBe('gpt-5.4');
|
||||
expect(matchModelName('gpt-5.4-thinking')).toBe('gpt-5.4');
|
||||
expect(matchModelName('gpt-5.4-pro')).toBe('gpt-5.4-pro');
|
||||
});
|
||||
|
||||
it('should return the input model name if no match is found - Google models', () => {
|
||||
|
|
@ -617,11 +836,6 @@ describe('matchModelName', () => {
|
|||
'unknown-google-model',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the closest matching key for partial matches - Google models', () => {
|
||||
expect(matchModelName('code-', EModelEndpoint.google)).toBe('code-');
|
||||
expect(matchModelName('chat-', EModelEndpoint.google)).toBe('chat-');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Meta Models Tests', () => {
|
||||
|
|
@ -1162,6 +1376,56 @@ describe('Claude Model Tests', () => {
|
|||
expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-opus-4-6');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return correct context length for Claude Sonnet 4.6 (1M)', () => {
|
||||
expect(getModelMaxTokens('claude-sonnet-4-6', EModelEndpoint.anthropic)).toBe(
|
||||
maxTokensMap[EModelEndpoint.anthropic]['claude-sonnet-4-6'],
|
||||
);
|
||||
expect(getModelMaxTokens('claude-sonnet-4-6')).toBe(
|
||||
maxTokensMap[EModelEndpoint.anthropic]['claude-sonnet-4-6'],
|
||||
);
|
||||
});
|
||||
|
||||
it('should return correct max output tokens for Claude Sonnet 4.6 (64K)', () => {
|
||||
const { getModelMaxOutputTokens } = require('@librechat/api');
|
||||
expect(getModelMaxOutputTokens('claude-sonnet-4-6', EModelEndpoint.anthropic)).toBe(
|
||||
maxOutputTokensMap[EModelEndpoint.anthropic]['claude-sonnet-4-6'],
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Claude Sonnet 4.6 model name variations', () => {
|
||||
const modelVariations = [
|
||||
'claude-sonnet-4-6',
|
||||
'claude-sonnet-4-6-20260101',
|
||||
'claude-sonnet-4-6-latest',
|
||||
'anthropic/claude-sonnet-4-6',
|
||||
'claude-sonnet-4-6/anthropic',
|
||||
'claude-sonnet-4-6-preview',
|
||||
];
|
||||
|
||||
modelVariations.forEach((model) => {
|
||||
const modelKey = findMatchingPattern(model, maxTokensMap[EModelEndpoint.anthropic]);
|
||||
expect(modelKey).toBe('claude-sonnet-4-6');
|
||||
expect(getModelMaxTokens(model, EModelEndpoint.anthropic)).toBe(
|
||||
maxTokensMap[EModelEndpoint.anthropic]['claude-sonnet-4-6'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should match model names correctly for Claude Sonnet 4.6', () => {
|
||||
const modelVariations = [
|
||||
'claude-sonnet-4-6',
|
||||
'claude-sonnet-4-6-20260101',
|
||||
'claude-sonnet-4-6-latest',
|
||||
'anthropic/claude-sonnet-4-6',
|
||||
'claude-sonnet-4-6/anthropic',
|
||||
'claude-sonnet-4-6-preview',
|
||||
];
|
||||
|
||||
modelVariations.forEach((model) => {
|
||||
expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-sonnet-4-6');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Moonshot/Kimi Model Tests', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue