| import { |
| Verbosity, |
| EModelEndpoint, |
| ReasoningEffort, |
| ReasoningSummary, |
| } from 'librechat-data-provider'; |
| import { getOpenAILLMConfig, extractDefaultParams, applyDefaultParams } from './llm'; |
| import type * as t from '~/types'; |
|
|
| describe('getOpenAILLMConfig', () => { |
| describe('Basic Configuration', () => { |
| it('should create a basic configuration with required fields', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4', |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('apiKey', 'test-api-key'); |
| expect(result.llmConfig).toHaveProperty('model', 'gpt-4'); |
| expect(result.llmConfig).toHaveProperty('streaming', true); |
| expect(result.tools).toEqual([]); |
| }); |
|
|
| it('should handle model options including temperature and penalties', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4', |
| temperature: 0.7, |
| frequency_penalty: 0.5, |
| presence_penalty: 0.3, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('temperature', 0.7); |
| expect(result.llmConfig).toHaveProperty('frequencyPenalty', 0.5); |
| expect(result.llmConfig).toHaveProperty('presencePenalty', 0.3); |
| }); |
|
|
| it('should handle max_tokens conversion to maxTokens', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4', |
| max_tokens: 4096, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('maxTokens', 4096); |
| expect(result.llmConfig).not.toHaveProperty('max_tokens'); |
| }); |
| }); |
|
|
| describe('OpenAI Reasoning Models (o1/o3/gpt-5)', () => { |
| const reasoningModels = [ |
| 'o1', |
| 'o1-mini', |
| 'o1-preview', |
| 'o1-pro', |
| 'o3', |
| 'o3-mini', |
| 'gpt-5', |
| 'gpt-5-pro', |
| 'gpt-5-turbo', |
| ]; |
|
|
| const excludedParams = [ |
| 'frequencyPenalty', |
| 'presencePenalty', |
| 'temperature', |
| 'topP', |
| 'logitBias', |
| 'n', |
| 'logprobs', |
| ]; |
|
|
| it.each(reasoningModels)( |
| 'should exclude unsupported parameters for reasoning model: %s', |
| (model) => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model, |
| temperature: 0.7, |
| frequency_penalty: 0.5, |
| presence_penalty: 0.3, |
| topP: 0.9, |
| logitBias: { '50256': -100 }, |
| n: 2, |
| logprobs: true, |
| } as Partial<t.OpenAIParameters>, |
| }); |
|
|
| excludedParams.forEach((param) => { |
| expect(result.llmConfig).not.toHaveProperty(param); |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('model', model); |
| expect(result.llmConfig).toHaveProperty('streaming', true); |
| }, |
| ); |
|
|
| it('should preserve maxTokens for reasoning models', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'o1', |
| max_tokens: 4096, |
| temperature: 0.7, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('maxTokens', 4096); |
| expect(result.llmConfig).not.toHaveProperty('temperature'); |
| }); |
|
|
| it('should preserve other valid parameters for reasoning models', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'o1', |
| max_tokens: 8192, |
| stop: ['END'], |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('maxTokens', 8192); |
| expect(result.llmConfig).toHaveProperty('stop', ['END']); |
| }); |
|
|
| it('should handle GPT-5 max_tokens conversion to max_completion_tokens', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-5', |
| max_tokens: 8192, |
| stop: ['END'], |
| }, |
| }); |
|
|
| expect(result.llmConfig.modelKwargs).toHaveProperty('max_completion_tokens', 8192); |
| expect(result.llmConfig).not.toHaveProperty('maxTokens'); |
| expect(result.llmConfig).toHaveProperty('stop', ['END']); |
| }); |
|
|
| it('should combine user dropParams with reasoning exclusion params', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'o3-mini', |
| temperature: 0.7, |
| stop: ['END'], |
| }, |
| dropParams: ['stop'], |
| }); |
|
|
| expect(result.llmConfig).not.toHaveProperty('temperature'); |
| expect(result.llmConfig).not.toHaveProperty('stop'); |
| }); |
|
|
| it('should NOT exclude parameters for non-reasoning models', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4-turbo', |
| temperature: 0.7, |
| frequency_penalty: 0.5, |
| presence_penalty: 0.3, |
| topP: 0.9, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('temperature', 0.7); |
| expect(result.llmConfig).toHaveProperty('frequencyPenalty', 0.5); |
| expect(result.llmConfig).toHaveProperty('presencePenalty', 0.3); |
| expect(result.llmConfig).toHaveProperty('topP', 0.9); |
| }); |
|
|
| it('should NOT exclude parameters for gpt-5.x versioned models (they support sampling params)', () => { |
| const versionedModels = ['gpt-5.1', 'gpt-5.1-turbo', 'gpt-5.2', 'gpt-5.5-preview']; |
|
|
| versionedModels.forEach((model) => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model, |
| temperature: 0.7, |
| frequency_penalty: 0.5, |
| presence_penalty: 0.3, |
| topP: 0.9, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('temperature', 0.7); |
| expect(result.llmConfig).toHaveProperty('frequencyPenalty', 0.5); |
| expect(result.llmConfig).toHaveProperty('presencePenalty', 0.3); |
| expect(result.llmConfig).toHaveProperty('topP', 0.9); |
| }); |
| }); |
|
|
| it('should NOT exclude parameters for gpt-5-chat (it supports sampling params)', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-5-chat', |
| temperature: 0.7, |
| frequency_penalty: 0.5, |
| presence_penalty: 0.3, |
| topP: 0.9, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('temperature', 0.7); |
| expect(result.llmConfig).toHaveProperty('frequencyPenalty', 0.5); |
| expect(result.llmConfig).toHaveProperty('presencePenalty', 0.3); |
| expect(result.llmConfig).toHaveProperty('topP', 0.9); |
| }); |
|
|
| it('should handle reasoning models with reasoning_effort parameter', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| endpoint: EModelEndpoint.openAI, |
| modelOptions: { |
| model: 'o1', |
| reasoning_effort: ReasoningEffort.high, |
| temperature: 0.7, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('reasoning_effort', ReasoningEffort.high); |
| expect(result.llmConfig).not.toHaveProperty('temperature'); |
| }); |
| }); |
|
|
| describe('OpenAI Web Search Models', () => { |
| it('should exclude parameters for gpt-4o search models', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4o-search-preview', |
| temperature: 0.7, |
| top_p: 0.9, |
| seed: 42, |
| } as Partial<t.OpenAIParameters>, |
| }); |
|
|
| expect(result.llmConfig).not.toHaveProperty('temperature'); |
| expect(result.llmConfig).not.toHaveProperty('top_p'); |
| expect(result.llmConfig).not.toHaveProperty('seed'); |
| }); |
|
|
| it('should preserve max_tokens for search models', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4o-search', |
| max_tokens: 4096, |
| temperature: 0.7, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('maxTokens', 4096); |
| expect(result.llmConfig).not.toHaveProperty('temperature'); |
| }); |
| }); |
|
|
| describe('Web Search Functionality', () => { |
| it('should enable web search with Responses API', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4', |
| web_search: true, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('useResponsesApi', true); |
| expect(result.tools).toContainEqual({ type: 'web_search' }); |
| }); |
|
|
| it('should handle web search with OpenRouter', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| useOpenRouter: true, |
| modelOptions: { |
| model: 'gpt-4', |
| web_search: true, |
| }, |
| }); |
|
|
| expect(result.llmConfig.modelKwargs).toHaveProperty('plugins', [{ id: 'web' }]); |
| expect(result.llmConfig).toHaveProperty('include_reasoning', true); |
| }); |
|
|
| it('should disable web search via dropParams', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4', |
| web_search: true, |
| }, |
| dropParams: ['web_search'], |
| }); |
|
|
| expect(result.tools).not.toContainEqual({ type: 'web_search' }); |
| }); |
| }); |
|
|
| describe('GPT-5 max_tokens Handling', () => { |
| it('should convert maxTokens to max_completion_tokens for GPT-5 models', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-5', |
| max_tokens: 8192, |
| }, |
| }); |
|
|
| expect(result.llmConfig.modelKwargs).toHaveProperty('max_completion_tokens', 8192); |
| expect(result.llmConfig).not.toHaveProperty('maxTokens'); |
| }); |
|
|
| it('should convert maxTokens to max_output_tokens for GPT-5 with Responses API', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-5', |
| max_tokens: 8192, |
| }, |
| addParams: { |
| useResponsesApi: true, |
| }, |
| }); |
|
|
| expect(result.llmConfig.modelKwargs).toHaveProperty('max_output_tokens', 8192); |
| expect(result.llmConfig).not.toHaveProperty('maxTokens'); |
| }); |
| }); |
|
|
| describe('Reasoning Parameters', () => { |
| it('should handle reasoning_effort for OpenAI endpoint', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| endpoint: EModelEndpoint.openAI, |
| modelOptions: { |
| model: 'o1', |
| reasoning_effort: ReasoningEffort.high, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('reasoning_effort', ReasoningEffort.high); |
| }); |
|
|
| it('should use reasoning object for non-OpenAI endpoints', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| endpoint: 'custom', |
| modelOptions: { |
| model: 'o1', |
| reasoning_effort: ReasoningEffort.high, |
| reasoning_summary: ReasoningSummary.concise, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('reasoning'); |
| expect(result.llmConfig.reasoning).toEqual({ |
| effort: ReasoningEffort.high, |
| summary: ReasoningSummary.concise, |
| }); |
| }); |
|
|
| it('should use reasoning object when useResponsesApi is true', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| endpoint: EModelEndpoint.openAI, |
| modelOptions: { |
| model: 'o1', |
| reasoning_effort: ReasoningEffort.medium, |
| reasoning_summary: ReasoningSummary.detailed, |
| }, |
| addParams: { |
| useResponsesApi: true, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('reasoning'); |
| expect(result.llmConfig.reasoning).toEqual({ |
| effort: ReasoningEffort.medium, |
| summary: ReasoningSummary.detailed, |
| }); |
| }); |
| }); |
|
|
| describe('Default and Add Parameters', () => { |
| it('should apply default parameters when fields are undefined', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4', |
| }, |
| defaultParams: { |
| temperature: 0.5, |
| topP: 0.9, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('temperature', 0.5); |
| expect(result.llmConfig).toHaveProperty('topP', 0.9); |
| }); |
|
|
| it('should NOT override existing values with default parameters', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4', |
| temperature: 0.8, |
| }, |
| defaultParams: { |
| temperature: 0.5, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('temperature', 0.8); |
| }); |
|
|
| it('should apply addParams and override defaults', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4', |
| }, |
| defaultParams: { |
| temperature: 0.5, |
| }, |
| addParams: { |
| temperature: 0.9, |
| seed: 42, |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('temperature', 0.9); |
| expect(result.llmConfig).toHaveProperty('seed', 42); |
| }); |
|
|
| it('should handle unknown params via modelKwargs', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4', |
| }, |
| addParams: { |
| custom_param: 'custom_value', |
| }, |
| }); |
|
|
| expect(result.llmConfig.modelKwargs).toHaveProperty('custom_param', 'custom_value'); |
| }); |
| }); |
|
|
| describe('Drop Parameters', () => { |
| it('should drop specified parameters', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4', |
| temperature: 0.7, |
| topP: 0.9, |
| }, |
| dropParams: ['temperature'], |
| }); |
|
|
| expect(result.llmConfig).not.toHaveProperty('temperature'); |
| expect(result.llmConfig).toHaveProperty('topP', 0.9); |
| }); |
| }); |
|
|
| describe('OpenRouter Configuration', () => { |
| it('should include include_reasoning for OpenRouter', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| useOpenRouter: true, |
| modelOptions: { |
| model: 'gpt-4', |
| }, |
| }); |
|
|
| expect(result.llmConfig).toHaveProperty('include_reasoning', true); |
| }); |
| }); |
|
|
| describe('Verbosity Handling', () => { |
| it('should add verbosity to modelKwargs', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4', |
| verbosity: Verbosity.high, |
| }, |
| }); |
|
|
| expect(result.llmConfig.modelKwargs).toHaveProperty('verbosity', Verbosity.high); |
| }); |
|
|
| it('should convert verbosity to text object with Responses API', () => { |
| const result = getOpenAILLMConfig({ |
| apiKey: 'test-api-key', |
| streaming: true, |
| modelOptions: { |
| model: 'gpt-4', |
| verbosity: Verbosity.low, |
| }, |
| addParams: { |
| useResponsesApi: true, |
| }, |
| }); |
|
|
| expect(result.llmConfig.modelKwargs).toHaveProperty('text', { verbosity: Verbosity.low }); |
| expect(result.llmConfig.modelKwargs).not.toHaveProperty('verbosity'); |
| }); |
| }); |
| }); |
|
|
| describe('extractDefaultParams', () => { |
| it('should extract default values from param definitions', () => { |
| const paramDefinitions = [ |
| { key: 'temperature', default: 0.7 }, |
| { key: 'maxTokens', default: 4096 }, |
| { key: 'noDefault' }, |
| ]; |
|
|
| const result = extractDefaultParams(paramDefinitions); |
|
|
| expect(result).toEqual({ |
| temperature: 0.7, |
| maxTokens: 4096, |
| }); |
| }); |
|
|
| it('should return undefined for undefined or non-array input', () => { |
| expect(extractDefaultParams(undefined)).toBeUndefined(); |
| expect(extractDefaultParams(null as unknown as undefined)).toBeUndefined(); |
| }); |
|
|
| it('should handle empty array', () => { |
| const result = extractDefaultParams([]); |
| expect(result).toEqual({}); |
| }); |
| }); |
|
|
| describe('applyDefaultParams', () => { |
| it('should apply defaults only when field is undefined', () => { |
| const target: Record<string, unknown> = { |
| temperature: 0.8, |
| maxTokens: undefined, |
| }; |
|
|
| const defaults = { |
| temperature: 0.5, |
| maxTokens: 4096, |
| topP: 0.9, |
| }; |
|
|
| applyDefaultParams(target, defaults); |
|
|
| expect(target).toEqual({ |
| temperature: 0.8, |
| maxTokens: 4096, |
| topP: 0.9, |
| }); |
| }); |
| }); |
|
|