mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
🎚️ feat: Anthropic Parameter Set Support via Custom Endpoints (#9415)
* refactor: modularize openai llm config logic into new getOpenAILLMConfig function (#9412) * ✈️ refactor: Migrate Anthropic's getLLMConfig to TypeScript (#9413) * refactor: move tokens.js over to packages/api and update imports * refactor: port tokens.js to typescript * refactor: move helpers.js over to packages/api and update imports * refactor: port helpers.js to typescript * refactor: move anthropic/llm.js over to packages/api and update imports * refactor: port anthropic/llm.js to typescript with supporting types in types/anthropic.ts and updated tests in llm.spec.js * refactor: move llm.spec.js over to packages/api and update import * refactor: port llm.spec.js over to typescript * 📝 Add Prompt Parameter Support for Anthropic Custom Endpoints (#9414) feat: add anthropic llm config support for openai-like (custom) endpoints * fix: missed compiler / type issues from addition of getAnthropicLLMConfig * refactor: update tokens.ts to export constants and functions, enhance type definitions, and adjust default values * WIP: first pass, decouple `llmConfig` from `configOptions` * chore: update import path for OpenAI configuration from 'llm' to 'config' * refactor: enhance type definitions for ThinkingConfig and update modelOptions in AnthropicConfigOptions * refactor: cleanup type, introduce openai transform from alt provider * chore: integrate removeNullishValues in Google llmConfig and update OpenAI exports * chore: bump version of @librechat/api to 1.3.5 in package.json and package-lock.json * refactor: update customParams type in OpenAIConfigOptions to use TConfig['customParams'] * refactor: enhance transformToOpenAIConfig to include fromEndpoint and improve config extraction * refactor: conform userId field for anthropic/openai, cleanup anthropic typing * ci: add backward compatibility tests for getOpenAIConfig with various endpoints and configurations * ci: replace userId with user in clientOptions for getLLMConfig * test: add Azure OpenAI endpoint tests for various configurations in getOpenAIConfig * refactor: defaultHeaders retrieval for prompt caching for anthropic-based custom endpoint (litellm) * test: add unit tests for getOpenAIConfig with various Anthropic model configurations * test: enhance Anthropic compatibility tests with addParams and dropParams handling * chore: update @librechat/agents dependency to version 2.4.78 in package.json and package-lock.json * chore: update @librechat/agents dependency to version 2.4.79 in package.json and package-lock.json --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
7de6f6e44c
commit
c6ecf0095b
40 changed files with 1736 additions and 432 deletions
|
@ -10,7 +10,17 @@ const {
|
||||||
validateVisionModel,
|
validateVisionModel,
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
const { SplitStreamHandler: _Handler } = require('@librechat/agents');
|
const { SplitStreamHandler: _Handler } = require('@librechat/agents');
|
||||||
const { Tokenizer, createFetch, createStreamEventHandlers } = require('@librechat/api');
|
const {
|
||||||
|
Tokenizer,
|
||||||
|
createFetch,
|
||||||
|
matchModelName,
|
||||||
|
getClaudeHeaders,
|
||||||
|
getModelMaxTokens,
|
||||||
|
configureReasoning,
|
||||||
|
checkPromptCacheSupport,
|
||||||
|
getModelMaxOutputTokens,
|
||||||
|
createStreamEventHandlers,
|
||||||
|
} = require('@librechat/api');
|
||||||
const {
|
const {
|
||||||
truncateText,
|
truncateText,
|
||||||
formatMessage,
|
formatMessage,
|
||||||
|
@ -19,12 +29,6 @@ const {
|
||||||
parseParamFromPrompt,
|
parseParamFromPrompt,
|
||||||
createContextHandlers,
|
createContextHandlers,
|
||||||
} = require('./prompts');
|
} = require('./prompts');
|
||||||
const {
|
|
||||||
getClaudeHeaders,
|
|
||||||
configureReasoning,
|
|
||||||
checkPromptCacheSupport,
|
|
||||||
} = require('~/server/services/Endpoints/anthropic/helpers');
|
|
||||||
const { getModelMaxTokens, getModelMaxOutputTokens, matchModelName } = require('~/utils');
|
|
||||||
const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens');
|
const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens');
|
||||||
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||||
const { sleep } = require('~/server/utils');
|
const { sleep } = require('~/server/utils');
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const { google } = require('googleapis');
|
const { google } = require('googleapis');
|
||||||
|
const { getModelMaxTokens } = require('@librechat/api');
|
||||||
const { concat } = require('@langchain/core/utils/stream');
|
const { concat } = require('@langchain/core/utils/stream');
|
||||||
const { ChatVertexAI } = require('@langchain/google-vertexai');
|
const { ChatVertexAI } = require('@langchain/google-vertexai');
|
||||||
const { Tokenizer, getSafetySettings } = require('@librechat/api');
|
const { Tokenizer, getSafetySettings } = require('@librechat/api');
|
||||||
|
@ -21,7 +22,6 @@ const {
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
const { encodeAndFormat } = require('~/server/services/Files/images');
|
const { encodeAndFormat } = require('~/server/services/Files/images');
|
||||||
const { spendTokens } = require('~/models/spendTokens');
|
const { spendTokens } = require('~/models/spendTokens');
|
||||||
const { getModelMaxTokens } = require('~/utils');
|
|
||||||
const { sleep } = require('~/server/utils');
|
const { sleep } = require('~/server/utils');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -7,7 +7,9 @@ const {
|
||||||
createFetch,
|
createFetch,
|
||||||
resolveHeaders,
|
resolveHeaders,
|
||||||
constructAzureURL,
|
constructAzureURL,
|
||||||
|
getModelMaxTokens,
|
||||||
genAzureChatCompletion,
|
genAzureChatCompletion,
|
||||||
|
getModelMaxOutputTokens,
|
||||||
createStreamEventHandlers,
|
createStreamEventHandlers,
|
||||||
} = require('@librechat/api');
|
} = require('@librechat/api');
|
||||||
const {
|
const {
|
||||||
|
@ -31,13 +33,13 @@ const {
|
||||||
titleInstruction,
|
titleInstruction,
|
||||||
createContextHandlers,
|
createContextHandlers,
|
||||||
} = require('./prompts');
|
} = require('./prompts');
|
||||||
const { extractBaseURL, getModelMaxTokens, getModelMaxOutputTokens } = require('~/utils');
|
|
||||||
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||||
const { addSpaceIfNeeded, sleep } = require('~/server/utils');
|
const { addSpaceIfNeeded, sleep } = require('~/server/utils');
|
||||||
const { spendTokens } = require('~/models/spendTokens');
|
const { spendTokens } = require('~/models/spendTokens');
|
||||||
const { handleOpenAIErrors } = require('./tools/util');
|
const { handleOpenAIErrors } = require('./tools/util');
|
||||||
const { summaryBuffer } = require('./memory');
|
const { summaryBuffer } = require('./memory');
|
||||||
const { runTitleChain } = require('./chains');
|
const { runTitleChain } = require('./chains');
|
||||||
|
const { extractBaseURL } = require('~/utils');
|
||||||
const { tokenSplit } = require('./document');
|
const { tokenSplit } = require('./document');
|
||||||
const BaseClient = require('./BaseClient');
|
const BaseClient = require('./BaseClient');
|
||||||
const { createLLM } = require('./llm');
|
const { createLLM } = require('./llm');
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
const { getModelMaxTokens } = require('@librechat/api');
|
||||||
const BaseClient = require('../BaseClient');
|
const BaseClient = require('../BaseClient');
|
||||||
const { getModelMaxTokens } = require('../../../utils');
|
|
||||||
|
|
||||||
class FakeClient extends BaseClient {
|
class FakeClient extends BaseClient {
|
||||||
constructor(apiKey, options = {}) {
|
constructor(apiKey, options = {}) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const { matchModelName } = require('../utils/tokens');
|
const { matchModelName } = require('@librechat/api');
|
||||||
const defaultRate = 6;
|
const defaultRate = 6;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
"@langchain/google-vertexai": "^0.2.13",
|
"@langchain/google-vertexai": "^0.2.13",
|
||||||
"@langchain/openai": "^0.5.18",
|
"@langchain/openai": "^0.5.18",
|
||||||
"@langchain/textsplitters": "^0.1.0",
|
"@langchain/textsplitters": "^0.1.0",
|
||||||
"@librechat/agents": "^2.4.77",
|
"@librechat/agents": "^2.4.79",
|
||||||
"@librechat/api": "*",
|
"@librechat/api": "*",
|
||||||
"@librechat/data-schemas": "*",
|
"@librechat/data-schemas": "*",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
|
|
|
@ -872,11 +872,10 @@ class AgentClient extends BaseClient {
|
||||||
if (agent.useLegacyContent === true) {
|
if (agent.useLegacyContent === true) {
|
||||||
messages = formatContentStrings(messages);
|
messages = formatContentStrings(messages);
|
||||||
}
|
}
|
||||||
if (
|
const defaultHeaders =
|
||||||
agent.model_parameters?.clientOptions?.defaultHeaders?.['anthropic-beta']?.includes(
|
agent.model_parameters?.clientOptions?.defaultHeaders ??
|
||||||
'prompt-caching',
|
agent.model_parameters?.configuration?.defaultHeaders;
|
||||||
)
|
if (defaultHeaders?.['anthropic-beta']?.includes('prompt-caching')) {
|
||||||
) {
|
|
||||||
messages = addCacheControl(messages);
|
messages = addCacheControl(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const { v4 } = require('uuid');
|
const { v4 } = require('uuid');
|
||||||
const { sleep } = require('@librechat/agents');
|
const { sleep } = require('@librechat/agents');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { sendEvent, getBalanceConfig } = require('@librechat/api');
|
const { sendEvent, getBalanceConfig, getModelMaxTokens } = require('@librechat/api');
|
||||||
const {
|
const {
|
||||||
Time,
|
Time,
|
||||||
Constants,
|
Constants,
|
||||||
|
@ -34,7 +34,6 @@ const { checkBalance } = require('~/models/balanceMethods');
|
||||||
const { getConvo } = require('~/models/Conversation');
|
const { getConvo } = require('~/models/Conversation');
|
||||||
const getLogStores = require('~/cache/getLogStores');
|
const getLogStores = require('~/cache/getLogStores');
|
||||||
const { countTokens } = require('~/server/utils');
|
const { countTokens } = require('~/server/utils');
|
||||||
const { getModelMaxTokens } = require('~/utils');
|
|
||||||
const { getOpenAIClient } = require('./helpers');
|
const { getOpenAIClient } = require('./helpers');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const { v4 } = require('uuid');
|
const { v4 } = require('uuid');
|
||||||
const { sleep } = require('@librechat/agents');
|
const { sleep } = require('@librechat/agents');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { sendEvent, getBalanceConfig } = require('@librechat/api');
|
const { sendEvent, getBalanceConfig, getModelMaxTokens } = require('@librechat/api');
|
||||||
const {
|
const {
|
||||||
Time,
|
Time,
|
||||||
Constants,
|
Constants,
|
||||||
|
@ -31,7 +31,6 @@ const { checkBalance } = require('~/models/balanceMethods');
|
||||||
const { getConvo } = require('~/models/Conversation');
|
const { getConvo } = require('~/models/Conversation');
|
||||||
const getLogStores = require('~/cache/getLogStores');
|
const getLogStores = require('~/cache/getLogStores');
|
||||||
const { countTokens } = require('~/server/utils');
|
const { countTokens } = require('~/server/utils');
|
||||||
const { getModelMaxTokens } = require('~/utils');
|
|
||||||
const { getOpenAIClient } = require('./helpers');
|
const { getOpenAIClient } = require('./helpers');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const { Providers } = require('@librechat/agents');
|
const { Providers } = require('@librechat/agents');
|
||||||
const {
|
const {
|
||||||
primeResources,
|
primeResources,
|
||||||
|
getModelMaxTokens,
|
||||||
extractLibreChatParams,
|
extractLibreChatParams,
|
||||||
optionalChainWithEmptyCheck,
|
optionalChainWithEmptyCheck,
|
||||||
} = require('@librechat/api');
|
} = require('@librechat/api');
|
||||||
|
@ -17,7 +18,6 @@ const { getProviderConfig } = require('~/server/services/Endpoints');
|
||||||
const { processFiles } = require('~/server/services/Files/process');
|
const { processFiles } = require('~/server/services/Files/process');
|
||||||
const { getFiles, getToolFilesByIds } = require('~/models/File');
|
const { getFiles, getToolFilesByIds } = require('~/models/File');
|
||||||
const { getConvoFiles } = require('~/models/Conversation');
|
const { getConvoFiles } = require('~/models/Conversation');
|
||||||
const { getModelMaxTokens } = require('~/utils');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} params
|
* @param {object} params
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
const { getLLMConfig } = require('@librechat/api');
|
||||||
const { EModelEndpoint } = require('librechat-data-provider');
|
const { EModelEndpoint } = require('librechat-data-provider');
|
||||||
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||||
const { getLLMConfig } = require('~/server/services/Endpoints/anthropic/llm');
|
|
||||||
const AnthropicClient = require('~/app/clients/AnthropicClient');
|
const AnthropicClient = require('~/app/clients/AnthropicClient');
|
||||||
|
|
||||||
const initializeClient = async ({ req, res, endpointOption, overrideModel, optionsOnly }) => {
|
const initializeClient = async ({ req, res, endpointOption, overrideModel, optionsOnly }) => {
|
||||||
|
@ -40,7 +40,6 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio
|
||||||
clientOptions = Object.assign(
|
clientOptions = Object.assign(
|
||||||
{
|
{
|
||||||
proxy: PROXY ?? null,
|
proxy: PROXY ?? null,
|
||||||
userId: req.user.id,
|
|
||||||
reverseProxyUrl: ANTHROPIC_REVERSE_PROXY ?? null,
|
reverseProxyUrl: ANTHROPIC_REVERSE_PROXY ?? null,
|
||||||
modelOptions: endpointOption?.model_parameters ?? {},
|
modelOptions: endpointOption?.model_parameters ?? {},
|
||||||
},
|
},
|
||||||
|
@ -49,6 +48,7 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio
|
||||||
if (overrideModel) {
|
if (overrideModel) {
|
||||||
clientOptions.modelOptions.model = overrideModel;
|
clientOptions.modelOptions.model = overrideModel;
|
||||||
}
|
}
|
||||||
|
clientOptions.modelOptions.user = req.user.id;
|
||||||
return getLLMConfig(anthropicApiKey, clientOptions);
|
return getLLMConfig(anthropicApiKey, clientOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,103 +0,0 @@
|
||||||
const { ProxyAgent } = require('undici');
|
|
||||||
const { anthropicSettings, removeNullishValues } = require('librechat-data-provider');
|
|
||||||
const { checkPromptCacheSupport, getClaudeHeaders, configureReasoning } = require('./helpers');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates configuration options for creating an Anthropic language model (LLM) instance.
|
|
||||||
*
|
|
||||||
* @param {string} apiKey - The API key for authentication with Anthropic.
|
|
||||||
* @param {Object} [options={}] - Additional options for configuring the LLM.
|
|
||||||
* @param {Object} [options.modelOptions] - Model-specific options.
|
|
||||||
* @param {string} [options.modelOptions.model] - The name of the model to use.
|
|
||||||
* @param {number} [options.modelOptions.maxOutputTokens] - The maximum number of tokens to generate.
|
|
||||||
* @param {number} [options.modelOptions.temperature] - Controls randomness in output generation.
|
|
||||||
* @param {number} [options.modelOptions.topP] - Controls diversity of output generation.
|
|
||||||
* @param {number} [options.modelOptions.topK] - Controls the number of top tokens to consider.
|
|
||||||
* @param {string[]} [options.modelOptions.stop] - Sequences where the API will stop generating further tokens.
|
|
||||||
* @param {boolean} [options.modelOptions.stream] - Whether to stream the response.
|
|
||||||
* @param {string} options.userId - The user ID for tracking and personalization.
|
|
||||||
* @param {string} [options.proxy] - Proxy server URL.
|
|
||||||
* @param {string} [options.reverseProxyUrl] - URL for a reverse proxy, if used.
|
|
||||||
*
|
|
||||||
* @returns {Object} Configuration options for creating an Anthropic LLM instance, with null and undefined values removed.
|
|
||||||
*/
|
|
||||||
function getLLMConfig(apiKey, options = {}) {
|
|
||||||
const systemOptions = {
|
|
||||||
thinking: options.modelOptions.thinking ?? anthropicSettings.thinking.default,
|
|
||||||
promptCache: options.modelOptions.promptCache ?? anthropicSettings.promptCache.default,
|
|
||||||
thinkingBudget: options.modelOptions.thinkingBudget ?? anthropicSettings.thinkingBudget.default,
|
|
||||||
};
|
|
||||||
for (let key in systemOptions) {
|
|
||||||
delete options.modelOptions[key];
|
|
||||||
}
|
|
||||||
const defaultOptions = {
|
|
||||||
model: anthropicSettings.model.default,
|
|
||||||
maxOutputTokens: anthropicSettings.maxOutputTokens.default,
|
|
||||||
stream: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mergedOptions = Object.assign(defaultOptions, options.modelOptions);
|
|
||||||
|
|
||||||
/** @type {AnthropicClientOptions} */
|
|
||||||
let requestOptions = {
|
|
||||||
apiKey,
|
|
||||||
model: mergedOptions.model,
|
|
||||||
stream: mergedOptions.stream,
|
|
||||||
temperature: mergedOptions.temperature,
|
|
||||||
stopSequences: mergedOptions.stop,
|
|
||||||
maxTokens:
|
|
||||||
mergedOptions.maxOutputTokens || anthropicSettings.maxOutputTokens.reset(mergedOptions.model),
|
|
||||||
clientOptions: {},
|
|
||||||
invocationKwargs: {
|
|
||||||
metadata: {
|
|
||||||
user_id: options.userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
requestOptions = configureReasoning(requestOptions, systemOptions);
|
|
||||||
|
|
||||||
if (!/claude-3[-.]7/.test(mergedOptions.model)) {
|
|
||||||
requestOptions.topP = mergedOptions.topP;
|
|
||||||
requestOptions.topK = mergedOptions.topK;
|
|
||||||
} else if (requestOptions.thinking == null) {
|
|
||||||
requestOptions.topP = mergedOptions.topP;
|
|
||||||
requestOptions.topK = mergedOptions.topK;
|
|
||||||
}
|
|
||||||
|
|
||||||
const supportsCacheControl =
|
|
||||||
systemOptions.promptCache === true && checkPromptCacheSupport(requestOptions.model);
|
|
||||||
const headers = getClaudeHeaders(requestOptions.model, supportsCacheControl);
|
|
||||||
if (headers) {
|
|
||||||
requestOptions.clientOptions.defaultHeaders = headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.proxy) {
|
|
||||||
const proxyAgent = new ProxyAgent(options.proxy);
|
|
||||||
requestOptions.clientOptions.fetchOptions = {
|
|
||||||
dispatcher: proxyAgent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.reverseProxyUrl) {
|
|
||||||
requestOptions.clientOptions.baseURL = options.reverseProxyUrl;
|
|
||||||
requestOptions.anthropicApiUrl = options.reverseProxyUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tools = [];
|
|
||||||
|
|
||||||
if (mergedOptions.web_search) {
|
|
||||||
tools.push({
|
|
||||||
type: 'web_search_20250305',
|
|
||||||
name: 'web_search',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
tools,
|
|
||||||
/** @type {AnthropicClientOptions} */
|
|
||||||
llmConfig: removeNullishValues(requestOptions),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { getLLMConfig };
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
const { getModelMaxTokens } = require('@librechat/api');
|
||||||
const { createContentAggregator } = require('@librechat/agents');
|
const { createContentAggregator } = require('@librechat/agents');
|
||||||
const {
|
const {
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
|
@ -7,7 +8,6 @@ const {
|
||||||
const { getDefaultHandlers } = require('~/server/controllers/agents/callbacks');
|
const { getDefaultHandlers } = require('~/server/controllers/agents/callbacks');
|
||||||
const getOptions = require('~/server/services/Endpoints/bedrock/options');
|
const getOptions = require('~/server/services/Endpoints/bedrock/options');
|
||||||
const AgentClient = require('~/server/controllers/agents/client');
|
const AgentClient = require('~/server/controllers/agents/client');
|
||||||
const { getModelMaxTokens } = require('~/utils');
|
|
||||||
|
|
||||||
const initializeClient = async ({ req, res, endpointOption }) => {
|
const initializeClient = async ({ req, res, endpointOption }) => {
|
||||||
if (!endpointOption) {
|
if (!endpointOption) {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const { Providers } = require('@librechat/agents');
|
const { Providers } = require('@librechat/agents');
|
||||||
const { logAxiosError } = require('@librechat/api');
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||||
|
const { logAxiosError, inputSchema, processModelData } = require('@librechat/api');
|
||||||
const { EModelEndpoint, defaultModels, CacheKeys } = require('librechat-data-provider');
|
const { EModelEndpoint, defaultModels, CacheKeys } = require('librechat-data-provider');
|
||||||
const { inputSchema, extractBaseURL, processModelData } = require('~/utils');
|
|
||||||
const { OllamaClient } = require('~/app/clients/OllamaClient');
|
const { OllamaClient } = require('~/app/clients/OllamaClient');
|
||||||
const { isUserProvided } = require('~/server/utils');
|
const { isUserProvided } = require('~/server/utils');
|
||||||
const getLogStores = require('~/cache/getLogStores');
|
const getLogStores = require('~/cache/getLogStores');
|
||||||
|
const { extractBaseURL } = require('~/utils');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits a string by commas and trims each resulting value.
|
* Splits a string by commas and trims each resulting value.
|
||||||
|
|
|
@ -11,8 +11,8 @@ const {
|
||||||
getAnthropicModels,
|
getAnthropicModels,
|
||||||
} = require('./ModelService');
|
} = require('./ModelService');
|
||||||
|
|
||||||
jest.mock('~/utils', () => {
|
jest.mock('@librechat/api', () => {
|
||||||
const originalUtils = jest.requireActual('~/utils');
|
const originalUtils = jest.requireActual('@librechat/api');
|
||||||
return {
|
return {
|
||||||
...originalUtils,
|
...originalUtils,
|
||||||
processModelData: jest.fn((...args) => {
|
processModelData: jest.fn((...args) => {
|
||||||
|
@ -108,7 +108,7 @@ describe('fetchModels with createTokenConfig true', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Clears the mock's history before each test
|
// Clears the mock's history before each test
|
||||||
const _utils = require('~/utils');
|
const _utils = require('@librechat/api');
|
||||||
axios.get.mockResolvedValue({ data });
|
axios.get.mockResolvedValue({ data });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ describe('fetchModels with createTokenConfig true', () => {
|
||||||
createTokenConfig: true,
|
createTokenConfig: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { processModelData } = require('~/utils');
|
const { processModelData } = require('@librechat/api');
|
||||||
expect(processModelData).toHaveBeenCalled();
|
expect(processModelData).toHaveBeenCalled();
|
||||||
expect(processModelData).toHaveBeenCalledWith(data);
|
expect(processModelData).toHaveBeenCalledWith(data);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const deriveBaseURL = require('./deriveBaseURL');
|
const deriveBaseURL = require('./deriveBaseURL');
|
||||||
jest.mock('~/utils', () => {
|
jest.mock('@librechat/api', () => {
|
||||||
const originalUtils = jest.requireActual('~/utils');
|
const originalUtils = jest.requireActual('@librechat/api');
|
||||||
return {
|
return {
|
||||||
...originalUtils,
|
...originalUtils,
|
||||||
processModelData: jest.fn((...args) => {
|
processModelData: jest.fn((...args) => {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
const tokenHelpers = require('./tokens');
|
|
||||||
const deriveBaseURL = require('./deriveBaseURL');
|
const deriveBaseURL = require('./deriveBaseURL');
|
||||||
const extractBaseURL = require('./extractBaseURL');
|
const extractBaseURL = require('./extractBaseURL');
|
||||||
const findMessageContent = require('./findMessageContent');
|
const findMessageContent = require('./findMessageContent');
|
||||||
|
@ -6,6 +5,5 @@ const findMessageContent = require('./findMessageContent');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
deriveBaseURL,
|
deriveBaseURL,
|
||||||
extractBaseURL,
|
extractBaseURL,
|
||||||
...tokenHelpers,
|
|
||||||
findMessageContent,
|
findMessageContent,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
const { EModelEndpoint } = require('librechat-data-provider');
|
const { EModelEndpoint } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
|
maxTokensMap,
|
||||||
|
matchModelName,
|
||||||
|
processModelData,
|
||||||
|
getModelMaxTokens,
|
||||||
maxOutputTokensMap,
|
maxOutputTokensMap,
|
||||||
findMatchingPattern,
|
findMatchingPattern,
|
||||||
getModelMaxTokens,
|
} = require('@librechat/api');
|
||||||
processModelData,
|
|
||||||
matchModelName,
|
|
||||||
maxTokensMap,
|
|
||||||
} = require('./tokens');
|
|
||||||
|
|
||||||
describe('getModelMaxTokens', () => {
|
describe('getModelMaxTokens', () => {
|
||||||
test('should return correct tokens for exact match', () => {
|
test('should return correct tokens for exact match', () => {
|
||||||
|
@ -394,7 +394,7 @@ describe('getModelMaxTokens', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return correct max output tokens for GPT-5 models', () => {
|
test('should return correct max output tokens for GPT-5 models', () => {
|
||||||
const { getModelMaxOutputTokens } = require('./tokens');
|
const { getModelMaxOutputTokens } = require('@librechat/api');
|
||||||
['gpt-5', 'gpt-5-mini', 'gpt-5-nano'].forEach((model) => {
|
['gpt-5', 'gpt-5-mini', 'gpt-5-nano'].forEach((model) => {
|
||||||
expect(getModelMaxOutputTokens(model)).toBe(maxOutputTokensMap[EModelEndpoint.openAI][model]);
|
expect(getModelMaxOutputTokens(model)).toBe(maxOutputTokensMap[EModelEndpoint.openAI][model]);
|
||||||
expect(getModelMaxOutputTokens(model, EModelEndpoint.openAI)).toBe(
|
expect(getModelMaxOutputTokens(model, EModelEndpoint.openAI)).toBe(
|
||||||
|
@ -407,7 +407,7 @@ describe('getModelMaxTokens', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return correct max output tokens for GPT-OSS models', () => {
|
test('should return correct max output tokens for GPT-OSS models', () => {
|
||||||
const { getModelMaxOutputTokens } = require('./tokens');
|
const { getModelMaxOutputTokens } = require('@librechat/api');
|
||||||
['gpt-oss-20b', 'gpt-oss-120b'].forEach((model) => {
|
['gpt-oss-20b', 'gpt-oss-120b'].forEach((model) => {
|
||||||
expect(getModelMaxOutputTokens(model)).toBe(maxOutputTokensMap[EModelEndpoint.openAI][model]);
|
expect(getModelMaxOutputTokens(model)).toBe(maxOutputTokensMap[EModelEndpoint.openAI][model]);
|
||||||
expect(getModelMaxOutputTokens(model, EModelEndpoint.openAI)).toBe(
|
expect(getModelMaxOutputTokens(model, EModelEndpoint.openAI)).toBe(
|
||||||
|
|
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -64,7 +64,7 @@
|
||||||
"@langchain/google-vertexai": "^0.2.13",
|
"@langchain/google-vertexai": "^0.2.13",
|
||||||
"@langchain/openai": "^0.5.18",
|
"@langchain/openai": "^0.5.18",
|
||||||
"@langchain/textsplitters": "^0.1.0",
|
"@langchain/textsplitters": "^0.1.0",
|
||||||
"@librechat/agents": "^2.4.77",
|
"@librechat/agents": "^2.4.79",
|
||||||
"@librechat/api": "*",
|
"@librechat/api": "*",
|
||||||
"@librechat/data-schemas": "*",
|
"@librechat/data-schemas": "*",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
|
@ -21909,9 +21909,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@librechat/agents": {
|
"node_modules/@librechat/agents": {
|
||||||
"version": "2.4.77",
|
"version": "2.4.79",
|
||||||
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.77.tgz",
|
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.79.tgz",
|
||||||
"integrity": "sha512-x7fWbbdJpy8VpIYJa7E0laBUmtgveTmTzYS8QFkXUMjzqSx7nN5ruM6rzmcodOWRXt7IrB12k4VehJ1zUnb29A==",
|
"integrity": "sha512-Ha8tBPNy9ycPMH+GfBL8lUKz4vC3aXWSO1BZt7x9wDkfVLQBd3XhtkYv0xMvA8y7i6YMowBoyAkkWpX3R8DeJg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@langchain/anthropic": "^0.3.26",
|
"@langchain/anthropic": "^0.3.26",
|
||||||
|
@ -51711,7 +51711,7 @@
|
||||||
},
|
},
|
||||||
"packages/api": {
|
"packages/api": {
|
||||||
"name": "@librechat/api",
|
"name": "@librechat/api",
|
||||||
"version": "1.3.4",
|
"version": "1.3.5",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-env": "^7.21.5",
|
"@babel/preset-env": "^7.21.5",
|
||||||
|
@ -51744,7 +51744,7 @@
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@langchain/core": "^0.3.62",
|
"@langchain/core": "^0.3.62",
|
||||||
"@librechat/agents": "^2.4.77",
|
"@librechat/agents": "^2.4.79",
|
||||||
"@librechat/data-schemas": "*",
|
"@librechat/data-schemas": "*",
|
||||||
"@modelcontextprotocol/sdk": "^1.17.1",
|
"@modelcontextprotocol/sdk": "^1.17.1",
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@librechat/api",
|
"name": "@librechat/api",
|
||||||
"version": "1.3.4",
|
"version": "1.3.5",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"description": "MCP services for LibreChat",
|
"description": "MCP services for LibreChat",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
@ -73,7 +73,7 @@
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@langchain/core": "^0.3.62",
|
"@langchain/core": "^0.3.62",
|
||||||
"@librechat/agents": "^2.4.77",
|
"@librechat/agents": "^2.4.79",
|
||||||
"@librechat/data-schemas": "*",
|
"@librechat/data-schemas": "*",
|
||||||
"@modelcontextprotocol/sdk": "^1.17.1",
|
"@modelcontextprotocol/sdk": "^1.17.1",
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
const { EModelEndpoint, anthropicSettings } = require('librechat-data-provider');
|
import { logger } from '@librechat/data-schemas';
|
||||||
const { matchModelName } = require('~/utils');
|
import { AnthropicClientOptions } from '@librechat/agents';
|
||||||
const { logger } = require('~/config');
|
import { EModelEndpoint, anthropicSettings } from 'librechat-data-provider';
|
||||||
|
import { matchModelName } from '~/utils/tokens';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} modelName
|
* @param {string} modelName
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function checkPromptCacheSupport(modelName) {
|
function checkPromptCacheSupport(modelName: string): boolean {
|
||||||
const modelMatch = matchModelName(modelName, EModelEndpoint.anthropic);
|
const modelMatch = matchModelName(modelName, EModelEndpoint.anthropic) ?? '';
|
||||||
if (
|
if (
|
||||||
modelMatch.includes('claude-3-5-sonnet-latest') ||
|
modelMatch.includes('claude-3-5-sonnet-latest') ||
|
||||||
modelMatch.includes('claude-3.5-sonnet-latest')
|
modelMatch.includes('claude-3.5-sonnet-latest')
|
||||||
|
@ -31,7 +32,10 @@ function checkPromptCacheSupport(modelName) {
|
||||||
* @param {boolean} supportsCacheControl Whether the model supports cache control
|
* @param {boolean} supportsCacheControl Whether the model supports cache control
|
||||||
* @returns {AnthropicClientOptions['extendedOptions']['defaultHeaders']|undefined} The headers object or undefined if not applicable
|
* @returns {AnthropicClientOptions['extendedOptions']['defaultHeaders']|undefined} The headers object or undefined if not applicable
|
||||||
*/
|
*/
|
||||||
function getClaudeHeaders(model, supportsCacheControl) {
|
function getClaudeHeaders(
|
||||||
|
model: string,
|
||||||
|
supportsCacheControl: boolean,
|
||||||
|
): Record<string, string> | undefined {
|
||||||
if (!supportsCacheControl) {
|
if (!supportsCacheControl) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -72,9 +76,13 @@ function getClaudeHeaders(model, supportsCacheControl) {
|
||||||
* @param {number|null} extendedOptions.thinkingBudget The token budget for thinking
|
* @param {number|null} extendedOptions.thinkingBudget The token budget for thinking
|
||||||
* @returns {Object} Updated request options
|
* @returns {Object} Updated request options
|
||||||
*/
|
*/
|
||||||
function configureReasoning(anthropicInput, extendedOptions = {}) {
|
function configureReasoning(
|
||||||
|
anthropicInput: AnthropicClientOptions & { max_tokens?: number },
|
||||||
|
extendedOptions: { thinking?: boolean; thinkingBudget?: number | null } = {},
|
||||||
|
): AnthropicClientOptions & { max_tokens?: number } {
|
||||||
const updatedOptions = { ...anthropicInput };
|
const updatedOptions = { ...anthropicInput };
|
||||||
const currentMaxTokens = updatedOptions.max_tokens ?? updatedOptions.maxTokens;
|
const currentMaxTokens = updatedOptions.max_tokens ?? updatedOptions.maxTokens;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
extendedOptions.thinking &&
|
extendedOptions.thinking &&
|
||||||
updatedOptions?.model &&
|
updatedOptions?.model &&
|
||||||
|
@ -82,11 +90,16 @@ function configureReasoning(anthropicInput, extendedOptions = {}) {
|
||||||
/claude-(?:sonnet|opus|haiku)-[4-9]/.test(updatedOptions.model))
|
/claude-(?:sonnet|opus|haiku)-[4-9]/.test(updatedOptions.model))
|
||||||
) {
|
) {
|
||||||
updatedOptions.thinking = {
|
updatedOptions.thinking = {
|
||||||
|
...updatedOptions.thinking,
|
||||||
type: 'enabled',
|
type: 'enabled',
|
||||||
};
|
} as { type: 'enabled'; budget_tokens: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedOptions.thinking != null && extendedOptions.thinkingBudget != null) {
|
if (
|
||||||
|
updatedOptions.thinking != null &&
|
||||||
|
extendedOptions.thinkingBudget != null &&
|
||||||
|
updatedOptions.thinking.type === 'enabled'
|
||||||
|
) {
|
||||||
updatedOptions.thinking = {
|
updatedOptions.thinking = {
|
||||||
...updatedOptions.thinking,
|
...updatedOptions.thinking,
|
||||||
budget_tokens: extendedOptions.thinkingBudget,
|
budget_tokens: extendedOptions.thinkingBudget,
|
||||||
|
@ -95,9 +108,10 @@ function configureReasoning(anthropicInput, extendedOptions = {}) {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
updatedOptions.thinking != null &&
|
updatedOptions.thinking != null &&
|
||||||
|
updatedOptions.thinking.type === 'enabled' &&
|
||||||
(currentMaxTokens == null || updatedOptions.thinking.budget_tokens > currentMaxTokens)
|
(currentMaxTokens == null || updatedOptions.thinking.budget_tokens > currentMaxTokens)
|
||||||
) {
|
) {
|
||||||
const maxTokens = anthropicSettings.maxOutputTokens.reset(updatedOptions.model);
|
const maxTokens = anthropicSettings.maxOutputTokens.reset(updatedOptions.model ?? '');
|
||||||
updatedOptions.max_tokens = currentMaxTokens ?? maxTokens;
|
updatedOptions.max_tokens = currentMaxTokens ?? maxTokens;
|
||||||
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
@ -115,4 +129,4 @@ function configureReasoning(anthropicInput, extendedOptions = {}) {
|
||||||
return updatedOptions;
|
return updatedOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { checkPromptCacheSupport, getClaudeHeaders, configureReasoning };
|
export { checkPromptCacheSupport, getClaudeHeaders, configureReasoning };
|
2
packages/api/src/endpoints/anthropic/index.ts
Normal file
2
packages/api/src/endpoints/anthropic/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './helpers';
|
||||||
|
export * from './llm';
|
|
@ -1,4 +1,5 @@
|
||||||
const { getLLMConfig } = require('~/server/services/Endpoints/anthropic/llm');
|
import { getLLMConfig } from './llm';
|
||||||
|
import type * as t from '~/types';
|
||||||
|
|
||||||
jest.mock('https-proxy-agent', () => ({
|
jest.mock('https-proxy-agent', () => ({
|
||||||
HttpsProxyAgent: jest.fn().mockImplementation((proxy) => ({ proxy })),
|
HttpsProxyAgent: jest.fn().mockImplementation((proxy) => ({ proxy })),
|
||||||
|
@ -25,9 +26,9 @@ describe('getLLMConfig', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.llmConfig.clientOptions).toHaveProperty('fetchOptions');
|
expect(result.llmConfig.clientOptions).toHaveProperty('fetchOptions');
|
||||||
expect(result.llmConfig.clientOptions.fetchOptions).toHaveProperty('dispatcher');
|
expect(result.llmConfig.clientOptions?.fetchOptions).toHaveProperty('dispatcher');
|
||||||
expect(result.llmConfig.clientOptions.fetchOptions.dispatcher).toBeDefined();
|
expect(result.llmConfig.clientOptions?.fetchOptions?.dispatcher).toBeDefined();
|
||||||
expect(result.llmConfig.clientOptions.fetchOptions.dispatcher.constructor.name).toBe(
|
expect(result.llmConfig.clientOptions?.fetchOptions?.dispatcher.constructor.name).toBe(
|
||||||
'ProxyAgent',
|
'ProxyAgent',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -93,9 +94,10 @@ describe('getLLMConfig', () => {
|
||||||
};
|
};
|
||||||
const result = getLLMConfig('test-key', { modelOptions });
|
const result = getLLMConfig('test-key', { modelOptions });
|
||||||
const clientOptions = result.llmConfig.clientOptions;
|
const clientOptions = result.llmConfig.clientOptions;
|
||||||
expect(clientOptions.defaultHeaders).toBeDefined();
|
expect(clientOptions?.defaultHeaders).toBeDefined();
|
||||||
expect(clientOptions.defaultHeaders).toHaveProperty('anthropic-beta');
|
expect(clientOptions?.defaultHeaders).toHaveProperty('anthropic-beta');
|
||||||
expect(clientOptions.defaultHeaders['anthropic-beta']).toBe(
|
const defaultHeaders = clientOptions?.defaultHeaders as Record<string, string>;
|
||||||
|
expect(defaultHeaders['anthropic-beta']).toBe(
|
||||||
'prompt-caching-2024-07-31,context-1m-2025-08-07',
|
'prompt-caching-2024-07-31,context-1m-2025-08-07',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -111,9 +113,10 @@ describe('getLLMConfig', () => {
|
||||||
const modelOptions = { model, promptCache: true };
|
const modelOptions = { model, promptCache: true };
|
||||||
const result = getLLMConfig('test-key', { modelOptions });
|
const result = getLLMConfig('test-key', { modelOptions });
|
||||||
const clientOptions = result.llmConfig.clientOptions;
|
const clientOptions = result.llmConfig.clientOptions;
|
||||||
expect(clientOptions.defaultHeaders).toBeDefined();
|
expect(clientOptions?.defaultHeaders).toBeDefined();
|
||||||
expect(clientOptions.defaultHeaders).toHaveProperty('anthropic-beta');
|
expect(clientOptions?.defaultHeaders).toHaveProperty('anthropic-beta');
|
||||||
expect(clientOptions.defaultHeaders['anthropic-beta']).toBe(
|
const defaultHeaders = clientOptions?.defaultHeaders as Record<string, string>;
|
||||||
|
expect(defaultHeaders['anthropic-beta']).toBe(
|
||||||
'prompt-caching-2024-07-31,context-1m-2025-08-07',
|
'prompt-caching-2024-07-31,context-1m-2025-08-07',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -211,13 +214,13 @@ describe('getLLMConfig', () => {
|
||||||
it('should handle empty modelOptions', () => {
|
it('should handle empty modelOptions', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
getLLMConfig('test-api-key', {});
|
getLLMConfig('test-api-key', {});
|
||||||
}).toThrow("Cannot read properties of undefined (reading 'thinking')");
|
}).toThrow('No modelOptions provided');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle no options parameter', () => {
|
it('should handle no options parameter', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
getLLMConfig('test-api-key');
|
getLLMConfig('test-api-key');
|
||||||
}).toThrow("Cannot read properties of undefined (reading 'thinking')");
|
}).toThrow('No modelOptions provided');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle temperature, stop sequences, and stream settings', () => {
|
it('should handle temperature, stop sequences, and stream settings', () => {
|
||||||
|
@ -238,7 +241,7 @@ describe('getLLMConfig', () => {
|
||||||
const result = getLLMConfig('test-api-key', {
|
const result = getLLMConfig('test-api-key', {
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
model: 'claude-3-opus',
|
model: 'claude-3-opus',
|
||||||
maxOutputTokens: null,
|
maxOutputTokens: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -254,9 +257,9 @@ describe('getLLMConfig', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.llmConfig.clientOptions).toHaveProperty('fetchOptions');
|
expect(result.llmConfig.clientOptions).toHaveProperty('fetchOptions');
|
||||||
expect(result.llmConfig.clientOptions.fetchOptions).toHaveProperty('dispatcher');
|
expect(result.llmConfig.clientOptions?.fetchOptions).toHaveProperty('dispatcher');
|
||||||
expect(result.llmConfig.clientOptions.fetchOptions.dispatcher).toBeDefined();
|
expect(result.llmConfig.clientOptions?.fetchOptions?.dispatcher).toBeDefined();
|
||||||
expect(result.llmConfig.clientOptions.fetchOptions.dispatcher.constructor.name).toBe(
|
expect(result.llmConfig.clientOptions?.fetchOptions?.dispatcher.constructor.name).toBe(
|
||||||
'ProxyAgent',
|
'ProxyAgent',
|
||||||
);
|
);
|
||||||
expect(result.llmConfig.clientOptions).toHaveProperty('baseURL', 'https://reverse-proxy.com');
|
expect(result.llmConfig.clientOptions).toHaveProperty('baseURL', 'https://reverse-proxy.com');
|
||||||
|
@ -272,7 +275,7 @@ describe('getLLMConfig', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// claude-3-5-sonnet supports prompt caching and should get the appropriate headers
|
// claude-3-5-sonnet supports prompt caching and should get the appropriate headers
|
||||||
expect(result.llmConfig.clientOptions.defaultHeaders).toEqual({
|
expect(result.llmConfig.clientOptions?.defaultHeaders).toEqual({
|
||||||
'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15,prompt-caching-2024-07-31',
|
'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15,prompt-caching-2024-07-31',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -325,7 +328,7 @@ describe('getLLMConfig', () => {
|
||||||
it('should handle all nullish values removal', () => {
|
it('should handle all nullish values removal', () => {
|
||||||
const result = getLLMConfig('test-api-key', {
|
const result = getLLMConfig('test-api-key', {
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
temperature: null,
|
temperature: undefined,
|
||||||
topP: undefined,
|
topP: undefined,
|
||||||
topK: 0,
|
topK: 0,
|
||||||
stop: [],
|
stop: [],
|
||||||
|
@ -359,9 +362,11 @@ describe('getLLMConfig', () => {
|
||||||
// Simulate clientOptions from initialize.js
|
// Simulate clientOptions from initialize.js
|
||||||
const clientOptions = {
|
const clientOptions = {
|
||||||
proxy: null,
|
proxy: null,
|
||||||
userId: 'test-user-id-123',
|
|
||||||
reverseProxyUrl: null,
|
reverseProxyUrl: null,
|
||||||
modelOptions: endpointOption.model_parameters,
|
modelOptions: {
|
||||||
|
...endpointOption.model_parameters,
|
||||||
|
user: 'test-user-id-123',
|
||||||
|
},
|
||||||
streamRate: 25,
|
streamRate: 25,
|
||||||
titleModel: 'claude-3-haiku',
|
titleModel: 'claude-3-haiku',
|
||||||
};
|
};
|
||||||
|
@ -390,12 +395,12 @@ describe('getLLMConfig', () => {
|
||||||
const anthropicApiKey = 'sk-ant-proxy-key';
|
const anthropicApiKey = 'sk-ant-proxy-key';
|
||||||
const clientOptions = {
|
const clientOptions = {
|
||||||
proxy: 'http://corporate-proxy:8080',
|
proxy: 'http://corporate-proxy:8080',
|
||||||
userId: 'proxy-user-456',
|
|
||||||
reverseProxyUrl: null,
|
reverseProxyUrl: null,
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
model: 'claude-3-opus',
|
model: 'claude-3-opus',
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
maxOutputTokens: 2048,
|
maxOutputTokens: 2048,
|
||||||
|
user: 'proxy-user-456',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -412,8 +417,8 @@ describe('getLLMConfig', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(result.llmConfig.clientOptions.fetchOptions).toHaveProperty('dispatcher');
|
expect(result.llmConfig.clientOptions?.fetchOptions).toHaveProperty('dispatcher');
|
||||||
expect(result.llmConfig.clientOptions.fetchOptions.dispatcher.constructor.name).toBe(
|
expect(result.llmConfig.clientOptions?.fetchOptions?.dispatcher.constructor.name).toBe(
|
||||||
'ProxyAgent',
|
'ProxyAgent',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -423,12 +428,12 @@ describe('getLLMConfig', () => {
|
||||||
const reverseProxyUrl = 'https://api.custom-anthropic.com/v1';
|
const reverseProxyUrl = 'https://api.custom-anthropic.com/v1';
|
||||||
const clientOptions = {
|
const clientOptions = {
|
||||||
proxy: null,
|
proxy: null,
|
||||||
userId: 'reverse-proxy-user',
|
|
||||||
reverseProxyUrl: reverseProxyUrl,
|
reverseProxyUrl: reverseProxyUrl,
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
model: 'claude-3-5-haiku',
|
model: 'claude-3-5-haiku',
|
||||||
temperature: 0.5,
|
temperature: 0.5,
|
||||||
stream: false,
|
stream: false,
|
||||||
|
user: 'reverse-proxy-user',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -450,7 +455,6 @@ describe('getLLMConfig', () => {
|
||||||
describe('Model-Specific Real Usage Scenarios', () => {
|
describe('Model-Specific Real Usage Scenarios', () => {
|
||||||
it('should handle Claude-3.7 with thinking enabled like production', () => {
|
it('should handle Claude-3.7 with thinking enabled like production', () => {
|
||||||
const clientOptions = {
|
const clientOptions = {
|
||||||
userId: 'thinking-user-789',
|
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
model: 'claude-3-7-sonnet',
|
model: 'claude-3-7-sonnet',
|
||||||
temperature: 0.4,
|
temperature: 0.4,
|
||||||
|
@ -460,6 +464,7 @@ describe('getLLMConfig', () => {
|
||||||
thinking: true,
|
thinking: true,
|
||||||
thinkingBudget: 3000,
|
thinkingBudget: 3000,
|
||||||
promptCache: true,
|
promptCache: true,
|
||||||
|
user: 'thinking-user-789',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -479,7 +484,7 @@ describe('getLLMConfig', () => {
|
||||||
expect(result.llmConfig).not.toHaveProperty('topP');
|
expect(result.llmConfig).not.toHaveProperty('topP');
|
||||||
expect(result.llmConfig).not.toHaveProperty('topK');
|
expect(result.llmConfig).not.toHaveProperty('topK');
|
||||||
// Should have appropriate headers for Claude-3.7 with prompt cache
|
// Should have appropriate headers for Claude-3.7 with prompt cache
|
||||||
expect(result.llmConfig.clientOptions.defaultHeaders).toEqual({
|
expect(result.llmConfig.clientOptions?.defaultHeaders).toEqual({
|
||||||
'anthropic-beta':
|
'anthropic-beta':
|
||||||
'token-efficient-tools-2025-02-19,output-128k-2025-02-19,prompt-caching-2024-07-31',
|
'token-efficient-tools-2025-02-19,output-128k-2025-02-19,prompt-caching-2024-07-31',
|
||||||
});
|
});
|
||||||
|
@ -487,12 +492,12 @@ describe('getLLMConfig', () => {
|
||||||
|
|
||||||
it('should handle web search functionality like production', () => {
|
it('should handle web search functionality like production', () => {
|
||||||
const clientOptions = {
|
const clientOptions = {
|
||||||
userId: 'websearch-user-303',
|
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
model: 'claude-3-5-sonnet-latest',
|
model: 'claude-3-5-sonnet-latest',
|
||||||
temperature: 0.6,
|
temperature: 0.6,
|
||||||
maxOutputTokens: 4096,
|
maxOutputTokens: 4096,
|
||||||
web_search: true,
|
web_search: true,
|
||||||
|
user: 'websearch-user-303',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -516,7 +521,6 @@ describe('getLLMConfig', () => {
|
||||||
it('should handle complex production configuration', () => {
|
it('should handle complex production configuration', () => {
|
||||||
const clientOptions = {
|
const clientOptions = {
|
||||||
proxy: 'http://prod-proxy.company.com:3128',
|
proxy: 'http://prod-proxy.company.com:3128',
|
||||||
userId: 'prod-user-enterprise-404',
|
|
||||||
reverseProxyUrl: 'https://anthropic-gateway.company.com/v1',
|
reverseProxyUrl: 'https://anthropic-gateway.company.com/v1',
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
model: 'claude-3-opus-20240229',
|
model: 'claude-3-opus-20240229',
|
||||||
|
@ -527,6 +531,7 @@ describe('getLLMConfig', () => {
|
||||||
stop: ['\\n\\nHuman:', '\\n\\nAssistant:', 'END_CONVERSATION'],
|
stop: ['\\n\\nHuman:', '\\n\\nAssistant:', 'END_CONVERSATION'],
|
||||||
stream: true,
|
stream: true,
|
||||||
promptCache: true,
|
promptCache: true,
|
||||||
|
user: 'prod-user-enterprise-404',
|
||||||
},
|
},
|
||||||
streamRate: 15, // Conservative stream rate
|
streamRate: 15, // Conservative stream rate
|
||||||
titleModel: 'claude-3-haiku-20240307',
|
titleModel: 'claude-3-haiku-20240307',
|
||||||
|
@ -571,10 +576,10 @@ describe('getLLMConfig', () => {
|
||||||
// Regular options that should remain
|
// Regular options that should remain
|
||||||
topP: 0.9,
|
topP: 0.9,
|
||||||
topK: 40,
|
topK: 40,
|
||||||
|
user: 'system-options-user',
|
||||||
};
|
};
|
||||||
|
|
||||||
const clientOptions = {
|
const clientOptions = {
|
||||||
userId: 'system-options-user',
|
|
||||||
modelOptions,
|
modelOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -592,29 +597,30 @@ describe('getLLMConfig', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Error Handling and Edge Cases from Real Usage', () => {
|
describe('Error Handling and Edge Cases from Real Usage', () => {
|
||||||
it('should handle missing userId gracefully', () => {
|
it('should handle missing `user` ID string gracefully', () => {
|
||||||
const clientOptions = {
|
const clientOptions = {
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
model: 'claude-3-haiku',
|
model: 'claude-3-haiku',
|
||||||
temperature: 0.5,
|
temperature: 0.5,
|
||||||
|
// `user` is missing
|
||||||
},
|
},
|
||||||
// userId is missing
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = getLLMConfig('sk-ant-no-user-key', clientOptions);
|
const result = getLLMConfig('sk-ant-no-user-key', clientOptions);
|
||||||
|
|
||||||
expect(result.llmConfig.invocationKwargs.metadata).toMatchObject({
|
expect(result.llmConfig.invocationKwargs?.metadata).toMatchObject({
|
||||||
user_id: undefined,
|
user_id: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle large parameter sets without performance issues', () => {
|
it('should handle large parameter sets without performance issues', () => {
|
||||||
const largeModelOptions = {
|
const largeModelOptions: Record<string, string | number | boolean> = {
|
||||||
model: 'claude-3-opus',
|
model: 'claude-3-opus',
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
maxOutputTokens: 4096,
|
maxOutputTokens: 4096,
|
||||||
topP: 0.9,
|
topP: 0.9,
|
||||||
topK: 40,
|
topK: 40,
|
||||||
|
user: 'performance-test-user',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add many additional properties to test performance
|
// Add many additional properties to test performance
|
||||||
|
@ -623,7 +629,6 @@ describe('getLLMConfig', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientOptions = {
|
const clientOptions = {
|
||||||
userId: 'performance-test-user',
|
|
||||||
modelOptions: largeModelOptions,
|
modelOptions: largeModelOptions,
|
||||||
proxy: 'http://performance-proxy:8080',
|
proxy: 'http://performance-proxy:8080',
|
||||||
reverseProxyUrl: 'https://performance-reverse-proxy.com',
|
reverseProxyUrl: 'https://performance-reverse-proxy.com',
|
||||||
|
@ -654,7 +659,6 @@ describe('getLLMConfig', () => {
|
||||||
|
|
||||||
modelVariations.forEach((model) => {
|
modelVariations.forEach((model) => {
|
||||||
const clientOptions = {
|
const clientOptions = {
|
||||||
userId: 'model-variation-user',
|
|
||||||
modelOptions: {
|
modelOptions: {
|
||||||
model,
|
model,
|
||||||
temperature: 0.5,
|
temperature: 0.5,
|
||||||
|
@ -662,6 +666,7 @@ describe('getLLMConfig', () => {
|
||||||
topK: 40,
|
topK: 40,
|
||||||
thinking: true,
|
thinking: true,
|
||||||
promptCache: true,
|
promptCache: true,
|
||||||
|
user: 'model-variation-user',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -720,7 +725,7 @@ describe('getLLMConfig', () => {
|
||||||
budget_tokens: 2000, // default thinkingBudget
|
budget_tokens: 2000, // default thinkingBudget
|
||||||
});
|
});
|
||||||
// Should have prompt cache headers by default
|
// Should have prompt cache headers by default
|
||||||
expect(result.llmConfig.clientOptions.defaultHeaders).toBeDefined();
|
expect(result.llmConfig.clientOptions?.defaultHeaders).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -810,7 +815,9 @@ describe('getLLMConfig', () => {
|
||||||
thinkingBudget,
|
thinkingBudget,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(result.llmConfig.thinking.budget_tokens).toBe(expected);
|
expect((result.llmConfig.thinking as t.ThinkingConfigEnabled)?.budget_tokens).toBe(
|
||||||
|
expected,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -839,12 +846,14 @@ describe('getLLMConfig', () => {
|
||||||
thinkingBudget,
|
thinkingBudget,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(result.llmConfig.thinking.budget_tokens).toBe(expectedBudget);
|
expect((result.llmConfig.thinking as t.ThinkingConfigEnabled)?.budget_tokens).toBe(
|
||||||
|
expectedBudget,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle topP/topK exclusion logic for Claude-3.7 models', () => {
|
it('should handle topP/topK exclusion logic for Claude-3.7 models', () => {
|
||||||
const testCases = [
|
const testCases: (t.AnthropicModelOptions & { shouldInclude: boolean })[] = [
|
||||||
// Claude-3.7 with thinking = true - should exclude topP/topK
|
// Claude-3.7 with thinking = true - should exclude topP/topK
|
||||||
{ model: 'claude-3-7-sonnet', thinking: true, shouldInclude: false },
|
{ model: 'claude-3-7-sonnet', thinking: true, shouldInclude: false },
|
||||||
{ model: 'claude-3.7-sonnet', thinking: true, shouldInclude: false },
|
{ model: 'claude-3.7-sonnet', thinking: true, shouldInclude: false },
|
||||||
|
@ -900,13 +909,15 @@ describe('getLLMConfig', () => {
|
||||||
modelOptions: { model, promptCache },
|
modelOptions: { model, promptCache },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const headers = result.llmConfig.clientOptions?.defaultHeaders;
|
||||||
|
|
||||||
if (shouldHaveHeaders) {
|
if (shouldHaveHeaders) {
|
||||||
expect(result.llmConfig.clientOptions.defaultHeaders).toBeDefined();
|
expect(headers).toBeDefined();
|
||||||
expect(result.llmConfig.clientOptions.defaultHeaders['anthropic-beta']).toContain(
|
expect((headers as Record<string, string>)['anthropic-beta']).toContain(
|
||||||
'prompt-caching',
|
'prompt-caching',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
expect(result.llmConfig.clientOptions.defaultHeaders).toBeUndefined();
|
expect(headers).toBeUndefined();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -926,8 +937,8 @@ describe('getLLMConfig', () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
testCases.forEach((testCase) => {
|
testCases.forEach((testCase) => {
|
||||||
const key = Object.keys(testCase)[0];
|
const key = Object.keys(testCase)[0] as keyof t.AnthropicModelOptions;
|
||||||
const value = testCase[key];
|
const value = (testCase as unknown as t.AnthropicModelOptions)[key];
|
||||||
const expected = testCase.expected;
|
const expected = testCase.expected;
|
||||||
|
|
||||||
const result = getLLMConfig('test-key', {
|
const result = getLLMConfig('test-key', {
|
||||||
|
@ -935,7 +946,7 @@ describe('getLLMConfig', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const outputKey = key === 'maxOutputTokens' ? 'maxTokens' : key;
|
const outputKey = key === 'maxOutputTokens' ? 'maxTokens' : key;
|
||||||
expect(result.llmConfig[outputKey]).toBe(expected);
|
expect(result.llmConfig[outputKey as keyof typeof result.llmConfig]).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -950,7 +961,7 @@ describe('getLLMConfig', () => {
|
||||||
|
|
||||||
testCases.forEach(({ stop, expected }) => {
|
testCases.forEach(({ stop, expected }) => {
|
||||||
const result = getLLMConfig('test-key', {
|
const result = getLLMConfig('test-key', {
|
||||||
modelOptions: { model: 'claude-3-opus', stop },
|
modelOptions: { model: 'claude-3-opus', stop } as t.AnthropicModelOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (expected === null || expected === undefined) {
|
if (expected === null || expected === undefined) {
|
||||||
|
@ -978,8 +989,8 @@ describe('getLLMConfig', () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
testCases.forEach((testCase) => {
|
testCases.forEach((testCase) => {
|
||||||
const key = Object.keys(testCase)[0];
|
const key = Object.keys(testCase)[0] as keyof t.AnthropicModelOptions;
|
||||||
const value = testCase[key];
|
const value = (testCase as unknown as t.AnthropicModelOptions)[key];
|
||||||
const expected = testCase.expected;
|
const expected = testCase.expected;
|
||||||
|
|
||||||
const result = getLLMConfig('test-key', {
|
const result = getLLMConfig('test-key', {
|
||||||
|
@ -1049,7 +1060,7 @@ describe('getLLMConfig', () => {
|
||||||
// thinking is false, so no thinking object should be created
|
// thinking is false, so no thinking object should be created
|
||||||
expect(result.llmConfig.thinking).toBeUndefined();
|
expect(result.llmConfig.thinking).toBeUndefined();
|
||||||
// promptCache default is true, so should have headers
|
// promptCache default is true, so should have headers
|
||||||
expect(result.llmConfig.clientOptions.defaultHeaders).toBeDefined();
|
expect(result.llmConfig.clientOptions?.defaultHeaders).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1125,7 +1136,7 @@ describe('getLLMConfig', () => {
|
||||||
|
|
||||||
testCases.forEach(({ stop, expected }) => {
|
testCases.forEach(({ stop, expected }) => {
|
||||||
const result = getLLMConfig('test-key', {
|
const result = getLLMConfig('test-key', {
|
||||||
modelOptions: { model: 'claude-3-opus', stop },
|
modelOptions: { model: 'claude-3-opus', stop } as t.AnthropicModelOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.llmConfig.stopSequences).toEqual(expected);
|
expect(result.llmConfig.stopSequences).toEqual(expected);
|
105
packages/api/src/endpoints/anthropic/llm.ts
Normal file
105
packages/api/src/endpoints/anthropic/llm.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import { Dispatcher, ProxyAgent } from 'undici';
|
||||||
|
import { AnthropicClientOptions } from '@librechat/agents';
|
||||||
|
import { anthropicSettings, removeNullishValues } from 'librechat-data-provider';
|
||||||
|
import type { AnthropicLLMConfigResult, AnthropicConfigOptions } from '~/types/anthropic';
|
||||||
|
import { checkPromptCacheSupport, getClaudeHeaders, configureReasoning } from './helpers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates configuration options for creating an Anthropic language model (LLM) instance.
|
||||||
|
* @param apiKey - The API key for authentication with Anthropic.
|
||||||
|
* @param options={} - Additional options for configuring the LLM.
|
||||||
|
* @returns Configuration options for creating an Anthropic LLM instance, with null and undefined values removed.
|
||||||
|
*/
|
||||||
|
function getLLMConfig(
|
||||||
|
apiKey?: string,
|
||||||
|
options: AnthropicConfigOptions = {} as AnthropicConfigOptions,
|
||||||
|
): AnthropicLLMConfigResult {
|
||||||
|
const systemOptions = {
|
||||||
|
thinking: options.modelOptions?.thinking ?? anthropicSettings.thinking.default,
|
||||||
|
promptCache: options.modelOptions?.promptCache ?? anthropicSettings.promptCache.default,
|
||||||
|
thinkingBudget:
|
||||||
|
options.modelOptions?.thinkingBudget ?? anthropicSettings.thinkingBudget.default,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Couldn't figure out a way to still loop through the object while deleting the overlapping keys when porting this
|
||||||
|
* over from javascript, so for now they are being deleted manually until a better way presents itself.
|
||||||
|
*/
|
||||||
|
if (options.modelOptions) {
|
||||||
|
delete options.modelOptions.thinking;
|
||||||
|
delete options.modelOptions.promptCache;
|
||||||
|
delete options.modelOptions.thinkingBudget;
|
||||||
|
} else {
|
||||||
|
throw new Error('No modelOptions provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
model: anthropicSettings.model.default,
|
||||||
|
maxOutputTokens: anthropicSettings.maxOutputTokens.default,
|
||||||
|
stream: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergedOptions = Object.assign(defaultOptions, options.modelOptions);
|
||||||
|
|
||||||
|
let requestOptions: AnthropicClientOptions & { stream?: boolean } = {
|
||||||
|
apiKey,
|
||||||
|
model: mergedOptions.model,
|
||||||
|
stream: mergedOptions.stream,
|
||||||
|
temperature: mergedOptions.temperature,
|
||||||
|
stopSequences: mergedOptions.stop,
|
||||||
|
maxTokens:
|
||||||
|
mergedOptions.maxOutputTokens || anthropicSettings.maxOutputTokens.reset(mergedOptions.model),
|
||||||
|
clientOptions: {},
|
||||||
|
invocationKwargs: {
|
||||||
|
metadata: {
|
||||||
|
user_id: mergedOptions.user,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
requestOptions = configureReasoning(requestOptions, systemOptions);
|
||||||
|
|
||||||
|
if (!/claude-3[-.]7/.test(mergedOptions.model)) {
|
||||||
|
requestOptions.topP = mergedOptions.topP;
|
||||||
|
requestOptions.topK = mergedOptions.topK;
|
||||||
|
} else if (requestOptions.thinking == null) {
|
||||||
|
requestOptions.topP = mergedOptions.topP;
|
||||||
|
requestOptions.topK = mergedOptions.topK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportsCacheControl =
|
||||||
|
systemOptions.promptCache === true && checkPromptCacheSupport(requestOptions.model ?? '');
|
||||||
|
const headers = getClaudeHeaders(requestOptions.model ?? '', supportsCacheControl);
|
||||||
|
if (headers && requestOptions.clientOptions) {
|
||||||
|
requestOptions.clientOptions.defaultHeaders = headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.proxy && requestOptions.clientOptions) {
|
||||||
|
const proxyAgent = new ProxyAgent(options.proxy);
|
||||||
|
requestOptions.clientOptions.fetchOptions = {
|
||||||
|
dispatcher: proxyAgent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.reverseProxyUrl && requestOptions.clientOptions) {
|
||||||
|
requestOptions.clientOptions.baseURL = options.reverseProxyUrl;
|
||||||
|
requestOptions.anthropicApiUrl = options.reverseProxyUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tools = [];
|
||||||
|
|
||||||
|
if (mergedOptions.web_search) {
|
||||||
|
tools.push({
|
||||||
|
type: 'web_search_20250305',
|
||||||
|
name: 'web_search',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tools,
|
||||||
|
llmConfig: removeNullishValues(
|
||||||
|
requestOptions as Record<string, unknown>,
|
||||||
|
) as AnthropicClientOptions & { clientOptions?: { fetchOptions?: { dispatcher: Dispatcher } } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getLLMConfig };
|
|
@ -1,5 +1,5 @@
|
||||||
import { Providers } from '@librechat/agents';
|
import { Providers } from '@librechat/agents';
|
||||||
import { googleSettings, AuthKeys } from 'librechat-data-provider';
|
import { googleSettings, AuthKeys, removeNullishValues } from 'librechat-data-provider';
|
||||||
import type { GoogleClientOptions, VertexAIClientOptions } from '@librechat/agents';
|
import type { GoogleClientOptions, VertexAIClientOptions } from '@librechat/agents';
|
||||||
import type { GoogleAIToolType } from '@langchain/google-common';
|
import type { GoogleAIToolType } from '@langchain/google-common';
|
||||||
import type * as t from '~/types';
|
import type * as t from '~/types';
|
||||||
|
@ -112,11 +112,15 @@ export function getGoogleConfig(
|
||||||
...modelOptions
|
...modelOptions
|
||||||
} = options.modelOptions || {};
|
} = options.modelOptions || {};
|
||||||
|
|
||||||
const llmConfig: GoogleClientOptions | VertexAIClientOptions = {
|
const llmConfig: GoogleClientOptions | VertexAIClientOptions = removeNullishValues({
|
||||||
...(modelOptions || {}),
|
...(modelOptions || {}),
|
||||||
model: modelOptions?.model ?? '',
|
model: modelOptions?.model ?? '',
|
||||||
maxRetries: 2,
|
maxRetries: 2,
|
||||||
};
|
topP: modelOptions?.topP ?? undefined,
|
||||||
|
topK: modelOptions?.topK ?? undefined,
|
||||||
|
temperature: modelOptions?.temperature ?? undefined,
|
||||||
|
maxOutputTokens: modelOptions?.maxOutputTokens ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
/** Used only for Safety Settings */
|
/** Used only for Safety Settings */
|
||||||
llmConfig.safetySettings = getSafetySettings(llmConfig.model);
|
llmConfig.safetySettings = getSafetySettings(llmConfig.model);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './custom';
|
export * from './custom';
|
||||||
export * from './google';
|
export * from './google';
|
||||||
export * from './openai';
|
export * from './openai';
|
||||||
|
export * from './anthropic';
|
||||||
|
|
551
packages/api/src/endpoints/openai/config.anthropic.spec.ts
Normal file
551
packages/api/src/endpoints/openai/config.anthropic.spec.ts
Normal file
|
@ -0,0 +1,551 @@
|
||||||
|
import { getOpenAIConfig } from './config';
|
||||||
|
|
||||||
|
describe('getOpenAIConfig - Anthropic Compatibility', () => {
|
||||||
|
describe('Anthropic via LiteLLM', () => {
|
||||||
|
it('should handle basic Anthropic configuration with defaultParamsEndpoint', () => {
|
||||||
|
const apiKey = 'sk-xxxx';
|
||||||
|
const endpoint = 'Anthropic (via LiteLLM)';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'claude-sonnet-4',
|
||||||
|
user: 'some_user_id',
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'http://host.docker.internal:4000/v1',
|
||||||
|
proxy: '',
|
||||||
|
headers: {},
|
||||||
|
addParams: undefined,
|
||||||
|
dropParams: undefined,
|
||||||
|
customParams: {
|
||||||
|
defaultParamsEndpoint: 'anthropic',
|
||||||
|
paramDefinitions: [],
|
||||||
|
},
|
||||||
|
endpoint: 'Anthropic (via LiteLLM)',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
apiKey: 'sk-xxxx',
|
||||||
|
model: 'claude-sonnet-4',
|
||||||
|
stream: true,
|
||||||
|
maxTokens: 8192,
|
||||||
|
modelKwargs: {
|
||||||
|
metadata: {
|
||||||
|
user_id: 'some_user_id',
|
||||||
|
},
|
||||||
|
thinking: {
|
||||||
|
type: 'enabled',
|
||||||
|
budget_tokens: 2000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'http://host.docker.internal:4000/v1',
|
||||||
|
defaultHeaders: {
|
||||||
|
'anthropic-beta': 'prompt-caching-2024-07-31,context-1m-2025-08-07',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Claude 3.7 model with thinking enabled', () => {
|
||||||
|
const apiKey = 'sk-yyyy';
|
||||||
|
const endpoint = 'Anthropic (via LiteLLM)';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'claude-3.7-sonnet-20241022',
|
||||||
|
user: 'user123',
|
||||||
|
temperature: 0.7,
|
||||||
|
thinking: true,
|
||||||
|
thinkingBudget: 3000,
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'http://localhost:4000/v1',
|
||||||
|
customParams: {
|
||||||
|
defaultParamsEndpoint: 'anthropic',
|
||||||
|
},
|
||||||
|
endpoint: 'Anthropic (via LiteLLM)',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
apiKey: 'sk-yyyy',
|
||||||
|
model: 'claude-3.7-sonnet-20241022',
|
||||||
|
stream: true,
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 8192,
|
||||||
|
modelKwargs: {
|
||||||
|
metadata: {
|
||||||
|
user_id: 'user123',
|
||||||
|
},
|
||||||
|
thinking: {
|
||||||
|
type: 'enabled',
|
||||||
|
budget_tokens: 3000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'http://localhost:4000/v1',
|
||||||
|
defaultHeaders: {
|
||||||
|
'anthropic-beta':
|
||||||
|
'token-efficient-tools-2025-02-19,output-128k-2025-02-19,prompt-caching-2024-07-31',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Claude 3.7 model with thinking disabled (topP and topK included)', () => {
|
||||||
|
const apiKey = 'sk-yyyy';
|
||||||
|
const endpoint = 'Anthropic (via LiteLLM)';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'claude-3.7-sonnet-20241022',
|
||||||
|
user: 'user123',
|
||||||
|
temperature: 0.7,
|
||||||
|
topP: 0.9,
|
||||||
|
topK: 50,
|
||||||
|
thinking: false,
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'http://localhost:4000/v1',
|
||||||
|
customParams: {
|
||||||
|
defaultParamsEndpoint: 'anthropic',
|
||||||
|
},
|
||||||
|
endpoint: 'Anthropic (via LiteLLM)',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
apiKey: 'sk-yyyy',
|
||||||
|
model: 'claude-3.7-sonnet-20241022',
|
||||||
|
stream: true,
|
||||||
|
temperature: 0.7,
|
||||||
|
topP: 0.9,
|
||||||
|
maxTokens: 8192,
|
||||||
|
modelKwargs: {
|
||||||
|
metadata: {
|
||||||
|
user_id: 'user123',
|
||||||
|
},
|
||||||
|
topK: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'http://localhost:4000/v1',
|
||||||
|
defaultHeaders: {
|
||||||
|
'anthropic-beta':
|
||||||
|
'token-efficient-tools-2025-02-19,output-128k-2025-02-19,prompt-caching-2024-07-31',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Claude 3.5 sonnet with special headers', () => {
|
||||||
|
const apiKey = 'sk-zzzz';
|
||||||
|
const endpoint = 'Anthropic (via LiteLLM)';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'claude-3.5-sonnet-20240620',
|
||||||
|
user: 'user456',
|
||||||
|
maxOutputTokens: 4096,
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'https://api.anthropic.proxy.com/v1',
|
||||||
|
customParams: {
|
||||||
|
defaultParamsEndpoint: 'anthropic',
|
||||||
|
},
|
||||||
|
endpoint: 'Anthropic (via LiteLLM)',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
apiKey: 'sk-zzzz',
|
||||||
|
model: 'claude-3.5-sonnet-20240620',
|
||||||
|
stream: true,
|
||||||
|
maxTokens: 4096,
|
||||||
|
modelKwargs: {
|
||||||
|
metadata: {
|
||||||
|
user_id: 'user456',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'https://api.anthropic.proxy.com/v1',
|
||||||
|
defaultHeaders: {
|
||||||
|
'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15,prompt-caching-2024-07-31',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply anthropic-beta headers based on model pattern', () => {
|
||||||
|
const apiKey = 'sk-custom';
|
||||||
|
const endpoint = 'Anthropic (via LiteLLM)';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'claude-3-sonnet',
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'http://custom.proxy/v1',
|
||||||
|
headers: {
|
||||||
|
'Custom-Header': 'custom-value',
|
||||||
|
Authorization: 'Bearer custom-token',
|
||||||
|
},
|
||||||
|
customParams: {
|
||||||
|
defaultParamsEndpoint: 'anthropic',
|
||||||
|
},
|
||||||
|
endpoint: 'Anthropic (via LiteLLM)',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
apiKey: 'sk-custom',
|
||||||
|
model: 'claude-3-sonnet',
|
||||||
|
stream: true,
|
||||||
|
maxTokens: 8192,
|
||||||
|
modelKwargs: {
|
||||||
|
metadata: {
|
||||||
|
user_id: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'http://custom.proxy/v1',
|
||||||
|
defaultHeaders: {
|
||||||
|
'Custom-Header': 'custom-value',
|
||||||
|
Authorization: 'Bearer custom-token',
|
||||||
|
'anthropic-beta': 'prompt-caching-2024-07-31',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle models that do not match Claude patterns', () => {
|
||||||
|
const apiKey = 'sk-other';
|
||||||
|
const endpoint = 'Anthropic (via LiteLLM)';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'gpt-4-turbo',
|
||||||
|
user: 'userGPT',
|
||||||
|
temperature: 0.8,
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'http://litellm:4000/v1',
|
||||||
|
customParams: {
|
||||||
|
defaultParamsEndpoint: 'anthropic',
|
||||||
|
},
|
||||||
|
endpoint: 'Anthropic (via LiteLLM)',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
apiKey: 'sk-other',
|
||||||
|
model: 'gpt-4-turbo',
|
||||||
|
stream: true,
|
||||||
|
temperature: 0.8,
|
||||||
|
maxTokens: 8192,
|
||||||
|
modelKwargs: {
|
||||||
|
metadata: {
|
||||||
|
user_id: 'userGPT',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'http://litellm:4000/v1',
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle dropParams correctly in Anthropic path', () => {
|
||||||
|
const apiKey = 'sk-drop';
|
||||||
|
const endpoint = 'Anthropic (via LiteLLM)';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'claude-3-opus-20240229',
|
||||||
|
user: 'userDrop',
|
||||||
|
temperature: 0.5,
|
||||||
|
maxOutputTokens: 2048,
|
||||||
|
topP: 0.9,
|
||||||
|
topK: 40,
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'http://proxy.litellm/v1',
|
||||||
|
dropParams: ['temperature', 'topK', 'metadata'],
|
||||||
|
customParams: {
|
||||||
|
defaultParamsEndpoint: 'anthropic',
|
||||||
|
},
|
||||||
|
endpoint: 'Anthropic (via LiteLLM)',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
apiKey: 'sk-drop',
|
||||||
|
model: 'claude-3-opus-20240229',
|
||||||
|
stream: true,
|
||||||
|
topP: 0.9,
|
||||||
|
maxTokens: 2048,
|
||||||
|
// temperature is dropped
|
||||||
|
// modelKwargs.topK is dropped
|
||||||
|
// modelKwargs.metadata is dropped completely
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'http://proxy.litellm/v1',
|
||||||
|
defaultHeaders: {
|
||||||
|
'anthropic-beta': 'prompt-caching-2024-07-31',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty user string', () => {
|
||||||
|
const apiKey = 'sk-edge';
|
||||||
|
const endpoint = 'Anthropic (via LiteLLM)';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'claude-2.1',
|
||||||
|
user: '',
|
||||||
|
temperature: 0,
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'http://litellm/v1',
|
||||||
|
customParams: {
|
||||||
|
defaultParamsEndpoint: 'anthropic',
|
||||||
|
},
|
||||||
|
endpoint: 'Anthropic (via LiteLLM)',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
apiKey: 'sk-edge',
|
||||||
|
model: 'claude-2.1',
|
||||||
|
stream: true,
|
||||||
|
temperature: 0,
|
||||||
|
maxTokens: 8192,
|
||||||
|
modelKwargs: {
|
||||||
|
metadata: {
|
||||||
|
user_id: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'http://litellm/v1',
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle web_search tool', () => {
|
||||||
|
const apiKey = 'sk-search';
|
||||||
|
const endpoint = 'Anthropic (via LiteLLM)';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'claude-3-opus-20240229',
|
||||||
|
user: 'searchUser',
|
||||||
|
web_search: true,
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'http://litellm/v1',
|
||||||
|
customParams: {
|
||||||
|
defaultParamsEndpoint: 'anthropic',
|
||||||
|
},
|
||||||
|
endpoint: 'Anthropic (via LiteLLM)',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
apiKey: 'sk-search',
|
||||||
|
model: 'claude-3-opus-20240229',
|
||||||
|
stream: true,
|
||||||
|
maxTokens: 8192,
|
||||||
|
modelKwargs: {
|
||||||
|
metadata: {
|
||||||
|
user_id: 'searchUser',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'http://litellm/v1',
|
||||||
|
defaultHeaders: {
|
||||||
|
'anthropic-beta': 'prompt-caching-2024-07-31',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
type: 'web_search_20250305',
|
||||||
|
name: 'web_search',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly transform Anthropic config with invocationKwargs', () => {
|
||||||
|
const apiKey = 'sk-test';
|
||||||
|
const endpoint = 'Anthropic (via LiteLLM)';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'claude-3.5-haiku-20241022',
|
||||||
|
user: 'testUser',
|
||||||
|
topP: 0.9,
|
||||||
|
topK: 40,
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'http://litellm/v1',
|
||||||
|
customParams: {
|
||||||
|
defaultParamsEndpoint: 'anthropic',
|
||||||
|
},
|
||||||
|
endpoint: 'Anthropic (via LiteLLM)',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
apiKey: 'sk-test',
|
||||||
|
model: 'claude-3.5-haiku-20241022',
|
||||||
|
stream: true,
|
||||||
|
topP: 0.9,
|
||||||
|
maxTokens: 8192,
|
||||||
|
modelKwargs: {
|
||||||
|
metadata: {
|
||||||
|
user_id: 'testUser',
|
||||||
|
},
|
||||||
|
topK: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'http://litellm/v1',
|
||||||
|
defaultHeaders: {
|
||||||
|
'anthropic-beta': 'prompt-caching-2024-07-31',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle addParams with Anthropic defaults', () => {
|
||||||
|
const apiKey = 'sk-add';
|
||||||
|
const endpoint = 'Anthropic (via LiteLLM)';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'claude-3-opus-20240229',
|
||||||
|
user: 'addUser',
|
||||||
|
temperature: 0.7,
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'http://litellm/v1',
|
||||||
|
addParams: {
|
||||||
|
customParam1: 'value1',
|
||||||
|
customParam2: 42,
|
||||||
|
frequencyPenalty: 0.5, // Known OpenAI param
|
||||||
|
},
|
||||||
|
customParams: {
|
||||||
|
defaultParamsEndpoint: 'anthropic',
|
||||||
|
},
|
||||||
|
endpoint: 'Anthropic (via LiteLLM)',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
apiKey: 'sk-add',
|
||||||
|
model: 'claude-3-opus-20240229',
|
||||||
|
stream: true,
|
||||||
|
temperature: 0.7,
|
||||||
|
frequencyPenalty: 0.5, // Known param added to main config
|
||||||
|
maxTokens: 8192,
|
||||||
|
modelKwargs: {
|
||||||
|
metadata: {
|
||||||
|
user_id: 'addUser',
|
||||||
|
},
|
||||||
|
customParam1: 'value1', // Unknown params added to modelKwargs
|
||||||
|
customParam2: 42,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'http://litellm/v1',
|
||||||
|
defaultHeaders: {
|
||||||
|
'anthropic-beta': 'prompt-caching-2024-07-31',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle both addParams and dropParams together', () => {
|
||||||
|
const apiKey = 'sk-both';
|
||||||
|
const endpoint = 'Anthropic (via LiteLLM)';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'claude-3.5-sonnet-20240620',
|
||||||
|
user: 'bothUser',
|
||||||
|
temperature: 0.6,
|
||||||
|
topP: 0.9,
|
||||||
|
topK: 40,
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'http://litellm/v1',
|
||||||
|
addParams: {
|
||||||
|
customParam: 'customValue',
|
||||||
|
maxRetries: 3, // Known OpenAI param
|
||||||
|
},
|
||||||
|
dropParams: ['temperature', 'topK'], // Drop one known and one unknown param
|
||||||
|
customParams: {
|
||||||
|
defaultParamsEndpoint: 'anthropic',
|
||||||
|
},
|
||||||
|
endpoint: 'Anthropic (via LiteLLM)',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
apiKey: 'sk-both',
|
||||||
|
model: 'claude-3.5-sonnet-20240620',
|
||||||
|
stream: true,
|
||||||
|
topP: 0.9,
|
||||||
|
maxRetries: 3,
|
||||||
|
maxTokens: 8192,
|
||||||
|
modelKwargs: {
|
||||||
|
metadata: {
|
||||||
|
user_id: 'bothUser',
|
||||||
|
},
|
||||||
|
customParam: 'customValue',
|
||||||
|
// topK is dropped
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'http://litellm/v1',
|
||||||
|
defaultHeaders: {
|
||||||
|
'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15,prompt-caching-2024-07-31',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
431
packages/api/src/endpoints/openai/config.backward-compat.spec.ts
Normal file
431
packages/api/src/endpoints/openai/config.backward-compat.spec.ts
Normal file
|
@ -0,0 +1,431 @@
|
||||||
|
import {
|
||||||
|
Verbosity,
|
||||||
|
EModelEndpoint,
|
||||||
|
ReasoningEffort,
|
||||||
|
ReasoningSummary,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
|
import { getOpenAIConfig } from './config';
|
||||||
|
|
||||||
|
describe('getOpenAIConfig - Backward Compatibility', () => {
|
||||||
|
describe('OpenAI endpoint', () => {
|
||||||
|
it('should handle GPT-5 model with reasoning and web search', () => {
|
||||||
|
const apiKey = 'sk-proj-somekey';
|
||||||
|
const endpoint = undefined;
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'gpt-5-nano',
|
||||||
|
verbosity: Verbosity.high,
|
||||||
|
reasoning_effort: ReasoningEffort.high,
|
||||||
|
reasoning_summary: ReasoningSummary.detailed,
|
||||||
|
useResponsesApi: true,
|
||||||
|
web_search: true,
|
||||||
|
user: 'some-user',
|
||||||
|
},
|
||||||
|
proxy: '',
|
||||||
|
reverseProxyUrl: null,
|
||||||
|
endpoint: EModelEndpoint.openAI,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
streaming: true,
|
||||||
|
model: 'gpt-5-nano',
|
||||||
|
useResponsesApi: true,
|
||||||
|
user: 'some-user',
|
||||||
|
apiKey: 'sk-proj-somekey',
|
||||||
|
reasoning: {
|
||||||
|
effort: ReasoningEffort.high,
|
||||||
|
summary: ReasoningSummary.detailed,
|
||||||
|
},
|
||||||
|
modelKwargs: {
|
||||||
|
text: {
|
||||||
|
verbosity: Verbosity.high,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configOptions: {},
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
type: 'web_search_preview',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OpenRouter endpoint', () => {
|
||||||
|
it('should handle OpenRouter configuration with dropParams and custom headers', () => {
|
||||||
|
const apiKey = 'sk-xxxx';
|
||||||
|
const endpoint = 'OpenRouter';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'qwen/qwen3-max',
|
||||||
|
user: 'some-user',
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'https://gateway.ai.cloudflare.com/v1/account-id/gateway-id/openrouter',
|
||||||
|
headers: {
|
||||||
|
'x-librechat-thread-id': '{{LIBRECHAT_BODY_CONVERSATIONID}}',
|
||||||
|
'x-test-key': '{{TESTING_USER_VAR}}',
|
||||||
|
},
|
||||||
|
proxy: '',
|
||||||
|
dropParams: ['user'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
streaming: true,
|
||||||
|
model: 'qwen/qwen3-max',
|
||||||
|
include_reasoning: true,
|
||||||
|
apiKey: 'sk-xxxx',
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'https://gateway.ai.cloudflare.com/v1/account-id/gateway-id/openrouter',
|
||||||
|
defaultHeaders: {
|
||||||
|
'HTTP-Referer': 'https://librechat.ai',
|
||||||
|
'X-Title': 'LibreChat',
|
||||||
|
'x-librechat-thread-id': '{{LIBRECHAT_BODY_CONVERSATIONID}}',
|
||||||
|
'x-test-key': '{{TESTING_USER_VAR}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
provider: 'openrouter',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Azure OpenAI endpoint', () => {
|
||||||
|
it('should handle basic Azure OpenAI configuration', () => {
|
||||||
|
const apiKey = 'some_key';
|
||||||
|
const endpoint = undefined;
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'gpt-4o',
|
||||||
|
user: 'some_user_id',
|
||||||
|
},
|
||||||
|
reverseProxyUrl: null,
|
||||||
|
endpoint: 'azureOpenAI',
|
||||||
|
azure: {
|
||||||
|
azureOpenAIApiKey: 'some_azure_key',
|
||||||
|
azureOpenAIApiInstanceName: 'some_instance_name',
|
||||||
|
azureOpenAIApiDeploymentName: 'gpt-4o',
|
||||||
|
azureOpenAIApiVersion: '2024-02-15-preview',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
streaming: true,
|
||||||
|
model: 'gpt-4o',
|
||||||
|
user: 'some_user_id',
|
||||||
|
azureOpenAIApiKey: 'some_azure_key',
|
||||||
|
azureOpenAIApiInstanceName: 'some_instance_name',
|
||||||
|
azureOpenAIApiDeploymentName: 'gpt-4o',
|
||||||
|
azureOpenAIApiVersion: '2024-02-15-preview',
|
||||||
|
},
|
||||||
|
configOptions: {},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Azure OpenAI with Responses API and reasoning', () => {
|
||||||
|
const apiKey = 'some_azure_key';
|
||||||
|
const endpoint = undefined;
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'gpt-5',
|
||||||
|
reasoning_effort: ReasoningEffort.high,
|
||||||
|
reasoning_summary: ReasoningSummary.detailed,
|
||||||
|
verbosity: Verbosity.high,
|
||||||
|
useResponsesApi: true,
|
||||||
|
user: 'some_user_id',
|
||||||
|
},
|
||||||
|
endpoint: 'azureOpenAI',
|
||||||
|
azure: {
|
||||||
|
azureOpenAIApiKey: 'some_azure_key',
|
||||||
|
azureOpenAIApiInstanceName: 'some_instance_name',
|
||||||
|
azureOpenAIApiDeploymentName: 'gpt-5',
|
||||||
|
azureOpenAIApiVersion: '2024-12-01-preview',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
streaming: true,
|
||||||
|
model: 'gpt-5',
|
||||||
|
useResponsesApi: true,
|
||||||
|
user: 'some_user_id',
|
||||||
|
apiKey: 'some_azure_key',
|
||||||
|
reasoning: {
|
||||||
|
effort: ReasoningEffort.high,
|
||||||
|
summary: ReasoningSummary.detailed,
|
||||||
|
},
|
||||||
|
modelKwargs: {
|
||||||
|
text: {
|
||||||
|
verbosity: Verbosity.high,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'https://some_instance_name.openai.azure.com/openai/v1',
|
||||||
|
defaultHeaders: {
|
||||||
|
'api-key': 'some_azure_key',
|
||||||
|
},
|
||||||
|
defaultQuery: {
|
||||||
|
'api-version': 'preview',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Azure serverless configuration with dropParams', () => {
|
||||||
|
const apiKey = 'some_azure_key';
|
||||||
|
const endpoint = undefined;
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'jais-30b-chat',
|
||||||
|
user: 'some_user_id',
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'https://some_endpoint_name.services.ai.azure.com/models',
|
||||||
|
endpoint: 'azureOpenAI',
|
||||||
|
headers: {
|
||||||
|
'api-key': 'some_azure_key',
|
||||||
|
},
|
||||||
|
dropParams: ['stream_options', 'user'],
|
||||||
|
azure: false as const,
|
||||||
|
defaultQuery: {
|
||||||
|
'api-version': '2024-05-01-preview',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
streaming: true,
|
||||||
|
model: 'jais-30b-chat',
|
||||||
|
apiKey: 'some_azure_key',
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'https://some_endpoint_name.services.ai.azure.com/models',
|
||||||
|
defaultHeaders: {
|
||||||
|
'api-key': 'some_azure_key',
|
||||||
|
},
|
||||||
|
defaultQuery: {
|
||||||
|
'api-version': '2024-05-01-preview',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Azure serverless with user-provided key configuration', () => {
|
||||||
|
const apiKey = 'some_azure_key';
|
||||||
|
const endpoint = undefined;
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'grok-3',
|
||||||
|
user: 'some_user_id',
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'https://some_endpoint_name.services.ai.azure.com/models',
|
||||||
|
endpoint: 'azureOpenAI',
|
||||||
|
headers: {
|
||||||
|
'api-key': 'some_azure_key',
|
||||||
|
},
|
||||||
|
dropParams: ['stream_options', 'user'],
|
||||||
|
azure: false as const,
|
||||||
|
defaultQuery: {
|
||||||
|
'api-version': '2024-05-01-preview',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
streaming: true,
|
||||||
|
model: 'grok-3',
|
||||||
|
apiKey: 'some_azure_key',
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'https://some_endpoint_name.services.ai.azure.com/models',
|
||||||
|
defaultHeaders: {
|
||||||
|
'api-key': 'some_azure_key',
|
||||||
|
},
|
||||||
|
defaultQuery: {
|
||||||
|
'api-version': '2024-05-01-preview',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Azure serverless with Mistral model configuration', () => {
|
||||||
|
const apiKey = 'some_azure_key';
|
||||||
|
const endpoint = undefined;
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'Mistral-Large-2411',
|
||||||
|
user: 'some_user_id',
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'https://some_endpoint_name.services.ai.azure.com/models',
|
||||||
|
endpoint: 'azureOpenAI',
|
||||||
|
headers: {
|
||||||
|
'api-key': 'some_azure_key',
|
||||||
|
},
|
||||||
|
dropParams: ['stream_options', 'user'],
|
||||||
|
azure: false as const,
|
||||||
|
defaultQuery: {
|
||||||
|
'api-version': '2024-05-01-preview',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
streaming: true,
|
||||||
|
model: 'Mistral-Large-2411',
|
||||||
|
apiKey: 'some_azure_key',
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'https://some_endpoint_name.services.ai.azure.com/models',
|
||||||
|
defaultHeaders: {
|
||||||
|
'api-key': 'some_azure_key',
|
||||||
|
},
|
||||||
|
defaultQuery: {
|
||||||
|
'api-version': '2024-05-01-preview',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Azure serverless with DeepSeek model without dropParams', () => {
|
||||||
|
const apiKey = 'some_azure_key';
|
||||||
|
const endpoint = undefined;
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'DeepSeek-R1',
|
||||||
|
user: 'some_user_id',
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'https://some_endpoint_name.models.ai.azure.com/v1/',
|
||||||
|
endpoint: 'azureOpenAI',
|
||||||
|
headers: {
|
||||||
|
'api-key': 'some_azure_key',
|
||||||
|
},
|
||||||
|
azure: false as const,
|
||||||
|
defaultQuery: {
|
||||||
|
'api-version': '2024-08-01-preview',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
streaming: true,
|
||||||
|
model: 'DeepSeek-R1',
|
||||||
|
user: 'some_user_id',
|
||||||
|
apiKey: 'some_azure_key',
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'https://some_endpoint_name.models.ai.azure.com/v1/',
|
||||||
|
defaultHeaders: {
|
||||||
|
'api-key': 'some_azure_key',
|
||||||
|
},
|
||||||
|
defaultQuery: {
|
||||||
|
'api-version': '2024-08-01-preview',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Custom endpoints', () => {
|
||||||
|
it('should handle Groq custom endpoint configuration', () => {
|
||||||
|
const apiKey = 'gsk_somekey';
|
||||||
|
const endpoint = 'groq';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: 'qwen/qwen3-32b',
|
||||||
|
user: 'some-user',
|
||||||
|
},
|
||||||
|
reverseProxyUrl: 'https://api.groq.com/openai/v1/',
|
||||||
|
proxy: '',
|
||||||
|
headers: {},
|
||||||
|
endpoint: 'groq',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
streaming: true,
|
||||||
|
model: 'qwen/qwen3-32b',
|
||||||
|
user: 'some-user',
|
||||||
|
apiKey: 'gsk_somekey',
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL: 'https://api.groq.com/openai/v1/',
|
||||||
|
defaultHeaders: {},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Cloudflare Workers AI with custom headers and addParams', () => {
|
||||||
|
const apiKey = 'someKey';
|
||||||
|
const endpoint = 'Cloudflare Workers AI';
|
||||||
|
const options = {
|
||||||
|
modelOptions: {
|
||||||
|
model: '@cf/deepseek-ai/deepseek-r1-distill-qwen-32b',
|
||||||
|
user: 'some-user',
|
||||||
|
},
|
||||||
|
reverseProxyUrl:
|
||||||
|
'https://gateway.ai.cloudflare.com/v1/${CF_ACCOUNT_ID}/${CF_GATEWAY_ID}/workers-ai/v1',
|
||||||
|
proxy: '',
|
||||||
|
headers: {
|
||||||
|
'x-librechat-thread-id': '{{LIBRECHAT_BODY_CONVERSATIONID}}',
|
||||||
|
'x-test-key': '{{TESTING_USER_VAR}}',
|
||||||
|
},
|
||||||
|
addParams: {
|
||||||
|
disableStreaming: true,
|
||||||
|
},
|
||||||
|
endpoint: 'Cloudflare Workers AI',
|
||||||
|
endpointType: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
llmConfig: {
|
||||||
|
streaming: true,
|
||||||
|
model: '@cf/deepseek-ai/deepseek-r1-distill-qwen-32b',
|
||||||
|
user: 'some-user',
|
||||||
|
disableStreaming: true,
|
||||||
|
apiKey: 'someKey',
|
||||||
|
},
|
||||||
|
configOptions: {
|
||||||
|
baseURL:
|
||||||
|
'https://gateway.ai.cloudflare.com/v1/${CF_ACCOUNT_ID}/${CF_GATEWAY_ID}/workers-ai/v1',
|
||||||
|
defaultHeaders: {
|
||||||
|
'x-librechat-thread-id': '{{LIBRECHAT_BODY_CONVERSATIONID}}',
|
||||||
|
'x-test-key': '{{TESTING_USER_VAR}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,7 +1,8 @@
|
||||||
import { Verbosity, ReasoningEffort, ReasoningSummary } from 'librechat-data-provider';
|
import { Verbosity, ReasoningEffort, ReasoningSummary } from 'librechat-data-provider';
|
||||||
import type { RequestInit } from 'undici';
|
import type { RequestInit } from 'undici';
|
||||||
import type { OpenAIParameters, AzureOptions } from '~/types';
|
import type { OpenAIParameters, AzureOptions } from '~/types';
|
||||||
import { getOpenAIConfig, knownOpenAIParams } from './llm';
|
import { getOpenAIConfig } from './config';
|
||||||
|
import { knownOpenAIParams } from './llm';
|
||||||
|
|
||||||
describe('getOpenAIConfig', () => {
|
describe('getOpenAIConfig', () => {
|
||||||
const mockApiKey = 'test-api-key';
|
const mockApiKey = 'test-api-key';
|
150
packages/api/src/endpoints/openai/config.ts
Normal file
150
packages/api/src/endpoints/openai/config.ts
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import { ProxyAgent } from 'undici';
|
||||||
|
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 { 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>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates configuration options for creating a language model (LLM) instance.
|
||||||
|
* @param apiKey - The API key for authentication.
|
||||||
|
* @param options - Additional options for configuring the LLM.
|
||||||
|
* @param endpoint - The endpoint name
|
||||||
|
* @returns Configuration options for creating an LLM instance.
|
||||||
|
*/
|
||||||
|
export function getOpenAIConfig(
|
||||||
|
apiKey: string,
|
||||||
|
options: t.OpenAIConfigOptions = {},
|
||||||
|
endpoint?: string | null,
|
||||||
|
): t.OpenAIConfigResult {
|
||||||
|
const {
|
||||||
|
proxy,
|
||||||
|
addParams,
|
||||||
|
dropParams,
|
||||||
|
defaultQuery,
|
||||||
|
directEndpoint,
|
||||||
|
streaming = true,
|
||||||
|
modelOptions = {},
|
||||||
|
reverseProxyUrl: baseURL,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
let llmConfig: t.OAIClientOptions;
|
||||||
|
let tools: t.LLMConfigResult['tools'];
|
||||||
|
const isAnthropic = options.customParams?.defaultParamsEndpoint === EModelEndpoint.anthropic;
|
||||||
|
|
||||||
|
const useOpenRouter =
|
||||||
|
!isAnthropic &&
|
||||||
|
((baseURL && baseURL.includes(KnownEndpoints.openrouter)) ||
|
||||||
|
(endpoint != null && endpoint.toLowerCase().includes(KnownEndpoints.openrouter)));
|
||||||
|
|
||||||
|
let azure = options.azure;
|
||||||
|
let headers = options.headers;
|
||||||
|
if (isAnthropic) {
|
||||||
|
const anthropicResult = getAnthropicLLMConfig(apiKey, {
|
||||||
|
modelOptions,
|
||||||
|
proxy: options.proxy,
|
||||||
|
});
|
||||||
|
const transformed = transformToOpenAIConfig({
|
||||||
|
addParams,
|
||||||
|
dropParams,
|
||||||
|
llmConfig: anthropicResult.llmConfig,
|
||||||
|
fromEndpoint: EModelEndpoint.anthropic,
|
||||||
|
});
|
||||||
|
llmConfig = transformed.llmConfig;
|
||||||
|
tools = anthropicResult.tools;
|
||||||
|
if (transformed.configOptions?.defaultHeaders) {
|
||||||
|
headers = Object.assign(headers ?? {}, transformed.configOptions?.defaultHeaders);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const openaiResult = getOpenAILLMConfig({
|
||||||
|
azure,
|
||||||
|
apiKey,
|
||||||
|
baseURL,
|
||||||
|
streaming,
|
||||||
|
addParams,
|
||||||
|
dropParams,
|
||||||
|
modelOptions,
|
||||||
|
useOpenRouter,
|
||||||
|
});
|
||||||
|
llmConfig = openaiResult.llmConfig;
|
||||||
|
azure = openaiResult.azure;
|
||||||
|
tools = openaiResult.tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configOptions: t.OpenAIConfiguration = {};
|
||||||
|
if (baseURL) {
|
||||||
|
configOptions.baseURL = baseURL;
|
||||||
|
}
|
||||||
|
if (useOpenRouter) {
|
||||||
|
configOptions.defaultHeaders = Object.assign(
|
||||||
|
{
|
||||||
|
'HTTP-Referer': 'https://librechat.ai',
|
||||||
|
'X-Title': 'LibreChat',
|
||||||
|
},
|
||||||
|
headers,
|
||||||
|
);
|
||||||
|
} else if (headers) {
|
||||||
|
configOptions.defaultHeaders = headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultQuery) {
|
||||||
|
configOptions.defaultQuery = defaultQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxy) {
|
||||||
|
const proxyAgent = new ProxyAgent(proxy);
|
||||||
|
configOptions.fetchOptions = {
|
||||||
|
dispatcher: proxyAgent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (azure && !isAnthropic) {
|
||||||
|
const constructAzureResponsesApi = () => {
|
||||||
|
if (!llmConfig.useResponsesApi || !azure) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
configOptions.baseURL = constructAzureURL({
|
||||||
|
baseURL: configOptions.baseURL || 'https://${INSTANCE_NAME}.openai.azure.com/openai/v1',
|
||||||
|
azureOptions: azure,
|
||||||
|
});
|
||||||
|
|
||||||
|
configOptions.defaultHeaders = {
|
||||||
|
...configOptions.defaultHeaders,
|
||||||
|
'api-key': apiKey,
|
||||||
|
};
|
||||||
|
configOptions.defaultQuery = {
|
||||||
|
...configOptions.defaultQuery,
|
||||||
|
'api-version': configOptions.defaultQuery?.['api-version'] ?? 'preview',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
constructAzureResponsesApi();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.OPENAI_ORGANIZATION && !isAnthropic) {
|
||||||
|
configOptions.organization = process.env.OPENAI_ORGANIZATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directEndpoint === true && configOptions?.baseURL != null) {
|
||||||
|
configOptions.fetch = createFetch({
|
||||||
|
directEndpoint: directEndpoint,
|
||||||
|
reverseProxyUrl: configOptions?.baseURL,
|
||||||
|
}) as unknown as Fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: t.OpenAIConfigResult = {
|
||||||
|
llmConfig,
|
||||||
|
configOptions,
|
||||||
|
tools,
|
||||||
|
};
|
||||||
|
if (useOpenRouter) {
|
||||||
|
result.provider = Providers.OPENROUTER;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './llm';
|
export * from './llm';
|
||||||
|
export * from './config';
|
||||||
export * from './initialize';
|
export * from './initialize';
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { createHandleLLMNewToken } from '~/utils/generators';
|
||||||
import { getAzureCredentials } from '~/utils/azure';
|
import { getAzureCredentials } from '~/utils/azure';
|
||||||
import { isUserProvided } from '~/utils/common';
|
import { isUserProvided } from '~/utils/common';
|
||||||
import { resolveHeaders } from '~/utils/env';
|
import { resolveHeaders } from '~/utils/env';
|
||||||
import { getOpenAIConfig } from './llm';
|
import { getOpenAIConfig } from './config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes OpenAI options for agent usage. This function always returns configuration
|
* Initializes OpenAI options for agent usage. This function always returns configuration
|
||||||
|
@ -115,7 +115,7 @@ export const initializeOpenAI = async ({
|
||||||
} else if (isAzureOpenAI) {
|
} else if (isAzureOpenAI) {
|
||||||
clientOptions.azure =
|
clientOptions.azure =
|
||||||
userProvidesKey && userValues?.apiKey ? JSON.parse(userValues.apiKey) : getAzureCredentials();
|
userProvidesKey && userValues?.apiKey ? JSON.parse(userValues.apiKey) : getAzureCredentials();
|
||||||
apiKey = clientOptions.azure?.azureOpenAIApiKey;
|
apiKey = clientOptions.azure ? clientOptions.azure.azureOpenAIApiKey : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userProvidesKey && !apiKey) {
|
if (userProvidesKey && !apiKey) {
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
import { ProxyAgent } from 'undici';
|
import { removeNullishValues } from 'librechat-data-provider';
|
||||||
import { Providers } from '@librechat/agents';
|
|
||||||
import { KnownEndpoints, removeNullishValues } from 'librechat-data-provider';
|
|
||||||
import type { BindToolsInput } from '@langchain/core/language_models/chat_models';
|
import type { BindToolsInput } from '@langchain/core/language_models/chat_models';
|
||||||
import type { AzureOpenAIInput } from '@langchain/openai';
|
import type { AzureOpenAIInput } from '@langchain/openai';
|
||||||
import type { OpenAI } from 'openai';
|
import type { OpenAI } from 'openai';
|
||||||
import type * as t from '~/types';
|
import type * as t from '~/types';
|
||||||
import { sanitizeModelName, constructAzureURL } from '~/utils/azure';
|
import { sanitizeModelName, constructAzureURL } from '~/utils/azure';
|
||||||
import { createFetch } from '~/utils/generators';
|
|
||||||
import { isEnabled } from '~/utils/common';
|
import { isEnabled } from '~/utils/common';
|
||||||
|
|
||||||
type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
||||||
|
|
||||||
export const knownOpenAIParams = new Set([
|
export const knownOpenAIParams = new Set([
|
||||||
// Constructor/Instance Parameters
|
// Constructor/Instance Parameters
|
||||||
'model',
|
'model',
|
||||||
|
@ -80,47 +75,44 @@ function hasReasoningParams({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function getOpenAILLMConfig({
|
||||||
* Generates configuration options for creating a language model (LLM) instance.
|
|
||||||
* @param apiKey - The API key for authentication.
|
|
||||||
* @param options - Additional options for configuring the LLM.
|
|
||||||
* @param endpoint - The endpoint name
|
|
||||||
* @returns Configuration options for creating an LLM instance.
|
|
||||||
*/
|
|
||||||
export function getOpenAIConfig(
|
|
||||||
apiKey: string,
|
|
||||||
options: t.OpenAIConfigOptions = {},
|
|
||||||
endpoint?: string | null,
|
|
||||||
): t.LLMConfigResult {
|
|
||||||
const {
|
|
||||||
modelOptions: _modelOptions = {},
|
|
||||||
reverseProxyUrl,
|
|
||||||
directEndpoint,
|
|
||||||
defaultQuery,
|
|
||||||
headers,
|
|
||||||
proxy,
|
|
||||||
azure,
|
azure,
|
||||||
streaming = true,
|
apiKey,
|
||||||
|
baseURL,
|
||||||
|
streaming,
|
||||||
addParams,
|
addParams,
|
||||||
dropParams,
|
dropParams,
|
||||||
} = options;
|
useOpenRouter,
|
||||||
|
modelOptions: _modelOptions,
|
||||||
|
}: {
|
||||||
|
apiKey: string;
|
||||||
|
streaming: boolean;
|
||||||
|
baseURL?: string | null;
|
||||||
|
modelOptions: Partial<t.OpenAIParameters>;
|
||||||
|
addParams?: Record<string, unknown>;
|
||||||
|
dropParams?: string[];
|
||||||
|
useOpenRouter?: boolean;
|
||||||
|
azure?: false | t.AzureOptions;
|
||||||
|
}): Pick<t.LLMConfigResult, 'llmConfig' | 'tools'> & {
|
||||||
|
azure?: t.AzureOptions;
|
||||||
|
} {
|
||||||
const {
|
const {
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
reasoning_summary,
|
reasoning_summary,
|
||||||
verbosity,
|
verbosity,
|
||||||
|
web_search,
|
||||||
frequency_penalty,
|
frequency_penalty,
|
||||||
presence_penalty,
|
presence_penalty,
|
||||||
...modelOptions
|
...modelOptions
|
||||||
} = _modelOptions;
|
} = _modelOptions;
|
||||||
const llmConfig: Partial<t.ClientOptions> &
|
|
||||||
Partial<t.OpenAIParameters> &
|
const llmConfig = Object.assign(
|
||||||
Partial<AzureOpenAIInput> = Object.assign(
|
|
||||||
{
|
{
|
||||||
streaming,
|
streaming,
|
||||||
model: modelOptions.model ?? '',
|
model: modelOptions.model ?? '',
|
||||||
},
|
},
|
||||||
modelOptions,
|
modelOptions,
|
||||||
);
|
) as Partial<t.OAIClientOptions> & Partial<t.OpenAIParameters> & Partial<AzureOpenAIInput>;
|
||||||
|
|
||||||
if (frequency_penalty != null) {
|
if (frequency_penalty != null) {
|
||||||
llmConfig.frequencyPenalty = frequency_penalty;
|
llmConfig.frequencyPenalty = frequency_penalty;
|
||||||
|
@ -148,104 +140,8 @@ export function getOpenAIConfig(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let useOpenRouter = false;
|
if (useOpenRouter) {
|
||||||
const configOptions: t.OpenAIConfiguration = {};
|
|
||||||
|
|
||||||
if (
|
|
||||||
(reverseProxyUrl && reverseProxyUrl.includes(KnownEndpoints.openrouter)) ||
|
|
||||||
(endpoint && endpoint.toLowerCase().includes(KnownEndpoints.openrouter))
|
|
||||||
) {
|
|
||||||
useOpenRouter = true;
|
|
||||||
llmConfig.include_reasoning = true;
|
llmConfig.include_reasoning = true;
|
||||||
configOptions.baseURL = reverseProxyUrl;
|
|
||||||
configOptions.defaultHeaders = Object.assign(
|
|
||||||
{
|
|
||||||
'HTTP-Referer': 'https://librechat.ai',
|
|
||||||
'X-Title': 'LibreChat',
|
|
||||||
},
|
|
||||||
headers,
|
|
||||||
);
|
|
||||||
} else if (reverseProxyUrl) {
|
|
||||||
configOptions.baseURL = reverseProxyUrl;
|
|
||||||
if (headers) {
|
|
||||||
configOptions.defaultHeaders = headers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defaultQuery) {
|
|
||||||
configOptions.defaultQuery = defaultQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (proxy) {
|
|
||||||
const proxyAgent = new ProxyAgent(proxy);
|
|
||||||
configOptions.fetchOptions = {
|
|
||||||
dispatcher: proxyAgent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (azure) {
|
|
||||||
const useModelName = isEnabled(process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME);
|
|
||||||
const updatedAzure = { ...azure };
|
|
||||||
updatedAzure.azureOpenAIApiDeploymentName = useModelName
|
|
||||||
? sanitizeModelName(llmConfig.model || '')
|
|
||||||
: azure.azureOpenAIApiDeploymentName;
|
|
||||||
|
|
||||||
if (process.env.AZURE_OPENAI_DEFAULT_MODEL) {
|
|
||||||
llmConfig.model = process.env.AZURE_OPENAI_DEFAULT_MODEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
const constructBaseURL = () => {
|
|
||||||
if (!configOptions.baseURL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const azureURL = constructAzureURL({
|
|
||||||
baseURL: configOptions.baseURL,
|
|
||||||
azureOptions: updatedAzure,
|
|
||||||
});
|
|
||||||
updatedAzure.azureOpenAIBasePath = azureURL.split(
|
|
||||||
`/${updatedAzure.azureOpenAIApiDeploymentName}`,
|
|
||||||
)[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
constructBaseURL();
|
|
||||||
Object.assign(llmConfig, updatedAzure);
|
|
||||||
|
|
||||||
const constructAzureResponsesApi = () => {
|
|
||||||
if (!llmConfig.useResponsesApi) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
configOptions.baseURL = constructAzureURL({
|
|
||||||
baseURL: configOptions.baseURL || 'https://${INSTANCE_NAME}.openai.azure.com/openai/v1',
|
|
||||||
azureOptions: llmConfig,
|
|
||||||
});
|
|
||||||
|
|
||||||
delete llmConfig.azureOpenAIApiDeploymentName;
|
|
||||||
delete llmConfig.azureOpenAIApiInstanceName;
|
|
||||||
delete llmConfig.azureOpenAIApiVersion;
|
|
||||||
delete llmConfig.azureOpenAIBasePath;
|
|
||||||
delete llmConfig.azureOpenAIApiKey;
|
|
||||||
llmConfig.apiKey = apiKey;
|
|
||||||
|
|
||||||
configOptions.defaultHeaders = {
|
|
||||||
...configOptions.defaultHeaders,
|
|
||||||
'api-key': apiKey,
|
|
||||||
};
|
|
||||||
configOptions.defaultQuery = {
|
|
||||||
...configOptions.defaultQuery,
|
|
||||||
'api-version': configOptions.defaultQuery?.['api-version'] ?? 'preview',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
constructAzureResponsesApi();
|
|
||||||
|
|
||||||
llmConfig.model = updatedAzure.azureOpenAIApiDeploymentName;
|
|
||||||
} else {
|
|
||||||
llmConfig.apiKey = apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.OPENAI_ORGANIZATION && azure) {
|
|
||||||
configOptions.organization = process.env.OPENAI_ORGANIZATION;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -270,7 +166,7 @@ export function getOpenAIConfig(
|
||||||
|
|
||||||
const tools: BindToolsInput[] = [];
|
const tools: BindToolsInput[] = [];
|
||||||
|
|
||||||
if (modelOptions.web_search) {
|
if (web_search) {
|
||||||
llmConfig.useResponsesApi = true;
|
llmConfig.useResponsesApi = true;
|
||||||
tools.push({ type: 'web_search_preview' });
|
tools.push({ type: 'web_search_preview' });
|
||||||
}
|
}
|
||||||
|
@ -278,7 +174,7 @@ export function getOpenAIConfig(
|
||||||
/**
|
/**
|
||||||
* Note: OpenAI Web Search models do not support any known parameters besides `max_tokens`
|
* Note: OpenAI Web Search models do not support any known parameters besides `max_tokens`
|
||||||
*/
|
*/
|
||||||
if (modelOptions.model && /gpt-4o.*search/.test(modelOptions.model)) {
|
if (modelOptions.model && /gpt-4o.*search/.test(modelOptions.model as string)) {
|
||||||
const searchExcludeParams = [
|
const searchExcludeParams = [
|
||||||
'frequency_penalty',
|
'frequency_penalty',
|
||||||
'presence_penalty',
|
'presence_penalty',
|
||||||
|
@ -301,13 +197,13 @@ export function getOpenAIConfig(
|
||||||
|
|
||||||
combinedDropParams.forEach((param) => {
|
combinedDropParams.forEach((param) => {
|
||||||
if (param in llmConfig) {
|
if (param in llmConfig) {
|
||||||
delete llmConfig[param as keyof t.ClientOptions];
|
delete llmConfig[param as keyof t.OAIClientOptions];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (dropParams && Array.isArray(dropParams)) {
|
} else if (dropParams && Array.isArray(dropParams)) {
|
||||||
dropParams.forEach((param) => {
|
dropParams.forEach((param) => {
|
||||||
if (param in llmConfig) {
|
if (param in llmConfig) {
|
||||||
delete llmConfig[param as keyof t.ClientOptions];
|
delete llmConfig[param as keyof t.OAIClientOptions];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -329,20 +225,52 @@ export function getOpenAIConfig(
|
||||||
llmConfig.modelKwargs = modelKwargs;
|
llmConfig.modelKwargs = modelKwargs;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (directEndpoint === true && configOptions?.baseURL != null) {
|
if (!azure) {
|
||||||
configOptions.fetch = createFetch({
|
llmConfig.apiKey = apiKey;
|
||||||
directEndpoint: directEndpoint,
|
return { llmConfig, tools };
|
||||||
reverseProxyUrl: configOptions?.baseURL,
|
|
||||||
}) as unknown as Fetch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: t.LLMConfigResult = {
|
const useModelName = isEnabled(process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME);
|
||||||
llmConfig,
|
const updatedAzure = { ...azure };
|
||||||
configOptions,
|
updatedAzure.azureOpenAIApiDeploymentName = useModelName
|
||||||
tools,
|
? sanitizeModelName(llmConfig.model || '')
|
||||||
|
: azure.azureOpenAIApiDeploymentName;
|
||||||
|
|
||||||
|
if (process.env.AZURE_OPENAI_DEFAULT_MODEL) {
|
||||||
|
llmConfig.model = process.env.AZURE_OPENAI_DEFAULT_MODEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const constructAzureOpenAIBasePath = () => {
|
||||||
|
if (!baseURL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const azureURL = constructAzureURL({
|
||||||
|
baseURL,
|
||||||
|
azureOptions: updatedAzure,
|
||||||
|
});
|
||||||
|
updatedAzure.azureOpenAIBasePath = azureURL.split(
|
||||||
|
`/${updatedAzure.azureOpenAIApiDeploymentName}`,
|
||||||
|
)[0];
|
||||||
};
|
};
|
||||||
if (useOpenRouter) {
|
|
||||||
result.provider = Providers.OPENROUTER;
|
constructAzureOpenAIBasePath();
|
||||||
|
Object.assign(llmConfig, updatedAzure);
|
||||||
|
|
||||||
|
const constructAzureResponsesApi = () => {
|
||||||
|
if (!llmConfig.useResponsesApi) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
|
delete llmConfig.azureOpenAIApiDeploymentName;
|
||||||
|
delete llmConfig.azureOpenAIApiInstanceName;
|
||||||
|
delete llmConfig.azureOpenAIApiVersion;
|
||||||
|
delete llmConfig.azureOpenAIBasePath;
|
||||||
|
delete llmConfig.azureOpenAIApiKey;
|
||||||
|
llmConfig.apiKey = apiKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructAzureResponsesApi();
|
||||||
|
|
||||||
|
llmConfig.model = updatedAzure.azureOpenAIApiDeploymentName;
|
||||||
|
return { llmConfig, tools, azure: updatedAzure };
|
||||||
}
|
}
|
||||||
|
|
95
packages/api/src/endpoints/openai/transform.ts
Normal file
95
packages/api/src/endpoints/openai/transform.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
|
import type { ClientOptions } from '@librechat/agents';
|
||||||
|
import type * as t from '~/types';
|
||||||
|
import { knownOpenAIParams } from './llm';
|
||||||
|
|
||||||
|
const anthropicExcludeParams = new Set(['anthropicApiUrl']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a Non-OpenAI LLM config to an OpenAI-conformant config.
|
||||||
|
* Non-OpenAI parameters are moved to modelKwargs.
|
||||||
|
* Also extracts configuration options that belong in configOptions.
|
||||||
|
* Handles addParams and dropParams for parameter customization.
|
||||||
|
*/
|
||||||
|
export function transformToOpenAIConfig({
|
||||||
|
addParams,
|
||||||
|
dropParams,
|
||||||
|
llmConfig,
|
||||||
|
fromEndpoint,
|
||||||
|
}: {
|
||||||
|
addParams?: Record<string, unknown>;
|
||||||
|
dropParams?: string[];
|
||||||
|
llmConfig: ClientOptions;
|
||||||
|
fromEndpoint: string;
|
||||||
|
}): {
|
||||||
|
llmConfig: t.OAIClientOptions;
|
||||||
|
configOptions: Partial<t.OpenAIConfiguration>;
|
||||||
|
} {
|
||||||
|
const openAIConfig: Partial<t.OAIClientOptions> = {};
|
||||||
|
let configOptions: Partial<t.OpenAIConfiguration> = {};
|
||||||
|
let modelKwargs: Record<string, unknown> = {};
|
||||||
|
let hasModelKwargs = false;
|
||||||
|
|
||||||
|
const isAnthropic = fromEndpoint === EModelEndpoint.anthropic;
|
||||||
|
const excludeParams = isAnthropic ? anthropicExcludeParams : new Set();
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(llmConfig)) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeParams.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAnthropic && key === 'clientOptions') {
|
||||||
|
configOptions = Object.assign({}, configOptions, value as Partial<t.OpenAIConfiguration>);
|
||||||
|
continue;
|
||||||
|
} else if (isAnthropic && key === 'invocationKwargs') {
|
||||||
|
modelKwargs = Object.assign({}, modelKwargs, value as Record<string, unknown>);
|
||||||
|
hasModelKwargs = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (knownOpenAIParams.has(key)) {
|
||||||
|
(openAIConfig as Record<string, unknown>)[key] = value;
|
||||||
|
} else {
|
||||||
|
modelKwargs[key] = value;
|
||||||
|
hasModelKwargs = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addParams && typeof addParams === 'object') {
|
||||||
|
for (const [key, value] of Object.entries(addParams)) {
|
||||||
|
if (knownOpenAIParams.has(key)) {
|
||||||
|
(openAIConfig as Record<string, unknown>)[key] = value;
|
||||||
|
} else {
|
||||||
|
modelKwargs[key] = value;
|
||||||
|
hasModelKwargs = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasModelKwargs) {
|
||||||
|
openAIConfig.modelKwargs = modelKwargs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropParams && Array.isArray(dropParams)) {
|
||||||
|
dropParams.forEach((param) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
llmConfig: openAIConfig as t.OAIClientOptions,
|
||||||
|
configOptions,
|
||||||
|
};
|
||||||
|
}
|
69
packages/api/src/types/anthropic.ts
Normal file
69
packages/api/src/types/anthropic.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { Dispatcher } from 'undici';
|
||||||
|
import { anthropicSchema } from 'librechat-data-provider';
|
||||||
|
import type { AnthropicClientOptions } from '@librechat/agents';
|
||||||
|
import type { LLMConfigResult } from './openai';
|
||||||
|
|
||||||
|
export type AnthropicParameters = z.infer<typeof anthropicSchema>;
|
||||||
|
|
||||||
|
export interface ThinkingConfigDisabled {
|
||||||
|
type: 'disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThinkingConfigEnabled {
|
||||||
|
/**
|
||||||
|
* Determines how many tokens Claude can use for its internal reasoning process.
|
||||||
|
* Larger budgets can enable more thorough analysis for complex problems, improving
|
||||||
|
* response quality.
|
||||||
|
*
|
||||||
|
* Must be ≥1024 and less than `max_tokens`.
|
||||||
|
*
|
||||||
|
* See
|
||||||
|
* [extended thinking](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking)
|
||||||
|
* for details.
|
||||||
|
*/
|
||||||
|
budget_tokens: number;
|
||||||
|
|
||||||
|
type: 'enabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for enabling Claude's extended thinking.
|
||||||
|
*
|
||||||
|
* When enabled, responses include `thinking` content blocks showing Claude's
|
||||||
|
* thinking process before the final answer. Requires a minimum budget of 1,024
|
||||||
|
* tokens and counts towards your `max_tokens` limit.
|
||||||
|
*
|
||||||
|
* See
|
||||||
|
* [extended thinking](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking)
|
||||||
|
* for details.
|
||||||
|
*/
|
||||||
|
export type ThinkingConfigParam = ThinkingConfigEnabled | ThinkingConfigDisabled;
|
||||||
|
|
||||||
|
export type AnthropicModelOptions = Partial<Omit<AnthropicParameters, 'thinking'>> & {
|
||||||
|
thinking?: AnthropicParameters['thinking'] | null;
|
||||||
|
user?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options for the getLLMConfig function
|
||||||
|
*/
|
||||||
|
export interface AnthropicConfigOptions {
|
||||||
|
modelOptions?: AnthropicModelOptions;
|
||||||
|
/** Proxy server URL */
|
||||||
|
proxy?: string | null;
|
||||||
|
/** URL for a reverse proxy, if used */
|
||||||
|
reverseProxyUrl?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return type for getLLMConfig function
|
||||||
|
*/
|
||||||
|
export type AnthropicLLMConfigResult = LLMConfigResult<
|
||||||
|
AnthropicClientOptions & {
|
||||||
|
clientOptions?: {
|
||||||
|
fetchOptions?: { dispatcher: Dispatcher };
|
||||||
|
};
|
||||||
|
stream?: boolean;
|
||||||
|
}
|
||||||
|
>;
|
|
@ -13,3 +13,4 @@ export * from './prompts';
|
||||||
export * from './run';
|
export * from './run';
|
||||||
export * from './tools';
|
export * from './tools';
|
||||||
export * from './zod';
|
export * from './zod';
|
||||||
|
export * from './anthropic';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { openAISchema, EModelEndpoint } from 'librechat-data-provider';
|
import { openAISchema, EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type { TEndpointOption, TAzureConfig, TEndpoint } from 'librechat-data-provider';
|
import type { TEndpointOption, TAzureConfig, TEndpoint, TConfig } from 'librechat-data-provider';
|
||||||
import type { BindToolsInput } from '@langchain/core/language_models/chat_models';
|
import type { BindToolsInput } from '@langchain/core/language_models/chat_models';
|
||||||
import type { OpenAIClientOptions, Providers } from '@librechat/agents';
|
import type { OpenAIClientOptions, Providers } from '@librechat/agents';
|
||||||
import type { AzureOptions } from './azure';
|
import type { AzureOptions } from './azure';
|
||||||
|
@ -8,11 +8,13 @@ import type { AppConfig } from './config';
|
||||||
|
|
||||||
export type OpenAIParameters = z.infer<typeof openAISchema>;
|
export type OpenAIParameters = z.infer<typeof openAISchema>;
|
||||||
|
|
||||||
|
export type OpenAIModelOptions = Partial<OpenAIParameters>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration options for the getLLMConfig function
|
* Configuration options for the getLLMConfig function
|
||||||
*/
|
*/
|
||||||
export interface OpenAIConfigOptions {
|
export interface OpenAIConfigOptions {
|
||||||
modelOptions?: Partial<OpenAIParameters>;
|
modelOptions?: OpenAIModelOptions;
|
||||||
directEndpoint?: boolean;
|
directEndpoint?: boolean;
|
||||||
reverseProxyUrl?: string | null;
|
reverseProxyUrl?: string | null;
|
||||||
defaultQuery?: Record<string, string | undefined>;
|
defaultQuery?: Record<string, string | undefined>;
|
||||||
|
@ -22,24 +24,28 @@ export interface OpenAIConfigOptions {
|
||||||
streaming?: boolean;
|
streaming?: boolean;
|
||||||
addParams?: Record<string, unknown>;
|
addParams?: Record<string, unknown>;
|
||||||
dropParams?: string[];
|
dropParams?: string[];
|
||||||
|
customParams?: Partial<TConfig['customParams']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OpenAIConfiguration = OpenAIClientOptions['configuration'];
|
export type OpenAIConfiguration = OpenAIClientOptions['configuration'];
|
||||||
|
|
||||||
export type ClientOptions = OpenAIClientOptions & {
|
export type OAIClientOptions = OpenAIClientOptions & {
|
||||||
include_reasoning?: boolean;
|
include_reasoning?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return type for getLLMConfig function
|
* Return type for getLLMConfig function
|
||||||
*/
|
*/
|
||||||
export interface LLMConfigResult {
|
export interface LLMConfigResult<T = OAIClientOptions> {
|
||||||
llmConfig: ClientOptions;
|
llmConfig: T;
|
||||||
configOptions: OpenAIConfiguration;
|
|
||||||
tools?: BindToolsInput[];
|
|
||||||
provider?: Providers;
|
provider?: Providers;
|
||||||
|
tools?: BindToolsInput[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OpenAIConfigResult = LLMConfigResult<OAIClientOptions> & {
|
||||||
|
configOptions?: OpenAIConfiguration;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for user values retrieved from the database
|
* Interface for user values retrieved from the database
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -15,3 +15,4 @@ export * from './text';
|
||||||
export { default as Tokenizer } from './tokenizer';
|
export { default as Tokenizer } from './tokenizer';
|
||||||
export * from './yaml';
|
export * from './yaml';
|
||||||
export * from './http';
|
export * from './http';
|
||||||
|
export * from './tokens';
|
||||||
|
|
|
@ -1,5 +1,23 @@
|
||||||
const z = require('zod');
|
import z from 'zod';
|
||||||
const { EModelEndpoint } = require('librechat-data-provider');
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
/** Configuration object mapping model keys to their respective prompt, completion rates, and context limit
|
||||||
|
*
|
||||||
|
* Note: the [key: string]: unknown is not in the original JSDoc typedef in /api/typedefs.js, but I've included it since
|
||||||
|
* getModelMaxOutputTokens calls getModelTokenValue with a key of 'output', which was not in the original JSDoc typedef,
|
||||||
|
* but would be referenced in a TokenConfig in the if(matchedPattern) portion of getModelTokenValue.
|
||||||
|
* So in order to preserve functionality for that case and any others which might reference an additional key I'm unaware of,
|
||||||
|
* I've included it here until the interface can be typed more tightly.
|
||||||
|
*/
|
||||||
|
export interface TokenConfig {
|
||||||
|
prompt: number;
|
||||||
|
completion: number;
|
||||||
|
context: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An endpoint's config object mapping model keys to their respective prompt, completion rates, and context limit */
|
||||||
|
export type EndpointTokenConfig = Record<string, TokenConfig>;
|
||||||
|
|
||||||
const openAIModels = {
|
const openAIModels = {
|
||||||
'o4-mini': 200000,
|
'o4-mini': 200000,
|
||||||
|
@ -242,7 +260,7 @@ const aggregateModels = {
|
||||||
'gpt-oss-120b': 131000,
|
'gpt-oss-120b': 131000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const maxTokensMap = {
|
export const maxTokensMap = {
|
||||||
[EModelEndpoint.azureOpenAI]: openAIModels,
|
[EModelEndpoint.azureOpenAI]: openAIModels,
|
||||||
[EModelEndpoint.openAI]: aggregateModels,
|
[EModelEndpoint.openAI]: aggregateModels,
|
||||||
[EModelEndpoint.agents]: aggregateModels,
|
[EModelEndpoint.agents]: aggregateModels,
|
||||||
|
@ -252,7 +270,7 @@ const maxTokensMap = {
|
||||||
[EModelEndpoint.bedrock]: bedrockModels,
|
[EModelEndpoint.bedrock]: bedrockModels,
|
||||||
};
|
};
|
||||||
|
|
||||||
const modelMaxOutputs = {
|
export const modelMaxOutputs = {
|
||||||
o1: 32268, // -500 from max: 32,768
|
o1: 32268, // -500 from max: 32,768
|
||||||
'o1-mini': 65136, // -500 from max: 65,536
|
'o1-mini': 65136, // -500 from max: 65,536
|
||||||
'o1-preview': 32268, // -500 from max: 32,768
|
'o1-preview': 32268, // -500 from max: 32,768
|
||||||
|
@ -261,7 +279,7 @@ const modelMaxOutputs = {
|
||||||
'gpt-5-nano': 128000,
|
'gpt-5-nano': 128000,
|
||||||
'gpt-oss-20b': 131000,
|
'gpt-oss-20b': 131000,
|
||||||
'gpt-oss-120b': 131000,
|
'gpt-oss-120b': 131000,
|
||||||
system_default: 1024,
|
system_default: 32000,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Outputs from https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-names */
|
/** Outputs from https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-names */
|
||||||
|
@ -277,7 +295,7 @@ const anthropicMaxOutputs = {
|
||||||
'claude-3-7-sonnet': 128000,
|
'claude-3-7-sonnet': 128000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const maxOutputTokensMap = {
|
export const maxOutputTokensMap = {
|
||||||
[EModelEndpoint.anthropic]: anthropicMaxOutputs,
|
[EModelEndpoint.anthropic]: anthropicMaxOutputs,
|
||||||
[EModelEndpoint.azureOpenAI]: modelMaxOutputs,
|
[EModelEndpoint.azureOpenAI]: modelMaxOutputs,
|
||||||
[EModelEndpoint.openAI]: modelMaxOutputs,
|
[EModelEndpoint.openAI]: modelMaxOutputs,
|
||||||
|
@ -287,10 +305,13 @@ const maxOutputTokensMap = {
|
||||||
/**
|
/**
|
||||||
* Finds the first matching pattern in the tokens map.
|
* Finds the first matching pattern in the tokens map.
|
||||||
* @param {string} modelName
|
* @param {string} modelName
|
||||||
* @param {Record<string, number>} tokensMap
|
* @param {Record<string, number> | EndpointTokenConfig} tokensMap
|
||||||
* @returns {string|null}
|
* @returns {string|null}
|
||||||
*/
|
*/
|
||||||
function findMatchingPattern(modelName, tokensMap) {
|
export function findMatchingPattern(
|
||||||
|
modelName: string,
|
||||||
|
tokensMap: Record<string, number> | EndpointTokenConfig,
|
||||||
|
): string | null {
|
||||||
const keys = Object.keys(tokensMap);
|
const keys = Object.keys(tokensMap);
|
||||||
for (let i = keys.length - 1; i >= 0; i--) {
|
for (let i = keys.length - 1; i >= 0; i--) {
|
||||||
const modelKey = keys[i];
|
const modelKey = keys[i];
|
||||||
|
@ -305,57 +326,79 @@ function findMatchingPattern(modelName, tokensMap) {
|
||||||
/**
|
/**
|
||||||
* Retrieves a token value for a given model name from a tokens map.
|
* Retrieves a token value for a given model name from a tokens map.
|
||||||
*
|
*
|
||||||
* @param {string} modelName - The name of the model to look up.
|
* @param modelName - The name of the model to look up.
|
||||||
* @param {EndpointTokenConfig | Record<string, number>} tokensMap - The map of model names to token values.
|
* @param tokensMap - The map of model names to token values.
|
||||||
* @param {string} [key='context'] - The key to look up in the tokens map.
|
* @param [key='context'] - The key to look up in the tokens map.
|
||||||
* @returns {number|undefined} The token value for the given model or undefined if no match is found.
|
* @returns The token value for the given model or undefined if no match is found.
|
||||||
*/
|
*/
|
||||||
function getModelTokenValue(modelName, tokensMap, key = 'context') {
|
export function getModelTokenValue(
|
||||||
|
modelName: string,
|
||||||
|
tokensMap?: EndpointTokenConfig | Record<string, number>,
|
||||||
|
key = 'context' as keyof TokenConfig,
|
||||||
|
): number | undefined {
|
||||||
if (typeof modelName !== 'string' || !tokensMap) {
|
if (typeof modelName !== 'string' || !tokensMap) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tokensMap[modelName]?.context) {
|
const value = tokensMap[modelName];
|
||||||
return tokensMap[modelName].context;
|
if (typeof value === 'number') {
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tokensMap[modelName]) {
|
if (value?.context) {
|
||||||
return tokensMap[modelName];
|
return value.context;
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchedPattern = findMatchingPattern(modelName, tokensMap);
|
const matchedPattern = findMatchingPattern(modelName, tokensMap);
|
||||||
|
|
||||||
if (matchedPattern) {
|
if (matchedPattern) {
|
||||||
const result = tokensMap[matchedPattern];
|
const result = tokensMap[matchedPattern];
|
||||||
return result?.[key] ?? result ?? tokensMap.system_default;
|
if (typeof result === 'number') {
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokensMap.system_default;
|
const tokenValue = result?.[key];
|
||||||
|
if (typeof tokenValue === 'number') {
|
||||||
|
return tokenValue;
|
||||||
|
}
|
||||||
|
return tokensMap.system_default as number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokensMap.system_default as number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the maximum tokens for a given model name.
|
* Retrieves the maximum tokens for a given model name.
|
||||||
*
|
*
|
||||||
* @param {string} modelName - The name of the model to look up.
|
* @param modelName - The name of the model to look up.
|
||||||
* @param {string} endpoint - The endpoint (default is 'openAI').
|
* @param endpoint - The endpoint (default is 'openAI').
|
||||||
* @param {EndpointTokenConfig} [endpointTokenConfig] - Token Config for current endpoint to use for max tokens lookup
|
* @param [endpointTokenConfig] - Token Config for current endpoint to use for max tokens lookup
|
||||||
* @returns {number|undefined} The maximum tokens for the given model or undefined if no match is found.
|
* @returns The maximum tokens for the given model or undefined if no match is found.
|
||||||
*/
|
*/
|
||||||
function getModelMaxTokens(modelName, endpoint = EModelEndpoint.openAI, endpointTokenConfig) {
|
export function getModelMaxTokens(
|
||||||
const tokensMap = endpointTokenConfig ?? maxTokensMap[endpoint];
|
modelName: string,
|
||||||
|
endpoint = EModelEndpoint.openAI,
|
||||||
|
endpointTokenConfig?: EndpointTokenConfig,
|
||||||
|
): number | undefined {
|
||||||
|
const tokensMap = endpointTokenConfig ?? maxTokensMap[endpoint as keyof typeof maxTokensMap];
|
||||||
return getModelTokenValue(modelName, tokensMap);
|
return getModelTokenValue(modelName, tokensMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the maximum output tokens for a given model name.
|
* Retrieves the maximum output tokens for a given model name.
|
||||||
*
|
*
|
||||||
* @param {string} modelName - The name of the model to look up.
|
* @param modelName - The name of the model to look up.
|
||||||
* @param {string} endpoint - The endpoint (default is 'openAI').
|
* @param endpoint - The endpoint (default is 'openAI').
|
||||||
* @param {EndpointTokenConfig} [endpointTokenConfig] - Token Config for current endpoint to use for max tokens lookup
|
* @param [endpointTokenConfig] - Token Config for current endpoint to use for max tokens lookup
|
||||||
* @returns {number|undefined} The maximum output tokens for the given model or undefined if no match is found.
|
* @returns The maximum output tokens for the given model or undefined if no match is found.
|
||||||
*/
|
*/
|
||||||
function getModelMaxOutputTokens(modelName, endpoint = EModelEndpoint.openAI, endpointTokenConfig) {
|
export function getModelMaxOutputTokens(
|
||||||
const tokensMap = endpointTokenConfig ?? maxOutputTokensMap[endpoint];
|
modelName: string,
|
||||||
|
endpoint = EModelEndpoint.openAI,
|
||||||
|
endpointTokenConfig?: EndpointTokenConfig,
|
||||||
|
): number | undefined {
|
||||||
|
const tokensMap =
|
||||||
|
endpointTokenConfig ?? maxOutputTokensMap[endpoint as keyof typeof maxOutputTokensMap];
|
||||||
return getModelTokenValue(modelName, tokensMap, 'output');
|
return getModelTokenValue(modelName, tokensMap, 'output');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,21 +406,24 @@ function getModelMaxOutputTokens(modelName, endpoint = EModelEndpoint.openAI, en
|
||||||
* Retrieves the model name key for a given model name input. If the exact model name isn't found,
|
* Retrieves the model name key for a given model name input. If the exact model name isn't found,
|
||||||
* it searches for partial matches within the model name, checking keys in reverse order.
|
* it searches for partial matches within the model name, checking keys in reverse order.
|
||||||
*
|
*
|
||||||
* @param {string} modelName - The name of the model to look up.
|
* @param modelName - The name of the model to look up.
|
||||||
* @param {string} endpoint - The endpoint (default is 'openAI').
|
* @param endpoint - The endpoint (default is 'openAI').
|
||||||
* @returns {string|undefined} The model name key for the given model; returns input if no match is found and is string.
|
* @returns The model name key for the given model; returns input if no match is found and is string.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* matchModelName('gpt-4-32k-0613'); // Returns 'gpt-4-32k-0613'
|
* matchModelName('gpt-4-32k-0613'); // Returns 'gpt-4-32k-0613'
|
||||||
* matchModelName('gpt-4-32k-unknown'); // Returns 'gpt-4-32k'
|
* matchModelName('gpt-4-32k-unknown'); // Returns 'gpt-4-32k'
|
||||||
* matchModelName('unknown-model'); // Returns undefined
|
* matchModelName('unknown-model'); // Returns undefined
|
||||||
*/
|
*/
|
||||||
function matchModelName(modelName, endpoint = EModelEndpoint.openAI) {
|
export function matchModelName(
|
||||||
|
modelName: string,
|
||||||
|
endpoint = EModelEndpoint.openAI,
|
||||||
|
): string | undefined {
|
||||||
if (typeof modelName !== 'string') {
|
if (typeof modelName !== 'string') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokensMap = maxTokensMap[endpoint];
|
const tokensMap: Record<string, number> = maxTokensMap[endpoint as keyof typeof maxTokensMap];
|
||||||
if (!tokensMap) {
|
if (!tokensMap) {
|
||||||
return modelName;
|
return modelName;
|
||||||
}
|
}
|
||||||
|
@ -390,7 +436,7 @@ function matchModelName(modelName, endpoint = EModelEndpoint.openAI) {
|
||||||
return matchedPattern || modelName;
|
return matchedPattern || modelName;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelSchema = z.object({
|
export const modelSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
pricing: z.object({
|
pricing: z.object({
|
||||||
prompt: z.string(),
|
prompt: z.string(),
|
||||||
|
@ -399,7 +445,7 @@ const modelSchema = z.object({
|
||||||
context_length: z.number(),
|
context_length: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputSchema = z.object({
|
export const inputSchema = z.object({
|
||||||
data: z.array(modelSchema),
|
data: z.array(modelSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -408,7 +454,7 @@ const inputSchema = z.object({
|
||||||
* @param {{ data: Array<z.infer<typeof modelSchema>> }} input The input object containing base URL and data fetched from the API.
|
* @param {{ data: Array<z.infer<typeof modelSchema>> }} input The input object containing base URL and data fetched from the API.
|
||||||
* @returns {EndpointTokenConfig} The processed model data.
|
* @returns {EndpointTokenConfig} The processed model data.
|
||||||
*/
|
*/
|
||||||
function processModelData(input) {
|
export function processModelData(input: z.infer<typeof inputSchema>): EndpointTokenConfig {
|
||||||
const validationResult = inputSchema.safeParse(input);
|
const validationResult = inputSchema.safeParse(input);
|
||||||
if (!validationResult.success) {
|
if (!validationResult.success) {
|
||||||
throw new Error('Invalid input data');
|
throw new Error('Invalid input data');
|
||||||
|
@ -416,7 +462,7 @@ function processModelData(input) {
|
||||||
const { data } = validationResult.data;
|
const { data } = validationResult.data;
|
||||||
|
|
||||||
/** @type {EndpointTokenConfig} */
|
/** @type {EndpointTokenConfig} */
|
||||||
const tokenConfig = {};
|
const tokenConfig: EndpointTokenConfig = {};
|
||||||
|
|
||||||
for (const model of data) {
|
for (const model of data) {
|
||||||
const modelKey = model.id;
|
const modelKey = model.id;
|
||||||
|
@ -439,7 +485,7 @@ function processModelData(input) {
|
||||||
return tokenConfig;
|
return tokenConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tiktokenModels = new Set([
|
export const tiktokenModels = new Set([
|
||||||
'text-davinci-003',
|
'text-davinci-003',
|
||||||
'text-davinci-002',
|
'text-davinci-002',
|
||||||
'text-davinci-001',
|
'text-davinci-001',
|
||||||
|
@ -477,17 +523,3 @@ const tiktokenModels = new Set([
|
||||||
'gpt-3.5-turbo',
|
'gpt-3.5-turbo',
|
||||||
'gpt-3.5-turbo-0301',
|
'gpt-3.5-turbo-0301',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
inputSchema,
|
|
||||||
modelSchema,
|
|
||||||
maxTokensMap,
|
|
||||||
tiktokenModels,
|
|
||||||
maxOutputTokensMap,
|
|
||||||
matchModelName,
|
|
||||||
processModelData,
|
|
||||||
getModelMaxTokens,
|
|
||||||
getModelTokenValue,
|
|
||||||
findMatchingPattern,
|
|
||||||
getModelMaxOutputTokens,
|
|
||||||
};
|
|
|
@ -619,14 +619,14 @@ export const tConversationSchema = z.object({
|
||||||
userLabel: z.string().optional(),
|
userLabel: z.string().optional(),
|
||||||
model: z.string().nullable().optional(),
|
model: z.string().nullable().optional(),
|
||||||
promptPrefix: z.string().nullable().optional(),
|
promptPrefix: z.string().nullable().optional(),
|
||||||
temperature: z.number().optional(),
|
temperature: z.number().nullable().optional(),
|
||||||
topP: z.number().optional(),
|
topP: z.number().optional(),
|
||||||
topK: z.number().optional(),
|
topK: z.number().optional(),
|
||||||
top_p: z.number().optional(),
|
top_p: z.number().optional(),
|
||||||
frequency_penalty: z.number().optional(),
|
frequency_penalty: z.number().optional(),
|
||||||
presence_penalty: z.number().optional(),
|
presence_penalty: z.number().optional(),
|
||||||
parentMessageId: z.string().optional(),
|
parentMessageId: z.string().optional(),
|
||||||
maxOutputTokens: coerceNumber.optional(),
|
maxOutputTokens: coerceNumber.nullable().optional(),
|
||||||
maxContextTokens: coerceNumber.optional(),
|
maxContextTokens: coerceNumber.optional(),
|
||||||
max_tokens: coerceNumber.optional(),
|
max_tokens: coerceNumber.optional(),
|
||||||
/* Anthropic */
|
/* Anthropic */
|
||||||
|
@ -634,6 +634,7 @@ export const tConversationSchema = z.object({
|
||||||
system: z.string().optional(),
|
system: z.string().optional(),
|
||||||
thinking: z.boolean().optional(),
|
thinking: z.boolean().optional(),
|
||||||
thinkingBudget: coerceNumber.optional(),
|
thinkingBudget: coerceNumber.optional(),
|
||||||
|
stream: z.boolean().optional(),
|
||||||
/* artifacts */
|
/* artifacts */
|
||||||
artifacts: z.string().optional(),
|
artifacts: z.string().optional(),
|
||||||
/* google */
|
/* google */
|
||||||
|
@ -1152,6 +1153,8 @@ export const anthropicBaseSchema = tConversationSchema.pick({
|
||||||
maxContextTokens: true,
|
maxContextTokens: true,
|
||||||
web_search: true,
|
web_search: true,
|
||||||
fileTokenLimit: true,
|
fileTokenLimit: true,
|
||||||
|
stop: true,
|
||||||
|
stream: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const anthropicSchema = anthropicBaseSchema
|
export const anthropicSchema = anthropicBaseSchema
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue