mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-28 04:37:37 +00:00
⚡️ refactor: Migrate @librechat/api build to tsdown (#13595)
* ⚡️ refactor: Migrate @librechat/api build to tsdown Replace Rollup with tsdown (rolldown + oxc isolated-declarations) for the @librechat/api package build, mirroring the merged data-schemas migration. - Add tsdown.config.mjs (cjs output, oxc dts, externalize all bare deps, bundle first-party `~/` + relative imports) - Annotate exports for isolatedDeclarations (codefix-driven). Collapse the tokens.ts model->token maps to Record<string, Record<string, number>> and switch validation.ts's runtime `files` field from z.any() to z.unknown() so no explicit `any` is introduced - Repoint package.json main/types/exports to tsdown's .cjs/.d.cts output - Add src/telemetry.ts entry shim so the two index.ts entries don't collide in oxc's flat dts output (stable dist/telemetry.{cjs,d.cts}) - Delete rollup.config.js Build time ~36s -> ~0.5s. No runtime behavior change: 5712 unit tests pass, both entries load via require(), legacy /api consumes them unchanged. * 👷 ci: Hash packages/api/tsdown.config.mjs in build-api cache keys The build-api cache keys hashed `packages/api/server-rollup.config.js`, which never existed (api used `rollup.config.js`, now removed) — a copy-paste artifact from the data-provider key that matched no file. Replace it with the new `packages/api/tsdown.config.mjs` so edits to the build config (entry, format, externals) bust the api build cache, matching the data-schemas key.
This commit is contained in:
parent
50a6a5b1cd
commit
6bc75d24c8
81 changed files with 2481 additions and 280 deletions
|
|
@ -5,8 +5,8 @@ import {
|
|||
INTERFACE_PERMISSION_FIELDS,
|
||||
PERMISSION_SUB_KEYS,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TCustomConfig } from 'librechat-data-provider';
|
||||
import type { AppConfig, ConfigSection, IConfig } from '@librechat/data-schemas';
|
||||
import type { TCustomConfig } from 'librechat-data-provider';
|
||||
import type { Types, ClientSession } from 'mongoose';
|
||||
import type { Response } from 'express';
|
||||
import type { CapabilityUser } from '~/middleware/capabilities';
|
||||
|
|
@ -165,7 +165,17 @@ function getCapabilityUser(req: ServerRequest): CapabilityUser | null {
|
|||
|
||||
// ── Handler factory ──────────────────────────────────────────────────
|
||||
|
||||
export function createAdminConfigHandlers(deps: AdminConfigDeps) {
|
||||
export function createAdminConfigHandlers(deps: AdminConfigDeps): {
|
||||
listConfigs: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
getBaseConfig: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
getConfig: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
upsertConfigOverrides: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
patchConfigField: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
tombstoneConfigField: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
deleteConfigField: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
deleteConfigOverrides: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
toggleConfig: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
} {
|
||||
const {
|
||||
listAllConfigs,
|
||||
findConfigByPrincipal,
|
||||
|
|
@ -183,7 +193,7 @@ export function createAdminConfigHandlers(deps: AdminConfigDeps) {
|
|||
/**
|
||||
* GET / — List all active config overrides.
|
||||
*/
|
||||
async function listConfigs(req: ServerRequest, res: Response) {
|
||||
async function listConfigs(req: ServerRequest, res: Response): Promise<Response> {
|
||||
try {
|
||||
const user = getCapabilityUser(req);
|
||||
if (!user) {
|
||||
|
|
@ -206,7 +216,7 @@ export function createAdminConfigHandlers(deps: AdminConfigDeps) {
|
|||
* GET /base — Return the raw AppConfig (YAML + DB base merged).
|
||||
* This is the full config structure admins can edit, NOT the startup payload.
|
||||
*/
|
||||
async function getBaseConfig(req: ServerRequest, res: Response) {
|
||||
async function getBaseConfig(req: ServerRequest, res: Response): Promise<Response> {
|
||||
try {
|
||||
const user = getCapabilityUser(req);
|
||||
if (!user) {
|
||||
|
|
@ -236,7 +246,7 @@ export function createAdminConfigHandlers(deps: AdminConfigDeps) {
|
|||
/**
|
||||
* GET /:principalType/:principalId — Get config for a specific principal.
|
||||
*/
|
||||
async function getConfig(req: ServerRequest, res: Response) {
|
||||
async function getConfig(req: ServerRequest, res: Response): Promise<Response> {
|
||||
try {
|
||||
const { principalType, principalId } = req.params as {
|
||||
principalType: string;
|
||||
|
|
@ -273,7 +283,7 @@ export function createAdminConfigHandlers(deps: AdminConfigDeps) {
|
|||
/**
|
||||
* PUT /:principalType/:principalId — Replace entire overrides for a principal.
|
||||
*/
|
||||
async function upsertConfigOverrides(req: ServerRequest, res: Response) {
|
||||
async function upsertConfigOverrides(req: ServerRequest, res: Response): Promise<Response> {
|
||||
try {
|
||||
const { principalType, principalId } = req.params as {
|
||||
principalType: string;
|
||||
|
|
@ -382,7 +392,7 @@ export function createAdminConfigHandlers(deps: AdminConfigDeps) {
|
|||
/**
|
||||
* PATCH /:principalType/:principalId/fields — Set individual fields via dot-paths.
|
||||
*/
|
||||
async function patchConfigField(req: ServerRequest, res: Response) {
|
||||
async function patchConfigField(req: ServerRequest, res: Response): Promise<Response> {
|
||||
try {
|
||||
const { principalType, principalId } = req.params as {
|
||||
principalType: string;
|
||||
|
|
@ -491,7 +501,7 @@ export function createAdminConfigHandlers(deps: AdminConfigDeps) {
|
|||
/**
|
||||
* POST /:principalType/:principalId/fields/tombstone — Suppress an inherited config path.
|
||||
*/
|
||||
async function tombstoneConfigField(req: ServerRequest, res: Response) {
|
||||
async function tombstoneConfigField(req: ServerRequest, res: Response): Promise<Response> {
|
||||
try {
|
||||
const { principalType, principalId } = req.params as {
|
||||
principalType: string;
|
||||
|
|
@ -565,7 +575,7 @@ export function createAdminConfigHandlers(deps: AdminConfigDeps) {
|
|||
/**
|
||||
* DELETE /:principalType/:principalId/fields?fieldPath=dotted.path
|
||||
*/
|
||||
async function deleteConfigField(req: ServerRequest, res: Response) {
|
||||
async function deleteConfigField(req: ServerRequest, res: Response): Promise<Response> {
|
||||
try {
|
||||
const { principalType, principalId } = req.params as {
|
||||
principalType: string;
|
||||
|
|
@ -623,7 +633,7 @@ export function createAdminConfigHandlers(deps: AdminConfigDeps) {
|
|||
/**
|
||||
* DELETE /:principalType/:principalId — Delete an entire config override.
|
||||
*/
|
||||
async function deleteConfigOverrides(req: ServerRequest, res: Response) {
|
||||
async function deleteConfigOverrides(req: ServerRequest, res: Response): Promise<Response> {
|
||||
try {
|
||||
const { principalType, principalId } = req.params as {
|
||||
principalType: string;
|
||||
|
|
@ -661,7 +671,7 @@ export function createAdminConfigHandlers(deps: AdminConfigDeps) {
|
|||
/**
|
||||
* PATCH /:principalType/:principalId/active — Toggle isActive.
|
||||
*/
|
||||
async function toggleConfig(req: ServerRequest, res: Response) {
|
||||
async function toggleConfig(req: ServerRequest, res: Response): Promise<Response> {
|
||||
try {
|
||||
const { principalType, principalId } = req.params as {
|
||||
principalType: string;
|
||||
|
|
|
|||
|
|
@ -78,7 +78,13 @@ export interface AdminGrantsDeps {
|
|||
export type GrantPrincipalType = PrincipalType.ROLE;
|
||||
|
||||
/** Creates admin grant handlers with dependency injection for the /api/admin/grants routes. */
|
||||
export function createAdminGrantsHandlers(deps: AdminGrantsDeps) {
|
||||
export function createAdminGrantsHandlers(deps: AdminGrantsDeps): {
|
||||
listGrants: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
getEffectiveCapabilities: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
getPrincipalGrants: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
assignGrant: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
revokeGrant: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
} {
|
||||
const {
|
||||
listGrants,
|
||||
countGrants,
|
||||
|
|
|
|||
|
|
@ -84,7 +84,16 @@ export interface AdminGroupsDeps {
|
|||
}) => Promise<DeleteResult>;
|
||||
}
|
||||
|
||||
export function createAdminGroupsHandlers(deps: AdminGroupsDeps) {
|
||||
export function createAdminGroupsHandlers(deps: AdminGroupsDeps): {
|
||||
listGroups: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
getGroup: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
createGroup: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
updateGroup: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
deleteGroup: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
getGroupMembers: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
addGroupMember: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
removeGroupMember: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
} {
|
||||
const {
|
||||
listGroups,
|
||||
countGroups,
|
||||
|
|
|
|||
|
|
@ -123,7 +123,17 @@ export interface AdminRolesDeps {
|
|||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export function createAdminRolesHandlers(deps: AdminRolesDeps) {
|
||||
export function createAdminRolesHandlers(deps: AdminRolesDeps): {
|
||||
listRoles: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
getRole: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
createRole: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
updateRole: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
updateRolePermissions: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
deleteRole: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
getRoleMembers: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
addRoleMember: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
removeRoleMember: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
} {
|
||||
const {
|
||||
listRoles,
|
||||
countRoles,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,11 @@ export interface AdminUsersDeps {
|
|||
}) => Promise<void>;
|
||||
}
|
||||
|
||||
export function createAdminUsersHandlers(deps: AdminUsersDeps) {
|
||||
export function createAdminUsersHandlers(deps: AdminUsersDeps): {
|
||||
listUsers: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
searchUsers: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
deleteUser: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
} {
|
||||
const { findUsers, countUsers, deleteUserById, deleteConfig, deleteAclEntries } = deps;
|
||||
|
||||
async function listUsersHandler(req: ServerRequest, res: Response) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const DEFAULT_PROMPT_TEMPLATE = `Based on the following conversation and analysi
|
|||
*/
|
||||
export async function createSequentialChainEdges(
|
||||
agentIds: string[],
|
||||
promptTemplate = DEFAULT_PROMPT_TEMPLATE,
|
||||
promptTemplate: string = DEFAULT_PROMPT_TEMPLATE,
|
||||
): Promise<GraphEdge[]> {
|
||||
const edges: GraphEdge[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import type { ServerRequest } from '~/types';
|
|||
import Tokenizer from '~/utils/tokenizer';
|
||||
import { logAxiosError } from '~/utils';
|
||||
|
||||
export const omitTitleOptions = new Set([
|
||||
export const omitTitleOptions: Set<string> = new Set([
|
||||
'stream',
|
||||
'thinking',
|
||||
'streaming',
|
||||
|
|
@ -26,7 +26,13 @@ export const omitTitleOptions = new Set([
|
|||
'additionalModelRequestFields',
|
||||
]);
|
||||
|
||||
export function payloadParser({ req, endpoint }: { req: ServerRequest; endpoint: string }) {
|
||||
export function payloadParser({
|
||||
req,
|
||||
endpoint,
|
||||
}: {
|
||||
req: ServerRequest;
|
||||
endpoint: string;
|
||||
}): Record<string, unknown> | undefined {
|
||||
if (isAgentsEndpoint(endpoint)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -303,7 +309,7 @@ export function countFormattedMessageTokens(
|
|||
export function createTokenCounter(encoding: Parameters<typeof Tokenizer.getTokenCount>[1]) {
|
||||
const isClaude = encoding === 'claude';
|
||||
const countTokens = (text: string) => Tokenizer.getTokenCount(text, encoding);
|
||||
return function (message: BaseMessage) {
|
||||
return function (message: BaseMessage): number {
|
||||
const count = getTokenCountForMessage(
|
||||
message,
|
||||
countTokens,
|
||||
|
|
@ -313,7 +319,7 @@ export function createTokenCounter(encoding: Parameters<typeof Tokenizer.getToke
|
|||
};
|
||||
}
|
||||
|
||||
export function logToolError(_graph: unknown, error: unknown, toolId: string) {
|
||||
export function logToolError(_graph: unknown, error: unknown, toolId: string): void {
|
||||
logAxiosError({
|
||||
error,
|
||||
message: `[api/server/controllers/agents/client.js #chatCompletion] Tool Error "${toolId}"`,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import type { Request, Response } from 'express';
|
||||
import type { Types } from 'mongoose';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
|
||||
export interface ApiKeyHandlerDependencies {
|
||||
createAgentApiKey: (params: {
|
||||
|
|
@ -49,8 +49,16 @@ interface AuthenticatedRequest extends Request {
|
|||
};
|
||||
}
|
||||
|
||||
export function createApiKeyHandlers(deps: ApiKeyHandlerDependencies) {
|
||||
async function createApiKey(req: AuthenticatedRequest, res: Response) {
|
||||
export function createApiKeyHandlers(deps: ApiKeyHandlerDependencies): {
|
||||
createApiKey: (req: AuthenticatedRequest, res: Response) => Promise<Response | undefined>;
|
||||
listApiKeys: (req: AuthenticatedRequest, res: Response) => Promise<void>;
|
||||
getApiKey: (req: AuthenticatedRequest, res: Response) => Promise<Response | undefined>;
|
||||
deleteApiKey: (req: AuthenticatedRequest, res: Response) => Promise<Response | undefined>;
|
||||
} {
|
||||
async function createApiKey(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
): Promise<Response | undefined> {
|
||||
try {
|
||||
const { name, expiresAt } = req.body;
|
||||
|
||||
|
|
@ -80,7 +88,7 @@ export function createApiKeyHandlers(deps: ApiKeyHandlerDependencies) {
|
|||
}
|
||||
}
|
||||
|
||||
async function listApiKeys(req: AuthenticatedRequest, res: Response) {
|
||||
async function listApiKeys(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const keys = await deps.listAgentApiKeys(req.user?.id || '');
|
||||
res.status(200).json({ keys });
|
||||
|
|
@ -90,7 +98,10 @@ export function createApiKeyHandlers(deps: ApiKeyHandlerDependencies) {
|
|||
}
|
||||
}
|
||||
|
||||
async function getApiKey(req: AuthenticatedRequest, res: Response) {
|
||||
async function getApiKey(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
): Promise<Response | undefined> {
|
||||
try {
|
||||
const key = await deps.getAgentApiKeyById(req.params.id, req.user?.id || '');
|
||||
|
||||
|
|
@ -105,7 +116,10 @@ export function createApiKeyHandlers(deps: ApiKeyHandlerDependencies) {
|
|||
}
|
||||
}
|
||||
|
||||
async function deleteApiKey(req: AuthenticatedRequest, res: Response) {
|
||||
async function deleteApiKey(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
): Promise<Response | undefined> {
|
||||
try {
|
||||
const deleted = await deps.deleteAgentApiKey(req.params.id, req.user?.id || '');
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,11 @@ export interface RemoteAgentAccessRequest extends ApiKeyAuthRequest {
|
|||
}
|
||||
|
||||
export function createRequireApiKeyAuth(deps: ApiKeyAuthDependencies) {
|
||||
return async (req: ApiKeyAuthRequest, res: Response, next: NextFunction) => {
|
||||
return async (
|
||||
req: ApiKeyAuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
): Promise<Response | undefined> => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
|
|
@ -105,7 +109,11 @@ export function createRequireApiKeyAuth(deps: ApiKeyAuthDependencies) {
|
|||
}
|
||||
|
||||
export function createCheckRemoteAgentAccess(deps: RemoteAgentAccessDependencies) {
|
||||
return async (req: RemoteAgentAccessRequest, res: Response, next: NextFunction) => {
|
||||
return async (
|
||||
req: RemoteAgentAccessRequest,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
): Promise<Response | undefined> => {
|
||||
const agentId = req.body?.model || req.params?.model;
|
||||
|
||||
if (!agentId) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { createMethods } from '@librechat/data-schemas';
|
||||
import { ResourceType, PermissionBits, hasPermissions } from 'librechat-data-provider';
|
||||
import type { AllMethods, IUser } from '@librechat/data-schemas';
|
||||
import type {
|
||||
AgentApiKeyListItem,
|
||||
AgentApiKeyCreateResult,
|
||||
AllMethods,
|
||||
IUser,
|
||||
} from '@librechat/data-schemas';
|
||||
import type { Types } from 'mongoose';
|
||||
|
||||
export interface ApiKeyServiceDependencies {
|
||||
|
|
@ -36,19 +41,25 @@ export class AgentApiKeyService {
|
|||
userId: string | Types.ObjectId;
|
||||
name: string;
|
||||
expiresAt?: Date | null;
|
||||
}) {
|
||||
}): Promise<AgentApiKeyCreateResult> {
|
||||
return this.deps.createAgentApiKey(params);
|
||||
}
|
||||
|
||||
async listApiKeys(userId: string | Types.ObjectId) {
|
||||
async listApiKeys(userId: string | Types.ObjectId): Promise<AgentApiKeyListItem[]> {
|
||||
return this.deps.listAgentApiKeys(userId);
|
||||
}
|
||||
|
||||
async deleteApiKey(keyId: string | Types.ObjectId, userId: string | Types.ObjectId) {
|
||||
async deleteApiKey(
|
||||
keyId: string | Types.ObjectId,
|
||||
userId: string | Types.ObjectId,
|
||||
): Promise<boolean> {
|
||||
return this.deps.deleteAgentApiKey(keyId, userId);
|
||||
}
|
||||
|
||||
async getApiKeyById(keyId: string | Types.ObjectId, userId: string | Types.ObjectId) {
|
||||
async getApiKeyById(
|
||||
keyId: string | Types.ObjectId,
|
||||
userId: string | Types.ObjectId,
|
||||
): Promise<AgentApiKeyListItem | null> {
|
||||
return this.deps.getAgentApiKeyById(keyId, userId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@ const deprecatedVariables = [
|
|||
},
|
||||
];
|
||||
|
||||
export const deprecatedAzureVariables = [
|
||||
export const deprecatedAzureVariables: {
|
||||
key: string;
|
||||
description: string;
|
||||
}[] = [
|
||||
/* "related to" precedes description text */
|
||||
{ key: 'AZURE_OPENAI_DEFAULT_MODEL', description: 'setting a default model' },
|
||||
{ key: 'AZURE_OPENAI_MODELS', description: 'setting models' },
|
||||
|
|
@ -59,7 +62,9 @@ export const deprecatedAzureVariables = [
|
|||
},
|
||||
];
|
||||
|
||||
export const conflictingAzureVariables = [
|
||||
export const conflictingAzureVariables: {
|
||||
key: string;
|
||||
}[] = [
|
||||
{
|
||||
key: 'INSTANCE_NAME',
|
||||
},
|
||||
|
|
@ -100,7 +105,7 @@ function checkPasswordReset() {
|
|||
* @param {Function} options.isEnabled - Function to check if a feature is enabled
|
||||
* @param {Function} options.checkEmailConfig - Function to check email configuration
|
||||
*/
|
||||
export function checkVariables() {
|
||||
export function checkVariables(): void {
|
||||
let hasDefaultSecrets = false;
|
||||
for (const [key, value] of Object.entries(secretDefaults)) {
|
||||
if (process.env[key] === value) {
|
||||
|
|
@ -134,7 +139,7 @@ export function checkVariables() {
|
|||
* Checks the health of auxiliary API's by attempting a fetch request to their respective `/health` endpoints.
|
||||
* Logs information or warning based on the API's availability and response.
|
||||
*/
|
||||
export async function checkHealth() {
|
||||
export async function checkHealth(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${process.env.RAG_API_URL}/health`);
|
||||
if (response?.ok && response?.status === 200) {
|
||||
|
|
@ -169,7 +174,7 @@ function checkAzureVariables() {
|
|||
});
|
||||
}
|
||||
|
||||
export function checkInterfaceConfig(appConfig: AppConfig) {
|
||||
export function checkInterfaceConfig(appConfig: AppConfig): void {
|
||||
const interfaceConfig = appConfig.interfaceConfig;
|
||||
let i = 0;
|
||||
const logSettings = () => {
|
||||
|
|
@ -220,7 +225,7 @@ export function checkInterfaceConfig(appConfig: AppConfig) {
|
|||
* This should be called during application startup before initializing services.
|
||||
* @param [appConfig] - The application configuration object.
|
||||
*/
|
||||
export async function performStartupChecks(appConfig?: AppConfig) {
|
||||
export async function performStartupChecks(appConfig?: AppConfig): Promise<void> {
|
||||
checkVariables();
|
||||
if (appConfig?.endpoints?.azureOpenAI) {
|
||||
checkAzureVariables();
|
||||
|
|
@ -244,7 +249,7 @@ export async function performStartupChecks(appConfig?: AppConfig) {
|
|||
* Performs basic checks on the loaded config object.
|
||||
* @param config - The loaded custom configuration.
|
||||
*/
|
||||
export function checkConfig(config: Partial<TCustomConfig>) {
|
||||
export function checkConfig(config: Partial<TCustomConfig>): void {
|
||||
if (config.version !== Constants.CONFIG_VERSION) {
|
||||
logger.info(
|
||||
`\nOutdated Config version: ${config.version}
|
||||
|
|
@ -263,7 +268,9 @@ Latest version: ${Constants.CONFIG_VERSION}
|
|||
* Logs debug information for properly configured environment variable references.
|
||||
* @param webSearchConfig - The loaded web search configuration object.
|
||||
*/
|
||||
export function checkWebSearchConfig(webSearchConfig?: Partial<TCustomConfig['webSearch']> | null) {
|
||||
export function checkWebSearchConfig(
|
||||
webSearchConfig?: Partial<TCustomConfig['webSearch']> | null,
|
||||
): void {
|
||||
if (!webSearchConfig) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type { TCustomConfig } from 'librechat-data-provider';
|
|||
*
|
||||
* @param rateLimits
|
||||
*/
|
||||
export const handleRateLimits = (rateLimits?: TCustomConfig['rateLimits']) => {
|
||||
export const handleRateLimits = (rateLimits?: TCustomConfig['rateLimits']): void => {
|
||||
if (!rateLimits) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import {
|
|||
mergeConfigOverrides,
|
||||
BASE_CONFIG_PRINCIPAL_ID,
|
||||
} from '@librechat/data-schemas';
|
||||
import type { Types } from 'mongoose';
|
||||
import type { AppConfig, IConfig } from '@librechat/data-schemas';
|
||||
import type { Types } from 'mongoose';
|
||||
|
||||
const BASE_CONFIG_KEY = '_BASE_';
|
||||
|
||||
|
|
@ -91,7 +91,11 @@ function overrideCacheKey(role?: string, userId?: string, tenantId?: string): st
|
|||
|
||||
// ── Service factory ──────────────────────────────────────────────────
|
||||
|
||||
export function createAppConfigService(deps: AppConfigServiceDeps) {
|
||||
export function createAppConfigService(deps: AppConfigServiceDeps): {
|
||||
getAppConfig: (options?: GetAppConfigOptions) => Promise<AppConfig>;
|
||||
clearAppConfigCache: () => Promise<void>;
|
||||
clearOverrideCache: (tenantId?: string) => Promise<void>;
|
||||
} {
|
||||
const {
|
||||
loadBaseConfig,
|
||||
setCachedTools,
|
||||
|
|
|
|||
|
|
@ -178,9 +178,9 @@ export async function exchangeAdminCode(
|
|||
}
|
||||
|
||||
/** PKCE challenge cache TTL: 5 minutes (enough for user to authenticate with IdP) */
|
||||
export const PKCE_CHALLENGE_TTL = 5 * 60 * 1000;
|
||||
export const PKCE_CHALLENGE_TTL: number = 5 * 60 * 1000;
|
||||
/** Regex pattern for valid PKCE challenges: 64 hex characters (SHA-256 hex digest) */
|
||||
export const PKCE_CHALLENGE_PATTERN = /^[a-f0-9]{64}$/;
|
||||
export const PKCE_CHALLENGE_PATTERN: RegExp = /^[a-f0-9]{64}$/;
|
||||
|
||||
const ADMIN_OAUTH_STRIPPED_QUERY_PARAMS = new Set(['code_challenge', 'redirect_uri', 'redirectTo']);
|
||||
|
||||
|
|
|
|||
72
packages/api/src/cache/cacheConfig.ts
vendored
72
packages/api/src/cache/cacheConfig.ts
vendored
|
|
@ -70,7 +70,77 @@ const getRedisCA = (): string | null => {
|
|||
}
|
||||
};
|
||||
|
||||
const cacheConfig = {
|
||||
const cacheConfig: {
|
||||
FORCED_IN_MEMORY_CACHE_NAMESPACES: string[];
|
||||
USE_REDIS: boolean;
|
||||
USE_REDIS_STREAMS: boolean;
|
||||
REDIS_URI: string | undefined;
|
||||
REDIS_USERNAME: string | undefined;
|
||||
REDIS_PASSWORD: string | undefined;
|
||||
REDIS_CA: string | null;
|
||||
REDIS_KEY_PREFIX: string;
|
||||
GLOBAL_PREFIX_SEPARATOR: string;
|
||||
REDIS_MAX_LISTENERS: number;
|
||||
REDIS_PING_INTERVAL: number;
|
||||
/** Max delay between reconnection attempts in ms */
|
||||
REDIS_RETRY_MAX_DELAY: number;
|
||||
/** Max number of reconnection attempts (0 = infinite) */
|
||||
REDIS_RETRY_MAX_ATTEMPTS: number;
|
||||
/** Connection timeout in ms */
|
||||
REDIS_CONNECT_TIMEOUT: number;
|
||||
/** Queue commands when disconnected */
|
||||
REDIS_ENABLE_OFFLINE_QUEUE: boolean;
|
||||
/** flag to modify redis connection by adding dnsLookup this is required when connecting to elasticache for ioredis
|
||||
* see "Special Note: Aws Elasticache Clusters with TLS" on this webpage: https://www.npmjs.com/package/ioredis **/
|
||||
REDIS_USE_ALTERNATIVE_DNS_LOOKUP: boolean;
|
||||
/** Enable redis cluster without the need of multiple URIs */
|
||||
USE_REDIS_CLUSTER: boolean;
|
||||
/**
|
||||
* Force cluster-safe (key-by-key) deletion even when connecting as a single-node Redis instance.
|
||||
* Needed for managed services like ElastiCache Serverless that present a single endpoint
|
||||
* but shard keys internally, causing CROSSSLOT errors on multi-key DEL commands.
|
||||
* Has no effect when USE_REDIS_CLUSTER is already true.
|
||||
*/
|
||||
REDIS_CLUSTER_SAFE_DELETE: boolean;
|
||||
CI: boolean;
|
||||
DEBUG_MEMORY_CACHE: boolean;
|
||||
BAN_DURATION: number; // 2 hours
|
||||
/**
|
||||
* Number of keys to delete in each batch during Redis DEL operations.
|
||||
* In cluster mode, keys are deleted individually in parallel chunks to avoid CROSSSLOT errors.
|
||||
* In single-node mode, keys are deleted in batches using DEL with arrays.
|
||||
* Lower values reduce memory usage but increase number of Redis calls.
|
||||
* @default 1000
|
||||
*/
|
||||
REDIS_DELETE_CHUNK_SIZE: number;
|
||||
/**
|
||||
* Number of keys to update in each batch during Redis SET operations.
|
||||
* In cluster mode, keys are updated individually in parallel chunks to avoid CROSSSLOT errors.
|
||||
* In single-node mode, keys are updated in batches using transactions (multi/exec).
|
||||
* Lower values reduce memory usage but increase number of Redis calls.
|
||||
* @default 1000
|
||||
*/
|
||||
REDIS_UPDATE_CHUNK_SIZE: number;
|
||||
/**
|
||||
* COUNT hint for Redis SCAN operations when scanning keys by pattern.
|
||||
* This is a hint to Redis about how many keys to scan in each iteration.
|
||||
* Higher values can reduce round trips but increase memory usage and latency per call.
|
||||
* Note: Redis may return more or fewer keys than this count depending on internal heuristics.
|
||||
* @default 1000
|
||||
*/
|
||||
REDIS_SCAN_COUNT: number;
|
||||
/**
|
||||
* TTL in milliseconds for MCP registry caches. Used by both:
|
||||
* - `MCPServersRegistry` read-through caches (`readThroughCache`/`readThroughCacheAll`)
|
||||
* - `ServerConfigsCacheRedisAggregateKey` local snapshot (avoids redundant Redis GETs)
|
||||
*
|
||||
* Both layers use this value, so the effective max cross-instance staleness is up
|
||||
* to 2× this value in multi-instance deployments. Set to 0 to disable the local
|
||||
* snapshot entirely (every `getAll()` hits Redis directly).
|
||||
* @default 5000 (5 seconds)
|
||||
*/
|
||||
MCP_REGISTRY_CACHE_TTL: number;
|
||||
} = {
|
||||
FORCED_IN_MEMORY_CACHE_NAMESPACES,
|
||||
USE_REDIS,
|
||||
USE_REDIS_STREAMS,
|
||||
|
|
|
|||
8
packages/api/src/cache/keyvFiles.ts
vendored
8
packages/api/src/cache/keyvFiles.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyvFile } from 'keyv-file';
|
||||
|
||||
export const logFile = new KeyvFile({ filename: './data/logs.json' }).setMaxListeners(20);
|
||||
export const violationFile = new KeyvFile({ filename: './data/violations.json' }).setMaxListeners(
|
||||
20,
|
||||
);
|
||||
export const logFile: KeyvFile = new KeyvFile({ filename: './data/logs.json' }).setMaxListeners(20);
|
||||
export const violationFile: KeyvFile = new KeyvFile({
|
||||
filename: './data/violations.json',
|
||||
}).setMaxListeners(20);
|
||||
|
|
|
|||
2
packages/api/src/cache/keyvMongo.ts
vendored
2
packages/api/src/cache/keyvMongo.ts
vendored
|
|
@ -271,7 +271,7 @@ class KeyvMongoCustom extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
const keyvMongo = new KeyvMongoCustom({
|
||||
const keyvMongo: KeyvMongoCustom = new KeyvMongoCustom({
|
||||
collection: 'logs',
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export const initializeAzureBlobService = async (): Promise<BlobServiceClient |
|
|||
* @returns The Azure ContainerClient.
|
||||
*/
|
||||
export const getAzureContainerClient = async (
|
||||
containerName = process.env.AZURE_CONTAINER_NAME || 'files',
|
||||
containerName: string = process.env.AZURE_CONTAINER_NAME || 'files',
|
||||
): Promise<ContainerClient | null> => {
|
||||
const serviceClient = await initializeAzureBlobService();
|
||||
return serviceClient ? serviceClient.getContainerClient(containerName) : null;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import { getSignedCookies } from '@aws-sdk/cloudfront-signer';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
|
||||
import { getSignedCookies } from '@aws-sdk/cloudfront-signer';
|
||||
import type { NextFunction, Response } from 'express';
|
||||
|
||||
import { INLINE_AVATAR_PATH_PREFIX, INLINE_IMAGE_PATH_PREFIX } from '~/storage/constants';
|
||||
import { assertPathSegment } from '~/storage/validation';
|
||||
import { s3Config } from '~/storage/s3/s3Config';
|
||||
import { getCloudFrontConfig } from './cloudfront';
|
||||
import { s3Config } from '~/storage/s3/s3Config';
|
||||
|
||||
const DEFAULT_COOKIE_EXPIRY = 1800;
|
||||
|
||||
|
|
@ -147,11 +145,17 @@ function getConfiguredCookieExpiry(): number {
|
|||
return config?.cookieExpiry ?? DEFAULT_COOKIE_EXPIRY;
|
||||
}
|
||||
|
||||
export function getCloudFrontCookieRefreshWindowSec(cookieExpiry = getConfiguredCookieExpiry()) {
|
||||
export function getCloudFrontCookieRefreshWindowSec(
|
||||
cookieExpiry: number = getConfiguredCookieExpiry(),
|
||||
): number {
|
||||
return Math.min(300, Math.floor(cookieExpiry / 4));
|
||||
}
|
||||
|
||||
export function getCloudFrontCookieTiming() {
|
||||
export function getCloudFrontCookieTiming(): {
|
||||
expiresInSec: number;
|
||||
refreshAfterSec: number;
|
||||
refreshWindowSec: number;
|
||||
} {
|
||||
const expiresInSec = getConfiguredCookieExpiry();
|
||||
const refreshWindowSec = getCloudFrontCookieRefreshWindowSec(expiresInSec);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { FirebaseApp } from 'firebase/app';
|
|||
let firebaseInitCount = 0;
|
||||
let firebaseApp: FirebaseApp | null = null;
|
||||
|
||||
export const initializeFirebase = () => {
|
||||
export const initializeFirebase = (): firebase.FirebaseApp | null => {
|
||||
if (firebaseApp) {
|
||||
return firebaseApp;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { keyvRedisClient } from '~/cache/redisClients';
|
||||
import { cacheConfig as cache } from '~/cache/cacheConfig';
|
||||
import { clusterConfig as cluster } from './config';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import { cacheConfig as cache } from '~/cache/cacheConfig';
|
||||
import { keyvRedisClient } from '~/cache/redisClients';
|
||||
import { clusterConfig as cluster } from './config';
|
||||
|
||||
/**
|
||||
* Distributed leader election implementation using Redis for coordination across multiple server instances.
|
||||
|
|
@ -21,7 +21,7 @@ import { logger } from '@librechat/data-schemas';
|
|||
*/
|
||||
export class LeaderElection {
|
||||
// We can't use Keyv namespace here because we need direct Redis access for atomic operations
|
||||
static readonly LEADER_KEY = `${cache.REDIS_KEY_PREFIX}${cache.GLOBAL_PREFIX_SEPARATOR}LeadingServerUUID`;
|
||||
static readonly LEADER_KEY: string = `${cache.REDIS_KEY_PREFIX}${cache.GLOBAL_PREFIX_SEPARATOR}LeadingServerUUID`;
|
||||
private static _instance = new LeaderElection();
|
||||
|
||||
readonly UUID: string = crypto.randomUUID();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
import { math } from '~/utils';
|
||||
|
||||
const clusterConfig = {
|
||||
const clusterConfig: {
|
||||
/** Duration in seconds that the leader lease is valid before it expires */
|
||||
LEADER_LEASE_DURATION: number;
|
||||
/** Interval in seconds at which the leader renews its lease */
|
||||
LEADER_RENEW_INTERVAL: number;
|
||||
/** Maximum number of retry attempts when renewing the lease fails */
|
||||
LEADER_RENEW_ATTEMPTS: number;
|
||||
/** Delay in seconds between retry attempts when renewing the lease */
|
||||
LEADER_RENEW_RETRY_DELAY: number;
|
||||
} = {
|
||||
/** Duration in seconds that the leader lease is valid before it expires */
|
||||
LEADER_LEASE_DURATION: math(process.env.LEADER_LEASE_DURATION, 25),
|
||||
/** Interval in seconds at which the leader renews its lease */
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ function parseCredentials(
|
|||
}
|
||||
|
||||
/** Known Anthropic parameters that map directly to the client config */
|
||||
export const knownAnthropicParams = new Set([
|
||||
export const knownAnthropicParams: Set<string> = new Set([
|
||||
'model',
|
||||
'temperature',
|
||||
'topP',
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import {
|
|||
orderEndpointsConfig,
|
||||
defaultAgentCapabilities,
|
||||
} from 'librechat-data-provider';
|
||||
import type { AppConfig } from '@librechat/data-schemas';
|
||||
import type { AgentCapabilities, TEndpointsConfig, TConfig } from 'librechat-data-provider';
|
||||
import type { AppConfig } from '@librechat/data-schemas';
|
||||
import type { ServerRequest, TCustomEndpointsConfig } from '~/types';
|
||||
import { loadCustomEndpointsConfig as defaultLoadCustomEndpoints } from '~/endpoints/custom';
|
||||
|
||||
|
|
@ -24,7 +24,10 @@ export interface EndpointsConfigDeps {
|
|||
loadCustomEndpointsConfig?: (custom: unknown) => TCustomEndpointsConfig | undefined;
|
||||
}
|
||||
|
||||
export function createEndpointsConfigService(deps: EndpointsConfigDeps) {
|
||||
export function createEndpointsConfigService(deps: EndpointsConfigDeps): {
|
||||
getEndpointsConfig: (req: ServerRequest) => Promise<TEndpointsConfig>;
|
||||
checkCapability: (req: ServerRequest, capability: AgentCapabilities) => Promise<boolean>;
|
||||
} {
|
||||
const {
|
||||
getAppConfig,
|
||||
loadDefaultEndpointsConfig,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ type GoogleModelOptions = Partial<t.GoogleParameters> &
|
|||
Partial<Record<BlockedModelOptionParam, unknown>>;
|
||||
|
||||
/** Known Google/Vertex AI parameters that map directly to the client config */
|
||||
export const knownGoogleParams = new Set([
|
||||
export const knownGoogleParams: Set<string> = new Set([
|
||||
'model',
|
||||
'modelName',
|
||||
'temperature',
|
||||
|
|
@ -320,7 +320,14 @@ export function getGoogleConfig(
|
|||
credentials: string | t.GoogleCredentials | undefined,
|
||||
options: t.GoogleConfigOptions = {},
|
||||
acceptRawApiKey = false,
|
||||
) {
|
||||
): {
|
||||
/** @type {GoogleAIToolType[]} */
|
||||
tools: GoogleAIToolType[];
|
||||
/** @type {Providers.GOOGLE | Providers.VERTEXAI} */
|
||||
provider: Providers.VERTEXAI | Providers.GOOGLE;
|
||||
/** @type {GoogleClientOptions | VertexAIClientOptions} */
|
||||
llmConfig: VertexAIClientOptions | GoogleClientOptions;
|
||||
} {
|
||||
let creds: t.GoogleCredentials = {};
|
||||
if (acceptRawApiKey && typeof credentials === 'string') {
|
||||
creds[AuthKeys.GOOGLE_API_KEY] = credentials;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ type OpenAILLMConfig = Omit<Partial<t.OAIClientOptions>, 'verbosity'> &
|
|||
verbosity?: string | null;
|
||||
};
|
||||
|
||||
export const knownOpenAIParams = new Set([
|
||||
export const knownOpenAIParams: Set<string> = new Set([
|
||||
// Constructor/Instance Parameters
|
||||
'model',
|
||||
'modelName',
|
||||
|
|
@ -407,7 +407,7 @@ export function extractDefaultParams(
|
|||
export function applyDefaultParams(
|
||||
target: Record<string, unknown>,
|
||||
defaults: Record<string, unknown>,
|
||||
) {
|
||||
): void {
|
||||
for (const [key, value] of Object.entries(defaults)) {
|
||||
if (target[key] === undefined) {
|
||||
target[key] = value;
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import * as fs from 'fs/promises';
|
|||
import { randomUUID } from 'crypto';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import type { CodeArtifactCategory } from './classify';
|
||||
import { parseDocument } from '~/files/documents/crud';
|
||||
import { bufferToOfficeHtml, officeHtmlBucket } from '~/files/documents/html';
|
||||
import { isBinaryBuffer } from '~/skills/binary';
|
||||
import { createConcurrencyLimiter, withTimeout } from '~/utils/promise';
|
||||
import { parseDocument } from '~/files/documents/crud';
|
||||
import { isBinaryBuffer } from '~/skills/binary';
|
||||
|
||||
export const MAX_TEXT_CACHE_BYTES = 512 * 1024;
|
||||
export const MAX_TEXT_EXTRACT_BYTES = 1024 * 1024;
|
||||
export const MAX_TEXT_CACHE_BYTES: number = 512 * 1024;
|
||||
export const MAX_TEXT_EXTRACT_BYTES: number = 1024 * 1024;
|
||||
const DOCUMENT_PARSE_TIMEOUT_MS = 8_000;
|
||||
const OFFICE_HTML_TIMEOUT_MS = 12_000;
|
||||
const TRUNCATION_MARKER = '\n\n…[truncated]';
|
||||
|
|
|
|||
|
|
@ -1479,7 +1479,14 @@ async function pptxToSlideListHtmlInternal(buffer: Buffer): Promise<string> {
|
|||
* padded fixtures. Not part of the public API — callers in production
|
||||
* code should always go through `wordDocToHtml` / `pptxToHtml`.
|
||||
*/
|
||||
export const _internal = {
|
||||
export const _internal: {
|
||||
wordDocToHtmlViaCdn: typeof wordDocToHtmlViaCdn;
|
||||
wordDocToHtmlViaMammoth: typeof wordDocToHtmlViaMammoth;
|
||||
MAX_DOCX_CDN_BINARY_BYTES: number;
|
||||
OFFICE_HTML_OUTPUT_CAP: number;
|
||||
pptxToHtmlViaCdn: typeof pptxToHtmlViaCdn;
|
||||
MAX_PPTX_CDN_BINARY_BYTES: number;
|
||||
} = {
|
||||
wordDocToHtmlViaCdn,
|
||||
wordDocToHtmlViaMammoth,
|
||||
MAX_DOCX_CDN_BINARY_BYTES,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { spawn } from 'child_process';
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'fs/promises';
|
||||
|
||||
/**
|
||||
* LibreOffice-backed office preview pipeline.
|
||||
|
|
@ -196,7 +196,7 @@ export function _resetLibreOfficeProbeCache(): void {
|
|||
export const LIBREOFFICE_TIMEOUT_MS = 30_000;
|
||||
|
||||
/** Maximum PDF output size; refuse anything larger so a runaway doc can't fill the disk. */
|
||||
export const MAX_LIBREOFFICE_PDF_BYTES = 50 * 1024 * 1024;
|
||||
export const MAX_LIBREOFFICE_PDF_BYTES: number = 50 * 1024 * 1024;
|
||||
|
||||
/** Tag-distinct error so callers can distinguish "binary missing" from "conversion failed". */
|
||||
export class LibreOfficeUnavailableError extends Error {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg|webp)$/i;
|
|||
* @returns The basename of the image file from the URL.
|
||||
* Returns an empty string if the URL does not contain a valid image basename.
|
||||
*/
|
||||
export function getImageBasename(urlString: string) {
|
||||
export function getImageBasename(urlString: string): string {
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
const basename = path.basename(url.pathname);
|
||||
|
|
@ -29,7 +29,7 @@ export function getImageBasename(urlString: string) {
|
|||
* @returns The basename of the file from the URL.
|
||||
* Returns an empty string if the URL parsing fails.
|
||||
*/
|
||||
export function getFileBasename(urlString: string) {
|
||||
export function getFileBasename(urlString: string): string {
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
return path.basename(url.pathname);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export const getConversationExpirationDate = (
|
|||
return Number.isNaN(expiredAt.getTime()) ? null : expiredAt;
|
||||
};
|
||||
|
||||
export const isActiveExpirationDate = (expiredAt: Date, now = new Date()): boolean =>
|
||||
export const isActiveExpirationDate = (expiredAt: Date, now: Date = new Date()): boolean =>
|
||||
expiredAt > now;
|
||||
|
||||
const createRetentionExpiry = (
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export type ExpiredFileSweepResult = {
|
|||
};
|
||||
|
||||
export function getFileRetentionSweepInterval(
|
||||
interval = process.env.FILE_RETENTION_SWEEP_INTERVAL_MS,
|
||||
interval: string | undefined = process.env.FILE_RETENTION_SWEEP_INTERVAL_MS,
|
||||
): number {
|
||||
if (interval == null || interval.trim() === '') {
|
||||
return DEFAULT_FILE_RETENTION_SWEEP_INTERVAL_MS;
|
||||
|
|
@ -90,7 +90,10 @@ export function getExpiredFileEndpoint(source?: string): string {
|
|||
return source === FileSources.azure ? EModelEndpoint.azureAssistants : EModelEndpoint.assistants;
|
||||
}
|
||||
|
||||
export function hasExpiredFileEndpointConfig(appConfig: AppConfig | undefined, source?: string) {
|
||||
export function hasExpiredFileEndpointConfig(
|
||||
appConfig: AppConfig | undefined,
|
||||
source?: string,
|
||||
): boolean {
|
||||
if (source === FileSources.azure) {
|
||||
return Boolean(appConfig?.endpoints?.[EModelEndpoint.azureOpenAI]?.assistants);
|
||||
}
|
||||
|
|
@ -201,7 +204,7 @@ export async function resolveExpiredFileSweepConfig({
|
|||
}
|
||||
|
||||
export async function sweepExpiredFiles(
|
||||
{ appConfig, limit = 100, loadAppConfig }: ExpiredFileSweepOptions = {},
|
||||
{ appConfig, limit = 100, loadAppConfig }: ExpiredFileSweepOptions | undefined = {},
|
||||
{ getExpiredFiles, processDeleteRequest, logger }: SweepDependencies,
|
||||
): Promise<ExpiredFileSweepResult> {
|
||||
const files = (await getExpiredFiles(limit)) ?? [];
|
||||
|
|
@ -254,7 +257,7 @@ export async function sweepExpiredFiles(
|
|||
}
|
||||
|
||||
export function startExpiredFileSweep(
|
||||
options: ExpiredFileSweepOptions = {},
|
||||
options: ExpiredFileSweepOptions | undefined = {},
|
||||
{ sweepExpiredFiles, runAsSystem, logger }: StartSweepDependencies,
|
||||
): NodeJS.Timeout | null {
|
||||
const intervalMs = getFileRetentionSweepInterval();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { StoredDataNoRaw } from 'keyv';
|
|||
import type { FlowState, FlowMetadata, FlowManagerOptions } from './types';
|
||||
import { registerShutdownTask } from '../app/shutdown';
|
||||
|
||||
export const PENDING_STALE_MS = 2 * 60 * 1000;
|
||||
export const PENDING_STALE_MS: number = 2 * 60 * 1000;
|
||||
|
||||
const SECONDS_THRESHOLD = 1e10;
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export class MCPManager extends UserConnectionManager {
|
|||
}
|
||||
|
||||
/** Initializes the MCPManager by setting up server registry and app connections */
|
||||
public async initialize(configs: t.MCPServers) {
|
||||
public async initialize(configs: t.MCPServers): Promise<void> {
|
||||
await MCPServersInitializer.initialize(configs);
|
||||
this.appConnections = new ConnectionsRepository(undefined);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@ export abstract class UserConnectionManager {
|
|||
}
|
||||
|
||||
/** Returns all connections for a specific user */
|
||||
public getUserConnections(userId: string) {
|
||||
public getUserConnections(userId: string): Map<string, MCPConnection> | undefined {
|
||||
return this.userConnections.get(userId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export async function getUserMCPAuthMap({
|
|||
servers?: (string | undefined)[];
|
||||
toolInstances?: (GenericTool | null)[];
|
||||
findPluginAuthsByKeys: PluginAuthMethods['findPluginAuthsByKeys'];
|
||||
}) {
|
||||
}): Promise<Record<string, Record<string, string>>> {
|
||||
let allMcpCustomUserVars: Record<string, Record<string, string>> = {};
|
||||
let mcpPluginKeysToFetch: string[] = [];
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -2,27 +2,27 @@ import { isIP } from 'node:net';
|
|||
import { EventEmitter } from 'events';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import { fetch as undiciFetch, Agent, ProxyAgent } from 'undici';
|
||||
import {
|
||||
StdioClientTransport,
|
||||
getDefaultEnvironment,
|
||||
} from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
|
||||
import { ResourceListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||
import {
|
||||
StdioClientTransport,
|
||||
getDefaultEnvironment,
|
||||
} from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
import type {
|
||||
RequestInit as UndiciRequestInit,
|
||||
RequestInfo as UndiciRequestInfo,
|
||||
Response as UndiciResponse,
|
||||
Dispatcher,
|
||||
} from 'undici';
|
||||
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||
import type { MCPOAuthTokens } from './oauth/types';
|
||||
import type * as t from './types';
|
||||
import { createSSRFSafeUndiciConnect, isSSRFTarget, resolveHostnameSSRF } from '~/auth';
|
||||
import { isAddressAllowed } from '~/auth/domain';
|
||||
import { runOutsideTracing } from '~/utils/tracing';
|
||||
import { isAddressAllowed } from '~/auth/domain';
|
||||
import { sanitizeUrlForLogging } from './utils';
|
||||
import { withTimeout } from '~/utils/promise';
|
||||
import { mcpConfig } from './mcpConfig';
|
||||
|
|
@ -2183,7 +2183,50 @@ export class MCPConnection extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
async fetchTools() {
|
||||
async fetchTools(): Promise<
|
||||
{
|
||||
inputSchema: {
|
||||
[x: string]: unknown;
|
||||
type: 'object';
|
||||
properties?: Record<string, object> | undefined;
|
||||
required?: string[] | undefined;
|
||||
};
|
||||
name: string;
|
||||
description?: string | undefined;
|
||||
outputSchema?:
|
||||
| {
|
||||
[x: string]: unknown;
|
||||
type: 'object';
|
||||
properties?: Record<string, object> | undefined;
|
||||
required?: string[] | undefined;
|
||||
}
|
||||
| undefined;
|
||||
annotations?:
|
||||
| {
|
||||
title?: string | undefined;
|
||||
readOnlyHint?: boolean | undefined;
|
||||
destructiveHint?: boolean | undefined;
|
||||
idempotentHint?: boolean | undefined;
|
||||
openWorldHint?: boolean | undefined;
|
||||
}
|
||||
| undefined;
|
||||
execution?:
|
||||
| {
|
||||
taskSupport?: 'optional' | 'required' | 'forbidden' | undefined;
|
||||
}
|
||||
| undefined;
|
||||
_meta?: Record<string, unknown> | undefined;
|
||||
icons?:
|
||||
| {
|
||||
src: string;
|
||||
mimeType?: string | undefined;
|
||||
sizes?: string[] | undefined;
|
||||
theme?: 'light' | 'dark' | undefined;
|
||||
}[]
|
||||
| undefined;
|
||||
title?: string | undefined;
|
||||
}[]
|
||||
> {
|
||||
try {
|
||||
const { tools } = await this.client.listTools();
|
||||
return tools;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export type MCPErrorCode = (typeof MCPErrorCodes)[keyof typeof MCPErrorCodes];
|
|||
* Thrown when a user attempts to connect to an MCP server whose domain is not in the allowlist.
|
||||
*/
|
||||
export class MCPDomainNotAllowedError extends Error {
|
||||
public readonly code = MCPErrorCodes.DOMAIN_NOT_ALLOWED;
|
||||
public readonly code: 'MCP_DOMAIN_NOT_ALLOWED' = MCPErrorCodes.DOMAIN_NOT_ALLOWED;
|
||||
public readonly statusCode = 403;
|
||||
public readonly domain: string;
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ export class MCPDomainNotAllowedError extends Error {
|
|||
* Thrown when attempting to connect/inspect an MCP server fails.
|
||||
*/
|
||||
export class MCPInspectionFailedError extends Error {
|
||||
public readonly code = MCPErrorCodes.INSPECTION_FAILED;
|
||||
public readonly code: 'MCP_INSPECTION_FAILED' = MCPErrorCodes.INSPECTION_FAILED;
|
||||
public readonly statusCode = 400;
|
||||
public readonly serverName: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,27 @@ import { math, isEnabled } from '~/utils';
|
|||
* Centralized configuration for MCP-related environment variables.
|
||||
* Provides typed access to MCP settings with default values.
|
||||
*/
|
||||
export const mcpConfig = {
|
||||
export const mcpConfig: {
|
||||
OAUTH_ON_AUTH_ERROR: boolean;
|
||||
OAUTH_DETECTION_TIMEOUT: number;
|
||||
CONNECTION_CHECK_TTL: number;
|
||||
/** Idle timeout (ms) after which user connections are disconnected. Default: 15 minutes */
|
||||
USER_CONNECTION_IDLE_TIMEOUT: number;
|
||||
/** Max connect/disconnect cycles before the circuit breaker trips. Default: 7 */
|
||||
CB_MAX_CYCLES: number;
|
||||
/** Sliding window (ms) for counting cycles. Default: 45s */
|
||||
CB_CYCLE_WINDOW_MS: number;
|
||||
/** Cooldown (ms) after the cycle breaker trips. Default: 15s */
|
||||
CB_CYCLE_COOLDOWN_MS: number;
|
||||
/** Max consecutive failed connection rounds before backoff. Default: 3 */
|
||||
CB_MAX_FAILED_ROUNDS: number;
|
||||
/** Sliding window (ms) for counting failed rounds. Default: 120s */
|
||||
CB_FAILED_WINDOW_MS: number;
|
||||
/** Base backoff (ms) after failed round threshold is reached. Default: 30s */
|
||||
CB_BASE_BACKOFF_MS: number;
|
||||
/** Max backoff cap (ms) for exponential backoff. Default: 300s */
|
||||
CB_MAX_BACKOFF_MS: number;
|
||||
} = {
|
||||
OAUTH_ON_AUTH_ERROR: isEnabled(process.env.MCP_OAUTH_ON_AUTH_ERROR ?? true),
|
||||
OAUTH_DETECTION_TIMEOUT: math(process.env.MCP_OAUTH_DETECTION_TIMEOUT ?? 5000),
|
||||
CONNECTION_CHECK_TTL: math(process.env.MCP_CONNECTION_CHECK_TTL ?? 60000),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import type { TokenMethods, IUser } from '@librechat/data-schemas';
|
||||
import type { MCPOAuthTokens } from './types';
|
||||
import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry';
|
||||
import { OAuthReconnectionTracker } from './OAuthReconnectionTracker';
|
||||
import { FlowStateManager } from '~/flow/manager';
|
||||
import { MCPManager } from '~/mcp/MCPManager';
|
||||
import { MCPServersRegistry } from '~/mcp/registry/MCPServersRegistry';
|
||||
|
||||
const DEFAULT_CONNECTION_TIMEOUT_MS = 10_000; // ms
|
||||
const RECONNECT_STAGGER_MS = 500; // ms between each server reconnection
|
||||
|
|
@ -62,7 +62,7 @@ export class OAuthReconnectionManager {
|
|||
return this.reconnectionsTracker.isStillReconnecting(userId, serverName);
|
||||
}
|
||||
|
||||
public async reconnectServers(userId: string) {
|
||||
public async reconnectServers(userId: string): Promise<void> {
|
||||
// Check if MCPManager is available
|
||||
if (this.mcpManager == null) {
|
||||
logger.warn(
|
||||
|
|
@ -138,7 +138,7 @@ export class OAuthReconnectionManager {
|
|||
}
|
||||
}
|
||||
|
||||
public clearReconnection(userId: string, serverName: string) {
|
||||
public clearReconnection(userId: string, serverName: string): void {
|
||||
this.reconnectionsTracker.removeFailed(userId, serverName);
|
||||
this.reconnectionsTracker.removeActive(userId, serverName);
|
||||
}
|
||||
|
|
@ -183,7 +183,11 @@ export class OAuthReconnectionManager {
|
|||
}
|
||||
}
|
||||
|
||||
public getTrackerStats() {
|
||||
public getTrackerStats(): {
|
||||
usersWithFailedServers: number;
|
||||
usersWithActiveReconnections: number;
|
||||
activeTimestamps: number;
|
||||
} {
|
||||
return this.reconnectionsTracker.getStats();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ export type OAuthToolCall = {
|
|||
output?: string;
|
||||
};
|
||||
|
||||
export function getOAuthPromptExpiresAt(options?: OAuthPromptOptions, now = Date.now()): number {
|
||||
export function getOAuthPromptExpiresAt(
|
||||
options?: OAuthPromptOptions,
|
||||
now: number = Date.now(),
|
||||
): number {
|
||||
return typeof options?.expiresAt === 'number' && Number.isFinite(options.expiresAt)
|
||||
? options.expiresAt
|
||||
: now + Time.TWO_MINUTES;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export type ReplayablePendingMCPOAuthStartOptions = {
|
|||
|
||||
export function getReplayablePendingMCPOAuthStartFromFlow(
|
||||
flow: PendingOAuthFlowState | null | undefined,
|
||||
now = Date.now(),
|
||||
now: number = Date.now(),
|
||||
): PendingOAuthStart | undefined {
|
||||
if (flow?.status !== 'PENDING') {
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Tools } from 'librechat-data-provider';
|
|||
import type { UIResource } from 'librechat-data-provider';
|
||||
import type * as t from './types';
|
||||
|
||||
export const DEFAULT_MCP_IMAGE_DATA_MAX_BYTES = 10 * 1024 * 1024;
|
||||
export const DEFAULT_MCP_IMAGE_DATA_MAX_BYTES: number = 10 * 1024 * 1024;
|
||||
|
||||
function generateResourceId(text: string): string {
|
||||
return crypto.createHash('sha256').update(text).digest('hex').substring(0, 10);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { standardCache } from '~/cache';
|
||||
import Keyv from 'keyv';
|
||||
import { BaseRegistryCache } from './BaseRegistryCache';
|
||||
import { standardCache } from '~/cache';
|
||||
|
||||
// Status keys
|
||||
const INITIALIZED = 'INITIALIZED';
|
||||
|
|
@ -19,7 +20,7 @@ type StatusSetOptions = {
|
|||
* This cache is only meant to be used internally by registry management components.
|
||||
*/
|
||||
class RegistryStatusCache extends BaseRegistryCache {
|
||||
protected readonly cache = standardCache(`${this.PREFIX}::Status`);
|
||||
protected readonly cache: Keyv = standardCache(`${this.PREFIX}::Status`);
|
||||
|
||||
public async isInitialized(): Promise<boolean> {
|
||||
return (await this.get(INITIALIZED)) === true;
|
||||
|
|
@ -65,4 +66,4 @@ class RegistryStatusCache extends BaseRegistryCache {
|
|||
}
|
||||
}
|
||||
|
||||
export const registryStatusCache = new RegistryStatusCache();
|
||||
export const registryStatusCache: RegistryStatusCache = new RegistryStatusCache();
|
||||
|
|
|
|||
|
|
@ -20,7 +20,19 @@ export interface MCPToolCacheDeps {
|
|||
) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export function createMCPToolCacheService(deps: MCPToolCacheDeps) {
|
||||
export function createMCPToolCacheService(deps: MCPToolCacheDeps): {
|
||||
updateMCPServerTools: (params: {
|
||||
userId: string;
|
||||
serverName: string;
|
||||
tools: MCPToolInput[] | null;
|
||||
}) => Promise<LCAvailableTools>;
|
||||
mergeAppTools: (appTools: LCAvailableTools) => Promise<void>;
|
||||
cacheMCPServerTools: (params: {
|
||||
userId: string;
|
||||
serverName: string;
|
||||
serverTools: LCAvailableTools;
|
||||
}) => Promise<void>;
|
||||
} {
|
||||
const { getCachedTools, setCachedTools } = deps;
|
||||
|
||||
async function updateMCPServerTools(params: {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Constants } from 'librechat-data-provider';
|
||||
import type { ParsedServerConfig } from '~/mcp/types';
|
||||
|
||||
export const mcpToolPattern = new RegExp(`^.+${Constants.mcp_delimiter}.+$`);
|
||||
export const mcpToolPattern: RegExp = new RegExp(`^.+${Constants.mcp_delimiter}.+$`);
|
||||
|
||||
/** Whether a server should use MCP OAuth handling. */
|
||||
export function isOAuthServer(
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ function convertToZodUnion(
|
|||
export function resolveJsonSchemaRefs<T extends Record<string, unknown>>(
|
||||
schema: T,
|
||||
definitions?: Record<string, unknown>,
|
||||
visited = new Set<string>(),
|
||||
visited: Set<string> = new Set<string>(),
|
||||
): T {
|
||||
// Handle null, undefined, or non-object values first
|
||||
if (!schema || typeof schema !== 'object') {
|
||||
|
|
@ -568,7 +568,7 @@ export function convertJsonSchemaToZod(
|
|||
export function convertWithResolvedRefs(
|
||||
schema: JsonSchemaType & Record<string, unknown>,
|
||||
options?: ConvertJsonSchemaToZodOptions,
|
||||
) {
|
||||
): z.ZodType | undefined {
|
||||
const resolved = resolveJsonSchemaRefs(schema);
|
||||
return convertJsonSchemaToZod(resolved, options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import type { ServerRequest } from '~/types/http';
|
|||
* Middleware to check if authenticated user has admin role.
|
||||
* Should be used AFTER authentication middleware (requireJwtAuth, requireLocalAuth, etc.)
|
||||
*/
|
||||
export const requireAdmin = (req: ServerRequest, res: Response, next: NextFunction) => {
|
||||
export const requireAdmin = (
|
||||
req: ServerRequest,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
): Response | undefined => {
|
||||
if (!req.user) {
|
||||
logger.warn('[requireAdmin] No user found in request');
|
||||
return res.status(401).json({
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@ export type HasConfigCapabilityFn = (
|
|||
* Outside a request context (background jobs, tests), the store is undefined
|
||||
* and every check falls through to the database — correct behavior.
|
||||
*/
|
||||
export const capabilityStore = new AsyncLocalStorage<CapabilityStore>();
|
||||
export const capabilityStore: AsyncLocalStorage<CapabilityStore> =
|
||||
new AsyncLocalStorage<CapabilityStore>();
|
||||
|
||||
export function capabilityContextMiddleware(
|
||||
_req: ServerRequest,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ function hasTenantContext(context: TenantContext): boolean {
|
|||
|
||||
export function buildTenantContext(
|
||||
req: ContextRequest,
|
||||
tenantId = req.tenantId ?? req.user?.tenantId,
|
||||
tenantId: string | undefined = req.tenantId ?? req.user?.tenantId,
|
||||
): TenantContext {
|
||||
return {
|
||||
tenantId: normalizeContextValue(tenantId),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,14 @@ import {
|
|||
type TUser,
|
||||
} from 'librechat-data-provider';
|
||||
|
||||
export const PRIVATE_MODEL_SPEC_PRESET_FIELDS = [
|
||||
export const PRIVATE_MODEL_SPEC_PRESET_FIELDS: readonly [
|
||||
'promptPrefix',
|
||||
'instructions',
|
||||
'additional_instructions',
|
||||
'system',
|
||||
'context',
|
||||
'examples',
|
||||
] = [
|
||||
'promptPrefix',
|
||||
'instructions',
|
||||
'additional_instructions',
|
||||
|
|
@ -23,7 +30,7 @@ export type PrivateModelSpecPresetField = (typeof PRIVATE_MODEL_SPEC_PRESET_FIEL
|
|||
export type ModelSpecParsedBody = Partial<TConversation | TPreset | TModelSpecPreset> &
|
||||
Record<string, unknown>;
|
||||
|
||||
export const ENFORCED_MODEL_SPEC_REQUEST_FIELDS = [
|
||||
export const ENFORCED_MODEL_SPEC_REQUEST_FIELDS: readonly ['chatProjectId'] = [
|
||||
'chatProjectId',
|
||||
] as const satisfies readonly (keyof ModelSpecParsedBody)[];
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import type { Request, Response, NextFunction } from 'express';
|
|||
import { isEnabled } from '~/utils/common';
|
||||
|
||||
export const OAUTH_CSRF_COOKIE = 'oauth_csrf';
|
||||
export const OAUTH_CSRF_MAX_AGE = 10 * 60 * 1000;
|
||||
export const OAUTH_CSRF_MAX_AGE: number = 10 * 60 * 1000;
|
||||
|
||||
export const OAUTH_SESSION_COOKIE = 'oauth_session';
|
||||
export const OAUTH_SESSION_MAX_AGE = 24 * 60 * 60 * 1000;
|
||||
export const OAUTH_SESSION_MAX_AGE: number = 24 * 60 * 60 * 1000;
|
||||
export const OAUTH_SESSION_COOKIE_PATH = '/api';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import axios from 'axios';
|
||||
import { logger, encryptV2, decryptV2 } from '@librechat/data-schemas';
|
||||
import { TokenExchangeMethodEnum } from 'librechat-data-provider';
|
||||
import type { TokenMethods } from '@librechat/data-schemas';
|
||||
import { logger, encryptV2, decryptV2 } from '@librechat/data-schemas';
|
||||
import type { IToken, TokenMethods } from '@librechat/data-schemas';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { validateActionOAuthEndpoint } from './validation';
|
||||
import { createSSRFSafeAgents } from '~/auth';
|
||||
import { logAxiosError } from '~/utils';
|
||||
import { validateActionOAuthEndpoint } from './validation';
|
||||
|
||||
const actionOAuthAgents = createSSRFSafeAgents();
|
||||
const actionOAuthAgentsByAddress = new Map<string, ReturnType<typeof createSSRFSafeAgents>>();
|
||||
|
|
@ -59,7 +59,7 @@ export function createHandleOAuthToken({
|
|||
expiresIn?: number | string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
type?: string;
|
||||
}) {
|
||||
}): Promise<IToken | null> {
|
||||
const encrypedToken = await encryptV2(token);
|
||||
let expiresInNumber = 3600;
|
||||
if (typeof expiresIn === 'number') {
|
||||
|
|
|
|||
|
|
@ -84,8 +84,15 @@ const createProjectInput = (req: ProjectRequest): CreateChatProjectInput | null
|
|||
};
|
||||
};
|
||||
|
||||
export function createProjectHandlers(deps: ProjectHandlerDependencies) {
|
||||
async function listProjects(req: ProjectRequest, res: Response) {
|
||||
export function createProjectHandlers(deps: ProjectHandlerDependencies): {
|
||||
listProjects: (req: ProjectRequest, res: Response) => Promise<Response>;
|
||||
createProject: (req: ProjectRequest, res: Response) => Promise<Response>;
|
||||
assignConversationToProject: (req: ProjectRequest, res: Response) => Promise<Response>;
|
||||
getProject: (req: ProjectRequest, res: Response) => Promise<Response>;
|
||||
updateProject: (req: ProjectRequest, res: Response) => Promise<Response>;
|
||||
deleteProject: (req: ProjectRequest, res: Response) => Promise<Response>;
|
||||
} {
|
||||
async function listProjects(req: ProjectRequest, res: Response): Promise<Response> {
|
||||
try {
|
||||
const result = await deps.listChatProjects(getUserId(req), {
|
||||
cursor: queryString(req.query.cursor),
|
||||
|
|
@ -101,7 +108,7 @@ export function createProjectHandlers(deps: ProjectHandlerDependencies) {
|
|||
}
|
||||
}
|
||||
|
||||
async function createProject(req: ProjectRequest, res: Response) {
|
||||
async function createProject(req: ProjectRequest, res: Response): Promise<Response> {
|
||||
const input = createProjectInput(req);
|
||||
if (!input) {
|
||||
return res.status(400).json({ error: 'name is required' });
|
||||
|
|
@ -116,7 +123,10 @@ export function createProjectHandlers(deps: ProjectHandlerDependencies) {
|
|||
}
|
||||
}
|
||||
|
||||
async function assignConversationToProject(req: ProjectRequest, res: Response) {
|
||||
async function assignConversationToProject(
|
||||
req: ProjectRequest,
|
||||
res: Response,
|
||||
): Promise<Response> {
|
||||
const { conversationId } = req.params;
|
||||
const projectId = req.body?.projectId ?? null;
|
||||
|
||||
|
|
@ -143,7 +153,7 @@ export function createProjectHandlers(deps: ProjectHandlerDependencies) {
|
|||
}
|
||||
}
|
||||
|
||||
async function getProject(req: ProjectRequest, res: Response) {
|
||||
async function getProject(req: ProjectRequest, res: Response): Promise<Response> {
|
||||
const { projectId } = req.params;
|
||||
if (!isValidObjectIdString(projectId)) {
|
||||
return res.status(404).json({ error: PROJECT_NOT_FOUND });
|
||||
|
|
@ -161,7 +171,7 @@ export function createProjectHandlers(deps: ProjectHandlerDependencies) {
|
|||
}
|
||||
}
|
||||
|
||||
async function updateProject(req: ProjectRequest, res: Response) {
|
||||
async function updateProject(req: ProjectRequest, res: Response): Promise<Response> {
|
||||
const { projectId } = req.params;
|
||||
if (!isValidObjectIdString(projectId)) {
|
||||
return res.status(404).json({ error: PROJECT_NOT_FOUND });
|
||||
|
|
@ -191,7 +201,7 @@ export function createProjectHandlers(deps: ProjectHandlerDependencies) {
|
|||
}
|
||||
}
|
||||
|
||||
async function deleteProject(req: ProjectRequest, res: Response) {
|
||||
async function deleteProject(req: ProjectRequest, res: Response): Promise<Response> {
|
||||
const { projectId } = req.params;
|
||||
if (!isValidObjectIdString(projectId)) {
|
||||
return res.status(404).json({ error: PROJECT_NOT_FOUND });
|
||||
|
|
|
|||
|
|
@ -6,7 +6,19 @@ import { Constants } from 'librechat-data-provider';
|
|||
* Only allows fields that users should be able to modify.
|
||||
* Sensitive fields like author, authorName, _id, productionId, etc. are excluded.
|
||||
*/
|
||||
export const updatePromptGroupSchema = z
|
||||
export const updatePromptGroupSchema: z.ZodObject<
|
||||
{
|
||||
/** The name of the prompt group */
|
||||
name: z.ZodOptional<z.ZodString>;
|
||||
/** Short description/oneliner for the prompt group */
|
||||
oneliner: z.ZodOptional<z.ZodString>;
|
||||
/** Category for organizing prompt groups */
|
||||
category: z.ZodOptional<z.ZodString>;
|
||||
/** Command shortcut for the prompt group */
|
||||
command: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
},
|
||||
'strict'
|
||||
> = z
|
||||
.object({
|
||||
/** The name of the prompt group */
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
|
|
@ -44,6 +56,19 @@ export function validatePromptGroupUpdate(data: unknown): TUpdatePromptGroupSche
|
|||
* @param data - The raw request body to validate
|
||||
* @returns A SafeParseResult with either the validated data or validation errors
|
||||
*/
|
||||
export function safeValidatePromptGroupUpdate(data: unknown) {
|
||||
export function safeValidatePromptGroupUpdate(data: unknown): z.SafeParseReturnType<
|
||||
{
|
||||
name?: string | undefined;
|
||||
category?: string | undefined;
|
||||
command?: string | null | undefined;
|
||||
oneliner?: string | undefined;
|
||||
},
|
||||
{
|
||||
name?: string | undefined;
|
||||
category?: string | undefined;
|
||||
command?: string | null | undefined;
|
||||
oneliner?: string | undefined;
|
||||
}
|
||||
> {
|
||||
return updatePromptGroupSchema.safeParse(data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,19 +5,6 @@ import {
|
|||
PrincipalType,
|
||||
PermissionBits,
|
||||
} from 'librechat-data-provider';
|
||||
import type { Response } from 'express';
|
||||
import type { Types } from 'mongoose';
|
||||
import type {
|
||||
ISkill,
|
||||
ISkillFile,
|
||||
ISkillSummary,
|
||||
CreateSkillInput,
|
||||
CreateSkillResult,
|
||||
UpdateSkillInput,
|
||||
ListSkillsByAccessResult,
|
||||
UpdateSkillResult,
|
||||
ValidationIssue,
|
||||
} from '@librechat/data-schemas';
|
||||
import type {
|
||||
TSkill,
|
||||
TSkillFile,
|
||||
|
|
@ -31,6 +18,19 @@ import type {
|
|||
TSkillConflictResponse,
|
||||
TSkillFileContentResponse,
|
||||
} from 'librechat-data-provider';
|
||||
import type {
|
||||
ISkill,
|
||||
ISkillFile,
|
||||
ISkillSummary,
|
||||
CreateSkillInput,
|
||||
CreateSkillResult,
|
||||
UpdateSkillInput,
|
||||
ListSkillsByAccessResult,
|
||||
UpdateSkillResult,
|
||||
ValidationIssue,
|
||||
} from '@librechat/data-schemas';
|
||||
import type { Response } from 'express';
|
||||
import type { Types } from 'mongoose';
|
||||
import type { ServerRequest, StrategyFunctions } from '~/types';
|
||||
import { isBinaryBuffer } from './binary';
|
||||
|
||||
|
|
@ -268,7 +268,16 @@ function parseLimit(raw: unknown): number {
|
|||
* deps from `~/models` + `PermissionService`, and wires the returned handlers
|
||||
* onto the Express router.
|
||||
*/
|
||||
export function createSkillsHandlers(deps: SkillsHandlersDeps) {
|
||||
export function createSkillsHandlers(deps: SkillsHandlersDeps): {
|
||||
list: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
create: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
get: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
patch: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
delete: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
listFiles: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
downloadFile: (req: ServerRequest, res: Response) => Promise<Response | undefined>;
|
||||
deleteFile: (req: ServerRequest, res: Response) => Promise<Response>;
|
||||
} {
|
||||
const {
|
||||
createSkill,
|
||||
getSkillById,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import crypto from 'crypto';
|
||||
import path from 'path';
|
||||
import JSZip from 'jszip';
|
||||
import { ResourceType, AccessRoleIds, PrincipalType } from 'librechat-data-provider';
|
||||
import crypto from 'crypto';
|
||||
import { logger, stripYamlTrailingComment } from '@librechat/data-schemas';
|
||||
import type { Request, Response } from 'express';
|
||||
import type { Types } from 'mongoose';
|
||||
import { ResourceType, AccessRoleIds, PrincipalType } from 'librechat-data-provider';
|
||||
import type {
|
||||
ISkill,
|
||||
ISkillFile,
|
||||
|
|
@ -12,6 +10,8 @@ import type {
|
|||
CreateSkillResult,
|
||||
UpsertSkillFileInput,
|
||||
} from '@librechat/data-schemas';
|
||||
import type { Request, Response } from 'express';
|
||||
import type { Types } from 'mongoose';
|
||||
import { resolveRequestTenantId } from '~/middleware/tenant';
|
||||
|
||||
/** Security limits for zip processing. */
|
||||
|
|
@ -231,7 +231,7 @@ interface ServerRequest extends Request {
|
|||
* Grants SKILL_OWNER to the uploader.
|
||||
*/
|
||||
export function createImportHandler(deps: ImportSkillDeps) {
|
||||
return async function importSkillHandler(req: ServerRequest, res: Response) {
|
||||
return async function importSkillHandler(req: ServerRequest, res: Response): Promise<Response> {
|
||||
const { file } = req;
|
||||
if (!file) {
|
||||
return res.status(400).json({ error: 'No file provided' });
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const MAX_KEY_LENGTH = 64;
|
|||
* Generous upper bound on raw payload size to reject abusive inputs before
|
||||
* we spend cycles validating or querying the DB for orphan cleanup.
|
||||
*/
|
||||
export const MAX_RAW_PAYLOAD = MAX_SKILL_STATES * 2;
|
||||
export const MAX_RAW_PAYLOAD: number = MAX_SKILL_STATES * 2;
|
||||
|
||||
/** Map of skillId → explicit active state override. */
|
||||
export type SkillStatesRecord = Record<string, boolean>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import { isEnabled } from '~/utils/common';
|
||||
import { DEFAULT_BASE_PATH } from '~/storage/constants';
|
||||
import { isEnabled } from '~/utils/common';
|
||||
|
||||
const MAX_EXPIRY_SECONDS = 7 * 24 * 60 * 60; // 7 days
|
||||
const DEFAULT_EXPIRY_SECONDS = 2 * 60; // 2 minutes
|
||||
|
|
@ -39,7 +39,22 @@ const parseRefreshExpiry = (): number | null => {
|
|||
};
|
||||
|
||||
// Internal module config — not part of the public @librechat/api surface
|
||||
export const s3Config = {
|
||||
export const s3Config: {
|
||||
/** AWS region for S3 */
|
||||
AWS_REGION: string;
|
||||
/** S3 bucket name */
|
||||
AWS_BUCKET_NAME: string;
|
||||
/** Custom endpoint URL (for MinIO, R2, etc.) */
|
||||
AWS_ENDPOINT_URL: string | undefined;
|
||||
/** Use path-style URLs instead of virtual-hosted-style */
|
||||
AWS_FORCE_PATH_STYLE: boolean;
|
||||
/** Presigned URL expiry in seconds */
|
||||
S3_URL_EXPIRY_SECONDS: number;
|
||||
/** Custom refresh expiry in milliseconds (null = use default buffer logic) */
|
||||
S3_REFRESH_EXPIRY_MS: number | null;
|
||||
/** Default base path for file storage */
|
||||
DEFAULT_BASE_PATH: string;
|
||||
} = {
|
||||
/** AWS region for S3 */
|
||||
AWS_REGION: process.env.AWS_REGION ?? '',
|
||||
/** S3 bucket name */
|
||||
|
|
|
|||
|
|
@ -1572,5 +1572,5 @@ class GenerationJobManagerClass {
|
|||
}
|
||||
}
|
||||
|
||||
export const GenerationJobManager = new GenerationJobManagerClass();
|
||||
export const GenerationJobManager: GenerationJobManagerClass = new GenerationJobManagerClass();
|
||||
export { GenerationJobManagerClass };
|
||||
|
|
|
|||
6
packages/api/src/telemetry.ts
Normal file
6
packages/api/src/telemetry.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Build entry shim for the `@librechat/api/telemetry` subpath export.
|
||||
* Re-exports the telemetry barrel under a unique basename so the bundler emits
|
||||
* stable `dist/telemetry.*` output (see tsdown.config.mjs for details).
|
||||
*/
|
||||
export * from './telemetry/index';
|
||||
|
|
@ -75,7 +75,15 @@ const geminiImageGenJsonSchema: ExtendedJsonSchema = {
|
|||
required: ['prompt'],
|
||||
};
|
||||
|
||||
export const geminiToolkit = {
|
||||
export const geminiToolkit: {
|
||||
readonly gemini_image_gen: {
|
||||
readonly name: 'gemini_image_gen';
|
||||
readonly description: string;
|
||||
readonly description_for_model: 'Use this tool to generate images from text descriptions using Vertex AI Gemini.\n1. Prompts should be detailed and specific for best results.\n2. One image per function call. Create only 1 image per request.\n3. IMPORTANT: When user asks to "edit", "modify", "change", or "swap" elements in an existing image:\n - ALWAYS include the original image ID in the image_ids array\n - Describe the desired changes clearly in the prompt\n - The tool will generate a new image based on the original image context + your prompt\n4. IMPORTANT: For editing requests, use DIRECT editing instructions:\n - User says "remove the gun" → prompt should be "remove the gun from this image"\n - User says "make it blue" → prompt should be "make this image blue"\n - User says "add sunglasses" → prompt should be "add sunglasses to this image"\n - DO NOT reconstruct or modify the original prompt - use the user\'s editing instruction directly\n - ALWAYS include the image being edited in image_ids array\n5. OPTIONAL: Use image_ids to provide context images that will influence the generation:\n - Include any relevant image IDs from the conversation in the image_ids array\n - These images will be used as visual context/inspiration for the new generation\n - For "editing" requests, always include the image being "edited"\n6. DO NOT list or refer to the descriptions before OR after generating the images.\n7. Always mention the image type (photo, oil painting, watercolor painting, illustration, cartoon, drawing, vector, render, etc.) at the beginning of the prompt.\n8. Use aspectRatio to control the shape of the image:\n - 16:9 or 3:2 for landscape/wide images\n - 9:16 or 2:3 for portrait/tall images\n - 21:9 for ultra-wide/cinematic images\n - 1:1 for square images (default)\n9. Use imageSize to control the resolution: 1K (standard), 2K (high), 4K (maximum quality).\n\nThe prompt should be a detailed paragraph describing every part of the image in concrete, objective detail.';
|
||||
readonly schema: ExtendedJsonSchema;
|
||||
readonly responseFormat: 'content_and_artifact';
|
||||
};
|
||||
} = {
|
||||
gemini_image_gen: {
|
||||
name: 'gemini_image_gen' as const,
|
||||
description: getGeminiImageGenDescription(),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
* When a toolkit key appears in an agent's tool list,
|
||||
* these extra tools should also be included.
|
||||
*/
|
||||
export const toolkitExpansion = {
|
||||
export const toolkitExpansion: {
|
||||
readonly image_gen_oai: readonly ['image_edit_oai'];
|
||||
} = {
|
||||
image_gen_oai: ['image_edit_oai'],
|
||||
} as const satisfies Readonly<Record<string, readonly string[]>>;
|
||||
|
||||
|
|
|
|||
|
|
@ -131,7 +131,20 @@ Guidelines:
|
|||
required: ['image_ids', 'prompt'],
|
||||
};
|
||||
|
||||
export const oaiToolkit = {
|
||||
export const oaiToolkit: {
|
||||
readonly image_gen_oai: {
|
||||
readonly name: 'image_gen_oai';
|
||||
readonly description: string;
|
||||
readonly schema: ExtendedJsonSchema;
|
||||
readonly responseFormat: 'content_and_artifact';
|
||||
};
|
||||
readonly image_edit_oai: {
|
||||
readonly name: 'image_edit_oai';
|
||||
readonly description: string;
|
||||
readonly schema: ExtendedJsonSchema;
|
||||
readonly responseFormat: 'content_and_artifact';
|
||||
};
|
||||
} = {
|
||||
image_gen_oai: {
|
||||
name: 'image_gen_oai' as const,
|
||||
description: getImageGenDescription(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Buffer } from 'buffer';
|
||||
import axios from 'axios';
|
||||
import { Buffer } from 'buffer';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import type { AxiosInstance, AxiosProxyConfig, AxiosError } from 'axios';
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ export const logAxiosError = ({
|
|||
}: {
|
||||
message: string;
|
||||
error: AxiosError | Error | unknown;
|
||||
}) => {
|
||||
}): string => {
|
||||
let logMessage = message;
|
||||
try {
|
||||
const stack =
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@ import https from 'https';
|
|||
* on Node 19+ (keepAlive: true by default), tainted sockets re-enter the global pool
|
||||
* and kill unrelated requests (e.g., node-fetch in CodeExecutor) after the idle timeout.
|
||||
*/
|
||||
export const codeServerHttpAgent = new http.Agent({ keepAlive: false });
|
||||
export const codeServerHttpsAgent = new https.Agent({ keepAlive: false });
|
||||
export const codeServerHttpAgent: http.Agent = new http.Agent({ keepAlive: false });
|
||||
export const codeServerHttpsAgent: https.Agent = new https.Agent({ keepAlive: false });
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@ export function resolveHeaders(options?: {
|
|||
user?: Partial<IUser> | { id: string };
|
||||
body?: RequestBody;
|
||||
customUserVars?: Record<string, string>;
|
||||
}) {
|
||||
}): Record<string, string> {
|
||||
const { headers, user, body, customUserVars } = options ?? {};
|
||||
const inputHeaders = headers ?? {};
|
||||
|
||||
|
|
|
|||
|
|
@ -46,19 +46,23 @@ export function createFetch({
|
|||
* @param res - The response object to send events to
|
||||
* @returns Object containing handler functions
|
||||
*/
|
||||
export function createStreamEventHandlers(res: ServerResponse) {
|
||||
export function createStreamEventHandlers(res: ServerResponse): {
|
||||
on_run_step: (event: ServerSentEvent) => void;
|
||||
on_message_delta: (event: ServerSentEvent) => void;
|
||||
on_reasoning_delta: (event: ServerSentEvent) => void;
|
||||
} {
|
||||
return {
|
||||
[GraphEvents.ON_RUN_STEP]: function (event: ServerSentEvent) {
|
||||
[GraphEvents.ON_RUN_STEP]: function (event: ServerSentEvent): void {
|
||||
if (res) {
|
||||
sendEvent(res, event);
|
||||
}
|
||||
},
|
||||
[GraphEvents.ON_MESSAGE_DELTA]: function (event: ServerSentEvent) {
|
||||
[GraphEvents.ON_MESSAGE_DELTA]: function (event: ServerSentEvent): void {
|
||||
if (res) {
|
||||
sendEvent(res, event);
|
||||
}
|
||||
},
|
||||
[GraphEvents.ON_REASONING_DELTA]: function (event: ServerSentEvent) {
|
||||
[GraphEvents.ON_REASONING_DELTA]: function (event: ServerSentEvent): void {
|
||||
if (res) {
|
||||
sendEvent(res, event);
|
||||
}
|
||||
|
|
@ -67,7 +71,7 @@ export function createStreamEventHandlers(res: ServerResponse) {
|
|||
}
|
||||
|
||||
export function createHandleLLMNewToken(streamRate: number) {
|
||||
return async function () {
|
||||
return async function (): Promise<void> {
|
||||
if (streamRate) {
|
||||
await sleep(streamRate);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@
|
|||
export function normalizeHttpError(
|
||||
err: Error | { status?: number; message?: string } | unknown,
|
||||
fallbackStatus = 400,
|
||||
) {
|
||||
): {
|
||||
status: number;
|
||||
message: string;
|
||||
} {
|
||||
let status = fallbackStatus;
|
||||
if (err && typeof err === 'object' && 'status' in err && typeof err.status === 'number') {
|
||||
status = err.status;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import { GenerationJobManager } from '~/stream';
|
||||
import { OAuthReconnectionManager } from '~/mcp/oauth/OAuthReconnectionManager';
|
||||
import { GenerationJobManager } from '~/stream';
|
||||
import { MCPManager } from '~/mcp/MCPManager';
|
||||
|
||||
type ConnectionStats = ReturnType<InstanceType<typeof MCPManager>['getConnectionStats']>;
|
||||
|
|
@ -147,4 +147,10 @@ function stop(): void {
|
|||
logger.info('[MemDiag] Stopped memory diagnostics');
|
||||
}
|
||||
|
||||
export const memoryDiagnostics = { start, stop, forceGC, getSnapshots, collectSnapshot };
|
||||
export const memoryDiagnostics: {
|
||||
start: typeof start;
|
||||
stop: typeof stop;
|
||||
forceGC: typeof forceGC;
|
||||
getSnapshots: typeof getSnapshots;
|
||||
collectSnapshot: typeof collectSnapshot;
|
||||
} = { start, stop, forceGC, getSnapshots, collectSnapshot };
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class Tokenizer {
|
|||
}
|
||||
}
|
||||
|
||||
const TokenizerSingleton = new Tokenizer();
|
||||
const TokenizerSingleton: Tokenizer = new Tokenizer();
|
||||
|
||||
/**
|
||||
* Counts the number of tokens in a given text using ai-tokenizer with o200k_base encoding.
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ const aggregateModels = {
|
|||
...openAIModels,
|
||||
};
|
||||
|
||||
export const maxTokensMap = {
|
||||
export const maxTokensMap: Record<string, Record<string, number>> = {
|
||||
[EModelEndpoint.azureOpenAI]: openAIModels,
|
||||
[EModelEndpoint.openAI]: aggregateModels,
|
||||
[EModelEndpoint.agents]: aggregateModels,
|
||||
|
|
@ -421,7 +421,7 @@ const deepseekMaxOutputs = {
|
|||
'deepseek.r1': 64000,
|
||||
};
|
||||
|
||||
export const maxOutputTokensMap = {
|
||||
export const maxOutputTokensMap: Record<string, Record<string, number>> = {
|
||||
[EModelEndpoint.anthropic]: anthropicMaxOutputs,
|
||||
[EModelEndpoint.azureOpenAI]: modelMaxOutputs,
|
||||
[EModelEndpoint.openAI]: { ...modelMaxOutputs, ...deepseekMaxOutputs },
|
||||
|
|
@ -506,7 +506,7 @@ export function getModelTokenValue(
|
|||
*/
|
||||
export function getModelMaxTokens(
|
||||
modelName: string,
|
||||
endpoint = EModelEndpoint.openAI,
|
||||
endpoint: EModelEndpoint = EModelEndpoint.openAI,
|
||||
endpointTokenConfig?: EndpointTokenConfig,
|
||||
): number | undefined {
|
||||
const tokensMap = endpointTokenConfig ?? maxTokensMap[endpoint as keyof typeof maxTokensMap];
|
||||
|
|
@ -523,7 +523,7 @@ export function getModelMaxTokens(
|
|||
*/
|
||||
export function getModelMaxOutputTokens(
|
||||
modelName: string,
|
||||
endpoint = EModelEndpoint.openAI,
|
||||
endpoint: EModelEndpoint = EModelEndpoint.openAI,
|
||||
endpointTokenConfig?: EndpointTokenConfig,
|
||||
): number | undefined {
|
||||
const tokensMap =
|
||||
|
|
@ -546,7 +546,7 @@ export function getModelMaxOutputTokens(
|
|||
*/
|
||||
export function matchModelName(
|
||||
modelName: string,
|
||||
endpoint = EModelEndpoint.openAI,
|
||||
endpoint: EModelEndpoint = EModelEndpoint.openAI,
|
||||
): string | undefined {
|
||||
if (typeof modelName !== 'string') {
|
||||
return undefined;
|
||||
|
|
@ -565,7 +565,29 @@ export function matchModelName(
|
|||
return matchedPattern || modelName;
|
||||
}
|
||||
|
||||
export const modelSchema = z.object({
|
||||
export const modelSchema: z.ZodObject<
|
||||
{
|
||||
id: z.ZodString;
|
||||
pricing: z.ZodObject<
|
||||
{
|
||||
prompt: z.ZodString;
|
||||
completion: z.ZodString;
|
||||
},
|
||||
'strip',
|
||||
z.ZodTypeAny,
|
||||
{
|
||||
prompt: string;
|
||||
completion: string;
|
||||
},
|
||||
{
|
||||
prompt: string;
|
||||
completion: string;
|
||||
}
|
||||
>;
|
||||
context_length: z.ZodNumber;
|
||||
},
|
||||
'strip'
|
||||
> = z.object({
|
||||
id: z.string(),
|
||||
pricing: z.object({
|
||||
prompt: z.string(),
|
||||
|
|
@ -574,7 +596,54 @@ export const modelSchema = z.object({
|
|||
context_length: z.number(),
|
||||
});
|
||||
|
||||
export const inputSchema = z.object({
|
||||
export const inputSchema: z.ZodObject<
|
||||
{
|
||||
data: z.ZodArray<
|
||||
z.ZodObject<
|
||||
{
|
||||
id: z.ZodString;
|
||||
pricing: z.ZodObject<
|
||||
{
|
||||
prompt: z.ZodString;
|
||||
completion: z.ZodString;
|
||||
},
|
||||
'strip',
|
||||
z.ZodTypeAny,
|
||||
{
|
||||
prompt: string;
|
||||
completion: string;
|
||||
},
|
||||
{
|
||||
prompt: string;
|
||||
completion: string;
|
||||
}
|
||||
>;
|
||||
context_length: z.ZodNumber;
|
||||
},
|
||||
'strip',
|
||||
z.ZodTypeAny,
|
||||
{
|
||||
id: string;
|
||||
pricing: {
|
||||
prompt: string;
|
||||
completion: string;
|
||||
};
|
||||
context_length: number;
|
||||
},
|
||||
{
|
||||
id: string;
|
||||
pricing: {
|
||||
prompt: string;
|
||||
completion: string;
|
||||
};
|
||||
context_length: number;
|
||||
}
|
||||
>,
|
||||
'many'
|
||||
>;
|
||||
},
|
||||
'strip'
|
||||
> = z.object({
|
||||
data: z.array(modelSchema),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import fs from 'fs';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
export function loadYaml(filepath: string) {
|
||||
export function loadYaml(filepath: string): unknown {
|
||||
try {
|
||||
const fileContents = fs.readFileSync(filepath, 'utf8');
|
||||
return yaml.load(fileContents);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue