Compare commits
9 Commits
feat/updat
...
chore/agen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9449f36ead | ||
|
|
9495520f6f | ||
|
|
87d7ee4b0e | ||
|
|
d8d5d59d92 | ||
|
|
e3d33fed8d | ||
|
|
cbf52eabe3 | ||
|
|
36f0365fd4 | ||
|
|
589f119310 | ||
|
|
d41b07c0af |
233
AGENTS.md
Normal file
233
AGENTS.md
Normal 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/`
|
||||
|
||||
258
api/models/tx.js
258
api/models/tx.js
@@ -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;
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -143,7 +143,7 @@ const initializeAgent = async ({
|
||||
const agentMaxContextTokens = optionalChainWithEmptyCheck(
|
||||
maxContextTokens,
|
||||
getModelMaxTokens(tokensModel, providerEndpointMap[provider], options.endpointTokenConfig),
|
||||
4096,
|
||||
18000,
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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 ?? {};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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": "您"
|
||||
}
|
||||
|
||||
54
client/src/store/fontSize.ts
Normal file
54
client/src/store/fontSize.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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], []);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
145
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user