🍞 fix: Minor fixes and improved Bun support (#1916)

* fix(bun): fix bun compatibility to allow gzip header: https://github.com/oven-sh/bun/issues/267#issuecomment-1854460357

* chore: update custom config examples

* fix(OpenAIClient.chatCompletion): remove redundant call of stream.controller.abort() as `break` aborts the request and prevents abort errors when not called redundantly

* chore: bump bun.lockb

* fix: remove result-thinking class when message is no longer streaming

* fix(bun): improve Bun support by forcing use of old method in bun env, also update old methods with new customizable params

* fix(ci): pass tests
This commit is contained in:
Danny Avila 2024-02-27 17:51:16 -05:00 committed by GitHub
parent 5d887492ea
commit c37d5568bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 175 additions and 59 deletions

View file

@ -1,9 +1,16 @@
const crypto = require('crypto');
const Keyv = require('keyv'); const Keyv = require('keyv');
const crypto = require('crypto');
const {
EModelEndpoint,
resolveHeaders,
mapModelToAzureConfig,
} = require('librechat-data-provider');
const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken'); const { encoding_for_model: encodingForModel, get_encoding: getEncoding } = require('tiktoken');
const { fetchEventSource } = require('@waylaidwanderer/fetch-event-source'); const { fetchEventSource } = require('@waylaidwanderer/fetch-event-source');
const { Agent, ProxyAgent } = require('undici'); const { Agent, ProxyAgent } = require('undici');
const BaseClient = require('./BaseClient'); const BaseClient = require('./BaseClient');
const { logger } = require('~/config');
const { extractBaseURL, constructAzureURL, genAzureChatCompletion } = require('~/utils');
const CHATGPT_MODEL = 'gpt-3.5-turbo'; const CHATGPT_MODEL = 'gpt-3.5-turbo';
const tokenizersCache = {}; const tokenizersCache = {};
@ -144,7 +151,8 @@ class ChatGPTClient extends BaseClient {
if (!abortController) { if (!abortController) {
abortController = new AbortController(); abortController = new AbortController();
} }
const modelOptions = { ...this.modelOptions };
let modelOptions = { ...this.modelOptions };
if (typeof onProgress === 'function') { if (typeof onProgress === 'function') {
modelOptions.stream = true; modelOptions.stream = true;
} }
@ -159,56 +167,171 @@ class ChatGPTClient extends BaseClient {
} }
const { debug } = this.options; const { debug } = this.options;
const url = this.completionsUrl; let baseURL = this.completionsUrl;
if (debug) { if (debug) {
console.debug(); console.debug();
console.debug(url); console.debug(baseURL);
console.debug(modelOptions); console.debug(modelOptions);
console.debug(); console.debug();
} }
if (this.azure || this.options.azure) {
// Azure does not accept `model` in the body, so we need to remove it.
delete modelOptions.model;
}
const opts = { const opts = {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(modelOptions),
dispatcher: new Agent({ dispatcher: new Agent({
bodyTimeout: 0, bodyTimeout: 0,
headersTimeout: 0, headersTimeout: 0,
}), }),
}; };
if (this.apiKey && this.options.azure) { if (this.isVisionModel) {
opts.headers['api-key'] = this.apiKey; modelOptions.max_tokens = 4000;
}
/** @type {TAzureConfig | undefined} */
const azureConfig = this.options?.req?.app?.locals?.[EModelEndpoint.azureOpenAI];
const isAzure = this.azure || this.options.azure;
if (
(isAzure && this.isVisionModel && azureConfig) ||
(azureConfig && this.isVisionModel && this.options.endpoint === EModelEndpoint.azureOpenAI)
) {
const { modelGroupMap, groupMap } = azureConfig;
const {
azureOptions,
baseURL,
headers = {},
serverless,
} = mapModelToAzureConfig({
modelName: modelOptions.model,
modelGroupMap,
groupMap,
});
opts.headers = resolveHeaders(headers);
this.langchainProxy = extractBaseURL(baseURL);
this.apiKey = azureOptions.azureOpenAIApiKey;
const groupName = modelGroupMap[modelOptions.model].group;
this.options.addParams = azureConfig.groupMap[groupName].addParams;
this.options.dropParams = azureConfig.groupMap[groupName].dropParams;
// Note: `forcePrompt` not re-assigned as only chat models are vision models
this.azure = !serverless && azureOptions;
this.azureEndpoint =
!serverless && genAzureChatCompletion(this.azure, modelOptions.model, this);
}
if (this.options.headers) {
opts.headers = { ...opts.headers, ...this.options.headers };
}
if (isAzure) {
// Azure does not accept `model` in the body, so we need to remove it.
delete modelOptions.model;
baseURL = this.langchainProxy
? constructAzureURL({
baseURL: this.langchainProxy,
azure: this.azure,
})
: this.azureEndpoint.split(/\/(chat|completion)/)[0];
if (this.options.forcePrompt) {
baseURL += '/completions';
} else {
baseURL += '/chat/completions';
}
opts.defaultQuery = { 'api-version': this.azure.azureOpenAIApiVersion };
opts.headers = { ...opts.headers, 'api-key': this.apiKey };
} else if (this.apiKey) { } else if (this.apiKey) {
opts.headers.Authorization = `Bearer ${this.apiKey}`; opts.headers.Authorization = `Bearer ${this.apiKey}`;
} }
if (process.env.OPENAI_ORGANIZATION) {
opts.headers['OpenAI-Organization'] = process.env.OPENAI_ORGANIZATION;
}
if (this.useOpenRouter) { if (this.useOpenRouter) {
opts.headers['HTTP-Referer'] = 'https://librechat.ai'; opts.headers['HTTP-Referer'] = 'https://librechat.ai';
opts.headers['X-Title'] = 'LibreChat'; opts.headers['X-Title'] = 'LibreChat';
} }
if (this.options.headers) {
opts.headers = { ...opts.headers, ...this.options.headers };
}
if (this.options.proxy) { if (this.options.proxy) {
opts.dispatcher = new ProxyAgent(this.options.proxy); opts.dispatcher = new ProxyAgent(this.options.proxy);
} }
/* hacky fixes for Mistral AI API:
- Re-orders system message to the top of the messages payload, as not allowed anywhere else
- If there is only one message and it's a system message, change the role to user
*/
if (baseURL.includes('https://api.mistral.ai/v1') && modelOptions.messages) {
const { messages } = modelOptions;
const systemMessageIndex = messages.findIndex((msg) => msg.role === 'system');
if (systemMessageIndex > 0) {
const [systemMessage] = messages.splice(systemMessageIndex, 1);
messages.unshift(systemMessage);
}
modelOptions.messages = messages;
if (messages.length === 1 && messages[0].role === 'system') {
modelOptions.messages[0].role = 'user';
}
}
if (this.options.addParams && typeof this.options.addParams === 'object') {
modelOptions = {
...modelOptions,
...this.options.addParams,
};
logger.debug('[ChatGPTClient] chatCompletion: added params', {
addParams: this.options.addParams,
modelOptions,
});
}
if (this.options.dropParams && Array.isArray(this.options.dropParams)) {
this.options.dropParams.forEach((param) => {
delete modelOptions[param];
});
logger.debug('[ChatGPTClient] chatCompletion: dropped params', {
dropParams: this.options.dropParams,
modelOptions,
});
}
if (baseURL.includes('v1') && !baseURL.includes('/completions') && !this.isChatCompletion) {
baseURL = baseURL.split('v1')[0] + 'v1/completions';
} else if (
baseURL.includes('v1') &&
!baseURL.includes('/chat/completions') &&
this.isChatCompletion
) {
baseURL = baseURL.split('v1')[0] + 'v1/chat/completions';
}
const BASE_URL = new URL(baseURL);
if (opts.defaultQuery) {
Object.entries(opts.defaultQuery).forEach(([key, value]) => {
BASE_URL.searchParams.append(key, value);
});
delete opts.defaultQuery;
}
const completionsURL = BASE_URL.toString();
opts.body = JSON.stringify(modelOptions);
if (modelOptions.stream) { if (modelOptions.stream) {
// eslint-disable-next-line no-async-promise-executor // eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
let done = false; let done = false;
await fetchEventSource(url, { await fetchEventSource(completionsURL, {
...opts, ...opts,
signal: abortController.signal, signal: abortController.signal,
async onopen(response) { async onopen(response) {
@ -236,7 +359,6 @@ class ChatGPTClient extends BaseClient {
// workaround for private API not sending [DONE] event // workaround for private API not sending [DONE] event
if (!done) { if (!done) {
onProgress('[DONE]'); onProgress('[DONE]');
abortController.abort();
resolve(); resolve();
} }
}, },
@ -249,14 +371,13 @@ class ChatGPTClient extends BaseClient {
}, },
onmessage(message) { onmessage(message) {
if (debug) { if (debug) {
// console.debug(message); console.debug(message);
} }
if (!message.data || message.event === 'ping') { if (!message.data || message.event === 'ping') {
return; return;
} }
if (message.data === '[DONE]') { if (message.data === '[DONE]') {
onProgress('[DONE]'); onProgress('[DONE]');
abortController.abort();
resolve(); resolve();
done = true; done = true;
return; return;
@ -269,7 +390,7 @@ class ChatGPTClient extends BaseClient {
} }
}); });
} }
const response = await fetch(url, { const response = await fetch(completionsURL, {
...opts, ...opts,
signal: abortController.signal, signal: abortController.signal,
}); });

View file

@ -560,7 +560,7 @@ class OpenAIClient extends BaseClient {
let streamResult = null; let streamResult = null;
this.modelOptions.user = this.user; this.modelOptions.user = this.user;
const invalidBaseUrl = this.completionsUrl && extractBaseURL(this.completionsUrl) === null; const invalidBaseUrl = this.completionsUrl && extractBaseURL(this.completionsUrl) === null;
const useOldMethod = !!(invalidBaseUrl || !this.isChatCompletion); const useOldMethod = !!(invalidBaseUrl || !this.isChatCompletion || typeof Bun !== 'undefined');
if (typeof opts.onProgress === 'function' && useOldMethod) { if (typeof opts.onProgress === 'function' && useOldMethod) {
await this.getCompletion( await this.getCompletion(
payload, payload,

View file

@ -2,6 +2,7 @@ require('dotenv').config();
const path = require('path'); const path = require('path');
require('module-alias')({ base: path.resolve(__dirname, '..') }); require('module-alias')({ base: path.resolve(__dirname, '..') });
const cors = require('cors'); const cors = require('cors');
const axios = require('axios');
const express = require('express'); const express = require('express');
const passport = require('passport'); const passport = require('passport');
const mongoSanitize = require('express-mongo-sanitize'); const mongoSanitize = require('express-mongo-sanitize');
@ -22,6 +23,9 @@ const port = Number(PORT) || 3080;
const host = HOST || 'localhost'; const host = HOST || 'localhost';
const startServer = async () => { const startServer = async () => {
if (typeof Bun !== 'undefined') {
axios.defaults.headers.common['Accept-Encoding'] = 'gzip';
}
await connectDb(); await connectDb();
logger.info('Connected to MongoDB'); logger.info('Connected to MongoDB');
await indexSync(); await indexSync();

BIN
bun.lockb

Binary file not shown.

View file

@ -10,7 +10,7 @@ import rehypeHighlight from 'rehype-highlight';
import type { TMessage } from 'librechat-data-provider'; import type { TMessage } from 'librechat-data-provider';
import type { PluggableList } from 'unified'; import type { PluggableList } from 'unified';
import CodeBlock from '~/components/Messages/Content/CodeBlock'; import CodeBlock from '~/components/Messages/Content/CodeBlock';
import { langSubset, validateIframe, processLaTeX } from '~/utils'; import { cn, langSubset, validateIframe, processLaTeX } from '~/utils';
import { useChatContext } from '~/Providers'; import { useChatContext } from '~/Providers';
import store from '~/store'; import store from '~/store';
@ -75,7 +75,7 @@ const Markdown = memo(({ content, message, showCursor }: TContentProps) => {
return ( return (
<div className="absolute"> <div className="absolute">
<p className="relative"> <p className="relative">
<span className="result-thinking" /> <span className={cn(isSubmitting ? 'result-thinking' : '')} />
</p> </p>
</div> </div>
); );

View file

@ -35,7 +35,6 @@ Some of the endpoints are marked as **Known,** which means they might have speci
] ]
fetch: false fetch: false
titleConvo: true titleConvo: true
titleMethod: "completion"
titleModel: "mixtral-8x7b-32768" titleModel: "mixtral-8x7b-32768"
modelDisplayLabel: "groq" modelDisplayLabel: "groq"
iconURL: "https://raw.githubusercontent.com/fuegovic/lc-config-yaml/main/icons/groq.png" iconURL: "https://raw.githubusercontent.com/fuegovic/lc-config-yaml/main/icons/groq.png"
@ -64,7 +63,6 @@ Some of the endpoints are marked as **Known,** which means they might have speci
default: ["mistral-tiny", "mistral-small", "mistral-medium", "mistral-large-latest"] default: ["mistral-tiny", "mistral-small", "mistral-medium", "mistral-large-latest"]
fetch: true fetch: true
titleConvo: true titleConvo: true
titleMethod: "completion"
titleModel: "mistral-tiny" titleModel: "mistral-tiny"
modelDisplayLabel: "Mistral" modelDisplayLabel: "Mistral"
# Drop Default params parameters from the request. See default params in guide linked below. # Drop Default params parameters from the request. See default params in guide linked below.
@ -81,7 +79,7 @@ Some of the endpoints are marked as **Known,** which means they might have speci
- **Known:** icon provided, fetching list of models is recommended as API token rates and pricing used for token credit balances when models are fetched. - **Known:** icon provided, fetching list of models is recommended as API token rates and pricing used for token credit balances when models are fetched.
- API may be strict for some models, and may not allow fields like `stop`, in which case, you should use [`dropParams`.](./custom_config.md#dropparams) - It's recommended, and for some models required, to use [`dropParams`](./custom_config.md#dropparams) to drop the `stop` as Openrouter models use a variety of stop tokens.
- Known issue: you should not use `OPENROUTER_API_KEY` as it will then override the `openAI` endpoint to use OpenRouter as well. - Known issue: you should not use `OPENROUTER_API_KEY` as it will then override the `openAI` endpoint to use OpenRouter as well.
@ -95,9 +93,10 @@ Some of the endpoints are marked as **Known,** which means they might have speci
default: ["gpt-3.5-turbo"] default: ["gpt-3.5-turbo"]
fetch: true fetch: true
titleConvo: true titleConvo: true
titleMethod: "completion"
titleModel: "gpt-3.5-turbo" # change to your preferred model titleModel: "gpt-3.5-turbo" # change to your preferred model
modelDisplayLabel: "OpenRouter" modelDisplayLabel: "OpenRouter"
# Recommended: Drop the stop parameter from the request as Openrouter models use a variety of stop tokens.
dropParams: ["stop"]
``` ```
![image](https://github.com/danny-avila/LibreChat/assets/110412045/c4a0415e-732c-46af-82a6-3598663b7f42) ![image](https://github.com/danny-avila/LibreChat/assets/110412045/c4a0415e-732c-46af-82a6-3598663b7f42)

View file

@ -80,18 +80,18 @@ fileConfig:
fileLimit: 5 fileLimit: 5
fileSizeLimit: 10 # Maximum size for an individual file in MB fileSizeLimit: 10 # Maximum size for an individual file in MB
totalSizeLimit: 50 # Maximum total size for all files in a single request in MB totalSizeLimit: 50 # Maximum total size for all files in a single request in MB
supportedMimeTypes: # supportedMimeTypes: # In case you wish to limit certain filetypes
- "image/.*" # - "image/.*"
- "application/pdf" # - "application/pdf"
openAI: openAI:
disabled: true # Disables file uploading to the OpenAI endpoint disabled: true # Disables file uploading to the OpenAI endpoint
default: default:
totalSizeLimit: 20 totalSizeLimit: 20
YourCustomEndpointName: # YourCustomEndpointName: # Example for custom endpoints
fileLimit: 2 # fileLimit: 2
fileSizeLimit: 5 # fileSizeLimit: 5
serverFileSizeLimit: 100 # Global server file size limit in MB serverFileSizeLimit: 100 # Global server file size limit in MB
avatarSizeLimit: 2 # Limit for user avatar image size in MB avatarSizeLimit: 4 # Limit for user avatar image size in MB, default: 2 MB
rateLimits: rateLimits:
fileUploads: fileUploads:
ipMax: 100 ipMax: 100
@ -116,19 +116,15 @@ endpoints:
apiKey: "${MISTRAL_API_KEY}" apiKey: "${MISTRAL_API_KEY}"
baseURL: "https://api.mistral.ai/v1" baseURL: "https://api.mistral.ai/v1"
models: models:
default: ["mistral-tiny", "mistral-small", "mistral-medium"] default: ["mistral-tiny", "mistral-small", "mistral-medium", "mistral-large-latest"]
fetch: true # Attempt to dynamically fetch available models fetch: true # Attempt to dynamically fetch available models
userIdQuery: false userIdQuery: false
iconURL: "https://example.com/mistral-icon.png" iconURL: "https://example.com/mistral-icon.png"
titleConvo: true titleConvo: true
titleMethod: "completion"
titleModel: "mistral-tiny" titleModel: "mistral-tiny"
summarize: true
summaryModel: "mistral-summary"
forcePrompt: false
modelDisplayLabel: "Mistral AI" modelDisplayLabel: "Mistral AI"
addParams: # addParams:
safe_prompt: true # safe_prompt: true # Mistral specific value for moderating messages
dropParams: dropParams:
- "stop" - "stop"
- "user" - "user"
@ -144,10 +140,9 @@ endpoints:
fetch: false fetch: false
titleConvo: true titleConvo: true
titleModel: "gpt-3.5-turbo" titleModel: "gpt-3.5-turbo"
summarize: false
forcePrompt: false
modelDisplayLabel: "OpenRouter" modelDisplayLabel: "OpenRouter"
dropParams: dropParams:
- "stop"
- "frequency_penalty" - "frequency_penalty"
``` ```
@ -521,15 +516,12 @@ endpoints:
apiKey: "${YOUR_ENV_VAR_KEY}" apiKey: "${YOUR_ENV_VAR_KEY}"
baseURL: "https://api.mistral.ai/v1" baseURL: "https://api.mistral.ai/v1"
models: models:
default: ["mistral-tiny", "mistral-small", "mistral-medium"] default: ["mistral-tiny", "mistral-small", "mistral-medium", "mistral-large-latest"]
titleConvo: true titleConvo: true
titleModel: "mistral-tiny" titleModel: "mistral-tiny"
summarize: false
summaryModel: "mistral-tiny"
forcePrompt: false
modelDisplayLabel: "Mistral" modelDisplayLabel: "Mistral"
addParams: # addParams:
safe_prompt: true # safe_prompt: true # Mistral specific value for moderating messages
# NOTE: For Mistral, it is necessary to drop the following parameters or you will encounter a 422 Error: # NOTE: For Mistral, it is necessary to drop the following parameters or you will encounter a 422 Error:
dropParams: ["stop", "user", "frequency_penalty", "presence_penalty"] dropParams: ["stop", "user", "frequency_penalty", "presence_penalty"]
``` ```

View file

@ -68,26 +68,26 @@ endpoints:
titleConvo: true # Set to true to enable title conversation titleConvo: true # Set to true to enable title conversation
# Title Method: Choose between "completion" or "functions". # Title Method: Choose between "completion" or "functions".
titleMethod: "completion" # Defaults to "completion" if omitted. # titleMethod: "completion" # Defaults to "completion" if omitted.
# Title Model: Specify the model to use for titles. # Title Model: Specify the model to use for titles.
titleModel: "mistral-tiny" # Defaults to "gpt-3.5-turbo" if omitted. titleModel: "mistral-tiny" # Defaults to "gpt-3.5-turbo" if omitted.
# Summarize setting: Set to true to enable summarization. # Summarize setting: Set to true to enable summarization.
summarize: false # summarize: false
# Summary Model: Specify the model to use if summarization is enabled. # Summary Model: Specify the model to use if summarization is enabled.
summaryModel: "mistral-tiny" # Defaults to "gpt-3.5-turbo" if omitted. # summaryModel: "mistral-tiny" # Defaults to "gpt-3.5-turbo" if omitted.
# Force Prompt setting: If true, sends a `prompt` parameter instead of `messages`. # Force Prompt setting: If true, sends a `prompt` parameter instead of `messages`.
forcePrompt: false # forcePrompt: false
# The label displayed for the AI model in messages. # The label displayed for the AI model in messages.
modelDisplayLabel: "Mistral" # Default is "AI" when not set. modelDisplayLabel: "Mistral" # Default is "AI" when not set.
# Add additional parameters to the request. Default params will be overwritten. # Add additional parameters to the request. Default params will be overwritten.
addParams: # addParams:
safe_prompt: true # This field is specific to Mistral AI: https://docs.mistral.ai/api/ # safe_prompt: true # This field is specific to Mistral AI: https://docs.mistral.ai/api/
# Drop Default params parameters from the request. See default params in guide linked below. # Drop Default params parameters from the request. See default params in guide linked below.
# NOTE: For Mistral, it is necessary to drop the following parameters or you will encounter a 422 Error: # NOTE: For Mistral, it is necessary to drop the following parameters or you will encounter a 422 Error:
@ -105,9 +105,8 @@ endpoints:
fetch: true fetch: true
titleConvo: true titleConvo: true
titleModel: "gpt-3.5-turbo" titleModel: "gpt-3.5-turbo"
summarize: false # Recommended: Drop the stop parameter from the request as Openrouter models use a variety of stop tokens.
summaryModel: "gpt-3.5-turbo" dropParams: ["stop"]
forcePrompt: false
modelDisplayLabel: "OpenRouter" modelDisplayLabel: "OpenRouter"
# See the Custom Configuration Guide for more information: # See the Custom Configuration Guide for more information:

View file

@ -49,6 +49,7 @@
"lint": "eslint \"{,!(node_modules)/**/}*.{js,jsx,ts,tsx}\"", "lint": "eslint \"{,!(node_modules)/**/}*.{js,jsx,ts,tsx}\"",
"format": "prettier-eslint --write \"{,!(node_modules)/**/}*.{js,jsx,ts,tsx}\"", "format": "prettier-eslint --write \"{,!(node_modules)/**/}*.{js,jsx,ts,tsx}\"",
"b:api": "NODE_ENV=production bun run api/server/index.js", "b:api": "NODE_ENV=production bun run api/server/index.js",
"b:api-inspect": "NODE_ENV=production bun --inspect run api/server/index.js",
"b:api:dev": "NODE_ENV=production bun run --watch api/server/index.js", "b:api:dev": "NODE_ENV=production bun run --watch api/server/index.js",
"b:data": "cd packages/data-provider && bun run b:build", "b:data": "cd packages/data-provider && bun run b:build",
"b:client": "bun --bun run b:data && cd client && bun --bun run b:build", "b:client": "bun --bun run b:data && cd client && bun --bun run b:build",