Compare commits

...

9 Commits

Author SHA1 Message Date
Danny Avila
9449f36ead docs: add AGENTS.md for project structure and development guidelines 2025-10-26 12:32:21 +01:00
Danny Avila
9495520f6f 📦 chore: update vite to v6.4.1 and @playwright/test to v1.56.1 (#10227)
* 📦 chore: update vite to v6.4.1

* 📦 chore: update @playwright/test to v1.56.1
2025-10-22 22:22:57 +02:00
Sebastien Bruel
87d7ee4b0e 🌐 feat: Configurable Domain and Port for Vite Dev Server (#10180) 2025-10-22 22:04:49 +02:00
Danny Avila
d8d5d59d92 ♻️ refactor: Message Cache Clearing Logic into Reusable Helper (#10226) 2025-10-22 22:02:29 +02:00
Danny Avila
e3d33fed8d 📦 chore: update @librechat/agents to v2.4.86 (#10216) 2025-10-22 16:51:58 +02:00
github-actions[bot]
cbf52eabe3 🌍 i18n: Update translation.json with latest translations (#10175)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-22 09:53:21 +02:00
Danny Avila
36f0365fd4 🧮 feat: Enhance Model Pricing Coverage and Pattern Matching (#10173)
* updated gpt5-pro

it is here and on openrouter
https://platform.openai.com/docs/models/gpt-5-pro

* feat: Add gpt-5-pro pricing
- Implemented handling for the new gpt-5-pro model in the getValueKey function.
- Updated tests to ensure correct behavior for gpt-5-pro across various scenarios.
- Adjusted token limits and multipliers for gpt-5-pro in the tokens utility files.
- Enhanced model matching functionality to include gpt-5-pro variations.

* refactor: optimize model pricing and validation logic

- Added new model pricing entries for llama2, llama3, and qwen variants in tx.js.
- Updated tokenValues to include additional models and their pricing structures.
- Implemented validation tests in tx.spec.js to ensure all models resolve correctly to pricing.
- Refactored getValueKey function to improve model matching and resolution efficiency.
- Removed outdated model entries from tokens.ts to streamline pricing management.

* fix: add missing pricing

* chore: update model pricing for qwen and gemma variants

* chore: update model pricing and add validation for context windows

- Removed outdated model entries from tx.js and updated tokenValues with new models.
- Added a test in tx.spec.js to ensure all models with pricing have corresponding context windows defined in tokens.ts.
- Introduced 'command-text' model pricing in tokens.ts to maintain consistency across model definitions.

* chore: update model names and pricing for AI21 and Amazon models

- Refactored model names in tx.js for AI21 and Amazon models to remove versioning and improve consistency.
- Updated pricing values in tokens.ts to reflect the new model names.
- Added comprehensive tests in tx.spec.js to validate pricing for both short and full model names across AI21 and Amazon models.

* feat: add pricing and validation for Claude Haiku 4.5 model

* chore: increase default max context tokens to 18000 for agents

* feat: add Qwen3 model pricing and validation tests

* chore: reorganize and update Qwen model pricing in tx.js and tokens.ts

---------

Co-authored-by: khfung <68192841+khfung@users.noreply.github.com>
2025-10-19 15:23:27 +02:00
Federico Ruggi
589f119310 🩹 fix: Wrap Attempt to Reconnect OAuth MCP Servers (#10172) 2025-10-18 05:54:05 -04:00
Marco Beretta
d41b07c0af ♻️ refactor: Replace fontSize Recoil atom with Jotai (#10171)
* fix: reapply chat font size on load

* refactor: streamline font size handling in localStorage

* fix: update matchMedia mock to accurately reflect desktop and touchscreen capabilities

* refactor: implement Jotai for font size management and initialize on app load

- Replaced Recoil with Jotai for font size state management across components.
- Added a new `fontSize` atom to handle font size changes and persist them in localStorage.
- Implemented `initializeFontSize` function to apply saved font size on app load.
- Updated relevant components to utilize the new font size atom.

---------

Co-authored-by: ddooochii <ddooochii@gmail.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-10-18 05:50:34 -04:00
32 changed files with 1503 additions and 210 deletions

233
AGENTS.md Normal file
View File

@@ -0,0 +1,233 @@
# LibreChat AGENTS.md
LibreChat is a multi-provider AI chat platform featuring agents, tools, and multimodal interactions. This file provides guidance for AI coding agents working on the codebase.
## Project Overview
LibreChat is a monorepo-based full-stack application providing a unified interface for multiple AI models and providers (OpenAI, Anthropic, Google Gemini, Azure, AWS Bedrock, Groq, Mistral, and more).
## Workspace Structure
LibreChat uses npm workspaces to organize code into distinct packages with clear responsibilities:
```
LibreChat/
├── api/ # Express.js backend (CJS, transitioning to TypeScript)
├── client/ # React/Vite frontend application
└── packages/
├── api/ # @librechat/api - Backend TypeScript package
├── client/ # @librechat/client - Frontend shared components
├── data-provider/ # librechat-data-provider - Shared frontend/backend
└── data-schemas/ # @librechat/data-schemas - Backend schemas & models
```
### Workspace Responsibilities
#### `/api` - Express.js Backend
- **Language**: CommonJS JavaScript (transitioning to TypeScript via shared packages)
- **Purpose**: Main Express.js server, routes, middleware, legacy services
- **Note**: Legacy workspace; **new backend logic should go in `packages/api` instead**
- **Uses**: `librechat-data-provider`, `@librechat/api`, `@librechat/data-schemas`
#### `/client` - React Frontend Application
- **Language**: TypeScript + React
- **Purpose**: Main web application UI
- **Uses**: `librechat-data-provider`, `@librechat/client`
#### `/packages/api` - `@librechat/api`
- **Language**: TypeScript (full support)
- **Purpose**: Backend-only package for all new backend logic
- **Used by**: `/api` workspace and potentially other backend projects
- **Key Modules**: Agents, MCP, tools, file handling, endpoints, authentication, caching, middleware
- **Critical**: **All new backend logic should be coded here first and foremost for full TypeScript support**
- **Depends on**: `@librechat/data-schemas`
#### `/packages/client` - `@librechat/client`
- **Language**: TypeScript + React
- **Purpose**: Reusable React components, hooks, and utilities
- **Used by**: `/client` workspace and other LibreChat team frontend repositories
- **Exports**: Common components, hooks, SVG icons, theme utilities, localization
#### `/packages/data-provider` - `librechat-data-provider`
- **Language**: TypeScript
- **Purpose**: **App-wide shared package** used by both frontend and backend
- **Scope**: Universal - used everywhere
- **Exports**: Data services, API endpoints, type definitions, utilities, React Query hooks
- **Note**: Foundation package that all other workspaces depend on
#### `/packages/data-schemas` - `@librechat/data-schemas`
- **Language**: TypeScript
- **Purpose**: Backend-only schemas, models, and data validation
- **Used by**: Backend workspaces only (`@librechat/api`, `/api`)
- **Exports**: Mongoose models, Zod schemas, database utilities, configuration schemas
## Build System & Dependencies
### Build Order
The build system has a specific dependency chain that must be followed:
```bash
# Full frontend build command:
npm run frontend
```
**Execution order:**
1. `librechat-data-provider` - Foundation package (all depend on this)
2. `@librechat/data-schemas` - Required by `@librechat/api`
3. `@librechat/api` - Backend TypeScript package
4. `@librechat/client` - Frontend shared components
5. `/client` - Final frontend compilation
### Individual Build Commands
Packages can be built separately as needed:
```bash
npm run build:data-provider # Build librechat-data-provider
npm run build:data-schemas # Build @librechat/data-schemas
npm run build:api # Build @librechat/api
npm run build:client-package # Build @librechat/client
npm run build:client # Build /client frontend app
npm run build:packages # Build all packages (excludes /client app)
```
**Note**: Not all packages need rebuilding for every change - only rebuild when files in that specific workspace are modified.
## Development Workflow
### Running Development Servers
```bash
# Backend
npm run backend:dev # Runs /api workspace with nodemon
# Frontend
npm run frontend:dev # Runs /client with Vite dev server
# Both (not recommended for active development)
npm run dev
```
### Where to Place New Code
#### Backend Logic
- **Primary location**: `/packages/api/src/` - Full TypeScript support
- **Legacy location**: `/api/` - Only for modifying existing CJS code
- **Strategy**: Prefer `@librechat/api` for all new features, utilities, and services
#### Frontend Components
- **Reusable components**: `/packages/client/src/components/`
- **App-specific components**: `/client/src/components/`
- **Strategy**: If it could be reused in other frontend projects, put it in `@librechat/client`
#### Shared Types & Utilities
- **Universal (frontend + backend)**: `/packages/data-provider/src/`
- **Backend-only**: `/packages/data-schemas/src/` or `/packages/api/src/`
- **Frontend-only**: `/packages/client/src/`
#### Database Models & Schemas
- **Always**: `/packages/data-schemas/src/models/` or `/packages/data-schemas/src/schema/`
## Technology Stack
### Backend
- **Runtime**: Node.js
- **Framework**: Express.js
- **Database**: MongoDB with Mongoose ODM
- **Authentication**: Passport.js (OAuth2, OpenID, LDAP, SAML, JWT)
- **AI Integration**: LangChain, direct SDK integrations
- **Streaming**: Server-Sent Events (SSE)
### Frontend
- **Framework**: React 18
- **Build Tool**: Vite
- **State Management**: Recoil
- **Styling**: TailwindCSS
- **UI Components**: Radix UI, Headless UI
- **Data Fetching**: TanStack Query (React Query)
- **HTTP Client**: Axios
### Package Manager
- **Primary**: npm (workspaces)
- **Alternative**: pnpm (compatible), bun (experimental scripts available)
## Configuration
### Environment Variables
- `.env` - Local development (never commit)
- `docker-compose.override.yml.example` - Example for Docker environment configuration
- Validate on server startup
### librechat.yaml
Main configuration for:
- AI provider endpoints
- Model configurations
- Agent and tool settings
- Authentication options
## Code Quality & Standards
### General Guidelines
- Use TypeScript for all new code (especially in `packages/`)
- Follow existing ESLint/Prettier configurations (formatting issues will be fixed manually)
- Keep functions focused and modular
- Handle errors appropriately with proper HTTP status codes
### File Naming
- React components: `PascalCase.tsx` (e.g., `ChatInterface.tsx`)
- Utilities: `camelCase.ts` (e.g., `formatMessage.ts`)
- Test files: `*.spec.ts` or `*.test.ts`
## Key Architectural Patterns
### Multi-Provider Pattern
Abstract AI provider implementations to support multiple services uniformly:
- Provider services implement common interfaces
- Handle streaming via SSE
- Support both completion and chat endpoints
### Agent System
- Built on LangChain
- Agent definitions in `packages/api/src/agents/`
- Tools in `packages/api/src/tools/`
- MCP (Model Context Protocol) support in `packages/api/src/mcp/`
### Data Layer
- `librechat-data-provider` provides unified data access
- React Query for frontend data fetching
- Backend services use Mongoose models from `@librechat/data-schemas`
## Testing
```bash
npm run test:api # Backend tests
npm run test:client # Frontend tests
npm run e2e # Playwright E2E tests
```
## Common Pitfalls
1. **Wrong workspace for backend code**: New backend logic belongs in `/packages/api`, not `/api`
- The `/api` workspace is still necessary as it's used to run the Express.js server, but all new logic should be written in TypeScript in `/packages/api` as much as possible. Existing logic should also be converted to TypeScript if possible.
2. **Build order matters**: Can't build `@librechat/api` before `@librechat/data-schemas`
3. **Unnecessary rebuilds**: Only rebuild packages when their source files change
4. **Import paths**: Use package names (`@librechat/api`) not relative paths across workspaces
5. **TypeScript vs CJS**: `/api` is still CJS; use `packages/api` for TypeScript
## Getting Help
- **Documentation**: https://docs.librechat.ai
- **Discord**: Active community support
- **GitHub Issues**: Bug reports and feature requests
- **Discussions**: General questions and ideas
---
**Remember**: The monorepo structure exists to enforce separation of concerns. Respect workspace boundaries and build dependencies. When in doubt about where code should live, consider:
- Is it backend logic? → `packages/api/src/`
- Is it a database model? → `packages/data-schemas/src/`
- Is it shared between frontend/backend? → `packages/data-provider/src/`
- Is it a reusable React component? → `packages/client/src/`
- Is it app-specific UI? → `client/src/`

View File

@@ -1,4 +1,4 @@
const { matchModelName } = require('@librechat/api');
const { matchModelName, findMatchingPattern } = require('@librechat/api');
const defaultRate = 6;
/**
@@ -6,44 +6,58 @@ const defaultRate = 6;
* source: https://aws.amazon.com/bedrock/pricing/
* */
const bedrockValues = {
// Basic llama2 patterns
// Basic llama2 patterns (base defaults to smallest variant)
llama2: { prompt: 0.75, completion: 1.0 },
'llama-2': { prompt: 0.75, completion: 1.0 },
'llama2-13b': { prompt: 0.75, completion: 1.0 },
'llama2:13b': { prompt: 0.75, completion: 1.0 },
'llama2:70b': { prompt: 1.95, completion: 2.56 },
'llama2-70b': { prompt: 1.95, completion: 2.56 },
// Basic llama3 patterns
// Basic llama3 patterns (base defaults to smallest variant)
llama3: { prompt: 0.3, completion: 0.6 },
'llama-3': { prompt: 0.3, completion: 0.6 },
'llama3-8b': { prompt: 0.3, completion: 0.6 },
'llama3:8b': { prompt: 0.3, completion: 0.6 },
'llama3-70b': { prompt: 2.65, completion: 3.5 },
'llama3:70b': { prompt: 2.65, completion: 3.5 },
// llama3-x-Nb pattern
// llama3-x-Nb pattern (base defaults to smallest variant)
'llama3-1': { prompt: 0.22, completion: 0.22 },
'llama3-1-8b': { prompt: 0.22, completion: 0.22 },
'llama3-1-70b': { prompt: 0.72, completion: 0.72 },
'llama3-1-405b': { prompt: 2.4, completion: 2.4 },
'llama3-2': { prompt: 0.1, completion: 0.1 },
'llama3-2-1b': { prompt: 0.1, completion: 0.1 },
'llama3-2-3b': { prompt: 0.15, completion: 0.15 },
'llama3-2-11b': { prompt: 0.16, completion: 0.16 },
'llama3-2-90b': { prompt: 0.72, completion: 0.72 },
'llama3-3': { prompt: 2.65, completion: 3.5 },
'llama3-3-70b': { prompt: 2.65, completion: 3.5 },
// llama3.x:Nb pattern
// llama3.x:Nb pattern (base defaults to smallest variant)
'llama3.1': { prompt: 0.22, completion: 0.22 },
'llama3.1:8b': { prompt: 0.22, completion: 0.22 },
'llama3.1:70b': { prompt: 0.72, completion: 0.72 },
'llama3.1:405b': { prompt: 2.4, completion: 2.4 },
'llama3.2': { prompt: 0.1, completion: 0.1 },
'llama3.2:1b': { prompt: 0.1, completion: 0.1 },
'llama3.2:3b': { prompt: 0.15, completion: 0.15 },
'llama3.2:11b': { prompt: 0.16, completion: 0.16 },
'llama3.2:90b': { prompt: 0.72, completion: 0.72 },
'llama3.3': { prompt: 2.65, completion: 3.5 },
'llama3.3:70b': { prompt: 2.65, completion: 3.5 },
// llama-3.x-Nb pattern
// llama-3.x-Nb pattern (base defaults to smallest variant)
'llama-3.1': { prompt: 0.22, completion: 0.22 },
'llama-3.1-8b': { prompt: 0.22, completion: 0.22 },
'llama-3.1-70b': { prompt: 0.72, completion: 0.72 },
'llama-3.1-405b': { prompt: 2.4, completion: 2.4 },
'llama-3.2': { prompt: 0.1, completion: 0.1 },
'llama-3.2-1b': { prompt: 0.1, completion: 0.1 },
'llama-3.2-3b': { prompt: 0.15, completion: 0.15 },
'llama-3.2-11b': { prompt: 0.16, completion: 0.16 },
'llama-3.2-90b': { prompt: 0.72, completion: 0.72 },
'llama-3.3': { prompt: 2.65, completion: 3.5 },
'llama-3.3-70b': { prompt: 2.65, completion: 3.5 },
'mistral-7b': { prompt: 0.15, completion: 0.2 },
'mistral-small': { prompt: 0.15, completion: 0.2 },
@@ -52,15 +66,19 @@ const bedrockValues = {
'mistral-large-2407': { prompt: 3.0, completion: 9.0 },
'command-text': { prompt: 1.5, completion: 2.0 },
'command-light': { prompt: 0.3, completion: 0.6 },
'ai21.j2-mid-v1': { prompt: 12.5, completion: 12.5 },
'ai21.j2-ultra-v1': { prompt: 18.8, completion: 18.8 },
'ai21.jamba-instruct-v1:0': { prompt: 0.5, completion: 0.7 },
'amazon.titan-text-lite-v1': { prompt: 0.15, completion: 0.2 },
'amazon.titan-text-express-v1': { prompt: 0.2, completion: 0.6 },
'amazon.titan-text-premier-v1:0': { prompt: 0.5, completion: 1.5 },
'amazon.nova-micro-v1:0': { prompt: 0.035, completion: 0.14 },
'amazon.nova-lite-v1:0': { prompt: 0.06, completion: 0.24 },
'amazon.nova-pro-v1:0': { prompt: 0.8, completion: 3.2 },
// AI21 models
'j2-mid': { prompt: 12.5, completion: 12.5 },
'j2-ultra': { prompt: 18.8, completion: 18.8 },
'jamba-instruct': { prompt: 0.5, completion: 0.7 },
// Amazon Titan models
'titan-text-lite': { prompt: 0.15, completion: 0.2 },
'titan-text-express': { prompt: 0.2, completion: 0.6 },
'titan-text-premier': { prompt: 0.5, completion: 1.5 },
// Amazon Nova models
'nova-micro': { prompt: 0.035, completion: 0.14 },
'nova-lite': { prompt: 0.06, completion: 0.24 },
'nova-pro': { prompt: 0.8, completion: 3.2 },
'nova-premier': { prompt: 2.5, completion: 12.5 },
'deepseek.r1': { prompt: 1.35, completion: 5.4 },
};
@@ -71,100 +89,136 @@ const bedrockValues = {
*/
const tokenValues = Object.assign(
{
// Legacy token size mappings (generic patterns - check LAST)
'8k': { prompt: 30, completion: 60 },
'32k': { prompt: 60, completion: 120 },
'4k': { prompt: 1.5, completion: 2 },
'16k': { prompt: 3, completion: 4 },
// Generic fallback patterns (check LAST)
'claude-': { prompt: 0.8, completion: 2.4 },
deepseek: { prompt: 0.28, completion: 0.42 },
command: { prompt: 0.38, completion: 0.38 },
gemma: { prompt: 0.02, completion: 0.04 }, // Base pattern (using gemma-3n-e4b pricing)
gemini: { prompt: 0.5, completion: 1.5 },
'gpt-oss': { prompt: 0.05, completion: 0.2 },
// Specific model variants (check FIRST - more specific patterns at end)
'gpt-3.5-turbo-1106': { prompt: 1, completion: 2 },
'o4-mini': { prompt: 1.1, completion: 4.4 },
'o3-mini': { prompt: 1.1, completion: 4.4 },
o3: { prompt: 2, completion: 8 },
'o1-mini': { prompt: 1.1, completion: 4.4 },
'o1-preview': { prompt: 15, completion: 60 },
o1: { prompt: 15, completion: 60 },
'gpt-3.5-turbo-0125': { prompt: 0.5, completion: 1.5 },
'gpt-4-1106': { prompt: 10, completion: 30 },
'gpt-4.1': { prompt: 2, completion: 8 },
'gpt-4.1-nano': { prompt: 0.1, completion: 0.4 },
'gpt-4.1-mini': { prompt: 0.4, completion: 1.6 },
'gpt-4.1': { prompt: 2, completion: 8 },
'gpt-4.5': { prompt: 75, completion: 150 },
'gpt-4o-mini': { prompt: 0.15, completion: 0.6 },
'gpt-5': { prompt: 1.25, completion: 10 },
'gpt-5-mini': { prompt: 0.25, completion: 2 },
'gpt-5-nano': { prompt: 0.05, completion: 0.4 },
'gpt-4o': { prompt: 2.5, completion: 10 },
'gpt-4o-2024-05-13': { prompt: 5, completion: 15 },
'gpt-4-1106': { prompt: 10, completion: 30 },
'gpt-3.5-turbo-0125': { prompt: 0.5, completion: 1.5 },
'claude-3-opus': { prompt: 15, completion: 75 },
'gpt-4o-mini': { prompt: 0.15, completion: 0.6 },
'gpt-5': { prompt: 1.25, completion: 10 },
'gpt-5-nano': { prompt: 0.05, completion: 0.4 },
'gpt-5-mini': { prompt: 0.25, completion: 2 },
'gpt-5-pro': { prompt: 15, completion: 120 },
o1: { prompt: 15, completion: 60 },
'o1-mini': { prompt: 1.1, completion: 4.4 },
'o1-preview': { prompt: 15, completion: 60 },
o3: { prompt: 2, completion: 8 },
'o3-mini': { prompt: 1.1, completion: 4.4 },
'o4-mini': { prompt: 1.1, completion: 4.4 },
'claude-instant': { prompt: 0.8, completion: 2.4 },
'claude-2': { prompt: 8, completion: 24 },
'claude-2.1': { prompt: 8, completion: 24 },
'claude-3-haiku': { prompt: 0.25, completion: 1.25 },
'claude-3-sonnet': { prompt: 3, completion: 15 },
'claude-3-opus': { prompt: 15, completion: 75 },
'claude-3-5-haiku': { prompt: 0.8, completion: 4 },
'claude-3.5-haiku': { prompt: 0.8, completion: 4 },
'claude-3-5-sonnet': { prompt: 3, completion: 15 },
'claude-3.5-sonnet': { prompt: 3, completion: 15 },
'claude-3-7-sonnet': { prompt: 3, completion: 15 },
'claude-3.7-sonnet': { prompt: 3, completion: 15 },
'claude-3-5-haiku': { prompt: 0.8, completion: 4 },
'claude-3.5-haiku': { prompt: 0.8, completion: 4 },
'claude-3-haiku': { prompt: 0.25, completion: 1.25 },
'claude-sonnet-4': { prompt: 3, completion: 15 },
'claude-haiku-4-5': { prompt: 1, completion: 5 },
'claude-opus-4': { prompt: 15, completion: 75 },
'claude-2.1': { prompt: 8, completion: 24 },
'claude-2': { prompt: 8, completion: 24 },
'claude-instant': { prompt: 0.8, completion: 2.4 },
'claude-': { prompt: 0.8, completion: 2.4 },
'command-r-plus': { prompt: 3, completion: 15 },
'claude-sonnet-4': { prompt: 3, completion: 15 },
'command-r': { prompt: 0.5, completion: 1.5 },
'command-r-plus': { prompt: 3, completion: 15 },
'command-text': { prompt: 1.5, completion: 2.0 },
'deepseek-reasoner': { prompt: 0.28, completion: 0.42 },
deepseek: { prompt: 0.28, completion: 0.42 },
/* cohere doesn't have rates for the older command models,
so this was from https://artificialanalysis.ai/models/command-light/providers */
command: { prompt: 0.38, completion: 0.38 },
gemma: { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
'gemma-2': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
'gemma-3': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
'gemma-3-27b': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
'gemini-2.0-flash-lite': { prompt: 0.075, completion: 0.3 },
'deepseek-r1': { prompt: 0.4, completion: 2.0 },
'deepseek-v3': { prompt: 0.2, completion: 0.8 },
'gemma-2': { prompt: 0.01, completion: 0.03 }, // Base pattern (using gemma-2-9b pricing)
'gemma-3': { prompt: 0.02, completion: 0.04 }, // Base pattern (using gemma-3n-e4b pricing)
'gemma-3-27b': { prompt: 0.09, completion: 0.16 },
'gemini-1.5': { prompt: 2.5, completion: 10 },
'gemini-1.5-flash': { prompt: 0.15, completion: 0.6 },
'gemini-1.5-flash-8b': { prompt: 0.075, completion: 0.3 },
'gemini-2.0': { prompt: 0.1, completion: 0.4 }, // Base pattern (using 2.0-flash pricing)
'gemini-2.0-flash': { prompt: 0.1, completion: 0.4 },
'gemini-2.0': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
'gemini-2.5-pro': { prompt: 1.25, completion: 10 },
'gemini-2.0-flash-lite': { prompt: 0.075, completion: 0.3 },
'gemini-2.5': { prompt: 0.3, completion: 2.5 }, // Base pattern (using 2.5-flash pricing)
'gemini-2.5-flash': { prompt: 0.3, completion: 2.5 },
'gemini-2.5-flash-lite': { prompt: 0.1, completion: 0.4 },
'gemini-2.5': { prompt: 0, completion: 0 }, // Free for a period of time
'gemini-1.5-flash-8b': { prompt: 0.075, completion: 0.3 },
'gemini-1.5-flash': { prompt: 0.15, completion: 0.6 },
'gemini-1.5': { prompt: 2.5, completion: 10 },
'gemini-2.5-pro': { prompt: 1.25, completion: 10 },
'gemini-pro-vision': { prompt: 0.5, completion: 1.5 },
gemini: { prompt: 0.5, completion: 1.5 },
'grok-2-vision-1212': { prompt: 2.0, completion: 10.0 },
'grok-2-vision-latest': { prompt: 2.0, completion: 10.0 },
'grok-2-vision': { prompt: 2.0, completion: 10.0 },
grok: { prompt: 2.0, completion: 10.0 }, // Base pattern defaults to grok-2
'grok-beta': { prompt: 5.0, completion: 15.0 },
'grok-vision-beta': { prompt: 5.0, completion: 15.0 },
'grok-2': { prompt: 2.0, completion: 10.0 },
'grok-2-1212': { prompt: 2.0, completion: 10.0 },
'grok-2-latest': { prompt: 2.0, completion: 10.0 },
'grok-2': { prompt: 2.0, completion: 10.0 },
'grok-3-mini-fast': { prompt: 0.6, completion: 4 },
'grok-3-mini': { prompt: 0.3, completion: 0.5 },
'grok-3-fast': { prompt: 5.0, completion: 25.0 },
'grok-2-vision': { prompt: 2.0, completion: 10.0 },
'grok-2-vision-1212': { prompt: 2.0, completion: 10.0 },
'grok-2-vision-latest': { prompt: 2.0, completion: 10.0 },
'grok-3': { prompt: 3.0, completion: 15.0 },
'grok-3-fast': { prompt: 5.0, completion: 25.0 },
'grok-3-mini': { prompt: 0.3, completion: 0.5 },
'grok-3-mini-fast': { prompt: 0.6, completion: 4 },
'grok-4': { prompt: 3.0, completion: 15.0 },
'grok-beta': { prompt: 5.0, completion: 15.0 },
'mistral-large': { prompt: 2.0, completion: 6.0 },
'pixtral-large': { prompt: 2.0, completion: 6.0 },
'mistral-saba': { prompt: 0.2, completion: 0.6 },
codestral: { prompt: 0.3, completion: 0.9 },
'ministral-8b': { prompt: 0.1, completion: 0.1 },
'ministral-3b': { prompt: 0.04, completion: 0.04 },
// GPT-OSS models
'gpt-oss': { prompt: 0.05, completion: 0.2 },
'ministral-8b': { prompt: 0.1, completion: 0.1 },
'mistral-nemo': { prompt: 0.15, completion: 0.15 },
'mistral-saba': { prompt: 0.2, completion: 0.6 },
'pixtral-large': { prompt: 2.0, completion: 6.0 },
'mistral-large': { prompt: 2.0, completion: 6.0 },
'mixtral-8x22b': { prompt: 0.65, completion: 0.65 },
kimi: { prompt: 0.14, completion: 2.49 }, // Base pattern (using kimi-k2 pricing)
// GPT-OSS models (specific sizes)
'gpt-oss:20b': { prompt: 0.05, completion: 0.2 },
'gpt-oss-20b': { prompt: 0.05, completion: 0.2 },
'gpt-oss:120b': { prompt: 0.15, completion: 0.6 },
'gpt-oss-120b': { prompt: 0.15, completion: 0.6 },
// GLM models (Zhipu AI)
// GLM models (Zhipu AI) - general to specific
glm4: { prompt: 0.1, completion: 0.1 },
'glm-4': { prompt: 0.1, completion: 0.1 },
'glm-4-32b': { prompt: 0.1, completion: 0.1 },
'glm-4.5': { prompt: 0.35, completion: 1.55 },
'glm-4.5v': { prompt: 0.6, completion: 1.8 },
'glm-4.5-air': { prompt: 0.14, completion: 0.86 },
'glm-4.5v': { prompt: 0.6, completion: 1.8 },
'glm-4.6': { prompt: 0.5, completion: 1.75 },
// Qwen models
qwen: { prompt: 0.08, completion: 0.33 }, // Qwen base pattern (using qwen2.5-72b pricing)
'qwen2.5': { prompt: 0.08, completion: 0.33 }, // Qwen 2.5 base pattern
'qwen-turbo': { prompt: 0.05, completion: 0.2 },
'qwen-plus': { prompt: 0.4, completion: 1.2 },
'qwen-max': { prompt: 1.6, completion: 6.4 },
'qwq-32b': { prompt: 0.15, completion: 0.4 },
// Qwen3 models
qwen3: { prompt: 0.035, completion: 0.138 }, // Qwen3 base pattern (using qwen3-4b pricing)
'qwen3-8b': { prompt: 0.035, completion: 0.138 },
'qwen3-14b': { prompt: 0.05, completion: 0.22 },
'qwen3-30b-a3b': { prompt: 0.06, completion: 0.22 },
'qwen3-32b': { prompt: 0.05, completion: 0.2 },
'qwen3-235b-a22b': { prompt: 0.08, completion: 0.55 },
// Qwen3 VL (Vision-Language) models
'qwen3-vl-8b-thinking': { prompt: 0.18, completion: 2.1 },
'qwen3-vl-8b-instruct': { prompt: 0.18, completion: 0.69 },
'qwen3-vl-30b-a3b': { prompt: 0.29, completion: 1.0 },
'qwen3-vl-235b-a22b': { prompt: 0.3, completion: 1.2 },
// Qwen3 specialized models
'qwen3-max': { prompt: 1.2, completion: 6 },
'qwen3-coder': { prompt: 0.22, completion: 0.95 },
'qwen3-coder-30b-a3b': { prompt: 0.06, completion: 0.25 },
'qwen3-coder-plus': { prompt: 1, completion: 5 },
'qwen3-coder-flash': { prompt: 0.3, completion: 1.5 },
'qwen3-next-80b-a3b': { prompt: 0.1, completion: 0.8 },
},
bedrockValues,
);
@@ -195,67 +249,39 @@ const cacheTokenValues = {
* @returns {string|undefined} The key corresponding to the model name, or undefined if no match is found.
*/
const getValueKey = (model, endpoint) => {
if (!model || typeof model !== 'string') {
return undefined;
}
// Use findMatchingPattern directly against tokenValues for efficient lookup
if (!endpoint || (typeof endpoint === 'string' && !tokenValues[endpoint])) {
const matchedKey = findMatchingPattern(model, tokenValues);
if (matchedKey) {
return matchedKey;
}
}
// Fallback: use matchModelName for edge cases and legacy handling
const modelName = matchModelName(model, endpoint);
if (!modelName) {
return undefined;
}
// Legacy token size mappings and aliases for older models
if (modelName.includes('gpt-3.5-turbo-16k')) {
return '16k';
} else if (modelName.includes('gpt-3.5-turbo-0125')) {
return 'gpt-3.5-turbo-0125';
} else if (modelName.includes('gpt-3.5-turbo-1106')) {
return 'gpt-3.5-turbo-1106';
} else if (modelName.includes('gpt-3.5')) {
return '4k';
} else if (modelName.includes('o4-mini')) {
return 'o4-mini';
} else if (modelName.includes('o4')) {
return 'o4';
} else if (modelName.includes('o3-mini')) {
return 'o3-mini';
} else if (modelName.includes('o3')) {
return 'o3';
} else if (modelName.includes('o1-preview')) {
return 'o1-preview';
} else if (modelName.includes('o1-mini')) {
return 'o1-mini';
} else if (modelName.includes('o1')) {
return 'o1';
} else if (modelName.includes('gpt-4.5')) {
return 'gpt-4.5';
} else if (modelName.includes('gpt-4.1-nano')) {
return 'gpt-4.1-nano';
} else if (modelName.includes('gpt-4.1-mini')) {
return 'gpt-4.1-mini';
} else if (modelName.includes('gpt-4.1')) {
return 'gpt-4.1';
} else if (modelName.includes('gpt-4o-2024-05-13')) {
return 'gpt-4o-2024-05-13';
} else if (modelName.includes('gpt-5-nano')) {
return 'gpt-5-nano';
} else if (modelName.includes('gpt-5-mini')) {
return 'gpt-5-mini';
} else if (modelName.includes('gpt-5')) {
return 'gpt-5';
} else if (modelName.includes('gpt-4o-mini')) {
return 'gpt-4o-mini';
} else if (modelName.includes('gpt-4o')) {
return 'gpt-4o';
} else if (modelName.includes('gpt-4-vision')) {
return 'gpt-4-1106';
} else if (modelName.includes('gpt-4-1106')) {
return 'gpt-4-1106';
return 'gpt-4-1106'; // Alias for gpt-4-vision
} else if (modelName.includes('gpt-4-0125')) {
return 'gpt-4-1106';
return 'gpt-4-1106'; // Alias for gpt-4-0125
} else if (modelName.includes('gpt-4-turbo')) {
return 'gpt-4-1106';
return 'gpt-4-1106'; // Alias for gpt-4-turbo
} else if (modelName.includes('gpt-4-32k')) {
return '32k';
} else if (modelName.includes('gpt-4')) {
return '8k';
} else if (tokenValues[modelName]) {
return modelName;
}
return undefined;

View File

@@ -1,3 +1,4 @@
const { maxTokensMap } = require('@librechat/api');
const { EModelEndpoint } = require('librechat-data-provider');
const {
defaultRate,
@@ -113,6 +114,14 @@ describe('getValueKey', () => {
expect(getValueKey('gpt-5-nano-2025-01-30-0130')).toBe('gpt-5-nano');
});
it('should return "gpt-5-pro" for model type of "gpt-5-pro"', () => {
expect(getValueKey('gpt-5-pro-2025-01-30')).toBe('gpt-5-pro');
expect(getValueKey('openai/gpt-5-pro')).toBe('gpt-5-pro');
expect(getValueKey('gpt-5-pro-0130')).toBe('gpt-5-pro');
expect(getValueKey('gpt-5-pro-2025-01-30-0130')).toBe('gpt-5-pro');
expect(getValueKey('gpt-5-pro-preview')).toBe('gpt-5-pro');
});
it('should return "gpt-4o" for model type of "gpt-4o"', () => {
expect(getValueKey('gpt-4o-2024-08-06')).toBe('gpt-4o');
expect(getValueKey('gpt-4o-2024-08-06-0718')).toBe('gpt-4o');
@@ -288,6 +297,20 @@ describe('getMultiplier', () => {
);
});
it('should return the correct multiplier for gpt-5-pro', () => {
const valueKey = getValueKey('gpt-5-pro-2025-01-30');
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-5-pro'].prompt);
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
tokenValues['gpt-5-pro'].completion,
);
expect(getMultiplier({ model: 'gpt-5-pro-preview', tokenType: 'prompt' })).toBe(
tokenValues['gpt-5-pro'].prompt,
);
expect(getMultiplier({ model: 'openai/gpt-5-pro', tokenType: 'completion' })).toBe(
tokenValues['gpt-5-pro'].completion,
);
});
it('should return the correct multiplier for gpt-4o', () => {
const valueKey = getValueKey('gpt-4o-2024-08-06');
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-4o'].prompt);
@@ -471,6 +494,249 @@ describe('AWS Bedrock Model Tests', () => {
});
});
describe('Amazon Model Tests', () => {
describe('Amazon Nova Models', () => {
it('should return correct pricing for nova-premier', () => {
expect(getMultiplier({ model: 'nova-premier', tokenType: 'prompt' })).toBe(
tokenValues['nova-premier'].prompt,
);
expect(getMultiplier({ model: 'nova-premier', tokenType: 'completion' })).toBe(
tokenValues['nova-premier'].completion,
);
expect(getMultiplier({ model: 'amazon.nova-premier-v1:0', tokenType: 'prompt' })).toBe(
tokenValues['nova-premier'].prompt,
);
expect(getMultiplier({ model: 'amazon.nova-premier-v1:0', tokenType: 'completion' })).toBe(
tokenValues['nova-premier'].completion,
);
});
it('should return correct pricing for nova-pro', () => {
expect(getMultiplier({ model: 'nova-pro', tokenType: 'prompt' })).toBe(
tokenValues['nova-pro'].prompt,
);
expect(getMultiplier({ model: 'nova-pro', tokenType: 'completion' })).toBe(
tokenValues['nova-pro'].completion,
);
expect(getMultiplier({ model: 'amazon.nova-pro-v1:0', tokenType: 'prompt' })).toBe(
tokenValues['nova-pro'].prompt,
);
expect(getMultiplier({ model: 'amazon.nova-pro-v1:0', tokenType: 'completion' })).toBe(
tokenValues['nova-pro'].completion,
);
});
it('should return correct pricing for nova-lite', () => {
expect(getMultiplier({ model: 'nova-lite', tokenType: 'prompt' })).toBe(
tokenValues['nova-lite'].prompt,
);
expect(getMultiplier({ model: 'nova-lite', tokenType: 'completion' })).toBe(
tokenValues['nova-lite'].completion,
);
expect(getMultiplier({ model: 'amazon.nova-lite-v1:0', tokenType: 'prompt' })).toBe(
tokenValues['nova-lite'].prompt,
);
expect(getMultiplier({ model: 'amazon.nova-lite-v1:0', tokenType: 'completion' })).toBe(
tokenValues['nova-lite'].completion,
);
});
it('should return correct pricing for nova-micro', () => {
expect(getMultiplier({ model: 'nova-micro', tokenType: 'prompt' })).toBe(
tokenValues['nova-micro'].prompt,
);
expect(getMultiplier({ model: 'nova-micro', tokenType: 'completion' })).toBe(
tokenValues['nova-micro'].completion,
);
expect(getMultiplier({ model: 'amazon.nova-micro-v1:0', tokenType: 'prompt' })).toBe(
tokenValues['nova-micro'].prompt,
);
expect(getMultiplier({ model: 'amazon.nova-micro-v1:0', tokenType: 'completion' })).toBe(
tokenValues['nova-micro'].completion,
);
});
it('should match both short and full model names to the same pricing', () => {
const models = ['nova-micro', 'nova-lite', 'nova-pro', 'nova-premier'];
const fullModels = [
'amazon.nova-micro-v1:0',
'amazon.nova-lite-v1:0',
'amazon.nova-pro-v1:0',
'amazon.nova-premier-v1:0',
];
models.forEach((shortModel, i) => {
const fullModel = fullModels[i];
const shortPrompt = getMultiplier({ model: shortModel, tokenType: 'prompt' });
const fullPrompt = getMultiplier({ model: fullModel, tokenType: 'prompt' });
const shortCompletion = getMultiplier({ model: shortModel, tokenType: 'completion' });
const fullCompletion = getMultiplier({ model: fullModel, tokenType: 'completion' });
expect(shortPrompt).toBe(fullPrompt);
expect(shortCompletion).toBe(fullCompletion);
expect(shortPrompt).toBe(tokenValues[shortModel].prompt);
expect(shortCompletion).toBe(tokenValues[shortModel].completion);
});
});
});
describe('Amazon Titan Models', () => {
it('should return correct pricing for titan-text-premier', () => {
expect(getMultiplier({ model: 'titan-text-premier', tokenType: 'prompt' })).toBe(
tokenValues['titan-text-premier'].prompt,
);
expect(getMultiplier({ model: 'titan-text-premier', tokenType: 'completion' })).toBe(
tokenValues['titan-text-premier'].completion,
);
expect(getMultiplier({ model: 'amazon.titan-text-premier-v1:0', tokenType: 'prompt' })).toBe(
tokenValues['titan-text-premier'].prompt,
);
expect(
getMultiplier({ model: 'amazon.titan-text-premier-v1:0', tokenType: 'completion' }),
).toBe(tokenValues['titan-text-premier'].completion);
});
it('should return correct pricing for titan-text-express', () => {
expect(getMultiplier({ model: 'titan-text-express', tokenType: 'prompt' })).toBe(
tokenValues['titan-text-express'].prompt,
);
expect(getMultiplier({ model: 'titan-text-express', tokenType: 'completion' })).toBe(
tokenValues['titan-text-express'].completion,
);
expect(getMultiplier({ model: 'amazon.titan-text-express-v1', tokenType: 'prompt' })).toBe(
tokenValues['titan-text-express'].prompt,
);
expect(
getMultiplier({ model: 'amazon.titan-text-express-v1', tokenType: 'completion' }),
).toBe(tokenValues['titan-text-express'].completion);
});
it('should return correct pricing for titan-text-lite', () => {
expect(getMultiplier({ model: 'titan-text-lite', tokenType: 'prompt' })).toBe(
tokenValues['titan-text-lite'].prompt,
);
expect(getMultiplier({ model: 'titan-text-lite', tokenType: 'completion' })).toBe(
tokenValues['titan-text-lite'].completion,
);
expect(getMultiplier({ model: 'amazon.titan-text-lite-v1', tokenType: 'prompt' })).toBe(
tokenValues['titan-text-lite'].prompt,
);
expect(getMultiplier({ model: 'amazon.titan-text-lite-v1', tokenType: 'completion' })).toBe(
tokenValues['titan-text-lite'].completion,
);
});
it('should match both short and full model names to the same pricing', () => {
const models = ['titan-text-lite', 'titan-text-express', 'titan-text-premier'];
const fullModels = [
'amazon.titan-text-lite-v1',
'amazon.titan-text-express-v1',
'amazon.titan-text-premier-v1:0',
];
models.forEach((shortModel, i) => {
const fullModel = fullModels[i];
const shortPrompt = getMultiplier({ model: shortModel, tokenType: 'prompt' });
const fullPrompt = getMultiplier({ model: fullModel, tokenType: 'prompt' });
const shortCompletion = getMultiplier({ model: shortModel, tokenType: 'completion' });
const fullCompletion = getMultiplier({ model: fullModel, tokenType: 'completion' });
expect(shortPrompt).toBe(fullPrompt);
expect(shortCompletion).toBe(fullCompletion);
expect(shortPrompt).toBe(tokenValues[shortModel].prompt);
expect(shortCompletion).toBe(tokenValues[shortModel].completion);
});
});
});
});
describe('AI21 Model Tests', () => {
describe('AI21 J2 Models', () => {
it('should return correct pricing for j2-mid', () => {
expect(getMultiplier({ model: 'j2-mid', tokenType: 'prompt' })).toBe(
tokenValues['j2-mid'].prompt,
);
expect(getMultiplier({ model: 'j2-mid', tokenType: 'completion' })).toBe(
tokenValues['j2-mid'].completion,
);
expect(getMultiplier({ model: 'ai21.j2-mid-v1', tokenType: 'prompt' })).toBe(
tokenValues['j2-mid'].prompt,
);
expect(getMultiplier({ model: 'ai21.j2-mid-v1', tokenType: 'completion' })).toBe(
tokenValues['j2-mid'].completion,
);
});
it('should return correct pricing for j2-ultra', () => {
expect(getMultiplier({ model: 'j2-ultra', tokenType: 'prompt' })).toBe(
tokenValues['j2-ultra'].prompt,
);
expect(getMultiplier({ model: 'j2-ultra', tokenType: 'completion' })).toBe(
tokenValues['j2-ultra'].completion,
);
expect(getMultiplier({ model: 'ai21.j2-ultra-v1', tokenType: 'prompt' })).toBe(
tokenValues['j2-ultra'].prompt,
);
expect(getMultiplier({ model: 'ai21.j2-ultra-v1', tokenType: 'completion' })).toBe(
tokenValues['j2-ultra'].completion,
);
});
it('should match both short and full model names to the same pricing', () => {
const models = ['j2-mid', 'j2-ultra'];
const fullModels = ['ai21.j2-mid-v1', 'ai21.j2-ultra-v1'];
models.forEach((shortModel, i) => {
const fullModel = fullModels[i];
const shortPrompt = getMultiplier({ model: shortModel, tokenType: 'prompt' });
const fullPrompt = getMultiplier({ model: fullModel, tokenType: 'prompt' });
const shortCompletion = getMultiplier({ model: shortModel, tokenType: 'completion' });
const fullCompletion = getMultiplier({ model: fullModel, tokenType: 'completion' });
expect(shortPrompt).toBe(fullPrompt);
expect(shortCompletion).toBe(fullCompletion);
expect(shortPrompt).toBe(tokenValues[shortModel].prompt);
expect(shortCompletion).toBe(tokenValues[shortModel].completion);
});
});
});
describe('AI21 Jamba Models', () => {
it('should return correct pricing for jamba-instruct', () => {
expect(getMultiplier({ model: 'jamba-instruct', tokenType: 'prompt' })).toBe(
tokenValues['jamba-instruct'].prompt,
);
expect(getMultiplier({ model: 'jamba-instruct', tokenType: 'completion' })).toBe(
tokenValues['jamba-instruct'].completion,
);
expect(getMultiplier({ model: 'ai21.jamba-instruct-v1:0', tokenType: 'prompt' })).toBe(
tokenValues['jamba-instruct'].prompt,
);
expect(getMultiplier({ model: 'ai21.jamba-instruct-v1:0', tokenType: 'completion' })).toBe(
tokenValues['jamba-instruct'].completion,
);
});
it('should match both short and full model names to the same pricing', () => {
const shortPrompt = getMultiplier({ model: 'jamba-instruct', tokenType: 'prompt' });
const fullPrompt = getMultiplier({
model: 'ai21.jamba-instruct-v1:0',
tokenType: 'prompt',
});
const shortCompletion = getMultiplier({ model: 'jamba-instruct', tokenType: 'completion' });
const fullCompletion = getMultiplier({
model: 'ai21.jamba-instruct-v1:0',
tokenType: 'completion',
});
expect(shortPrompt).toBe(fullPrompt);
expect(shortCompletion).toBe(fullCompletion);
expect(shortPrompt).toBe(tokenValues['jamba-instruct'].prompt);
expect(shortCompletion).toBe(tokenValues['jamba-instruct'].completion);
});
});
});
describe('Deepseek Model Tests', () => {
const deepseekModels = ['deepseek-chat', 'deepseek-coder', 'deepseek-reasoner', 'deepseek.r1'];
@@ -502,6 +768,187 @@ describe('Deepseek Model Tests', () => {
});
});
describe('Qwen3 Model Tests', () => {
describe('Qwen3 Base Models', () => {
it('should return correct pricing for qwen3 base pattern', () => {
expect(getMultiplier({ model: 'qwen3', tokenType: 'prompt' })).toBe(
tokenValues['qwen3'].prompt,
);
expect(getMultiplier({ model: 'qwen3', tokenType: 'completion' })).toBe(
tokenValues['qwen3'].completion,
);
});
it('should return correct pricing for qwen3-4b (falls back to qwen3)', () => {
expect(getMultiplier({ model: 'qwen3-4b', tokenType: 'prompt' })).toBe(
tokenValues['qwen3'].prompt,
);
expect(getMultiplier({ model: 'qwen3-4b', tokenType: 'completion' })).toBe(
tokenValues['qwen3'].completion,
);
});
it('should return correct pricing for qwen3-8b', () => {
expect(getMultiplier({ model: 'qwen3-8b', tokenType: 'prompt' })).toBe(
tokenValues['qwen3-8b'].prompt,
);
expect(getMultiplier({ model: 'qwen3-8b', tokenType: 'completion' })).toBe(
tokenValues['qwen3-8b'].completion,
);
});
it('should return correct pricing for qwen3-14b', () => {
expect(getMultiplier({ model: 'qwen3-14b', tokenType: 'prompt' })).toBe(
tokenValues['qwen3-14b'].prompt,
);
expect(getMultiplier({ model: 'qwen3-14b', tokenType: 'completion' })).toBe(
tokenValues['qwen3-14b'].completion,
);
});
it('should return correct pricing for qwen3-235b-a22b', () => {
expect(getMultiplier({ model: 'qwen3-235b-a22b', tokenType: 'prompt' })).toBe(
tokenValues['qwen3-235b-a22b'].prompt,
);
expect(getMultiplier({ model: 'qwen3-235b-a22b', tokenType: 'completion' })).toBe(
tokenValues['qwen3-235b-a22b'].completion,
);
});
it('should handle model name variations with provider prefixes', () => {
const models = [
{ input: 'qwen3', expected: 'qwen3' },
{ input: 'qwen3-4b', expected: 'qwen3' },
{ input: 'qwen3-8b', expected: 'qwen3-8b' },
{ input: 'qwen3-32b', expected: 'qwen3-32b' },
];
models.forEach(({ input, expected }) => {
const withPrefix = `alibaba/${input}`;
expect(getMultiplier({ model: withPrefix, tokenType: 'prompt' })).toBe(
tokenValues[expected].prompt,
);
expect(getMultiplier({ model: withPrefix, tokenType: 'completion' })).toBe(
tokenValues[expected].completion,
);
});
});
});
describe('Qwen3 VL (Vision-Language) Models', () => {
it('should return correct pricing for qwen3-vl-8b-thinking', () => {
expect(getMultiplier({ model: 'qwen3-vl-8b-thinking', tokenType: 'prompt' })).toBe(
tokenValues['qwen3-vl-8b-thinking'].prompt,
);
expect(getMultiplier({ model: 'qwen3-vl-8b-thinking', tokenType: 'completion' })).toBe(
tokenValues['qwen3-vl-8b-thinking'].completion,
);
});
it('should return correct pricing for qwen3-vl-8b-instruct', () => {
expect(getMultiplier({ model: 'qwen3-vl-8b-instruct', tokenType: 'prompt' })).toBe(
tokenValues['qwen3-vl-8b-instruct'].prompt,
);
expect(getMultiplier({ model: 'qwen3-vl-8b-instruct', tokenType: 'completion' })).toBe(
tokenValues['qwen3-vl-8b-instruct'].completion,
);
});
it('should return correct pricing for qwen3-vl-30b-a3b', () => {
expect(getMultiplier({ model: 'qwen3-vl-30b-a3b', tokenType: 'prompt' })).toBe(
tokenValues['qwen3-vl-30b-a3b'].prompt,
);
expect(getMultiplier({ model: 'qwen3-vl-30b-a3b', tokenType: 'completion' })).toBe(
tokenValues['qwen3-vl-30b-a3b'].completion,
);
});
it('should return correct pricing for qwen3-vl-235b-a22b', () => {
expect(getMultiplier({ model: 'qwen3-vl-235b-a22b', tokenType: 'prompt' })).toBe(
tokenValues['qwen3-vl-235b-a22b'].prompt,
);
expect(getMultiplier({ model: 'qwen3-vl-235b-a22b', tokenType: 'completion' })).toBe(
tokenValues['qwen3-vl-235b-a22b'].completion,
);
});
});
describe('Qwen3 Specialized Models', () => {
it('should return correct pricing for qwen3-max', () => {
expect(getMultiplier({ model: 'qwen3-max', tokenType: 'prompt' })).toBe(
tokenValues['qwen3-max'].prompt,
);
expect(getMultiplier({ model: 'qwen3-max', tokenType: 'completion' })).toBe(
tokenValues['qwen3-max'].completion,
);
});
it('should return correct pricing for qwen3-coder', () => {
expect(getMultiplier({ model: 'qwen3-coder', tokenType: 'prompt' })).toBe(
tokenValues['qwen3-coder'].prompt,
);
expect(getMultiplier({ model: 'qwen3-coder', tokenType: 'completion' })).toBe(
tokenValues['qwen3-coder'].completion,
);
});
it('should return correct pricing for qwen3-coder-plus', () => {
expect(getMultiplier({ model: 'qwen3-coder-plus', tokenType: 'prompt' })).toBe(
tokenValues['qwen3-coder-plus'].prompt,
);
expect(getMultiplier({ model: 'qwen3-coder-plus', tokenType: 'completion' })).toBe(
tokenValues['qwen3-coder-plus'].completion,
);
});
it('should return correct pricing for qwen3-coder-flash', () => {
expect(getMultiplier({ model: 'qwen3-coder-flash', tokenType: 'prompt' })).toBe(
tokenValues['qwen3-coder-flash'].prompt,
);
expect(getMultiplier({ model: 'qwen3-coder-flash', tokenType: 'completion' })).toBe(
tokenValues['qwen3-coder-flash'].completion,
);
});
it('should return correct pricing for qwen3-next-80b-a3b', () => {
expect(getMultiplier({ model: 'qwen3-next-80b-a3b', tokenType: 'prompt' })).toBe(
tokenValues['qwen3-next-80b-a3b'].prompt,
);
expect(getMultiplier({ model: 'qwen3-next-80b-a3b', tokenType: 'completion' })).toBe(
tokenValues['qwen3-next-80b-a3b'].completion,
);
});
});
describe('Qwen3 Model Variations', () => {
it('should handle all qwen3 models with provider prefixes', () => {
const models = ['qwen3', 'qwen3-8b', 'qwen3-max', 'qwen3-coder', 'qwen3-vl-8b-instruct'];
const prefixes = ['alibaba', 'qwen', 'openrouter'];
models.forEach((model) => {
prefixes.forEach((prefix) => {
const fullModel = `${prefix}/${model}`;
expect(getMultiplier({ model: fullModel, tokenType: 'prompt' })).toBe(
tokenValues[model].prompt,
);
expect(getMultiplier({ model: fullModel, tokenType: 'completion' })).toBe(
tokenValues[model].completion,
);
});
});
});
it('should handle qwen3-4b falling back to qwen3 base pattern', () => {
const testCases = ['qwen3-4b', 'alibaba/qwen3-4b', 'qwen/qwen3-4b-preview'];
testCases.forEach((model) => {
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(tokenValues['qwen3'].prompt);
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(
tokenValues['qwen3'].completion,
);
});
});
});
});
describe('getCacheMultiplier', () => {
it('should return the correct cache multiplier for a given valueKey and cacheType', () => {
expect(getCacheMultiplier({ valueKey: 'claude-3-5-sonnet', cacheType: 'write' })).toBe(
@@ -914,6 +1361,37 @@ describe('Claude Model Tests', () => {
);
});
it('should return correct prompt and completion rates for Claude Haiku 4.5', () => {
expect(getMultiplier({ model: 'claude-haiku-4-5', tokenType: 'prompt' })).toBe(
tokenValues['claude-haiku-4-5'].prompt,
);
expect(getMultiplier({ model: 'claude-haiku-4-5', tokenType: 'completion' })).toBe(
tokenValues['claude-haiku-4-5'].completion,
);
});
it('should handle Claude Haiku 4.5 model name variations', () => {
const modelVariations = [
'claude-haiku-4-5',
'claude-haiku-4-5-20250420',
'claude-haiku-4-5-latest',
'anthropic/claude-haiku-4-5',
'claude-haiku-4-5/anthropic',
'claude-haiku-4-5-preview',
];
modelVariations.forEach((model) => {
const valueKey = getValueKey(model);
expect(valueKey).toBe('claude-haiku-4-5');
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(
tokenValues['claude-haiku-4-5'].prompt,
);
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(
tokenValues['claude-haiku-4-5'].completion,
);
});
});
it('should handle Claude 4 model name variations with different prefixes and suffixes', () => {
const modelVariations = [
'claude-sonnet-4',
@@ -991,3 +1469,119 @@ describe('Claude Model Tests', () => {
});
});
});
describe('tokens.ts and tx.js sync validation', () => {
it('should resolve all models in maxTokensMap to pricing via getValueKey', () => {
const tokensKeys = Object.keys(maxTokensMap[EModelEndpoint.openAI]);
const txKeys = Object.keys(tokenValues);
const unresolved = [];
tokensKeys.forEach((key) => {
// Skip legacy token size mappings (e.g., '4k', '8k', '16k', '32k')
if (/^\d+k$/.test(key)) return;
// Skip generic pattern keys (end with '-' or ':')
if (key.endsWith('-') || key.endsWith(':')) return;
// Try to resolve via getValueKey
const resolvedKey = getValueKey(key);
// If it resolves and the resolved key has pricing, success
if (resolvedKey && txKeys.includes(resolvedKey)) return;
// If it resolves to a legacy key (4k, 8k, etc), also OK
if (resolvedKey && /^\d+k$/.test(resolvedKey)) return;
// If we get here, this model can't get pricing - flag it
unresolved.push({
key,
resolvedKey: resolvedKey || 'undefined',
context: maxTokensMap[EModelEndpoint.openAI][key],
});
});
if (unresolved.length > 0) {
console.log('\nModels that cannot resolve to pricing via getValueKey:');
unresolved.forEach(({ key, resolvedKey, context }) => {
console.log(` - '${key}' → '${resolvedKey}' (context: ${context})`);
});
}
expect(unresolved).toEqual([]);
});
it('should not have redundant dated variants with same pricing and context as base model', () => {
const txKeys = Object.keys(tokenValues);
const redundant = [];
txKeys.forEach((key) => {
// Check if this is a dated variant (ends with -YYYY-MM-DD)
if (key.match(/.*-\d{4}-\d{2}-\d{2}$/)) {
const baseKey = key.replace(/-\d{4}-\d{2}-\d{2}$/, '');
if (txKeys.includes(baseKey)) {
const variantPricing = tokenValues[key];
const basePricing = tokenValues[baseKey];
const variantContext = maxTokensMap[EModelEndpoint.openAI][key];
const baseContext = maxTokensMap[EModelEndpoint.openAI][baseKey];
const samePricing =
variantPricing.prompt === basePricing.prompt &&
variantPricing.completion === basePricing.completion;
const sameContext = variantContext === baseContext;
if (samePricing && sameContext) {
redundant.push({
key,
baseKey,
pricing: `${variantPricing.prompt}/${variantPricing.completion}`,
context: variantContext,
});
}
}
}
});
if (redundant.length > 0) {
console.log('\nRedundant dated variants found (same pricing and context as base):');
redundant.forEach(({ key, baseKey, pricing, context }) => {
console.log(` - '${key}' → '${baseKey}' (pricing: ${pricing}, context: ${context})`);
console.log(` Can be removed - pattern matching will handle it`);
});
}
expect(redundant).toEqual([]);
});
it('should have context windows in tokens.ts for all models with pricing in tx.js (openAI catch-all)', () => {
const txKeys = Object.keys(tokenValues);
const missingContext = [];
txKeys.forEach((key) => {
// Skip legacy token size mappings (4k, 8k, 16k, 32k)
if (/^\d+k$/.test(key)) return;
// Check if this model has a context window defined
const context = maxTokensMap[EModelEndpoint.openAI][key];
if (!context) {
const pricing = tokenValues[key];
missingContext.push({
key,
pricing: `${pricing.prompt}/${pricing.completion}`,
});
}
});
if (missingContext.length > 0) {
console.log('\nModels with pricing but missing context in tokens.ts:');
missingContext.forEach(({ key, pricing }) => {
console.log(` - '${key}' (pricing: ${pricing})`);
console.log(` Add to tokens.ts openAIModels/bedrockModels/etc.`);
});
}
expect(missingContext).toEqual([]);
});
});

View File

@@ -48,7 +48,7 @@
"@langchain/google-genai": "^0.2.13",
"@langchain/google-vertexai": "^0.2.13",
"@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.4.85",
"@librechat/agents": "^2.4.86",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",

View File

@@ -116,11 +116,15 @@ const refreshController = async (req, res) => {
const token = await setAuthTokens(userId, res, session);
// trigger OAuth MCP server reconnection asynchronously (best effort)
void getOAuthReconnectionManager()
.reconnectServers(userId)
.catch((err) => {
logger.error('Error reconnecting OAuth MCP servers:', err);
});
try {
void getOAuthReconnectionManager()
.reconnectServers(userId)
.catch((err) => {
logger.error('[refreshController] Error reconnecting OAuth MCP servers:', err);
});
} catch (err) {
logger.warn(`[refreshController] Cannot attempt OAuth MCP servers reconnection:`, err);
}
res.status(200).send({ token, user });
} else if (req?.query?.retry) {

View File

@@ -143,7 +143,7 @@ const initializeAgent = async ({
const agentMaxContextTokens = optionalChainWithEmptyCheck(
maxContextTokens,
getModelMaxTokens(tokensModel, providerEndpointMap[provider], options.endpointTokenConfig),
4096,
18000,
);
if (

View File

@@ -186,6 +186,19 @@ describe('getModelMaxTokens', () => {
);
});
test('should return correct tokens for gpt-5-pro matches', () => {
expect(getModelMaxTokens('gpt-5-pro')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5-pro']);
expect(getModelMaxTokens('gpt-5-pro-preview')).toBe(
maxTokensMap[EModelEndpoint.openAI]['gpt-5-pro'],
);
expect(getModelMaxTokens('openai/gpt-5-pro')).toBe(
maxTokensMap[EModelEndpoint.openAI]['gpt-5-pro'],
);
expect(getModelMaxTokens('gpt-5-pro-2025-01-30')).toBe(
maxTokensMap[EModelEndpoint.openAI]['gpt-5-pro'],
);
});
test('should return correct tokens for Anthropic models', () => {
const models = [
'claude-2.1',
@@ -469,7 +482,7 @@ describe('getModelMaxTokens', () => {
test('should return correct max output tokens for GPT-5 models', () => {
const { getModelMaxOutputTokens } = require('@librechat/api');
['gpt-5', 'gpt-5-mini', 'gpt-5-nano'].forEach((model) => {
['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro'].forEach((model) => {
expect(getModelMaxOutputTokens(model)).toBe(maxOutputTokensMap[EModelEndpoint.openAI][model]);
expect(getModelMaxOutputTokens(model, EModelEndpoint.openAI)).toBe(
maxOutputTokensMap[EModelEndpoint.openAI][model],
@@ -582,6 +595,13 @@ describe('matchModelName', () => {
expect(matchModelName('gpt-5-nano-2025-01-30')).toBe('gpt-5-nano');
});
it('should return the closest matching key for gpt-5-pro matches', () => {
expect(matchModelName('openai/gpt-5-pro')).toBe('gpt-5-pro');
expect(matchModelName('gpt-5-pro-preview')).toBe('gpt-5-pro');
expect(matchModelName('gpt-5-pro-2025-01-30')).toBe('gpt-5-pro');
expect(matchModelName('gpt-5-pro-2025-01-30-0130')).toBe('gpt-5-pro');
});
// Tests for Google models
it('should return the exact model name if it exists in maxTokensMap - Google models', () => {
expect(matchModelName('text-bison-32k', EModelEndpoint.google)).toBe('text-bison-32k');
@@ -832,6 +852,49 @@ describe('Claude Model Tests', () => {
);
});
it('should return correct context length for Claude Haiku 4.5', () => {
expect(getModelMaxTokens('claude-haiku-4-5', EModelEndpoint.anthropic)).toBe(
maxTokensMap[EModelEndpoint.anthropic]['claude-haiku-4-5'],
);
expect(getModelMaxTokens('claude-haiku-4-5')).toBe(
maxTokensMap[EModelEndpoint.anthropic]['claude-haiku-4-5'],
);
});
it('should handle Claude Haiku 4.5 model name variations', () => {
const modelVariations = [
'claude-haiku-4-5',
'claude-haiku-4-5-20250420',
'claude-haiku-4-5-latest',
'anthropic/claude-haiku-4-5',
'claude-haiku-4-5/anthropic',
'claude-haiku-4-5-preview',
];
modelVariations.forEach((model) => {
const modelKey = findMatchingPattern(model, maxTokensMap[EModelEndpoint.anthropic]);
expect(modelKey).toBe('claude-haiku-4-5');
expect(getModelMaxTokens(model, EModelEndpoint.anthropic)).toBe(
maxTokensMap[EModelEndpoint.anthropic]['claude-haiku-4-5'],
);
});
});
it('should match model names correctly for Claude Haiku 4.5', () => {
const modelVariations = [
'claude-haiku-4-5',
'claude-haiku-4-5-20250420',
'claude-haiku-4-5-latest',
'anthropic/claude-haiku-4-5',
'claude-haiku-4-5/anthropic',
'claude-haiku-4-5-preview',
];
modelVariations.forEach((model) => {
expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-haiku-4-5');
});
});
it('should handle Claude 4 model name variations with different prefixes and suffixes', () => {
const modelVariations = [
'claude-sonnet-4',
@@ -924,6 +987,121 @@ describe('Kimi Model Tests', () => {
});
});
describe('Qwen3 Model Tests', () => {
describe('getModelMaxTokens', () => {
test('should return correct tokens for Qwen3 base pattern', () => {
expect(getModelMaxTokens('qwen3')).toBe(maxTokensMap[EModelEndpoint.openAI]['qwen3']);
});
test('should return correct tokens for qwen3-4b (falls back to qwen3)', () => {
expect(getModelMaxTokens('qwen3-4b')).toBe(maxTokensMap[EModelEndpoint.openAI]['qwen3']);
});
test('should return correct tokens for Qwen3 base models', () => {
expect(getModelMaxTokens('qwen3-8b')).toBe(maxTokensMap[EModelEndpoint.openAI]['qwen3-8b']);
expect(getModelMaxTokens('qwen3-14b')).toBe(maxTokensMap[EModelEndpoint.openAI]['qwen3-14b']);
expect(getModelMaxTokens('qwen3-32b')).toBe(maxTokensMap[EModelEndpoint.openAI]['qwen3-32b']);
expect(getModelMaxTokens('qwen3-235b-a22b')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-235b-a22b'],
);
});
test('should return correct tokens for Qwen3 VL (Vision-Language) models', () => {
expect(getModelMaxTokens('qwen3-vl-8b-thinking')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-vl-8b-thinking'],
);
expect(getModelMaxTokens('qwen3-vl-8b-instruct')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-vl-8b-instruct'],
);
expect(getModelMaxTokens('qwen3-vl-30b-a3b')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-vl-30b-a3b'],
);
expect(getModelMaxTokens('qwen3-vl-235b-a22b')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-vl-235b-a22b'],
);
});
test('should return correct tokens for Qwen3 specialized models', () => {
expect(getModelMaxTokens('qwen3-max')).toBe(maxTokensMap[EModelEndpoint.openAI]['qwen3-max']);
expect(getModelMaxTokens('qwen3-coder')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-coder'],
);
expect(getModelMaxTokens('qwen3-coder-30b-a3b')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-coder-30b-a3b'],
);
expect(getModelMaxTokens('qwen3-coder-plus')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-coder-plus'],
);
expect(getModelMaxTokens('qwen3-coder-flash')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-coder-flash'],
);
expect(getModelMaxTokens('qwen3-next-80b-a3b')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-next-80b-a3b'],
);
});
test('should handle Qwen3 models with provider prefixes', () => {
expect(getModelMaxTokens('alibaba/qwen3')).toBe(maxTokensMap[EModelEndpoint.openAI]['qwen3']);
expect(getModelMaxTokens('alibaba/qwen3-4b')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3'],
);
expect(getModelMaxTokens('qwen/qwen3-8b')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-8b'],
);
expect(getModelMaxTokens('openrouter/qwen3-max')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-max'],
);
expect(getModelMaxTokens('alibaba/qwen3-vl-8b-instruct')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-vl-8b-instruct'],
);
expect(getModelMaxTokens('qwen/qwen3-coder')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-coder'],
);
});
test('should handle Qwen3 models with suffixes', () => {
expect(getModelMaxTokens('qwen3-preview')).toBe(maxTokensMap[EModelEndpoint.openAI]['qwen3']);
expect(getModelMaxTokens('qwen3-4b-preview')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3'],
);
expect(getModelMaxTokens('qwen3-8b-latest')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-8b'],
);
expect(getModelMaxTokens('qwen3-max-2024')).toBe(
maxTokensMap[EModelEndpoint.openAI]['qwen3-max'],
);
});
});
describe('matchModelName', () => {
test('should match exact Qwen3 model names', () => {
expect(matchModelName('qwen3')).toBe('qwen3');
expect(matchModelName('qwen3-4b')).toBe('qwen3');
expect(matchModelName('qwen3-8b')).toBe('qwen3-8b');
expect(matchModelName('qwen3-vl-8b-thinking')).toBe('qwen3-vl-8b-thinking');
expect(matchModelName('qwen3-max')).toBe('qwen3-max');
expect(matchModelName('qwen3-coder')).toBe('qwen3-coder');
});
test('should match Qwen3 model variations with provider prefixes', () => {
expect(matchModelName('alibaba/qwen3')).toBe('qwen3');
expect(matchModelName('alibaba/qwen3-4b')).toBe('qwen3');
expect(matchModelName('qwen/qwen3-8b')).toBe('qwen3-8b');
expect(matchModelName('openrouter/qwen3-max')).toBe('qwen3-max');
expect(matchModelName('alibaba/qwen3-vl-8b-instruct')).toBe('qwen3-vl-8b-instruct');
expect(matchModelName('qwen/qwen3-coder')).toBe('qwen3-coder');
});
test('should match Qwen3 model variations with suffixes', () => {
expect(matchModelName('qwen3-preview')).toBe('qwen3');
expect(matchModelName('qwen3-4b-preview')).toBe('qwen3');
expect(matchModelName('qwen3-8b-latest')).toBe('qwen3-8b');
expect(matchModelName('qwen3-max-2024')).toBe('qwen3-max');
expect(matchModelName('qwen3-coder-v1')).toBe('qwen3-coder');
});
});
});
describe('GLM Model Tests (Zhipu AI)', () => {
describe('getModelMaxTokens', () => {
test('should return correct tokens for GLM models', () => {

View File

@@ -149,7 +149,7 @@
"tailwindcss": "^3.4.1",
"ts-jest": "^29.2.5",
"typescript": "^5.3.3",
"vite": "^6.3.6",
"vite": "^6.4.1",
"vite-plugin-compression2": "^2.2.1",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^0.21.2"

View File

@@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { RecoilRoot } from 'recoil';
import { DndProvider } from 'react-dnd';
import { RouterProvider } from 'react-router-dom';
@@ -8,6 +9,7 @@ import { Toast, ThemeProvider, ToastProvider } from '@librechat/client';
import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query';
import { ScreenshotProvider, useApiErrorBoundary } from './hooks';
import { getThemeFromEnv } from './utils/getThemeFromEnv';
import { initializeFontSize } from '~/store/fontSize';
import { LiveAnnouncer } from '~/a11y';
import { router } from './routes';
@@ -24,6 +26,10 @@ const App = () => {
}),
});
useEffect(() => {
initializeFontSize();
}, []);
// Load theme from environment variables if available
const envTheme = getThemeFromEnv();

View File

@@ -11,9 +11,9 @@ import {
AgentListResponse,
} from 'librechat-data-provider';
import type t from 'librechat-data-provider';
import { renderAgentAvatar, clearMessagesCache } from '~/utils';
import { useLocalize, useDefaultConvo } from '~/hooks';
import { useChatContext } from '~/Providers';
import { renderAgentAvatar } from '~/utils';
interface SupportContact {
name?: string;
@@ -56,10 +56,7 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}0`, agent.id);
queryClient.setQueryData<t.TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
clearMessagesCache(queryClient, conversation?.conversationId);
queryClient.invalidateQueries([QueryKeys.messages]);
/** Template with agent configuration */

View File

@@ -4,7 +4,7 @@ import { useOutletContext } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useSearchParams, useParams, useNavigate } from 'react-router-dom';
import { TooltipAnchor, Button, NewChatIcon, useMediaQuery } from '@librechat/client';
import { PermissionTypes, Permissions, QueryKeys, Constants } from 'librechat-data-provider';
import { PermissionTypes, Permissions, QueryKeys } from 'librechat-data-provider';
import type t from 'librechat-data-provider';
import type { ContextType } from '~/common';
import { useDocumentTitle, useHasAccess, useLocalize, TranslationKeys } from '~/hooks';
@@ -13,11 +13,11 @@ import MarketplaceAdminSettings from './MarketplaceAdminSettings';
import { SidePanelProvider, useChatContext } from '~/Providers';
import { SidePanelGroup } from '~/components/SidePanel';
import { OpenSidebar } from '~/components/Chat/Menus';
import { cn, clearMessagesCache } from '~/utils';
import CategoryTabs from './CategoryTabs';
import AgentDetail from './AgentDetail';
import SearchBar from './SearchBar';
import AgentGrid from './AgentGrid';
import { cn } from '~/utils';
import store from '~/store';
interface AgentMarketplaceProps {
@@ -224,10 +224,7 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
window.open('/c/new', '_blank');
return;
}
queryClient.setQueryData<t.TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
clearMessagesCache(queryClient, conversation?.conversationId);
queryClient.invalidateQueries([QueryKeys.messages]);
newConversation();
};

View File

@@ -1,8 +1,8 @@
import { QueryKeys } from 'librechat-data-provider';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import { TooltipAnchor, Button, NewChatIcon } from '@librechat/client';
import type { TMessage } from 'librechat-data-provider';
import { useChatContext } from '~/Providers';
import { clearMessagesCache } from '~/utils';
import { useLocalize } from '~/hooks';
export default function HeaderNewChat() {
@@ -15,10 +15,7 @@ export default function HeaderNewChat() {
window.open('/c/new', '_blank');
return;
}
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
clearMessagesCache(queryClient, conversation?.conversationId);
queryClient.invalidateQueries([QueryKeys.messages]);
newConversation();
};

View File

@@ -1,10 +1,12 @@
import React, { useMemo } from 'react';
import { useAtomValue } from 'jotai';
import { useRecoilValue } from 'recoil';
import type { TMessageContentParts } from 'librechat-data-provider';
import type { TMessageProps, TMessageIcon } from '~/common';
import { useMessageHelpers, useLocalize, useAttachments } from '~/hooks';
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
import ContentParts from './Content/ContentParts';
import { fontSizeAtom } from '~/store/fontSize';
import SiblingSwitch from './SiblingSwitch';
import MultiMessage from './MultiMessage';
import HoverButtons from './HoverButtons';
@@ -36,7 +38,7 @@ export default function Message(props: TMessageProps) {
regenerateMessage,
} = useMessageHelpers(props);
const fontSize = useRecoilValue(store.fontSize);
const fontSize = useAtomValue(fontSizeAtom);
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
const { children, messageId = null, isCreatedByUser } = message ?? {};

View File

@@ -1,10 +1,12 @@
import { useState } from 'react';
import { useAtomValue } from 'jotai';
import { useRecoilValue } from 'recoil';
import { CSSTransition } from 'react-transition-group';
import type { TMessage } from 'librechat-data-provider';
import { useScreenshot, useMessageScrolling, useLocalize } from '~/hooks';
import ScrollToBottom from '~/components/Messages/ScrollToBottom';
import { MessagesViewProvider } from '~/Providers';
import { fontSizeAtom } from '~/store/fontSize';
import MultiMessage from './MultiMessage';
import { cn } from '~/utils';
import store from '~/store';
@@ -15,7 +17,7 @@ function MessagesViewContent({
messagesTree?: TMessage[] | null;
}) {
const localize = useLocalize();
const fontSize = useRecoilValue(store.fontSize);
const fontSize = useAtomValue(fontSizeAtom);
const { screenshotTargetRef } = useScreenshot();
const scrollButtonPreference = useRecoilValue(store.showScrollButton);
const [currentEditId, setCurrentEditId] = useState<number | string | null>(-1);

View File

@@ -1,10 +1,12 @@
import { useMemo } from 'react';
import { useAtomValue } from 'jotai';
import { useRecoilValue } from 'recoil';
import { useAuthContext, useLocalize } from '~/hooks';
import type { TMessageProps, TMessageIcon } from '~/common';
import MinimalHoverButtons from '~/components/Chat/Messages/MinimalHoverButtons';
import Icon from '~/components/Chat/Messages/MessageIcon';
import SearchContent from './Content/SearchContent';
import { fontSizeAtom } from '~/store/fontSize';
import SearchButtons from './SearchButtons';
import SubRow from './SubRow';
import { cn } from '~/utils';
@@ -34,8 +36,8 @@ const MessageBody = ({ message, messageLabel, fontSize }) => (
);
export default function SearchMessage({ message }: Pick<TMessageProps, 'message'>) {
const fontSize = useAtomValue(fontSizeAtom);
const UsernameDisplay = useRecoilValue<boolean>(store.UsernameDisplay);
const fontSize = useRecoilValue(store.fontSize);
const { user } = useAuthContext();
const localize = useLocalize();

View File

@@ -1,4 +1,5 @@
import React, { useCallback, useMemo, memo } from 'react';
import { useAtomValue } from 'jotai';
import { useRecoilValue } from 'recoil';
import { type TMessage } from 'librechat-data-provider';
import type { TMessageProps, TMessageIcon } from '~/common';
@@ -9,6 +10,7 @@ import HoverButtons from '~/components/Chat/Messages/HoverButtons';
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
import { Plugin } from '~/components/Messages/Content';
import SubRow from '~/components/Chat/Messages/SubRow';
import { fontSizeAtom } from '~/store/fontSize';
import { MessageContext } from '~/Providers';
import { useMessageActions } from '~/hooks';
import { cn, logger } from '~/utils';
@@ -58,8 +60,8 @@ const MessageRender = memo(
isMultiMessage,
setCurrentEditId,
});
const fontSize = useAtomValue(fontSizeAtom);
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
const fontSize = useRecoilValue(store.fontSize);
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
const hasNoChildren = !(msg?.children?.length ?? 0);

View File

@@ -1,5 +1,6 @@
import { useRecoilValue } from 'recoil';
import { useCallback, useMemo, memo } from 'react';
import { useAtomValue } from 'jotai';
import { useRecoilValue } from 'recoil';
import type { TMessage, TMessageContentParts } from 'librechat-data-provider';
import type { TMessageProps, TMessageIcon } from '~/common';
import ContentParts from '~/components/Chat/Messages/Content/ContentParts';
@@ -9,6 +10,7 @@ import HoverButtons from '~/components/Chat/Messages/HoverButtons';
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
import { useAttachments, useMessageActions } from '~/hooks';
import SubRow from '~/components/Chat/Messages/SubRow';
import { fontSizeAtom } from '~/store/fontSize';
import { cn, logger } from '~/utils';
import store from '~/store';
@@ -60,8 +62,8 @@ const ContentRender = memo(
isMultiMessage,
setCurrentEditId,
});
const fontSize = useAtomValue(fontSizeAtom);
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
const fontSize = useRecoilValue(store.fontSize);
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
const isLast = useMemo(

View File

@@ -5,6 +5,7 @@ import { QueryKeys, Constants } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
import type { Dispatch, SetStateAction } from 'react';
import { useLocalize, useNewConvo } from '~/hooks';
import { clearMessagesCache } from '~/utils';
import store from '~/store';
export default function MobileNav({
@@ -57,10 +58,7 @@ export default function MobileNav({
aria-label={localize('com_ui_new_chat')}
className="m-1 inline-flex size-10 items-center justify-center rounded-full hover:bg-surface-hover"
onClick={() => {
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
clearMessagesCache(queryClient, conversation?.conversationId);
queryClient.invalidateQueries([QueryKeys.messages]);
newConversation();
}}

View File

@@ -5,6 +5,7 @@ import { QueryKeys, Constants } from 'librechat-data-provider';
import { TooltipAnchor, NewChatIcon, MobileSidebar, Sidebar, Button } from '@librechat/client';
import type { TMessage } from 'librechat-data-provider';
import { useLocalize, useNewConvo } from '~/hooks';
import { clearMessagesCache } from '~/utils';
import store from '~/store';
export default function NewChat({
@@ -33,10 +34,7 @@ export default function NewChat({
window.open('/c/new', '_blank');
return;
}
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
clearMessagesCache(queryClient, conversation?.conversationId);
queryClient.invalidateQueries([QueryKeys.messages]);
newConvo();
navigate('/c/new', { state: { focusChat: true } });

View File

@@ -1,15 +1,14 @@
import { useRecoilState } from 'recoil';
import { Dropdown, applyFontSize } from '@librechat/client';
import { useAtom } from 'jotai';
import { Dropdown } from '@librechat/client';
import { fontSizeAtom } from '~/store/fontSize';
import { useLocalize } from '~/hooks';
import store from '~/store';
export default function FontSizeSelector() {
const [fontSize, setFontSize] = useRecoilState(store.fontSize);
const localize = useLocalize();
const [fontSize, setFontSize] = useAtom(fontSizeAtom);
const handleChange = (val: string) => {
setFontSize(val);
applyFontSize(val);
};
const options = [

View File

@@ -1,4 +1,4 @@
import { useRecoilValue } from 'recoil';
import { useAtomValue } from 'jotai';
import type { TMessageProps } from '~/common';
import MinimalHoverButtons from '~/components/Chat/Messages/MinimalHoverButtons';
import MessageContent from '~/components/Chat/Messages/Content/MessageContent';
@@ -6,16 +6,16 @@ import SearchContent from '~/components/Chat/Messages/Content/SearchContent';
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
import { Plugin } from '~/components/Messages/Content';
import SubRow from '~/components/Chat/Messages/SubRow';
import { fontSizeAtom } from '~/store/fontSize';
import { MessageContext } from '~/Providers';
import { useAttachments } from '~/hooks';
import MultiMessage from './MultiMessage';
import { cn } from '~/utils';
import store from '~/store';
import Icon from './MessageIcon';
export default function Message(props: TMessageProps) {
const fontSize = useRecoilValue(store.fontSize);
const fontSize = useAtomValue(fontSizeAtom);
const {
message,
siblingIdx,

View File

@@ -21,8 +21,8 @@ describe('useFocusChatEffect', () => {
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
// Mock window.matchMedia
window.matchMedia = jest.fn().mockImplementation(() => ({
matches: false,
window.matchMedia = jest.fn().mockImplementation((query) => ({
matches: query === '(hover: hover)', // Desktop has hover capability
media: '',
onchange: null,
addListener: jest.fn(),
@@ -83,8 +83,8 @@ describe('useFocusChatEffect', () => {
});
test('should not focus textarea on touchscreen devices', () => {
window.matchMedia = jest.fn().mockImplementation(() => ({
matches: true, // This indicates a touchscreen
window.matchMedia = jest.fn().mockImplementation((query) => ({
matches: query === '(pointer: coarse)', // Touchscreen has coarse pointer
media: '',
onchange: null,
addListener: jest.fn(),

View File

@@ -3,7 +3,13 @@ import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants, dataService } from 'librechat-data-provider';
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField, logger } from '~/utils';
import {
getDefaultEndpoint,
clearMessagesCache,
buildDefaultConvo,
getEndpointField,
logger,
} from '~/utils';
import store from '~/store';
const useNavigateToConvo = (index = 0) => {
@@ -80,7 +86,7 @@ const useNavigateToConvo = (index = 0) => {
});
}
clearAllConversations(true);
queryClient.setQueryData([QueryKeys.messages, currentConvoId], []);
clearMessagesCache(queryClient, currentConvoId);
if (convo.conversationId !== Constants.NEW_CONVO && convo.conversationId) {
queryClient.invalidateQueries([QueryKeys.conversation, convo.conversationId]);
fetchFreshData(convo);

View File

@@ -365,6 +365,7 @@
"com_error_files_process": "处理文件时发生错误",
"com_error_files_upload": "上传文件时发生错误",
"com_error_files_upload_canceled": "文件上传请求已取消。注意:文件上传可能仍在进行中,需要手动删除。",
"com_error_files_upload_too_large": "文件过大,请上传小于 {{0}} MB 的文件",
"com_error_files_validation": "验证文件时出错。",
"com_error_google_tool_conflict": "内置的 Google 工具与外部工具不兼容。请禁用内置工具或外部工具。",
"com_error_heic_conversion": "将 HEIC 图片转换为 JPEG 失败。请尝试手动转换图像或使用其他格式。",
@@ -560,6 +561,7 @@
"com_nav_setting_balance": "余额",
"com_nav_setting_chat": "对话",
"com_nav_setting_data": "数据管理",
"com_nav_setting_delay": "延迟(秒)",
"com_nav_setting_general": "通用",
"com_nav_setting_mcp": "MCP 设置",
"com_nav_setting_personalization": "个性化",
@@ -759,6 +761,7 @@
"com_ui_client_secret": "Client Secret",
"com_ui_close": "关闭",
"com_ui_close_menu": "关闭菜单",
"com_ui_close_settings": "关闭设置",
"com_ui_close_window": "关闭窗口",
"com_ui_code": "代码",
"com_ui_collapse_chat": "收起对话",
@@ -857,6 +860,7 @@
"com_ui_edit_editing_image": "编辑图片",
"com_ui_edit_mcp_server": "编辑 MCP 服务器",
"com_ui_edit_memory": "编辑记忆",
"com_ui_editor_instructions": "拖动图片调整位置 • 使用缩放滑块或按钮调整大小",
"com_ui_empty_category": "-",
"com_ui_endpoint": "端点",
"com_ui_endpoint_menu": "LLM 端点菜单",
@@ -891,6 +895,7 @@
"com_ui_feedback_tag_unjustified_refusal": "无故拒绝回答",
"com_ui_field_max_length": "{{field}} 最多 {{length}} 个字符",
"com_ui_field_required": "此字段为必填项",
"com_ui_file_input_avatar_label": "上传文件用作头像",
"com_ui_file_size": "文件大小",
"com_ui_file_token_limit": "文件词元数限制",
"com_ui_file_token_limit_desc": "为文件处理设定最大词元数限制,以控制成本和资源使用",
@@ -953,11 +958,13 @@
"com_ui_import_conversation_file_type_error": "不支持的导入类型",
"com_ui_import_conversation_info": "从 JSON 文件导入对话",
"com_ui_import_conversation_success": "对话导入成功",
"com_ui_import_conversation_upload_error": "上传文件时出错,请重试。",
"com_ui_include_shadcnui": "包含 shadcn/ui 组件指令",
"com_ui_initializing": "初始化中...",
"com_ui_input": "输入",
"com_ui_instructions": "指令",
"com_ui_key": "键",
"com_ui_key_required": "API Key 为必填项",
"com_ui_late_night": "夜深了",
"com_ui_latest_footer": "Every AI for Everyone.",
"com_ui_latest_production_version": "最新在用版本",
@@ -972,6 +979,7 @@
"com_ui_manage": "管理",
"com_ui_marketplace": "市场",
"com_ui_marketplace_allow_use": "允许使用市场",
"com_ui_max_file_size": "PNG、JPG 或 JPEG最大 {{0}}",
"com_ui_max_tags": "最多允许 {{0}} 个,用最新值。",
"com_ui_mcp_authenticated_success": "MCP 服务器 “{{0}}” 认证成功",
"com_ui_mcp_configure_server": "配置 {{0}}",
@@ -1066,6 +1074,7 @@
"com_ui_privacy_policy": "隐私政策",
"com_ui_privacy_policy_url": "隐私政策链接",
"com_ui_prompt": "提示词",
"com_ui_prompt_groups": "提示词组列表",
"com_ui_prompt_name": "提示词名称",
"com_ui_prompt_name_required": "提示词名称为必填项",
"com_ui_prompt_preview_not_shared": "作者未允许对此提示词进行协作。",
@@ -1095,6 +1104,8 @@
"com_ui_rename_failed": "重命名对话失败",
"com_ui_rename_prompt": "重命名 Prompt",
"com_ui_requires_auth": "需要认证",
"com_ui_reset": "重置",
"com_ui_reset_adjustments": "重置调整",
"com_ui_reset_var": "重置 {{0}}",
"com_ui_reset_zoom": "重置缩放",
"com_ui_resource": "资源",
@@ -1103,6 +1114,8 @@
"com_ui_revoke_info": "撤销所有用户提供的凭据",
"com_ui_revoke_key_confirm": "您确定要撤销此密钥吗?",
"com_ui_revoke_key_endpoint": "撤销 {{0}} 的密钥",
"com_ui_revoke_key_error": "撤销 API Key 失败,请重试。",
"com_ui_revoke_key_success": "API Key 撤销成功",
"com_ui_revoke_keys": "撤销密钥",
"com_ui_revoke_keys_confirm": "您确定要撤销所有密钥吗?",
"com_ui_role": "角色",
@@ -1116,11 +1129,15 @@
"com_ui_role_viewer": "查看者",
"com_ui_role_viewer_desc": "可以查看和使用智能体,但无法修改智能体",
"com_ui_roleplay": "角色扮演",
"com_ui_rotate": "旋转",
"com_ui_rotate_90": "旋转 90 度",
"com_ui_run_code": "运行代码",
"com_ui_run_code_error": "代码运行出错",
"com_ui_save": "保存",
"com_ui_save_badge_changes": "保存徽章更改?",
"com_ui_save_changes": "保存修改",
"com_ui_save_key_error": "保存 API Key 失败,请重试。",
"com_ui_save_key_success": "API Key 保存成功",
"com_ui_save_submit": "保存并提交",
"com_ui_saved": "保存成功!",
"com_ui_saving": "保存中...",
@@ -1217,6 +1234,7 @@
"com_ui_update_mcp_success": "已成功创建或更新 MCP",
"com_ui_upload": "上传",
"com_ui_upload_agent_avatar": "成功更新智能体头像",
"com_ui_upload_avatar_label": "上传头像图片",
"com_ui_upload_code_files": "上传代码解释器文件",
"com_ui_upload_delay": "上传 “{{0}}” 时比预期花了更长时间。文件正在进行检索索引,请稍候。",
"com_ui_upload_error": "上传文件错误",
@@ -1228,6 +1246,7 @@
"com_ui_upload_invalid": "上传的文件无效。必须是图片,且不得超过大小限制",
"com_ui_upload_invalid_var": "上传的文件无效。必须是图片,且不得超过 {{0}} MB。",
"com_ui_upload_ocr_text": "作为文本上传",
"com_ui_upload_provider": "上传至提供商",
"com_ui_upload_success": "上传文件成功",
"com_ui_upload_type": "选择上传类型",
"com_ui_usage": "用量",
@@ -1267,6 +1286,8 @@
"com_ui_web_search_scraper": "抓取器",
"com_ui_web_search_scraper_firecrawl": "Firecrawl API",
"com_ui_web_search_scraper_firecrawl_key": "获取您的 Firecrawl API Key",
"com_ui_web_search_scraper_serper": "Serper Scrape API",
"com_ui_web_search_scraper_serper_key": "获取您的 Serper API Key",
"com_ui_web_search_searxng_api_key": "输入 SearXNG API Key可选",
"com_ui_web_search_searxng_instance_url": "SearXNG 实例 URL",
"com_ui_web_searching": "正在搜索网络",
@@ -1276,5 +1297,8 @@
"com_ui_x_selected": "{{0}} 已选择",
"com_ui_yes": "是的",
"com_ui_zoom": "缩放",
"com_ui_zoom_in": "放大",
"com_ui_zoom_level": "缩放级别",
"com_ui_zoom_out": "缩小",
"com_user_message": "您"
}

View File

@@ -0,0 +1,54 @@
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { applyFontSize } from '@librechat/client';
const DEFAULT_FONT_SIZE = 'text-base';
/**
* Base storage atom for font size
*/
const fontSizeStorageAtom = atomWithStorage<string>('fontSize', DEFAULT_FONT_SIZE, undefined, {
getOnInit: true,
});
/**
* Derived atom that applies font size changes to the DOM
* Read: returns the current font size
* Write: updates storage and applies the font size to the DOM
*/
export const fontSizeAtom = atom(
(get) => get(fontSizeStorageAtom),
(get, set, newValue: string) => {
set(fontSizeStorageAtom, newValue);
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
applyFontSize(newValue);
}
},
);
/**
* Initialize font size on app load
*/
export const initializeFontSize = () => {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return;
}
const savedValue = localStorage.getItem('fontSize');
if (savedValue !== null) {
try {
const parsedValue = JSON.parse(savedValue);
applyFontSize(parsedValue);
} catch (error) {
console.error(
'Error parsing localStorage key "fontSize", resetting to default. Error:',
error,
);
localStorage.setItem('fontSize', JSON.stringify(DEFAULT_FONT_SIZE));
applyFontSize(DEFAULT_FONT_SIZE);
}
} else {
applyFontSize(DEFAULT_FONT_SIZE);
}
};

View File

@@ -21,7 +21,6 @@ const localStorageAtoms = {
// General settings
autoScroll: atomWithLocalStorage('autoScroll', false),
hideSidePanel: atomWithLocalStorage('hideSidePanel', false),
fontSize: atomWithLocalStorage('fontSize', 'text-base'),
enableUserMsgMarkdown: atomWithLocalStorage<boolean>(
LocalStorageKeys.ENABLE_USER_MSG_MARKDOWN,
true,

View File

@@ -1,5 +1,6 @@
import { ContentTypes } from 'librechat-data-provider';
import { ContentTypes, QueryKeys, Constants } from 'librechat-data-provider';
import type { TMessage, TMessageContentParts } from 'librechat-data-provider';
import type { QueryClient } from '@tanstack/react-query';
export const TEXT_KEY_DIVIDER = '|||';
@@ -146,3 +147,26 @@ export const scrollToEnd = (callback?: () => void) => {
}
}
};
/**
* Clears messages for both the specified conversation ID and the NEW_CONVO query key.
* This ensures that messages are properly cleared in all contexts, preventing stale data
* from persisting in the NEW_CONVO cache.
*
* @param queryClient - The React Query client instance
* @param conversationId - The conversation ID to clear messages for
*/
export const clearMessagesCache = (
queryClient: QueryClient,
conversationId: string | undefined | null,
): void => {
const convoId = conversationId ?? Constants.NEW_CONVO;
// Clear messages for the current conversation
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, convoId], []);
// Also clear NEW_CONVO messages if we're not already on NEW_CONVO
if (convoId !== Constants.NEW_CONVO) {
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, Constants.NEW_CONVO], []);
}
};

View File

@@ -1,4 +1,5 @@
import react from '@vitejs/plugin-react';
// @ts-ignore
import path from 'path';
import type { Plugin } from 'vite';
import { defineConfig } from 'vite';
@@ -7,19 +8,23 @@ import { nodePolyfills } from 'vite-plugin-node-polyfills';
import { VitePWA } from 'vite-plugin-pwa';
// https://vitejs.dev/config/
const backendPort = process.env.BACKEND_PORT && Number(process.env.BACKEND_PORT) || 3080;
const backendURL = process.env.HOST ? `http://${process.env.HOST}:${backendPort}` : `http://localhost:${backendPort}`;
export default defineConfig(({ command }) => ({
base: '',
server: {
host: 'localhost',
port: 3090,
allowedHosts: process.env.VITE_ALLOWED_HOSTS && process.env.VITE_ALLOWED_HOSTS.split(',') || [],
host: process.env.HOST || 'localhost',
port: process.env.PORT && Number(process.env.PORT) || 3090,
strictPort: false,
proxy: {
'/api': {
target: 'http://localhost:3080',
target: backendURL,
changeOrigin: true,
},
'/oauth': {
target: 'http://localhost:3080',
target: backendURL,
changeOrigin: true,
},
},
@@ -259,6 +264,7 @@ export default defineConfig(({ command }) => ({
interface SourcemapExclude {
excludeNodeModules?: boolean;
}
export function sourcemapExclude(opts?: SourcemapExclude): Plugin {
return {
name: 'sourcemap-exclude',

145
package-lock.json generated
View File

@@ -19,7 +19,7 @@
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.20.0",
"@microsoft/eslint-formatter-sarif": "^3.1.0",
"@playwright/test": "^1.50.1",
"@playwright/test": "^1.56.1",
"@types/react-virtualized": "^9.22.0",
"caniuse-lite": "^1.0.30001741",
"cross-env": "^7.0.3",
@@ -64,7 +64,7 @@
"@langchain/google-genai": "^0.2.13",
"@langchain/google-vertexai": "^0.2.13",
"@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.4.85",
"@librechat/agents": "^2.4.86",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",
@@ -2768,7 +2768,7 @@
"tailwindcss": "^3.4.1",
"ts-jest": "^29.2.5",
"typescript": "^5.3.3",
"vite": "^6.3.6",
"vite": "^6.4.1",
"vite-plugin-compression2": "^2.2.1",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^0.21.2"
@@ -4305,6 +4305,24 @@
"node": ">=6"
}
},
"client/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"client/node_modules/framer-motion": {
"version": "11.18.2",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz",
@@ -4358,6 +4376,19 @@
"dev": true,
"license": "MIT"
},
"client/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"client/node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -4453,6 +4484,81 @@
"browserslist": ">= 4.21.0"
}
},
"client/node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"client/node_modules/vite-plugin-pwa": {
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.21.2.tgz",
@@ -21531,9 +21637,9 @@
}
},
"node_modules/@librechat/agents": {
"version": "2.4.85",
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.85.tgz",
"integrity": "sha512-t6h5f6ApnoEC+x8kqBlke1RR6BPzT+9BvlkA8VxvQVJtYIt5Ey4BOTRDGjdilDoXUcLui11PbjCd17EbjPkTcA==",
"version": "2.4.86",
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.86.tgz",
"integrity": "sha512-Z3v+vMfFEyrDWrlPvgY9dUlhzYvtLXYYULEzkxUM1QpITuI3DsXr3xb1kXHAYOx3NmBGxiN9R/gjZN0tGBEo1g==",
"license": "MIT",
"dependencies": {
"@langchain/anthropic": "^0.3.26",
@@ -22902,12 +23008,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.50.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz",
"integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.50.1"
"playwright": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -43042,12 +43148,12 @@
}
},
"node_modules/playwright": {
"version": "1.50.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz",
"integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.1"
"playwright-core": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -43060,9 +43166,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.50.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz",
"integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@@ -49973,6 +50079,7 @@
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -50076,6 +50183,7 @@
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.0.0"
},
@@ -50094,6 +50202,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -51337,7 +51446,7 @@
"@azure/storage-blob": "^12.27.0",
"@keyv/redis": "^4.3.3",
"@langchain/core": "^0.3.62",
"@librechat/agents": "^2.4.85",
"@librechat/agents": "^2.4.86",
"@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.17.1",
"axios": "^1.12.1",

View File

@@ -100,7 +100,7 @@
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.20.0",
"@microsoft/eslint-formatter-sarif": "^3.1.0",
"@playwright/test": "^1.50.1",
"@playwright/test": "^1.56.1",
"@types/react-virtualized": "^9.22.0",
"caniuse-lite": "^1.0.30001741",
"cross-env": "^7.0.3",

View File

@@ -80,7 +80,7 @@
"@azure/storage-blob": "^12.27.0",
"@keyv/redis": "^4.3.3",
"@langchain/core": "^0.3.62",
"@librechat/agents": "^2.4.85",
"@librechat/agents": "^2.4.86",
"@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.17.1",
"axios": "^1.12.1",

View File

@@ -40,10 +40,10 @@ const openAIModels = {
'gpt-5': 400000,
'gpt-5-mini': 400000,
'gpt-5-nano': 400000,
'gpt-5-pro': 400000,
'gpt-4o': 127500, // -500 from max
'gpt-4o-mini': 127500, // -500 from max
'gpt-4o-2024-05-13': 127500, // -500 from max
'gpt-4o-2024-08-06': 127500, // -500 from max
'gpt-4-turbo': 127500, // -500 from max
'gpt-4-vision': 127500, // -500 from max
'gpt-3.5-turbo': 16375, // -10 from max
@@ -60,9 +60,11 @@ const mistralModels = {
'mistral-7b': 31990, // -10 from max
'mistral-small': 31990, // -10 from max
'mixtral-8x7b': 31990, // -10 from max
'mixtral-8x22b': 65536,
'mistral-large': 131000,
'mistral-large-2402': 127500,
'mistral-large-2407': 127500,
'mistral-nemo': 131000,
'pixtral-large': 131000,
'mistral-saba': 32000,
codestral: 256000,
@@ -75,6 +77,7 @@ const cohereModels = {
'command-light-nightly': 8182, // -10 from max
command: 4086, // -10 from max
'command-nightly': 8182, // -10 from max
'command-text': 4086, // -10 from max
'command-r': 127500, // -500 from max
'command-r-plus': 127500, // -500 from max
};
@@ -127,14 +130,17 @@ const anthropicModels = {
'claude-3.7-sonnet': 200000,
'claude-3-5-sonnet-latest': 200000,
'claude-3.5-sonnet-latest': 200000,
'claude-haiku-4-5': 200000,
'claude-sonnet-4': 1000000,
'claude-opus-4': 200000,
'claude-4': 200000,
};
const deepseekModels = {
'deepseek-reasoner': 128000,
deepseek: 128000,
'deepseek-reasoner': 128000,
'deepseek-r1': 128000,
'deepseek-v3': 128000,
'deepseek.r1': 128000,
};
@@ -200,32 +206,57 @@ const metaModels = {
'llama2:70b': 4000,
};
const ollamaModels = {
const qwenModels = {
qwen: 32000,
'qwen2.5': 32000,
'qwen-turbo': 1000000,
'qwen-plus': 131000,
'qwen-max': 32000,
'qwq-32b': 32000,
// Qwen3 models
qwen3: 40960, // Qwen3 base pattern (using qwen3-4b context)
'qwen3-8b': 128000,
'qwen3-14b': 40960,
'qwen3-30b-a3b': 40960,
'qwen3-32b': 40960,
'qwen3-235b-a22b': 40960,
// Qwen3 VL (Vision-Language) models
'qwen3-vl-8b-thinking': 256000,
'qwen3-vl-8b-instruct': 262144,
'qwen3-vl-30b-a3b': 262144,
'qwen3-vl-235b-a22b': 131072,
// Qwen3 specialized models
'qwen3-max': 256000,
'qwen3-coder': 262144,
'qwen3-coder-30b-a3b': 262144,
'qwen3-coder-plus': 128000,
'qwen3-coder-flash': 128000,
'qwen3-next-80b-a3b': 262144,
};
const ai21Models = {
'ai21.j2-mid-v1': 8182, // -10 from max
'ai21.j2-ultra-v1': 8182, // -10 from max
'ai21.jamba-instruct-v1:0': 255500, // -500 from max
'j2-mid': 8182, // -10 from max
'j2-ultra': 8182, // -10 from max
'jamba-instruct': 255500, // -500 from max
};
const amazonModels = {
'amazon.titan-text-lite-v1': 4000,
'amazon.titan-text-express-v1': 8000,
'amazon.titan-text-premier-v1:0': 31500, // -500 from max
// Amazon Titan models
'titan-text-lite': 4000,
'titan-text-express': 8000,
'titan-text-premier': 31500, // -500 from max
// Amazon Nova models
// https://aws.amazon.com/ai/generative-ai/nova/
'amazon.nova-micro-v1:0': 127000, // -1000 from max,
'amazon.nova-lite-v1:0': 295000, // -5000 from max,
'amazon.nova-pro-v1:0': 295000, // -5000 from max,
'amazon.nova-premier-v1:0': 995000, // -5000 from max,
'nova-micro': 127000, // -1000 from max
'nova-lite': 295000, // -5000 from max
'nova-pro': 295000, // -5000 from max
'nova-premier': 995000, // -5000 from max
};
const bedrockModels = {
...anthropicModels,
...mistralModels,
...cohereModels,
...ollamaModels,
...deepseekModels,
...metaModels,
...ai21Models,
@@ -254,6 +285,7 @@ const aggregateModels = {
...googleModels,
...bedrockModels,
...xAIModels,
...qwenModels,
// misc.
kimi: 131000,
// GPT-OSS
@@ -289,6 +321,7 @@ export const modelMaxOutputs = {
'gpt-5': 128000,
'gpt-5-mini': 128000,
'gpt-5-nano': 128000,
'gpt-5-pro': 128000,
'gpt-oss-20b': 131000,
'gpt-oss-120b': 131000,
system_default: 32000,
@@ -299,6 +332,7 @@ const anthropicMaxOutputs = {
'claude-3-haiku': 4096,
'claude-3-sonnet': 4096,
'claude-3-opus': 4096,
'claude-haiku-4-5': 64000,
'claude-opus-4': 32000,
'claude-sonnet-4': 64000,
'claude-3.5-sonnet': 8192,