mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-30 14:25:19 +01:00
Merge branch 'main' into feature/entra-id-azure-integration
This commit is contained in:
commit
af661b1df2
293 changed files with 20207 additions and 13884 deletions
|
|
@ -4,6 +4,38 @@ import { anthropicSettings, removeNullishValues } from 'librechat-data-provider'
|
|||
import type { AnthropicLLMConfigResult, AnthropicConfigOptions } from '~/types/anthropic';
|
||||
import { checkPromptCacheSupport, getClaudeHeaders, configureReasoning } from './helpers';
|
||||
|
||||
/** Known Anthropic parameters that map directly to the client config */
|
||||
export const knownAnthropicParams = new Set([
|
||||
'model',
|
||||
'temperature',
|
||||
'topP',
|
||||
'topK',
|
||||
'maxTokens',
|
||||
'maxOutputTokens',
|
||||
'stopSequences',
|
||||
'stop',
|
||||
'stream',
|
||||
'apiKey',
|
||||
'maxRetries',
|
||||
'timeout',
|
||||
'anthropicVersion',
|
||||
'anthropicApiUrl',
|
||||
'defaultHeaders',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Applies default parameters to the target object only if the field is undefined
|
||||
* @param target - The target object to apply defaults to
|
||||
* @param defaults - Record of default parameter values
|
||||
*/
|
||||
function applyDefaultParams(target: Record<string, unknown>, defaults: Record<string, unknown>) {
|
||||
for (const [key, value] of Object.entries(defaults)) {
|
||||
if (target[key] === undefined) {
|
||||
target[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates configuration options for creating an Anthropic language model (LLM) instance.
|
||||
* @param apiKey - The API key for authentication with Anthropic.
|
||||
|
|
@ -39,6 +71,8 @@ function getLLMConfig(
|
|||
|
||||
const mergedOptions = Object.assign(defaultOptions, options.modelOptions);
|
||||
|
||||
let enableWebSearch = mergedOptions.web_search;
|
||||
|
||||
let requestOptions: AnthropicClientOptions & { stream?: boolean } = {
|
||||
apiKey,
|
||||
model: mergedOptions.model,
|
||||
|
|
@ -84,9 +118,64 @@ function getLLMConfig(
|
|||
requestOptions.anthropicApiUrl = options.reverseProxyUrl;
|
||||
}
|
||||
|
||||
/** Handle defaultParams first - only process Anthropic-native params if undefined */
|
||||
if (options.defaultParams && typeof options.defaultParams === 'object') {
|
||||
for (const [key, value] of Object.entries(options.defaultParams)) {
|
||||
/** Handle web_search separately - don't add to config */
|
||||
if (key === 'web_search') {
|
||||
if (enableWebSearch === undefined && typeof value === 'boolean') {
|
||||
enableWebSearch = value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (knownAnthropicParams.has(key)) {
|
||||
/** Route known Anthropic params to requestOptions only if undefined */
|
||||
applyDefaultParams(requestOptions as Record<string, unknown>, { [key]: value });
|
||||
}
|
||||
/** Leave other params for transform to handle - they might be OpenAI params */
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle addParams - can override defaultParams */
|
||||
if (options.addParams && typeof options.addParams === 'object') {
|
||||
for (const [key, value] of Object.entries(options.addParams)) {
|
||||
/** Handle web_search separately - don't add to config */
|
||||
if (key === 'web_search') {
|
||||
if (typeof value === 'boolean') {
|
||||
enableWebSearch = value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (knownAnthropicParams.has(key)) {
|
||||
/** Route known Anthropic params to requestOptions */
|
||||
(requestOptions as Record<string, unknown>)[key] = value;
|
||||
}
|
||||
/** Leave other params for transform to handle - they might be OpenAI params */
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle dropParams - only drop from Anthropic config */
|
||||
if (options.dropParams && Array.isArray(options.dropParams)) {
|
||||
options.dropParams.forEach((param) => {
|
||||
if (param === 'web_search') {
|
||||
enableWebSearch = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (param in requestOptions) {
|
||||
delete requestOptions[param as keyof AnthropicClientOptions];
|
||||
}
|
||||
if (requestOptions.invocationKwargs && param in requestOptions.invocationKwargs) {
|
||||
delete (requestOptions.invocationKwargs as Record<string, unknown>)[param];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const tools = [];
|
||||
|
||||
if (mergedOptions.web_search) {
|
||||
if (enableWebSearch) {
|
||||
tools.push({
|
||||
type: 'web_search_20250305',
|
||||
name: 'web_search',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { EModelEndpoint, extractEnvVariable } from 'librechat-data-provider';
|
||||
import { EModelEndpoint, extractEnvVariable, normalizeEndpointName } from 'librechat-data-provider';
|
||||
import type { TCustomEndpoints, TEndpoint, TConfig } from 'librechat-data-provider';
|
||||
import type { TCustomEndpointsConfig } from '~/types/endpoints';
|
||||
import { isUserProvided, normalizeEndpointName } from '~/utils';
|
||||
import { isUserProvided } from '~/utils';
|
||||
|
||||
/**
|
||||
* Load config endpoints from the cached configuration object
|
||||
|
|
|
|||
|
|
@ -5,6 +5,46 @@ import type { GoogleAIToolType } from '@langchain/google-common';
|
|||
import type * as t from '~/types';
|
||||
import { isEnabled } from '~/utils';
|
||||
|
||||
/** Known Google/Vertex AI parameters that map directly to the client config */
|
||||
export const knownGoogleParams = new Set([
|
||||
'model',
|
||||
'modelName',
|
||||
'temperature',
|
||||
'maxOutputTokens',
|
||||
'maxReasoningTokens',
|
||||
'topP',
|
||||
'topK',
|
||||
'seed',
|
||||
'presencePenalty',
|
||||
'frequencyPenalty',
|
||||
'stopSequences',
|
||||
'stop',
|
||||
'logprobs',
|
||||
'topLogprobs',
|
||||
'safetySettings',
|
||||
'responseModalities',
|
||||
'convertSystemMessageToHumanContent',
|
||||
'speechConfig',
|
||||
'streamUsage',
|
||||
'apiKey',
|
||||
'baseUrl',
|
||||
'location',
|
||||
'authOptions',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Applies default parameters to the target object only if the field is undefined
|
||||
* @param target - The target object to apply defaults to
|
||||
* @param defaults - Record of default parameter values
|
||||
*/
|
||||
function applyDefaultParams(target: Record<string, unknown>, defaults: Record<string, unknown>) {
|
||||
for (const [key, value] of Object.entries(defaults)) {
|
||||
if (target[key] === undefined) {
|
||||
target[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getThresholdMapping(model: string) {
|
||||
const gemini1Pattern = /gemini-(1\.0|1\.5|pro$|1\.0-pro|1\.5-pro|1\.5-flash-001)/;
|
||||
const restrictedPattern = /(gemini-(1\.5-flash-8b|2\.0|exp)|learnlm)/;
|
||||
|
|
@ -112,6 +152,8 @@ export function getGoogleConfig(
|
|||
...modelOptions
|
||||
} = options.modelOptions || {};
|
||||
|
||||
let enableWebSearch = web_search;
|
||||
|
||||
const llmConfig: GoogleClientOptions | VertexAIClientOptions = removeNullishValues({
|
||||
...(modelOptions || {}),
|
||||
model: modelOptions?.model ?? '',
|
||||
|
|
@ -193,9 +235,61 @@ export function getGoogleConfig(
|
|||
};
|
||||
}
|
||||
|
||||
/** Handle defaultParams first - only process Google-native params if undefined */
|
||||
if (options.defaultParams && typeof options.defaultParams === 'object') {
|
||||
for (const [key, value] of Object.entries(options.defaultParams)) {
|
||||
/** Handle web_search separately - don't add to config */
|
||||
if (key === 'web_search') {
|
||||
if (enableWebSearch === undefined && typeof value === 'boolean') {
|
||||
enableWebSearch = value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (knownGoogleParams.has(key)) {
|
||||
/** Route known Google params to llmConfig only if undefined */
|
||||
applyDefaultParams(llmConfig as Record<string, unknown>, { [key]: value });
|
||||
}
|
||||
/** Leave other params for transform to handle - they might be OpenAI params */
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle addParams - can override defaultParams */
|
||||
if (options.addParams && typeof options.addParams === 'object') {
|
||||
for (const [key, value] of Object.entries(options.addParams)) {
|
||||
/** Handle web_search separately - don't add to config */
|
||||
if (key === 'web_search') {
|
||||
if (typeof value === 'boolean') {
|
||||
enableWebSearch = value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (knownGoogleParams.has(key)) {
|
||||
/** Route known Google params to llmConfig */
|
||||
(llmConfig as Record<string, unknown>)[key] = value;
|
||||
}
|
||||
/** Leave other params for transform to handle - they might be OpenAI params */
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle dropParams - only drop from Google config */
|
||||
if (options.dropParams && Array.isArray(options.dropParams)) {
|
||||
options.dropParams.forEach((param) => {
|
||||
if (param === 'web_search') {
|
||||
enableWebSearch = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (param in llmConfig) {
|
||||
delete (llmConfig as Record<string, unknown>)[param];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const tools: GoogleAIToolType[] = [];
|
||||
|
||||
if (web_search) {
|
||||
if (enableWebSearch) {
|
||||
tools.push({ googleSearch: {} });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -548,4 +548,375 @@ describe('getOpenAIConfig - Anthropic Compatibility', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Web Search Support via addParams', () => {
|
||||
it('should enable web_search tool when web_search: true in addParams', () => {
|
||||
const apiKey = 'sk-web-search';
|
||||
const endpoint = 'Anthropic (Custom)';
|
||||
const options = {
|
||||
modelOptions: {
|
||||
model: 'claude-3-5-sonnet-latest',
|
||||
user: 'search-user',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
},
|
||||
addParams: {
|
||||
web_search: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||
|
||||
expect(result.tools).toEqual([
|
||||
{
|
||||
type: 'web_search_20250305',
|
||||
name: 'web_search',
|
||||
},
|
||||
]);
|
||||
expect(result.llmConfig).toMatchObject({
|
||||
model: 'claude-3-5-sonnet-latest',
|
||||
stream: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable web_search tool when web_search: false in addParams', () => {
|
||||
const apiKey = 'sk-no-search';
|
||||
const endpoint = 'Anthropic (Custom)';
|
||||
const options = {
|
||||
modelOptions: {
|
||||
model: 'claude-3-opus-20240229',
|
||||
web_search: true, // This should be overridden by addParams
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
},
|
||||
addParams: {
|
||||
web_search: false,
|
||||
},
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should disable web_search when in dropParams', () => {
|
||||
const apiKey = 'sk-drop-search';
|
||||
const endpoint = 'Anthropic (Custom)';
|
||||
const options = {
|
||||
modelOptions: {
|
||||
model: 'claude-3-5-sonnet-latest',
|
||||
web_search: true,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
},
|
||||
dropParams: ['web_search'],
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle web_search with mixed Anthropic and OpenAI params in addParams', () => {
|
||||
const apiKey = 'sk-mixed';
|
||||
const endpoint = 'Anthropic (Custom)';
|
||||
const options = {
|
||||
modelOptions: {
|
||||
model: 'claude-3-opus-20240229',
|
||||
user: 'mixed-user',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
},
|
||||
addParams: {
|
||||
web_search: true,
|
||||
temperature: 0.7, // Anthropic native
|
||||
maxRetries: 3, // OpenAI param (known), should go to top level
|
||||
customParam: 'custom', // Unknown param, should go to modelKwargs
|
||||
},
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||
|
||||
expect(result.tools).toEqual([
|
||||
{
|
||||
type: 'web_search_20250305',
|
||||
name: 'web_search',
|
||||
},
|
||||
]);
|
||||
expect(result.llmConfig.temperature).toBe(0.7);
|
||||
expect(result.llmConfig.maxRetries).toBe(3); // Known OpenAI param at top level
|
||||
expect(result.llmConfig.modelKwargs).toMatchObject({
|
||||
customParam: 'custom', // Unknown param in modelKwargs
|
||||
metadata: { user_id: 'mixed-user' }, // From invocationKwargs
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle Anthropic native params in addParams without web_search', () => {
|
||||
const apiKey = 'sk-native';
|
||||
const endpoint = 'Anthropic (Custom)';
|
||||
const options = {
|
||||
modelOptions: {
|
||||
model: 'claude-3-opus-20240229',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
},
|
||||
addParams: {
|
||||
temperature: 0.9,
|
||||
topP: 0.95,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||
|
||||
expect(result.llmConfig).toMatchObject({
|
||||
model: 'claude-3-opus-20240229',
|
||||
temperature: 0.9,
|
||||
topP: 0.95,
|
||||
maxTokens: 4096,
|
||||
});
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
describe('defaultParams Support via customParams', () => {
|
||||
it('should apply defaultParams when fields are undefined', () => {
|
||||
const apiKey = 'sk-defaults';
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.7 },
|
||||
{ key: 'topP', default: 0.9 },
|
||||
{ key: 'maxRetries', default: 5 },
|
||||
],
|
||||
},
|
||||
reverseProxyUrl: 'https://api.anthropic.com',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.7);
|
||||
expect(result.llmConfig.topP).toBe(0.9);
|
||||
expect(result.llmConfig.maxRetries).toBe(5);
|
||||
});
|
||||
|
||||
it('should not override existing modelOptions with defaultParams', () => {
|
||||
const apiKey = 'sk-override';
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
temperature: 0.9,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.5 },
|
||||
{ key: 'topP', default: 0.8 },
|
||||
],
|
||||
},
|
||||
reverseProxyUrl: 'https://api.anthropic.com',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.9);
|
||||
expect(result.llmConfig.topP).toBe(0.8);
|
||||
});
|
||||
|
||||
it('should allow addParams to override defaultParams', () => {
|
||||
const apiKey = 'sk-add-override';
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'claude-3-opus-20240229',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.5 },
|
||||
{ key: 'topP', default: 0.7 },
|
||||
],
|
||||
},
|
||||
addParams: {
|
||||
temperature: 0.8,
|
||||
topP: 0.95,
|
||||
},
|
||||
reverseProxyUrl: 'https://api.anthropic.com',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.8);
|
||||
expect(result.llmConfig.topP).toBe(0.95);
|
||||
});
|
||||
|
||||
it('should handle defaultParams with web_search', () => {
|
||||
const apiKey = 'sk-web-default';
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'claude-3-5-sonnet-latest',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
paramDefinitions: [{ key: 'web_search', default: true }],
|
||||
},
|
||||
reverseProxyUrl: 'https://api.anthropic.com',
|
||||
});
|
||||
|
||||
expect(result.tools).toEqual([
|
||||
{
|
||||
type: 'web_search_20250305',
|
||||
name: 'web_search',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow addParams to override defaultParams web_search', () => {
|
||||
const apiKey = 'sk-web-override';
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'claude-3-opus-20240229',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
paramDefinitions: [{ key: 'web_search', default: true }],
|
||||
},
|
||||
addParams: {
|
||||
web_search: false,
|
||||
},
|
||||
reverseProxyUrl: 'https://api.anthropic.com',
|
||||
});
|
||||
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle dropParams overriding defaultParams', () => {
|
||||
const apiKey = 'sk-drop';
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'claude-3-opus-20240229',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.7 },
|
||||
{ key: 'topP', default: 0.9 },
|
||||
{ key: 'web_search', default: true },
|
||||
],
|
||||
},
|
||||
dropParams: ['topP', 'web_search'],
|
||||
reverseProxyUrl: 'https://api.anthropic.com',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.7);
|
||||
expect(result.llmConfig.topP).toBeUndefined();
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve order: defaultParams < addParams < modelOptions', () => {
|
||||
const apiKey = 'sk-precedence';
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
temperature: 0.9,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.3 },
|
||||
{ key: 'topP', default: 0.5 },
|
||||
{ key: 'timeout', default: 60000 },
|
||||
],
|
||||
},
|
||||
addParams: {
|
||||
topP: 0.8,
|
||||
},
|
||||
reverseProxyUrl: 'https://api.anthropic.com',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.9);
|
||||
expect(result.llmConfig.topP).toBe(0.8);
|
||||
expect(result.llmConfig.timeout).toBe(60000);
|
||||
});
|
||||
|
||||
it('should handle Claude 3.7 with defaultParams and thinking disabled', () => {
|
||||
const apiKey = 'sk-37-defaults';
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'claude-3.7-sonnet-20241022',
|
||||
thinking: false,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.7 },
|
||||
{ key: 'topP', default: 0.9 },
|
||||
{ key: 'topK', default: 50 },
|
||||
],
|
||||
},
|
||||
reverseProxyUrl: 'https://api.anthropic.com',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.7);
|
||||
expect(result.llmConfig.topP).toBe(0.9);
|
||||
expect(result.llmConfig.modelKwargs?.topK).toBe(50);
|
||||
});
|
||||
|
||||
it('should handle empty paramDefinitions', () => {
|
||||
const apiKey = 'sk-empty';
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'claude-3-opus-20240229',
|
||||
temperature: 0.8,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
paramDefinitions: [],
|
||||
},
|
||||
reverseProxyUrl: 'https://api.anthropic.com',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.8);
|
||||
});
|
||||
|
||||
it('should handle missing paramDefinitions', () => {
|
||||
const apiKey = 'sk-missing';
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'claude-3-opus-20240229',
|
||||
temperature: 0.8,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
},
|
||||
reverseProxyUrl: 'https://api.anthropic.com',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.8);
|
||||
});
|
||||
|
||||
it('should handle mixed Anthropic params in defaultParams', () => {
|
||||
const apiKey = 'sk-mixed';
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.7 },
|
||||
{ key: 'topP', default: 0.9 },
|
||||
{ key: 'maxRetries', default: 3 },
|
||||
],
|
||||
},
|
||||
reverseProxyUrl: 'https://api.anthropic.com',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.7);
|
||||
expect(result.llmConfig.topP).toBe(0.9);
|
||||
expect(result.llmConfig.maxRetries).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ describe('getOpenAIConfig - Backward Compatibility', () => {
|
|||
configOptions: {},
|
||||
tools: [
|
||||
{
|
||||
type: 'web_search_preview',
|
||||
type: 'web_search',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
387
packages/api/src/endpoints/openai/config.google.spec.ts
Normal file
387
packages/api/src/endpoints/openai/config.google.spec.ts
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
import { getOpenAIConfig } from './config';
|
||||
|
||||
describe('getOpenAIConfig - Google Compatibility', () => {
|
||||
describe('Google via Custom Endpoint', () => {
|
||||
describe('Web Search Support via addParams', () => {
|
||||
it('should enable googleSearch tool when web_search: true in addParams', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
const endpoint = 'Gemini (Custom)';
|
||||
const options = {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
},
|
||||
addParams: {
|
||||
web_search: true,
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||
|
||||
expect(result.tools).toEqual([{ googleSearch: {} }]);
|
||||
expect(result.llmConfig).toMatchObject({
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable googleSearch tool when web_search: false in addParams', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
const endpoint = 'Gemini (Custom)';
|
||||
const options = {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
web_search: true, // Should be overridden by addParams
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
},
|
||||
addParams: {
|
||||
web_search: false,
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should disable googleSearch when in dropParams', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
const endpoint = 'Gemini (Custom)';
|
||||
const options = {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
web_search: true,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
},
|
||||
dropParams: ['web_search'],
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle web_search with mixed Google and OpenAI params in addParams', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
const endpoint = 'Gemini (Custom)';
|
||||
const options = {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
},
|
||||
addParams: {
|
||||
web_search: true,
|
||||
temperature: 0.8, // Shared param (both Google and OpenAI)
|
||||
topK: 40, // Google-only param, goes to modelKwargs
|
||||
frequencyPenalty: 0.5, // Known OpenAI param, goes to top level
|
||||
customUnknown: 'test', // Unknown param, goes to modelKwargs
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||
|
||||
expect(result.tools).toEqual([{ googleSearch: {} }]);
|
||||
expect(result.llmConfig.temperature).toBe(0.8); // Shared param at top level
|
||||
expect(result.llmConfig.frequencyPenalty).toBe(0.5); // Known OpenAI param at top level
|
||||
expect(result.llmConfig.modelKwargs).toMatchObject({
|
||||
topK: 40, // Google-specific in modelKwargs
|
||||
customUnknown: 'test', // Unknown param in modelKwargs
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle Google native params in addParams without web_search', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
const endpoint = 'Gemini (Custom)';
|
||||
const options = {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
},
|
||||
addParams: {
|
||||
temperature: 0.9, // Shared param (both Google and OpenAI)
|
||||
topP: 0.95, // Shared param (both Google and OpenAI)
|
||||
topK: 50, // Google-only, goes to modelKwargs
|
||||
maxOutputTokens: 8192, // Google-only, goes to modelKwargs
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||
|
||||
expect(result.llmConfig).toMatchObject({
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
temperature: 0.9, // Shared params at top level
|
||||
topP: 0.95,
|
||||
});
|
||||
expect(result.llmConfig.modelKwargs).toMatchObject({
|
||||
topK: 50, // Google-specific in modelKwargs
|
||||
maxOutputTokens: 8192,
|
||||
});
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should drop Google native params with dropParams', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
const endpoint = 'Gemini (Custom)';
|
||||
const options = {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
temperature: 0.7,
|
||||
topK: 40,
|
||||
topP: 0.9,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
},
|
||||
dropParams: ['topK', 'topP'],
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.7);
|
||||
expect((result.llmConfig as Record<string, unknown>).topK).toBeUndefined();
|
||||
expect(result.llmConfig.topP).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle both addParams and dropParams for Google', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
const endpoint = 'Gemini (Custom)';
|
||||
const options = {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
topK: 30, // Will be dropped
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
},
|
||||
addParams: {
|
||||
web_search: true,
|
||||
temperature: 0.8, // Shared param
|
||||
maxOutputTokens: 4096, // Google-only param
|
||||
},
|
||||
dropParams: ['topK'],
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||
|
||||
expect(result.tools).toEqual([{ googleSearch: {} }]);
|
||||
expect(result.llmConfig).toMatchObject({
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
temperature: 0.8,
|
||||
});
|
||||
expect(result.llmConfig.modelKwargs).toMatchObject({
|
||||
maxOutputTokens: 4096, // Google-specific in modelKwargs
|
||||
});
|
||||
expect((result.llmConfig as Record<string, unknown>).topK).toBeUndefined();
|
||||
// Verify topK is not in modelKwargs either
|
||||
expect(result.llmConfig.modelKwargs?.topK).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultParams Support via customParams', () => {
|
||||
it('should apply defaultParams when fields are undefined', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.6 },
|
||||
{ key: 'topK', default: 40 },
|
||||
],
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.6);
|
||||
expect(result.llmConfig.modelKwargs?.topK).toBe(40);
|
||||
});
|
||||
|
||||
it('should not override existing modelOptions with defaultParams', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
temperature: 0.9,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.5 },
|
||||
{ key: 'topK', default: 40 },
|
||||
],
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.9);
|
||||
expect(result.llmConfig.modelKwargs?.topK).toBe(40);
|
||||
});
|
||||
|
||||
it('should allow addParams to override defaultParams', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.5 },
|
||||
{ key: 'topK', default: 30 },
|
||||
],
|
||||
},
|
||||
addParams: {
|
||||
temperature: 0.8,
|
||||
topK: 50,
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.8);
|
||||
expect(result.llmConfig.modelKwargs?.topK).toBe(50);
|
||||
});
|
||||
|
||||
it('should handle defaultParams with web_search', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
paramDefinitions: [{ key: 'web_search', default: true }],
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
});
|
||||
|
||||
expect(result.tools).toEqual([{ googleSearch: {} }]);
|
||||
});
|
||||
|
||||
it('should allow addParams to override defaultParams web_search', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
paramDefinitions: [{ key: 'web_search', default: true }],
|
||||
},
|
||||
addParams: {
|
||||
web_search: false,
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
});
|
||||
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle dropParams overriding defaultParams', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.7 },
|
||||
{ key: 'topK', default: 40 },
|
||||
{ key: 'web_search', default: true },
|
||||
],
|
||||
},
|
||||
dropParams: ['topK', 'web_search'],
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.7);
|
||||
expect(result.llmConfig.modelKwargs?.topK).toBeUndefined();
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve order: defaultParams < addParams < modelOptions', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
temperature: 0.9,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.3 },
|
||||
{ key: 'topP', default: 0.5 },
|
||||
{ key: 'topK', default: 20 },
|
||||
],
|
||||
},
|
||||
addParams: {
|
||||
topP: 0.8,
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.9);
|
||||
expect(result.llmConfig.topP).toBe(0.8);
|
||||
expect(result.llmConfig.modelKwargs?.topK).toBe(20);
|
||||
});
|
||||
|
||||
it('should handle empty paramDefinitions', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
temperature: 0.8,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
paramDefinitions: [],
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.8);
|
||||
});
|
||||
|
||||
it('should handle missing paramDefinitions', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
|
||||
const result = getOpenAIConfig(apiKey, {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
temperature: 0.8,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.8);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -230,7 +230,7 @@ describe('getOpenAIConfig', () => {
|
|||
const result = getOpenAIConfig(mockApiKey, { modelOptions });
|
||||
|
||||
expect(result.llmConfig.useResponsesApi).toBe(true);
|
||||
expect(result.tools).toEqual([{ type: 'web_search_preview' }]);
|
||||
expect(result.tools).toEqual([{ type: 'web_search' }]);
|
||||
});
|
||||
|
||||
it('should handle web_search from addParams overriding modelOptions', () => {
|
||||
|
|
@ -247,7 +247,7 @@ describe('getOpenAIConfig', () => {
|
|||
const result = getOpenAIConfig(mockApiKey, { modelOptions, addParams });
|
||||
|
||||
expect(result.llmConfig.useResponsesApi).toBe(true);
|
||||
expect(result.tools).toEqual([{ type: 'web_search_preview' }]);
|
||||
expect(result.tools).toEqual([{ type: 'web_search' }]);
|
||||
// web_search should not be in modelKwargs or llmConfig
|
||||
expect((result.llmConfig as Record<string, unknown>).web_search).toBeUndefined();
|
||||
expect(result.llmConfig.modelKwargs).toEqual({ customParam: 'value' });
|
||||
|
|
@ -299,7 +299,7 @@ describe('getOpenAIConfig', () => {
|
|||
|
||||
// Should keep the original web_search from modelOptions since addParams value is not boolean
|
||||
expect(result.llmConfig.useResponsesApi).toBe(true);
|
||||
expect(result.tools).toEqual([{ type: 'web_search_preview' }]);
|
||||
expect(result.tools).toEqual([{ type: 'web_search' }]);
|
||||
expect(result.llmConfig.temperature).toBe(0.7);
|
||||
// web_search should not be added to modelKwargs
|
||||
expect(result.llmConfig.modelKwargs).toBeUndefined();
|
||||
|
|
@ -335,7 +335,7 @@ describe('getOpenAIConfig', () => {
|
|||
|
||||
// web_search should trigger the tool but not appear in config
|
||||
expect(result.llmConfig.useResponsesApi).toBe(true);
|
||||
expect(result.tools).toEqual([{ type: 'web_search_preview' }]);
|
||||
expect(result.tools).toEqual([{ type: 'web_search' }]);
|
||||
expect((result.llmConfig as Record<string, unknown>).web_search).toBeUndefined();
|
||||
expect(result.llmConfig.temperature).toBe(0.5);
|
||||
expect(result.llmConfig.modelKwargs).toEqual({ customParam1: 'value1' });
|
||||
|
|
@ -1164,7 +1164,7 @@ describe('getOpenAIConfig', () => {
|
|||
text: { verbosity: Verbosity.medium },
|
||||
customParam: 'custom-value',
|
||||
});
|
||||
expect(result.tools).toEqual([{ type: 'web_search_preview' }]);
|
||||
expect(result.tools).toEqual([{ type: 'web_search' }]);
|
||||
expect(result.configOptions).toMatchObject({
|
||||
baseURL: 'https://api.custom.com',
|
||||
defaultHeaders: { 'X-Custom': 'value' },
|
||||
|
|
@ -1651,6 +1651,211 @@ describe('getOpenAIConfig', () => {
|
|||
expect(result.llmConfig.modelKwargs).toEqual(largeModelKwargs);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultParams Support via customParams', () => {
|
||||
it('should apply defaultParams when fields are undefined', () => {
|
||||
const result = getOpenAIConfig(mockApiKey, {
|
||||
modelOptions: {
|
||||
model: 'gpt-4o',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'azureOpenAI',
|
||||
paramDefinitions: [
|
||||
{ key: 'useResponsesApi', default: true },
|
||||
{ key: 'temperature', default: 0.5 },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig.useResponsesApi).toBe(true);
|
||||
expect(result.llmConfig.temperature).toBe(0.5);
|
||||
});
|
||||
|
||||
it('should not override existing modelOptions with defaultParams', () => {
|
||||
const result = getOpenAIConfig(mockApiKey, {
|
||||
modelOptions: {
|
||||
model: 'gpt-5',
|
||||
temperature: 0.9,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'azureOpenAI',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.5 },
|
||||
{ key: 'maxTokens', default: 1000 },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.9);
|
||||
expect(result.llmConfig.modelKwargs?.max_completion_tokens).toBe(1000);
|
||||
});
|
||||
|
||||
it('should allow addParams to override defaultParams', () => {
|
||||
const result = getOpenAIConfig(mockApiKey, {
|
||||
modelOptions: {
|
||||
model: 'gpt-4o',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'azureOpenAI',
|
||||
paramDefinitions: [
|
||||
{ key: 'useResponsesApi', default: true },
|
||||
{ key: 'temperature', default: 0.5 },
|
||||
],
|
||||
},
|
||||
addParams: {
|
||||
useResponsesApi: false,
|
||||
temperature: 0.8,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig.useResponsesApi).toBe(false);
|
||||
expect(result.llmConfig.temperature).toBe(0.8);
|
||||
});
|
||||
|
||||
it('should handle defaultParams with unknown parameters', () => {
|
||||
const result = getOpenAIConfig(mockApiKey, {
|
||||
modelOptions: {
|
||||
model: 'gpt-4o',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'azureOpenAI',
|
||||
paramDefinitions: [
|
||||
{ key: 'customParam1', default: 'defaultValue' },
|
||||
{ key: 'customParam2', default: 123 },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig.modelKwargs).toMatchObject({
|
||||
customParam1: 'defaultValue',
|
||||
customParam2: 123,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle defaultParams with web_search', () => {
|
||||
const result = getOpenAIConfig(mockApiKey, {
|
||||
modelOptions: {
|
||||
model: 'gpt-4o',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'openAI',
|
||||
paramDefinitions: [{ key: 'web_search', default: true }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig.useResponsesApi).toBe(true);
|
||||
expect(result.tools).toEqual([{ type: 'web_search' }]);
|
||||
});
|
||||
|
||||
it('should allow addParams to override defaultParams web_search', () => {
|
||||
const result = getOpenAIConfig(mockApiKey, {
|
||||
modelOptions: {
|
||||
model: 'gpt-4o',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'openAI',
|
||||
paramDefinitions: [{ key: 'web_search', default: true }],
|
||||
},
|
||||
addParams: {
|
||||
web_search: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should apply defaultParams for Anthropic via customParams', () => {
|
||||
const result = getOpenAIConfig('test-key', {
|
||||
modelOptions: {
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'anthropic',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.7 },
|
||||
{ key: 'topK', default: 50 },
|
||||
],
|
||||
},
|
||||
reverseProxyUrl: 'https://api.anthropic.com',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.7);
|
||||
expect(result.llmConfig.modelKwargs?.topK).toBe(50);
|
||||
});
|
||||
|
||||
it('should apply defaultParams for Google via customParams', () => {
|
||||
const credentials = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
const result = getOpenAIConfig(credentials, {
|
||||
modelOptions: {
|
||||
model: 'gemini-2.0-flash-exp',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.6 },
|
||||
{ key: 'topK', default: 40 },
|
||||
],
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.6);
|
||||
expect(result.llmConfig.modelKwargs?.topK).toBe(40);
|
||||
});
|
||||
|
||||
it('should handle empty paramDefinitions', () => {
|
||||
const result = getOpenAIConfig(mockApiKey, {
|
||||
modelOptions: {
|
||||
model: 'gpt-4o',
|
||||
temperature: 0.9,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'azureOpenAI',
|
||||
paramDefinitions: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.9);
|
||||
});
|
||||
|
||||
it('should handle missing paramDefinitions', () => {
|
||||
const result = getOpenAIConfig(mockApiKey, {
|
||||
modelOptions: {
|
||||
model: 'gpt-4o',
|
||||
temperature: 0.9,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'azureOpenAI',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.9);
|
||||
});
|
||||
|
||||
it('should preserve order: defaultParams < addParams < modelOptions', () => {
|
||||
const result = getOpenAIConfig(mockApiKey, {
|
||||
modelOptions: {
|
||||
model: 'gpt-5',
|
||||
temperature: 0.9,
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'openAI',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.3 },
|
||||
{ key: 'topP', default: 0.5 },
|
||||
{ key: 'maxTokens', default: 500 },
|
||||
],
|
||||
},
|
||||
addParams: {
|
||||
topP: 0.8,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig.temperature).toBe(0.9);
|
||||
expect(result.llmConfig.topP).toBe(0.8);
|
||||
expect(result.llmConfig.modelKwargs?.max_completion_tokens).toBe(500);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Entra ID Authentication', () => {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import { Providers } from '@librechat/agents';
|
|||
import { KnownEndpoints, EModelEndpoint } from 'librechat-data-provider';
|
||||
import type * as t from '~/types';
|
||||
import { getLLMConfig as getAnthropicLLMConfig } from '~/endpoints/anthropic/llm';
|
||||
import { getOpenAILLMConfig, extractDefaultParams } from './llm';
|
||||
import { getGoogleConfig } from '~/endpoints/google/llm';
|
||||
import { transformToOpenAIConfig } from './transform';
|
||||
import { constructAzureURL } from '~/utils/azure';
|
||||
import { createFetch } from '~/utils/generators';
|
||||
import { getOpenAILLMConfig } from './llm';
|
||||
|
||||
type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
|
|
@ -33,17 +34,24 @@ export function getOpenAIConfig(
|
|||
reverseProxyUrl: baseURL,
|
||||
} = options;
|
||||
|
||||
/** Extract default params from customParams.paramDefinitions */
|
||||
const defaultParams = extractDefaultParams(options.customParams?.paramDefinitions);
|
||||
|
||||
let llmConfig: t.OAIClientOptions;
|
||||
let tools: t.LLMConfigResult['tools'];
|
||||
const isAnthropic = options.customParams?.defaultParamsEndpoint === EModelEndpoint.anthropic;
|
||||
const isGoogle = options.customParams?.defaultParamsEndpoint === EModelEndpoint.google;
|
||||
|
||||
const useOpenRouter =
|
||||
!isAnthropic &&
|
||||
!isGoogle &&
|
||||
((baseURL && baseURL.includes(KnownEndpoints.openrouter)) ||
|
||||
(endpoint != null && endpoint.toLowerCase().includes(KnownEndpoints.openrouter)));
|
||||
const isVercel =
|
||||
(baseURL && baseURL.includes('ai-gateway.vercel.sh')) ||
|
||||
(endpoint != null && endpoint.toLowerCase().includes(KnownEndpoints.vercel));
|
||||
!isAnthropic &&
|
||||
!isGoogle &&
|
||||
((baseURL && baseURL.includes('ai-gateway.vercel.sh')) ||
|
||||
(endpoint != null && endpoint.toLowerCase().includes(KnownEndpoints.vercel)));
|
||||
|
||||
let azure = options.azure;
|
||||
let headers = options.headers;
|
||||
|
|
@ -51,7 +59,12 @@ export function getOpenAIConfig(
|
|||
const anthropicResult = getAnthropicLLMConfig(apiKey, {
|
||||
modelOptions,
|
||||
proxy: options.proxy,
|
||||
reverseProxyUrl: baseURL,
|
||||
addParams,
|
||||
dropParams,
|
||||
defaultParams,
|
||||
});
|
||||
/** Transform handles addParams/dropParams - it knows about OpenAI params */
|
||||
const transformed = transformToOpenAIConfig({
|
||||
addParams,
|
||||
dropParams,
|
||||
|
|
@ -63,6 +76,24 @@ export function getOpenAIConfig(
|
|||
if (transformed.configOptions?.defaultHeaders) {
|
||||
headers = Object.assign(headers ?? {}, transformed.configOptions?.defaultHeaders);
|
||||
}
|
||||
} else if (isGoogle) {
|
||||
const googleResult = getGoogleConfig(apiKey, {
|
||||
modelOptions,
|
||||
reverseProxyUrl: baseURL ?? undefined,
|
||||
authHeader: true,
|
||||
addParams,
|
||||
dropParams,
|
||||
defaultParams,
|
||||
});
|
||||
/** Transform handles addParams/dropParams - it knows about OpenAI params */
|
||||
const transformed = transformToOpenAIConfig({
|
||||
addParams,
|
||||
dropParams,
|
||||
llmConfig: googleResult.llmConfig,
|
||||
fromEndpoint: EModelEndpoint.google,
|
||||
});
|
||||
llmConfig = transformed.llmConfig;
|
||||
tools = googleResult.tools;
|
||||
} else {
|
||||
const openaiResult = getOpenAILLMConfig({
|
||||
azure,
|
||||
|
|
@ -72,6 +103,7 @@ export function getOpenAIConfig(
|
|||
streaming,
|
||||
addParams,
|
||||
dropParams,
|
||||
defaultParams,
|
||||
modelOptions,
|
||||
useOpenRouter,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { ErrorTypes, EModelEndpoint, mapModelToAzureConfig } from 'librechat-data-provider';
|
||||
import type {
|
||||
InitializeOpenAIOptionsParams,
|
||||
OpenAIOptionsResult,
|
||||
OpenAIConfigOptions,
|
||||
LLMConfigResult,
|
||||
UserKeyValues,
|
||||
} from '~/types';
|
||||
import { createHandleLLMNewToken } from '~/utils/generators';
|
||||
import { getAzureCredentials, getEntraIdAccessToken, shouldUseEntraId } from '~/utils/azure';
|
||||
import { isUserProvided } from '~/utils/common';
|
||||
import { resolveHeaders } from '~/utils/env';
|
||||
|
|
@ -27,7 +26,7 @@ export const initializeOpenAI = async ({
|
|||
overrideEndpoint,
|
||||
getUserKeyValues,
|
||||
checkUserKeyExpiry,
|
||||
}: InitializeOpenAIOptionsParams): Promise<OpenAIOptionsResult> => {
|
||||
}: InitializeOpenAIOptionsParams): Promise<LLMConfigResult> => {
|
||||
const { PROXY, OPENAI_API_KEY, AZURE_API_KEY, OPENAI_REVERSE_PROXY, AZURE_OPENAI_BASEURL } =
|
||||
process.env;
|
||||
|
||||
|
|
@ -178,17 +177,8 @@ export const initializeOpenAI = async ({
|
|||
}
|
||||
|
||||
if (streamRate) {
|
||||
options.llmConfig.callbacks = [
|
||||
{
|
||||
handleLLMNewToken: createHandleLLMNewToken(streamRate),
|
||||
},
|
||||
];
|
||||
options.llmConfig._lc_stream_delay = streamRate;
|
||||
}
|
||||
|
||||
const result: OpenAIOptionsResult = {
|
||||
...options,
|
||||
streamRate,
|
||||
};
|
||||
|
||||
return result;
|
||||
return options;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { EModelEndpoint, removeNullishValues } from 'librechat-data-provider';
|
||||
import type { BindToolsInput } from '@langchain/core/language_models/chat_models';
|
||||
import type { SettingDefinition } from 'librechat-data-provider';
|
||||
import type { AzureOpenAIInput } from '@langchain/openai';
|
||||
import type { OpenAI } from 'openai';
|
||||
import type * as t from '~/types';
|
||||
|
|
@ -75,6 +76,44 @@ function hasReasoningParams({
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts default parameters from customParams.paramDefinitions
|
||||
* @param paramDefinitions - Array of parameter definitions with key and default values
|
||||
* @returns Record of default parameters
|
||||
*/
|
||||
export function extractDefaultParams(
|
||||
paramDefinitions?: Partial<SettingDefinition>[],
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!paramDefinitions || !Array.isArray(paramDefinitions)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const defaults: Record<string, unknown> = {};
|
||||
for (let i = 0; i < paramDefinitions.length; i++) {
|
||||
const param = paramDefinitions[i];
|
||||
if (param.key !== undefined && param.default !== undefined) {
|
||||
defaults[param.key] = param.default;
|
||||
}
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies default parameters to the target object only if the field is undefined
|
||||
* @param target - The target object to apply defaults to
|
||||
* @param defaults - Record of default parameter values
|
||||
*/
|
||||
export function applyDefaultParams(
|
||||
target: Record<string, unknown>,
|
||||
defaults: Record<string, unknown>,
|
||||
) {
|
||||
for (const [key, value] of Object.entries(defaults)) {
|
||||
if (target[key] === undefined) {
|
||||
target[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getOpenAILLMConfig({
|
||||
azure,
|
||||
apiKey,
|
||||
|
|
@ -83,6 +122,7 @@ export function getOpenAILLMConfig({
|
|||
streaming,
|
||||
addParams,
|
||||
dropParams,
|
||||
defaultParams,
|
||||
useOpenRouter,
|
||||
modelOptions: _modelOptions,
|
||||
}: {
|
||||
|
|
@ -93,6 +133,7 @@ export function getOpenAILLMConfig({
|
|||
modelOptions: Partial<t.OpenAIParameters>;
|
||||
addParams?: Record<string, unknown>;
|
||||
dropParams?: string[];
|
||||
defaultParams?: Record<string, unknown>;
|
||||
useOpenRouter?: boolean;
|
||||
azure?: false | t.AzureOptions;
|
||||
}): Pick<t.LLMConfigResult, 'llmConfig' | 'tools'> & {
|
||||
|
|
@ -133,6 +174,30 @@ export function getOpenAILLMConfig({
|
|||
|
||||
let enableWebSearch = web_search;
|
||||
|
||||
/** Apply defaultParams first - only if fields are undefined */
|
||||
if (defaultParams && typeof defaultParams === 'object') {
|
||||
for (const [key, value] of Object.entries(defaultParams)) {
|
||||
/** Handle web_search separately - don't add to config */
|
||||
if (key === 'web_search') {
|
||||
if (enableWebSearch === undefined && typeof value === 'boolean') {
|
||||
enableWebSearch = value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (knownOpenAIParams.has(key)) {
|
||||
applyDefaultParams(llmConfig as Record<string, unknown>, { [key]: value });
|
||||
} else {
|
||||
/** Apply to modelKwargs if not a known param */
|
||||
if (modelKwargs[key] === undefined) {
|
||||
modelKwargs[key] = value;
|
||||
hasModelKwargs = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply addParams - can override defaultParams */
|
||||
if (addParams && typeof addParams === 'object') {
|
||||
for (const [key, value] of Object.entries(addParams)) {
|
||||
/** Handle web_search directly here instead of adding to modelKwargs or llmConfig */
|
||||
|
|
@ -190,7 +255,7 @@ export function getOpenAILLMConfig({
|
|||
} else if (enableWebSearch) {
|
||||
/** Standard OpenAI web search uses tools API */
|
||||
llmConfig.useResponsesApi = true;
|
||||
tools.push({ type: 'web_search_preview' });
|
||||
tools.push({ type: 'web_search' });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type * as t from '~/types';
|
|||
import { knownOpenAIParams } from './llm';
|
||||
|
||||
const anthropicExcludeParams = new Set(['anthropicApiUrl']);
|
||||
const googleExcludeParams = new Set(['safetySettings', 'location', 'baseUrl', 'customHeaders']);
|
||||
|
||||
/**
|
||||
* Transforms a Non-OpenAI LLM config to an OpenAI-conformant config.
|
||||
|
|
@ -31,7 +32,14 @@ export function transformToOpenAIConfig({
|
|||
let hasModelKwargs = false;
|
||||
|
||||
const isAnthropic = fromEndpoint === EModelEndpoint.anthropic;
|
||||
const excludeParams = isAnthropic ? anthropicExcludeParams : new Set();
|
||||
const isGoogle = fromEndpoint === EModelEndpoint.google;
|
||||
|
||||
let excludeParams = new Set<string>();
|
||||
if (isAnthropic) {
|
||||
excludeParams = anthropicExcludeParams;
|
||||
} else if (isGoogle) {
|
||||
excludeParams = googleExcludeParams;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(llmConfig)) {
|
||||
if (value === undefined || value === null) {
|
||||
|
|
@ -49,6 +57,19 @@ export function transformToOpenAIConfig({
|
|||
modelKwargs = Object.assign({}, modelKwargs, value as Record<string, unknown>);
|
||||
hasModelKwargs = true;
|
||||
continue;
|
||||
} else if (isGoogle && key === 'authOptions') {
|
||||
// Handle Google authOptions
|
||||
modelKwargs = Object.assign({}, modelKwargs, value as Record<string, unknown>);
|
||||
hasModelKwargs = true;
|
||||
continue;
|
||||
} else if (
|
||||
isGoogle &&
|
||||
(key === 'thinkingConfig' || key === 'thinkingBudget' || key === 'includeThoughts')
|
||||
) {
|
||||
// Handle Google thinking configuration
|
||||
modelKwargs = Object.assign({}, modelKwargs, { [key]: value });
|
||||
hasModelKwargs = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (knownOpenAIParams.has(key)) {
|
||||
|
|
@ -61,6 +82,11 @@ export function transformToOpenAIConfig({
|
|||
|
||||
if (addParams && typeof addParams === 'object') {
|
||||
for (const [key, value] of Object.entries(addParams)) {
|
||||
/** Skip web_search - it's handled separately as a tool */
|
||||
if (key === 'web_search') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (knownOpenAIParams.has(key)) {
|
||||
(openAIConfig as Record<string, unknown>)[key] = value;
|
||||
} else {
|
||||
|
|
@ -76,16 +102,23 @@ export function transformToOpenAIConfig({
|
|||
|
||||
if (dropParams && Array.isArray(dropParams)) {
|
||||
dropParams.forEach((param) => {
|
||||
/** Skip web_search - handled separately */
|
||||
if (param === 'web_search') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (param in openAIConfig) {
|
||||
delete openAIConfig[param as keyof t.OAIClientOptions];
|
||||
}
|
||||
if (openAIConfig.modelKwargs && param in openAIConfig.modelKwargs) {
|
||||
delete openAIConfig.modelKwargs[param];
|
||||
if (Object.keys(openAIConfig.modelKwargs).length === 0) {
|
||||
delete openAIConfig.modelKwargs;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/** Clean up empty modelKwargs after dropParams processing */
|
||||
if (openAIConfig.modelKwargs && Object.keys(openAIConfig.modelKwargs).length === 0) {
|
||||
delete openAIConfig.modelKwargs;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue