Compare commits
23 Commits
docs-crisp
...
v0.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49753a35e5 | ||
|
|
1605ef3793 | ||
|
|
8b3f80fe24 | ||
|
|
038063d4d1 | ||
|
|
5c8b16fbaf | ||
|
|
aff219c655 | ||
|
|
d07396d308 | ||
|
|
cc92597f14 | ||
|
|
4854b39f41 | ||
|
|
bb8a40dd98 | ||
|
|
56ea0f9ae7 | ||
|
|
6a6b2e79b0 | ||
|
|
bc2a628902 | ||
|
|
dec7879cc1 | ||
|
|
0a8118deed | ||
|
|
59a8165379 | ||
|
|
3a1d07136c | ||
|
|
a00756c469 | ||
|
|
7945fea0f9 | ||
|
|
84656b9812 | ||
|
|
b5d25f5e4f | ||
|
|
d4b0af3dba | ||
|
|
57d1f12574 |
27
.env.example
27
.env.example
@@ -23,6 +23,13 @@ DOMAIN_SERVER=http://localhost:3080
|
||||
|
||||
NO_INDEX=true
|
||||
|
||||
#===============#
|
||||
# JSON Logging #
|
||||
#===============#
|
||||
|
||||
# Use when process console logs in cloud deployment like GCP/AWS
|
||||
CONSOLE_JSON=false
|
||||
|
||||
#===============#
|
||||
# Debug Logging #
|
||||
#===============#
|
||||
@@ -128,7 +135,7 @@ DEBUG_OPENAI=false
|
||||
|
||||
# OPENAI_REVERSE_PROXY=
|
||||
|
||||
# OPENAI_ORGANIZATION=
|
||||
# OPENAI_ORGANIZATION=
|
||||
|
||||
#====================#
|
||||
# Assistants API #
|
||||
@@ -317,15 +324,15 @@ OPENID_IMAGE_URL=
|
||||
# Email Password Reset #
|
||||
#========================#
|
||||
|
||||
EMAIL_SERVICE=
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=25
|
||||
EMAIL_ENCRYPTION=
|
||||
EMAIL_ENCRYPTION_HOSTNAME=
|
||||
EMAIL_ALLOW_SELFSIGNED=
|
||||
EMAIL_USERNAME=
|
||||
EMAIL_PASSWORD=
|
||||
EMAIL_FROM_NAME=
|
||||
EMAIL_SERVICE=
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=25
|
||||
EMAIL_ENCRYPTION=
|
||||
EMAIL_ENCRYPTION_HOSTNAME=
|
||||
EMAIL_ALLOW_SELFSIGNED=
|
||||
EMAIL_USERNAME=
|
||||
EMAIL_PASSWORD=
|
||||
EMAIL_FROM_NAME=
|
||||
EMAIL_FROM=noreply@librechat.ai
|
||||
|
||||
#========================#
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
2
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
@@ -50,7 +50,7 @@ body:
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/CODE_OF_CONDUCT.md)
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
|
||||
83
.github/workflows/container.yml
vendored
83
.github/workflows/container.yml
vendored
@@ -1,83 +0,0 @@
|
||||
name: Docker Compose Build on Tag
|
||||
|
||||
# The workflow is triggered when a tag is pushed
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Set up Docker
|
||||
- name: Set up Docker
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Set up QEMU for cross-platform builds
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Prepare Docker Build
|
||||
- name: Build Docker images
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
# Tag and push librechat-api
|
||||
- name: Docker metadata for librechat-api
|
||||
id: meta-librechat-api
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository_owner }}/librechat-api
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Build and librechat-api
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: Dockerfile.multi
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta-librechat-api.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: api-build
|
||||
|
||||
# Tag and push librechat
|
||||
- name: Docker metadata for librechat
|
||||
id: meta-librechat
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository_owner }}/librechat
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Build and librechat
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta-librechat.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: node
|
||||
88
.github/workflows/latest-images-main.yml
vendored
88
.github/workflows/latest-images-main.yml
vendored
@@ -1,88 +0,0 @@
|
||||
name: Docker Compose Build Latest Tag (Manual Dispatch)
|
||||
|
||||
# The workflow is manually triggered
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Fetch all tags and set the latest tag
|
||||
- name: Fetch tags and set the latest tag
|
||||
run: |
|
||||
git fetch --tags
|
||||
echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
|
||||
|
||||
# Set up Docker
|
||||
- name: Set up Docker
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Prepare Docker Build
|
||||
- name: Build Docker images
|
||||
run: cp .env.example .env
|
||||
|
||||
# Docker metadata for librechat-api
|
||||
- name: Docker metadata for librechat-api
|
||||
id: meta-librechat-api
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/librechat-api
|
||||
tags: |
|
||||
type=raw,value=${{ env.LATEST_TAG }},enable=true
|
||||
type=raw,value=latest,enable=true
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
# Build and push librechat-api
|
||||
- name: Build and push librechat-api
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: Dockerfile.multi
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta-librechat-api.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: api-build
|
||||
|
||||
# Docker metadata for librechat
|
||||
- name: Docker metadata for librechat
|
||||
id: meta-librechat
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/librechat
|
||||
tags: |
|
||||
type=raw,value=${{ env.LATEST_TAG }},enable=true
|
||||
type=raw,value=latest,enable=true
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
# Build and push librechat
|
||||
- name: Build and push librechat
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta-librechat.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: node
|
||||
56
.github/workflows/main-image-workflow.yml
vendored
56
.github/workflows/main-image-workflow.yml
vendored
@@ -1,12 +1,20 @@
|
||||
name: Docker Compose Build Latest Main Image Tag (Manual Dispatch)
|
||||
|
||||
# The workflow is manually triggered
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: librechat-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: librechat
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -17,12 +25,15 @@ jobs:
|
||||
git fetch --tags
|
||||
echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Docker
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -30,26 +41,29 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Docker metadata for librechat
|
||||
- name: Docker metadata for librechat
|
||||
id: meta-librechat
|
||||
uses: docker/metadata-action@v5
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/librechat
|
||||
tags: |
|
||||
type=raw,value=${{ env.LATEST_TAG }},enable=true
|
||||
type=raw,value=latest,enable=true
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Build and push librechat with only linux/amd64 platform
|
||||
- name: Build and push librechat
|
||||
# Prepare the environment
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: Dockerfile
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: ${{ steps.meta-librechat.outputs.tags }}
|
||||
platforms: linux/amd64
|
||||
target: node
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ env.LATEST_TAG }}
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ env.LATEST_TAG }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
67
.github/workflows/tag-images.yml
vendored
Normal file
67
.github/workflows/tag-images.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Docker Images Build on Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: librechat-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: librechat
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Prepare the environment
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ github.ref_name }}
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ github.ref_name }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: ${{ matrix.target }}
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -50,6 +50,7 @@ bower_components/
|
||||
|
||||
#config file
|
||||
librechat.yaml
|
||||
librechat.yml
|
||||
|
||||
# Environment
|
||||
.npmrc
|
||||
@@ -92,4 +93,7 @@ auth.json
|
||||
!client/src/components/Nav/SettingsTabs/Data/
|
||||
|
||||
# User uploads
|
||||
uploads/
|
||||
uploads/
|
||||
|
||||
# owner
|
||||
release/
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
[ -n "$CI" ] && exit 0
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# v0.7.0
|
||||
|
||||
# Base node image
|
||||
FROM node:18-alpine AS node
|
||||
FROM node:18-alpine3.18 AS node
|
||||
|
||||
RUN apk add g++ make py3-pip
|
||||
RUN npm install -g node-gyp
|
||||
@@ -15,7 +17,9 @@ COPY --chown=node:node . .
|
||||
# Allow mounting of these files, which have no default
|
||||
# values.
|
||||
RUN touch .env
|
||||
RUN npm config set fetch-retry-maxtimeout 300000
|
||||
RUN npm config set fetch-retry-maxtimeout 600000
|
||||
RUN npm config set fetch-retries 5
|
||||
RUN npm config set fetch-retry-mintimeout 15000
|
||||
RUN npm install --no-audit
|
||||
|
||||
# React client build
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# v0.7.0
|
||||
|
||||
# Build API, Client and Data Provider
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
@@ -24,6 +26,8 @@ FROM data-provider-build AS api-build
|
||||
WORKDIR /app/api
|
||||
COPY api/package*.json ./
|
||||
COPY api/ ./
|
||||
# Copy helper scripts
|
||||
COPY config/ ./
|
||||
# Copy data-provider to API's node_modules
|
||||
RUN mkdir -p /app/api/node_modules/librechat-data-provider/
|
||||
RUN cp -R /app/packages/data-provider/* /app/api/node_modules/librechat-data-provider/
|
||||
|
||||
12
README.md
12
README.md
@@ -42,9 +42,11 @@
|
||||
|
||||
- 🖥️ UI matching ChatGPT, including Dark mode, Streaming, and latest updates
|
||||
- 💬 Multimodal Chat:
|
||||
- Upload and analyze images with GPT-4 and Gemini Vision 📸
|
||||
- General file support now available through the Assistants API integration. 🗃️
|
||||
- Local RAG in Active Development 🚧
|
||||
- Upload and analyze images with Claude 3, GPT-4, and Gemini Vision 📸
|
||||
- Chat with Files using Custom Endpoints, OpenAI, Azure, Anthropic, & Google. 🗃️
|
||||
- Advanced Agents with Files, Code Interpreter, Tools, and API Actions 🔦
|
||||
- Available through the [OpenAI Assistants API](https://platform.openai.com/docs/assistants/overview) 🌤️
|
||||
- Non-OpenAI Agents in Active Development 🚧
|
||||
- 🌎 Multilingual UI:
|
||||
- English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro,
|
||||
- Русский, 日本語, Svenska, 한국어, Tiếng Việt, 繁體中文, العربية, Türkçe, Nederlands, עברית
|
||||
@@ -55,7 +57,9 @@
|
||||
- 🔍 Search all messages/conversations
|
||||
- 🔌 Plugins, including web access, image generation with DALL-E-3 and more
|
||||
- 👥 Multi-User, Secure Authentication with Moderation and Token spend tools
|
||||
- ⚙️ Configure Proxy, Reverse Proxy, Docker, many Deployment options, and completely Open-Source
|
||||
- ⚙️ Configure Proxy, Reverse Proxy, Docker, & many Deployment options
|
||||
- 📖 Completely Open-Source & Built in Public
|
||||
- 🧑🤝🧑 Community-driven development, support, and feedback
|
||||
|
||||
[For a thorough review of our features, see our docs here](https://docs.librechat.ai/features/plugins/introduction.html) 📚
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const axios = require('axios');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const footer = `Use the context as your learned knowledge to better answer the user.
|
||||
|
||||
@@ -55,7 +56,7 @@ function createContextHandlers(req, userMessageContent) {
|
||||
processedFiles.push(file);
|
||||
processedIds.add(file.file_id);
|
||||
} catch (error) {
|
||||
console.error(`Error processing file ${file.filename}:`, error);
|
||||
logger.error(`Error processing file ${file.filename}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -144,8 +145,8 @@ function createContextHandlers(req, userMessageContent) {
|
||||
|
||||
return prompt;
|
||||
} catch (error) {
|
||||
console.error('Error creating context:', error);
|
||||
throw error; // Re-throw the error to propagate it to the caller
|
||||
logger.error('Error creating context:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -12,14 +12,15 @@ const { logger } = require('~/config');
|
||||
class DALLE3 extends Tool {
|
||||
constructor(fields = {}) {
|
||||
super();
|
||||
/* Used to initialize the Tool without necessary variables. */
|
||||
/** @type {boolean} Used to initialize the Tool without necessary variables. */
|
||||
this.override = fields.override ?? false;
|
||||
/* Necessary for output to contain all image metadata. */
|
||||
/** @type {boolean} Necessary for output to contain all image metadata. */
|
||||
this.returnMetadata = fields.returnMetadata ?? false;
|
||||
|
||||
this.userId = fields.userId;
|
||||
this.fileStrategy = fields.fileStrategy;
|
||||
if (fields.processFileURL) {
|
||||
/** @type {processFileURL} Necessary for output to contain all image metadata. */
|
||||
this.processFileURL = fields.processFileURL.bind(this);
|
||||
}
|
||||
|
||||
@@ -43,6 +44,7 @@ class DALLE3 extends Tool {
|
||||
config.httpAgent = new HttpsProxyAgent(process.env.PROXY);
|
||||
}
|
||||
|
||||
/** @type {OpenAI} */
|
||||
this.openai = new OpenAI(config);
|
||||
this.name = 'dalle';
|
||||
this.description = `Use DALLE to create images from text descriptions.
|
||||
@@ -164,13 +166,7 @@ Error Message: ${error.message}`;
|
||||
});
|
||||
|
||||
if (this.returnMetadata) {
|
||||
this.result = {
|
||||
file_id: result.file_id,
|
||||
filename: result.filename,
|
||||
filepath: result.filepath,
|
||||
height: result.height,
|
||||
width: result.width,
|
||||
};
|
||||
this.result = result;
|
||||
} else {
|
||||
this.result = this.wrapInMarkdown(result.filepath);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,27 @@ const { z } = require('zod');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const sharp = require('sharp');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { StructuredTool } = require('langchain/tools');
|
||||
const { FileContext } = require('librechat-data-provider');
|
||||
const paths = require('~/config/paths');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
class StableDiffusionAPI extends StructuredTool {
|
||||
constructor(fields) {
|
||||
super();
|
||||
/* Used to initialize the Tool without necessary variables. */
|
||||
/** @type {string} User ID */
|
||||
this.userId = fields.userId;
|
||||
/** @type {Express.Request | undefined} Express Request object, only provided by ToolService */
|
||||
this.req = fields.req;
|
||||
/** @type {boolean} Used to initialize the Tool without necessary variables. */
|
||||
this.override = fields.override ?? false;
|
||||
/** @type {boolean} Necessary for output to contain all image metadata. */
|
||||
this.returnMetadata = fields.returnMetadata ?? false;
|
||||
if (fields.uploadImageBuffer) {
|
||||
/** @type {uploadImageBuffer} Necessary for output to contain all image metadata. */
|
||||
this.uploadImageBuffer = fields.uploadImageBuffer.bind(this);
|
||||
}
|
||||
|
||||
this.name = 'stable-diffusion';
|
||||
this.url = fields.SD_WEBUI_URL || this.getServerURL();
|
||||
@@ -47,7 +60,7 @@ class StableDiffusionAPI extends StructuredTool {
|
||||
|
||||
getMarkdownImageUrl(imageName) {
|
||||
const imageUrl = path
|
||||
.join(this.relativeImageUrl, imageName)
|
||||
.join(this.relativePath, this.userId, imageName)
|
||||
.replace(/\\/g, '/')
|
||||
.replace('public/', '');
|
||||
return ``;
|
||||
@@ -73,46 +86,67 @@ class StableDiffusionAPI extends StructuredTool {
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
};
|
||||
const response = await axios.post(`${url}/sdapi/v1/txt2img`, payload);
|
||||
const image = response.data.images[0];
|
||||
const pngPayload = { image: `data:image/png;base64,${image}` };
|
||||
const response2 = await axios.post(`${url}/sdapi/v1/png-info`, pngPayload);
|
||||
const info = response2.data.info;
|
||||
const generationResponse = await axios.post(`${url}/sdapi/v1/txt2img`, payload);
|
||||
const image = generationResponse.data.images[0];
|
||||
|
||||
// Generate unique name
|
||||
const imageName = `${Date.now()}.png`;
|
||||
this.outputPath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'client',
|
||||
'public',
|
||||
'images',
|
||||
);
|
||||
const appRoot = path.resolve(__dirname, '..', '..', '..', '..', '..', 'client');
|
||||
this.relativeImageUrl = path.relative(appRoot, this.outputPath);
|
||||
/** @type {{ height: number, width: number, seed: number, infotexts: string[] }} */
|
||||
let info = {};
|
||||
try {
|
||||
info = JSON.parse(generationResponse.data.info);
|
||||
} catch (error) {
|
||||
logger.error('[StableDiffusion] Error while getting image metadata:', error);
|
||||
}
|
||||
|
||||
// Check if directory exists, if not create it
|
||||
if (!fs.existsSync(this.outputPath)) {
|
||||
fs.mkdirSync(this.outputPath, { recursive: true });
|
||||
const file_id = uuidv4();
|
||||
const imageName = `${file_id}.png`;
|
||||
const { imageOutput: imageOutputPath, clientPath } = paths;
|
||||
const filepath = path.join(imageOutputPath, this.userId, imageName);
|
||||
this.relativePath = path.relative(clientPath, imageOutputPath);
|
||||
|
||||
if (!fs.existsSync(path.join(imageOutputPath, this.userId))) {
|
||||
fs.mkdirSync(path.join(imageOutputPath, this.userId), { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = Buffer.from(image.split(',', 1)[0], 'base64');
|
||||
if (this.returnMetadata && this.uploadImageBuffer && this.req) {
|
||||
const file = await this.uploadImageBuffer({
|
||||
req: this.req,
|
||||
context: FileContext.image_generation,
|
||||
resize: false,
|
||||
metadata: {
|
||||
buffer,
|
||||
height: info.height,
|
||||
width: info.width,
|
||||
bytes: Buffer.byteLength(buffer),
|
||||
filename: imageName,
|
||||
type: 'image/png',
|
||||
file_id,
|
||||
},
|
||||
});
|
||||
|
||||
const generationInfo = info.infotexts[0].split('\n').pop();
|
||||
return {
|
||||
...file,
|
||||
prompt,
|
||||
metadata: {
|
||||
negative_prompt,
|
||||
seed: info.seed,
|
||||
info: generationInfo,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await sharp(buffer)
|
||||
.withMetadata({
|
||||
iptcpng: {
|
||||
parameters: info,
|
||||
parameters: info.infotexts[0],
|
||||
},
|
||||
})
|
||||
.toFile(this.outputPath + '/' + imageName);
|
||||
.toFile(filepath);
|
||||
this.result = this.getMarkdownImageUrl(imageName);
|
||||
} catch (error) {
|
||||
logger.error('[StableDiffusion] Error while saving the image:', error);
|
||||
// this.result = theImageUrl;
|
||||
}
|
||||
|
||||
return this.result;
|
||||
|
||||
@@ -237,9 +237,11 @@ const loadTools = async ({
|
||||
}
|
||||
|
||||
const imageGenOptions = {
|
||||
req: options.req,
|
||||
fileStrategy: options.fileStrategy,
|
||||
processFileURL: options.processFileURL,
|
||||
returnMetadata: options.returnMetadata,
|
||||
uploadImageBuffer: options.uploadImageBuffer,
|
||||
};
|
||||
|
||||
const toolOptions = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
||||
const { availableTools } = require('../');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Loads a suite of tools with authentication values for a given user, supporting alternate authentication fields.
|
||||
@@ -30,7 +31,7 @@ const loadToolSuite = async ({ pluginKey, tools, user, options = {} }) => {
|
||||
return value;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error fetching plugin auth value for ${field}: ${err.message}`);
|
||||
logger.error(`Error fetching plugin auth value for ${field}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -41,7 +42,7 @@ const loadToolSuite = async ({ pluginKey, tools, user, options = {} }) => {
|
||||
if (authValue !== null) {
|
||||
authValues[auth.authField] = authValue;
|
||||
} else {
|
||||
console.warn(`No auth value found for ${auth.authField}`);
|
||||
logger.warn(`[loadToolSuite] No auth value found for ${auth.authField}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
root: path.resolve(__dirname, '..', '..'),
|
||||
uploads: path.resolve(__dirname, '..', '..', 'uploads'),
|
||||
clientPath: path.resolve(__dirname, '..', '..', 'client'),
|
||||
dist: path.resolve(__dirname, '..', '..', 'client', 'dist'),
|
||||
publicPath: path.resolve(__dirname, '..', '..', 'client', 'public'),
|
||||
imageOutput: path.resolve(__dirname, '..', '..', 'client', 'public', 'images'),
|
||||
|
||||
@@ -5,7 +5,15 @@ const { redactFormat, redactMessage, debugTraverse } = require('./parsers');
|
||||
|
||||
const logDir = path.join(__dirname, '..', 'logs');
|
||||
|
||||
const { NODE_ENV, DEBUG_LOGGING = true, DEBUG_CONSOLE = false } = process.env;
|
||||
const { NODE_ENV, DEBUG_LOGGING = true, DEBUG_CONSOLE = false, CONSOLE_JSON = false } = process.env;
|
||||
|
||||
const useConsoleJson =
|
||||
(typeof CONSOLE_JSON === 'string' && CONSOLE_JSON?.toLowerCase() === 'true') ||
|
||||
CONSOLE_JSON === true;
|
||||
|
||||
const useDebugConsole =
|
||||
(typeof DEBUG_CONSOLE === 'string' && DEBUG_CONSOLE?.toLowerCase() === 'true') ||
|
||||
DEBUG_CONSOLE === true;
|
||||
|
||||
const levels = {
|
||||
error: 0,
|
||||
@@ -33,7 +41,7 @@ const level = () => {
|
||||
|
||||
const fileFormat = winston.format.combine(
|
||||
redactFormat(),
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.timestamp({ format: () => new Date().toISOString() }),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.splat(),
|
||||
// redactErrors(),
|
||||
@@ -99,14 +107,20 @@ const consoleFormat = winston.format.combine(
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
(typeof DEBUG_CONSOLE === 'string' && DEBUG_CONSOLE?.toLowerCase() === 'true') ||
|
||||
DEBUG_CONSOLE === true
|
||||
) {
|
||||
if (useDebugConsole) {
|
||||
transports.push(
|
||||
new winston.transports.Console({
|
||||
level: 'debug',
|
||||
format: winston.format.combine(fileFormat, debugTraverse),
|
||||
format: useConsoleJson
|
||||
? winston.format.combine(fileFormat, debugTraverse, winston.format.json())
|
||||
: winston.format.combine(fileFormat, debugTraverse),
|
||||
}),
|
||||
);
|
||||
} else if (useConsoleJson) {
|
||||
transports.push(
|
||||
new winston.transports.Console({
|
||||
level: 'info',
|
||||
format: winston.format.combine(fileFormat, winston.format.json()),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ const mongoose = require('mongoose');
|
||||
const { isEnabled } = require('../server/utils/handleText');
|
||||
const transactionSchema = require('./schema/transaction');
|
||||
const { getMultiplier } = require('./tx');
|
||||
const { logger } = require('~/config');
|
||||
const Balance = require('./Balance');
|
||||
const cancelRate = 1.15;
|
||||
|
||||
@@ -64,7 +65,7 @@ async function getTransactions(filter) {
|
||||
try {
|
||||
return await Transaction.find(filter).lean();
|
||||
} catch (error) {
|
||||
console.error('Error querying transactions:', error);
|
||||
logger.error('Error querying transactions:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ const mongoose = require('mongoose');
|
||||
* @property {'file'} object - Type of object, always 'file'
|
||||
* @property {string} type - Type of file
|
||||
* @property {number} usage - Number of uses of the file
|
||||
* @property {string} [context] - Context of the file origin
|
||||
* @property {boolean} [embedded] - Whether or not the file is embedded in vector db
|
||||
* @property {string} [model] - The model to identify the group region of the file (for Azure OpenAI hosting)
|
||||
* @property {string} [source] - The source of the file
|
||||
* @property {number} [width] - Optional width of the file
|
||||
* @property {number} [height] - Optional height of the file
|
||||
@@ -82,6 +84,9 @@ const fileSchema = mongoose.Schema(
|
||||
type: String,
|
||||
default: FileSources.local,
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
},
|
||||
width: Number,
|
||||
height: Number,
|
||||
expiresAt: {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
{
|
||||
"name": "@librechat/backend",
|
||||
"version": "0.6.10",
|
||||
"version": "0.7.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "echo 'please run this from the root directory'",
|
||||
"server-dev": "echo 'please run this from the root directory'",
|
||||
"test": "cross-env NODE_ENV=test jest",
|
||||
"b:test": "NODE_ENV=test bun jest",
|
||||
"test:ci": "jest --ci"
|
||||
"test:ci": "jest --ci",
|
||||
"add-balance": "node ./add-balance.js",
|
||||
"list-balances": "node ./list-balances.js",
|
||||
"user-stats": "node ./user-stats.js",
|
||||
"create-user": "node ./create-user.js",
|
||||
"ban-user": "node ./ban-user.js",
|
||||
"delete-user": "node ./delete-user.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -59,7 +65,7 @@
|
||||
"langchain": "^0.0.214",
|
||||
"librechat-data-provider": "*",
|
||||
"lodash": "^4.17.21",
|
||||
"meilisearch": "^0.37.0",
|
||||
"meilisearch": "^0.38.0",
|
||||
"mime": "^3.0.0",
|
||||
"module-alias": "^2.2.3",
|
||||
"mongoose": "^7.1.1",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const axios = require('axios');
|
||||
const denyRequest = require('./denyRequest');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
async function moderateText(req, res, next) {
|
||||
if (process.env.OPENAI_MODERATION === 'true') {
|
||||
@@ -28,7 +29,7 @@ async function moderateText(req, res, next) {
|
||||
return await denyRequest(req, res, errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in moderateText:', error);
|
||||
logger.error('Error in moderateText:', error);
|
||||
const errorMessage = 'error in moderation check';
|
||||
return await denyRequest(req, res, errorMessage);
|
||||
}
|
||||
|
||||
@@ -597,7 +597,7 @@ router.post('/', validateModel, buildEndpointOption, setHeaders, async (req, res
|
||||
|
||||
/** @type {ResponseMessage} */
|
||||
const responseMessage = {
|
||||
...response.finalMessage,
|
||||
...(response.responseMessage ?? response.finalMessage),
|
||||
parentMessageId: userMessageId,
|
||||
conversationId,
|
||||
user: req.user.id,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
const axios = require('axios');
|
||||
const fs = require('fs').promises;
|
||||
const express = require('express');
|
||||
const { isUUID } = require('librechat-data-provider');
|
||||
const { isUUID, FileSources } = require('librechat-data-provider');
|
||||
const {
|
||||
filterFile,
|
||||
processFileUpload,
|
||||
processDeleteRequest,
|
||||
} = require('~/server/services/Files/process');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/assistants');
|
||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
const { getFiles } = require('~/models/File');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
@@ -65,28 +66,64 @@ router.delete('/', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/download/:fileId', async (req, res) => {
|
||||
router.get('/download/:userId/:filepath', async (req, res) => {
|
||||
try {
|
||||
const { fileId } = req.params;
|
||||
const { userId, filepath } = req.params;
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
// TODO: Client initialization for OpenAI API Authentication
|
||||
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
|
||||
},
|
||||
responseType: 'stream',
|
||||
if (userId !== req.user.id) {
|
||||
logger.warn(`${errorPrefix} forbidden: ${file_id}`);
|
||||
return res.status(403).send('Forbidden');
|
||||
}
|
||||
|
||||
const parts = filepath.split('/');
|
||||
const file_id = parts[2];
|
||||
const [file] = await getFiles({ file_id });
|
||||
const errorPrefix = `File download requested by user ${userId}`;
|
||||
|
||||
if (!file) {
|
||||
logger.warn(`${errorPrefix} not found: ${file_id}`);
|
||||
return res.status(404).send('File not found');
|
||||
}
|
||||
|
||||
if (!file.filepath.includes(userId)) {
|
||||
logger.warn(`${errorPrefix} forbidden: ${file_id}`);
|
||||
return res.status(403).send('Forbidden');
|
||||
}
|
||||
|
||||
if (file.source === FileSources.openai && !file.model) {
|
||||
logger.warn(`${errorPrefix} has no associated model: ${file_id}`);
|
||||
return res.status(400).send('The model used when creating this file is not available');
|
||||
}
|
||||
|
||||
const { getDownloadStream } = getStrategyFunctions(file.source);
|
||||
if (!getDownloadStream) {
|
||||
logger.warn(`${errorPrefix} has no stream method implemented: ${file.source}`);
|
||||
return res.status(501).send('Not Implemented');
|
||||
}
|
||||
|
||||
const setHeaders = () => {
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${file.filename}"`);
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
res.setHeader('X-File-Metadata', JSON.stringify(file));
|
||||
};
|
||||
|
||||
const fileResponse = await axios.get(`https://api.openai.com/v1/files/${fileId}`, {
|
||||
headers: options.headers,
|
||||
});
|
||||
const { filename } = fileResponse.data;
|
||||
|
||||
const response = await axios.get(`https://api.openai.com/v1/files/${fileId}/content`, options);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
response.data.pipe(res);
|
||||
/** @type {{ body: import('stream').PassThrough } | undefined} */
|
||||
let passThrough;
|
||||
/** @type {ReadableStream | undefined} */
|
||||
let fileStream;
|
||||
if (file.source === FileSources.openai) {
|
||||
req.body = { model: file.model };
|
||||
const { openai } = await initializeClient({ req, res });
|
||||
passThrough = await getDownloadStream(file_id, openai);
|
||||
setHeaders();
|
||||
passThrough.body.pipe(res);
|
||||
} else {
|
||||
fileStream = getDownloadStream(file_id);
|
||||
setHeaders();
|
||||
fileStream.pipe(res);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
logger.error('Error downloading file:', error);
|
||||
res.status(500).send('Error downloading file');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ const storage = multer.diskStorage({
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
req.file_id = crypto.randomUUID();
|
||||
file.originalname = decodeURIComponent(file.originalname);
|
||||
cb(null, `${file.originalname}`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -156,6 +156,17 @@ const AppService = async (app) => {
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${process.env.RAG_API_URL}/health`);
|
||||
if (response?.ok && response?.status === 200) {
|
||||
logger.info(`RAG API is running and reachable at ${process.env.RAG_API_URL}.`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`RAG API is either not running or not reachable at ${process.env.RAG_API_URL}, you may experience errors with file uploads.`,
|
||||
);
|
||||
}
|
||||
|
||||
app.locals = {
|
||||
socialLogins,
|
||||
availableTools,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const path = require('path');
|
||||
const { klona } = require('klona');
|
||||
const {
|
||||
StepTypes,
|
||||
@@ -233,14 +232,9 @@ function createInProgressHandler(openai, thread_id, messages) {
|
||||
file_id,
|
||||
basename: `${file_id}.png`,
|
||||
});
|
||||
// toolCall.asset_pointer = file.filepath;
|
||||
const prelimImage = {
|
||||
file_id,
|
||||
filename: path.basename(file.filepath),
|
||||
filepath: file.filepath,
|
||||
height: file.height,
|
||||
width: file.width,
|
||||
};
|
||||
|
||||
const prelimImage = file;
|
||||
|
||||
// check if every key has a value before adding to content
|
||||
const prelimImageKeys = Object.keys(prelimImage);
|
||||
const validImageFile = prelimImageKeys.every((key) => prelimImage[key]);
|
||||
|
||||
@@ -46,12 +46,23 @@ async function loadConfigModels(req) {
|
||||
(endpoint.models.fetch || endpoint.models.default),
|
||||
);
|
||||
|
||||
const fetchPromisesMap = {}; // Map for promises keyed by unique combination of baseURL and apiKey
|
||||
const uniqueKeyToNameMap = {}; // Map to associate unique keys with endpoint names
|
||||
/**
|
||||
* @type {Record<string, string[]>}
|
||||
* Map for promises keyed by unique combination of baseURL and apiKey */
|
||||
const fetchPromisesMap = {};
|
||||
/**
|
||||
* @type {Record<string, string[]>}
|
||||
* Map to associate unique keys with endpoint names; note: one key may can correspond to multiple endpoints */
|
||||
const uniqueKeyToEndpointsMap = {};
|
||||
/**
|
||||
* @type {Record<string, Partial<TEndpoint>>}
|
||||
* Map to associate endpoint names to their configurations */
|
||||
const endpointsMap = {};
|
||||
|
||||
for (let i = 0; i < customEndpoints.length; i++) {
|
||||
const endpoint = customEndpoints[i];
|
||||
const { models, name, baseURL, apiKey } = endpoint;
|
||||
endpointsMap[name] = endpoint;
|
||||
|
||||
const API_KEY = extractEnvVariable(apiKey);
|
||||
const BASE_URL = extractEnvVariable(baseURL);
|
||||
@@ -70,8 +81,8 @@ async function loadConfigModels(req) {
|
||||
name,
|
||||
userIdQuery: models.userIdQuery,
|
||||
});
|
||||
uniqueKeyToNameMap[uniqueKey] = uniqueKeyToNameMap[uniqueKey] || [];
|
||||
uniqueKeyToNameMap[uniqueKey].push(name);
|
||||
uniqueKeyToEndpointsMap[uniqueKey] = uniqueKeyToEndpointsMap[uniqueKey] || [];
|
||||
uniqueKeyToEndpointsMap[uniqueKey].push(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -86,10 +97,11 @@ async function loadConfigModels(req) {
|
||||
for (let i = 0; i < fetchedData.length; i++) {
|
||||
const currentKey = uniqueKeys[i];
|
||||
const modelData = fetchedData[i];
|
||||
const associatedNames = uniqueKeyToNameMap[currentKey];
|
||||
const associatedNames = uniqueKeyToEndpointsMap[currentKey];
|
||||
|
||||
for (const name of associatedNames) {
|
||||
modelsConfig[name] = modelData;
|
||||
const endpoint = endpointsMap[name];
|
||||
modelsConfig[name] = !modelData?.length ? endpoint.models.default ?? [] : modelData;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -262,4 +262,68 @@ describe('loadConfigModels', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to default models if fetching returns an empty array', async () => {
|
||||
getCustomConfig.mockResolvedValue({
|
||||
endpoints: {
|
||||
custom: [
|
||||
{
|
||||
name: 'EndpointWithSameFetchKey',
|
||||
apiKey: 'API_KEY',
|
||||
baseURL: 'http://example.com',
|
||||
models: {
|
||||
fetch: true,
|
||||
default: ['defaultModel1'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'EmptyFetchModel',
|
||||
apiKey: 'API_KEY',
|
||||
baseURL: 'http://example.com',
|
||||
models: {
|
||||
fetch: true,
|
||||
default: ['defaultModel1', 'defaultModel2'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
fetchModels.mockResolvedValue([]);
|
||||
|
||||
const result = await loadConfigModels(mockRequest);
|
||||
expect(fetchModels).toHaveBeenCalledTimes(1);
|
||||
expect(result.EmptyFetchModel).toEqual(['defaultModel1', 'defaultModel2']);
|
||||
});
|
||||
|
||||
it('falls back to default models if fetching returns a falsy value', async () => {
|
||||
getCustomConfig.mockResolvedValue({
|
||||
endpoints: {
|
||||
custom: [
|
||||
{
|
||||
name: 'FalsyFetchModel',
|
||||
apiKey: 'API_KEY',
|
||||
baseURL: 'http://example.com',
|
||||
models: {
|
||||
fetch: true,
|
||||
default: ['defaultModel1', 'defaultModel2'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
fetchModels.mockResolvedValue(false);
|
||||
|
||||
const result = await loadConfigModels(mockRequest);
|
||||
|
||||
expect(fetchModels).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'FalsyFetchModel',
|
||||
apiKey: 'API_KEY',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.FalsyFetchModel).toEqual(['defaultModel1', 'defaultModel2']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,9 +2,10 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const fetch = require('node-fetch');
|
||||
const { ref, uploadBytes, getDownloadURL, deleteObject } = require('firebase/storage');
|
||||
const { ref, uploadBytes, getDownloadURL, getStream, deleteObject } = require('firebase/storage');
|
||||
const { getBufferMetadata } = require('~/server/utils');
|
||||
const { getFirebaseStorage } = require('./initialize');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Deletes a file from Firebase Storage.
|
||||
@@ -15,7 +16,7 @@ const { getFirebaseStorage } = require('./initialize');
|
||||
async function deleteFile(basePath, fileName) {
|
||||
const storage = getFirebaseStorage();
|
||||
if (!storage) {
|
||||
console.error('Firebase is not initialized. Cannot delete file from Firebase Storage.');
|
||||
logger.error('Firebase is not initialized. Cannot delete file from Firebase Storage.');
|
||||
throw new Error('Firebase is not initialized');
|
||||
}
|
||||
|
||||
@@ -23,9 +24,9 @@ async function deleteFile(basePath, fileName) {
|
||||
|
||||
try {
|
||||
await deleteObject(storageRef);
|
||||
console.log('File deleted successfully from Firebase Storage');
|
||||
logger.debug('File deleted successfully from Firebase Storage');
|
||||
} catch (error) {
|
||||
console.error('Error deleting file from Firebase Storage:', error.message);
|
||||
logger.error('Error deleting file from Firebase Storage:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -51,7 +52,7 @@ async function deleteFile(basePath, fileName) {
|
||||
async function saveURLToFirebase({ userId, URL, fileName, basePath = 'images' }) {
|
||||
const storage = getFirebaseStorage();
|
||||
if (!storage) {
|
||||
console.error('Firebase is not initialized. Cannot save file to Firebase Storage.');
|
||||
logger.error('Firebase is not initialized. Cannot save file to Firebase Storage.');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ async function saveURLToFirebase({ userId, URL, fileName, basePath = 'images' })
|
||||
await uploadBytes(storageRef, buffer);
|
||||
return await getBufferMetadata(buffer);
|
||||
} catch (error) {
|
||||
console.error('Error uploading file to Firebase Storage:', error.message);
|
||||
logger.error('Error uploading file to Firebase Storage:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -87,7 +88,7 @@ async function saveURLToFirebase({ userId, URL, fileName, basePath = 'images' })
|
||||
async function getFirebaseURL({ fileName, basePath = 'images' }) {
|
||||
const storage = getFirebaseStorage();
|
||||
if (!storage) {
|
||||
console.error('Firebase is not initialized. Cannot get image URL from Firebase Storage.');
|
||||
logger.error('Firebase is not initialized. Cannot get image URL from Firebase Storage.');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -96,7 +97,7 @@ async function getFirebaseURL({ fileName, basePath = 'images' }) {
|
||||
try {
|
||||
return await getDownloadURL(storageRef);
|
||||
} catch (error) {
|
||||
console.error('Error fetching file URL from Firebase Storage:', error.message);
|
||||
logger.error('Error fetching file URL from Firebase Storage:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -212,6 +213,26 @@ async function uploadFileToFirebase({ req, file, file_id }) {
|
||||
return { filepath: downloadURL, bytes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a readable stream for a file from Firebase storage.
|
||||
*
|
||||
* @param {string} filepath - The filepath.
|
||||
* @returns {ReadableStream} A readable stream of the file.
|
||||
*/
|
||||
function getFirebaseFileStream(filepath) {
|
||||
try {
|
||||
const storage = getFirebaseStorage();
|
||||
if (!storage) {
|
||||
throw new Error('Firebase is not initialized');
|
||||
}
|
||||
const fileRef = ref(storage, filepath);
|
||||
return getStream(fileRef);
|
||||
} catch (error) {
|
||||
logger.error('Error getting Firebase file stream:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
deleteFile,
|
||||
getFirebaseURL,
|
||||
@@ -219,4 +240,5 @@ module.exports = {
|
||||
deleteFirebaseFile,
|
||||
uploadFileToFirebase,
|
||||
saveBufferToFirebase,
|
||||
getFirebaseFileStream,
|
||||
};
|
||||
|
||||
@@ -255,6 +255,21 @@ async function uploadLocalFile({ req, file, file_id }) {
|
||||
return { filepath, bytes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a readable stream for a file from local storage.
|
||||
*
|
||||
* @param {string} filepath - The filepath.
|
||||
* @returns {ReadableStream} A readable stream of the file.
|
||||
*/
|
||||
function getLocalFileStream(filepath) {
|
||||
try {
|
||||
return fs.createReadStream(filepath);
|
||||
} catch (error) {
|
||||
logger.error('Error getting local file stream:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
saveLocalFile,
|
||||
saveLocalImage,
|
||||
@@ -263,4 +278,5 @@ module.exports = {
|
||||
getLocalFileURL,
|
||||
deleteLocalFile,
|
||||
uploadLocalFile,
|
||||
getLocalFileStream,
|
||||
};
|
||||
|
||||
@@ -60,4 +60,20 @@ async function deleteOpenAIFile(req, file, openai) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { uploadOpenAIFile, deleteOpenAIFile };
|
||||
/**
|
||||
* Retrieves a readable stream for a file from local storage.
|
||||
*
|
||||
* @param {string} file_id - The file_id.
|
||||
* @param {OpenAI} openai - The initialized OpenAI client.
|
||||
* @returns {Promise<ReadableStream>} A readable stream of the file.
|
||||
*/
|
||||
async function getOpenAIFileStream(file_id, openai) {
|
||||
try {
|
||||
return await openai.files.content(file_id);
|
||||
} catch (error) {
|
||||
logger.error('Error getting OpenAI file download stream:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { uploadOpenAIFile, deleteOpenAIFile, getOpenAIFileStream };
|
||||
|
||||
@@ -3,6 +3,7 @@ const path = require('path');
|
||||
const sharp = require('sharp');
|
||||
const { resizeImageBuffer } = require('./resize');
|
||||
const { getStrategyFunctions } = require('../strategies');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Converts an image file or buffer to WebP format with specified resolution.
|
||||
@@ -61,7 +62,7 @@ async function convertToWebP(req, file, resolution = 'high', basename = '') {
|
||||
const bytes = Buffer.byteLength(outputBuffer);
|
||||
return { filepath: savedFilePath, bytes, width, height };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
logger.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,14 +62,14 @@ async function resizeImageBuffer(inputBuffer, resolution, endpoint) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes an image buffer to webp format as well as reduces 150 px width.
|
||||
* Resizes an image buffer to webp format as well as reduces by specified or default 150 px width.
|
||||
*
|
||||
* @param {Buffer} inputBuffer - The buffer of the image to be resized.
|
||||
* @returns {Promise<{ buffer: Buffer, width: number, height: number, bytes: number }>} An object containing the resized image buffer, its size and dimensions.
|
||||
* @throws Will throw an error if the resolution parameter is invalid.
|
||||
*/
|
||||
async function resizeAndConvert(inputBuffer) {
|
||||
const resizedBuffer = await sharp(inputBuffer).resize({ width: 150 }).toFormat('webp').toBuffer();
|
||||
async function resizeAndConvert(inputBuffer, width = 150) {
|
||||
const resizedBuffer = await sharp(inputBuffer).resize({ width }).toFormat('webp').toBuffer();
|
||||
const resizedMetadata = await sharp(resizedBuffer).metadata();
|
||||
return {
|
||||
buffer: resizedBuffer,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const path = require('path');
|
||||
const mime = require('mime');
|
||||
const { v4 } = require('uuid');
|
||||
const mime = require('mime/lite');
|
||||
const {
|
||||
isUUID,
|
||||
megabyte,
|
||||
@@ -9,17 +9,17 @@ const {
|
||||
imageExtRegex,
|
||||
EModelEndpoint,
|
||||
mergeFileConfig,
|
||||
hostImageIdSuffix,
|
||||
hostImageNamePrefix,
|
||||
} = require('librechat-data-provider');
|
||||
const { convertToWebP, resizeAndConvert } = require('~/server/services/Files/images');
|
||||
const { initializeClient } = require('~/server/services/Endpoints/assistants');
|
||||
const { createFile, updateFileUsage, deleteFiles } = require('~/models/File');
|
||||
const { isEnabled, determineFileType } = require('~/server/utils');
|
||||
const { LB_QueueAsyncCall } = require('~/server/utils/queue');
|
||||
const { getStrategyFunctions } = require('./strategies');
|
||||
const { determineFileType } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const { GPTS_DOWNLOAD_IMAGES = 'true' } = process.env;
|
||||
|
||||
const processFiles = async (files) => {
|
||||
const promises = [];
|
||||
for (let file of files) {
|
||||
@@ -223,26 +223,32 @@ const processImageFile = async ({ req, res, file, metadata }) => {
|
||||
* @param {Object} params - The parameters object.
|
||||
* @param {Express.Request} params.req - The Express request object.
|
||||
* @param {FileContext} params.context - The context of the file (e.g., 'avatar', 'image_generation', etc.)
|
||||
* @param {boolean} [params.resize=true] - Whether to resize and convert the image to WebP. Default is `true`.
|
||||
* @param {{ buffer: Buffer, width: number, height: number, bytes: number, filename: string, type: string, file_id: string }} [params.metadata] - Required metadata for the file if resize is false.
|
||||
* @returns {Promise<{ filepath: string, filename: string, source: string, type: 'image/webp'}>}
|
||||
*/
|
||||
const uploadImageBuffer = async ({ req, context }) => {
|
||||
const uploadImageBuffer = async ({ req, context, metadata = {}, resize = true }) => {
|
||||
const source = req.app.locals.fileStrategy;
|
||||
const { saveBuffer } = getStrategyFunctions(source);
|
||||
const { buffer, width, height, bytes } = await resizeAndConvert(req.file.buffer);
|
||||
const file_id = v4();
|
||||
const fileName = `img-${file_id}.webp`;
|
||||
let { buffer, width, height, bytes, filename, file_id, type } = metadata;
|
||||
if (resize) {
|
||||
file_id = v4();
|
||||
type = 'image/webp';
|
||||
({ buffer, width, height, bytes } = await resizeAndConvert(req.file.buffer));
|
||||
filename = path.basename(req.file.originalname, path.extname(req.file.originalname)) + '.webp';
|
||||
}
|
||||
|
||||
const filepath = await saveBuffer({ userId: req.user.id, fileName, buffer });
|
||||
const filepath = await saveBuffer({ userId: req.user.id, fileName: filename, buffer });
|
||||
return await createFile(
|
||||
{
|
||||
user: req.user.id,
|
||||
file_id,
|
||||
bytes,
|
||||
filepath,
|
||||
filename: req.file.originalname,
|
||||
filename,
|
||||
context,
|
||||
source,
|
||||
type: 'image/webp',
|
||||
type,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
@@ -293,9 +299,10 @@ const processFileUpload = async ({ req, res, file, metadata }) => {
|
||||
file_id: id ?? file_id,
|
||||
temp_file_id,
|
||||
bytes,
|
||||
filepath: isAssistantUpload ? `${openai.baseURL}/files/${id}` : filepath,
|
||||
filename: filename ?? file.originalname,
|
||||
filepath: isAssistantUpload ? `${openai.baseURL}/files/${id}` : filepath,
|
||||
context: isAssistantUpload ? FileContext.assistants : FileContext.message_attachment,
|
||||
model: isAssistantUpload ? req.body.model : undefined,
|
||||
type: file.mimetype,
|
||||
embedded,
|
||||
source,
|
||||
@@ -305,6 +312,96 @@ const processFileUpload = async ({ req, res, file, metadata }) => {
|
||||
res.status(200).json({ message: 'File uploaded and processed successfully', ...result });
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {object} params - The params object.
|
||||
* @param {OpenAI} params.openai - The OpenAI client instance.
|
||||
* @param {string} params.file_id - The ID of the file to retrieve.
|
||||
* @param {string} params.userId - The user ID.
|
||||
* @param {string} [params.filename] - The name of the file. `undefined` for `file_citation` annotations.
|
||||
* @param {boolean} [params.saveFile=false] - Whether to save the file metadata to the database.
|
||||
* @param {boolean} [params.updateUsage=false] - Whether to update file usage in database.
|
||||
*/
|
||||
const processOpenAIFile = async ({
|
||||
openai,
|
||||
file_id,
|
||||
userId,
|
||||
filename,
|
||||
saveFile = false,
|
||||
updateUsage = false,
|
||||
}) => {
|
||||
const _file = await openai.files.retrieve(file_id);
|
||||
const originalName = filename ?? (_file.filename ? path.basename(_file.filename) : undefined);
|
||||
const filepath = `${openai.baseURL}/files/${userId}/${file_id}${
|
||||
originalName ? `/${originalName}` : ''
|
||||
}`;
|
||||
const type = mime.getType(originalName ?? file_id);
|
||||
|
||||
const file = {
|
||||
..._file,
|
||||
type,
|
||||
file_id,
|
||||
filepath,
|
||||
usage: 1,
|
||||
user: userId,
|
||||
context: _file.purpose,
|
||||
source: FileSources.openai,
|
||||
model: openai.req.body.model,
|
||||
filename: originalName ?? file_id,
|
||||
};
|
||||
|
||||
if (saveFile) {
|
||||
await createFile(file, true);
|
||||
} else if (updateUsage) {
|
||||
try {
|
||||
await updateFileUsage({ file_id });
|
||||
} catch (error) {
|
||||
logger.error('Error updating file usage', error);
|
||||
}
|
||||
}
|
||||
|
||||
return file;
|
||||
};
|
||||
|
||||
/**
|
||||
* Process OpenAI image files, convert to webp, save and return file metadata.
|
||||
* @param {object} params - The params object.
|
||||
* @param {Express.Request} params.req - The Express request object.
|
||||
* @param {Buffer} params.buffer - The image buffer.
|
||||
* @param {string} params.file_id - The file ID.
|
||||
* @param {string} params.filename - The filename.
|
||||
* @param {string} params.fileExt - The file extension.
|
||||
* @returns {Promise<MongoFile>} The file metadata.
|
||||
*/
|
||||
const processOpenAIImageOutput = async ({ req, buffer, file_id, filename, fileExt }) => {
|
||||
const currentDate = new Date();
|
||||
const formattedDate = currentDate.toISOString();
|
||||
const _file = await convertToWebP(req, buffer, 'high', `${file_id}${fileExt}`);
|
||||
const file = {
|
||||
..._file,
|
||||
usage: 1,
|
||||
user: req.user.id,
|
||||
type: 'image/webp',
|
||||
createdAt: formattedDate,
|
||||
updatedAt: formattedDate,
|
||||
source: req.app.locals.fileStrategy,
|
||||
context: FileContext.assistants_output,
|
||||
file_id: `${file_id}${hostImageIdSuffix}`,
|
||||
filename: `${hostImageNamePrefix}${filename}`,
|
||||
};
|
||||
createFile(file, true);
|
||||
createFile(
|
||||
{
|
||||
...file,
|
||||
file_id,
|
||||
filename,
|
||||
source: FileSources.openai,
|
||||
type: mime.getType(fileExt),
|
||||
},
|
||||
true,
|
||||
);
|
||||
return file;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves and processes an OpenAI file based on its type.
|
||||
*
|
||||
@@ -312,7 +409,7 @@ const processFileUpload = async ({ req, res, file, metadata }) => {
|
||||
* @param {OpenAIClient} params.openai - The OpenAI client instance.
|
||||
* @param {RunClient} params.client - The LibreChat client instance: either refers to `openai` or `streamRunManager`.
|
||||
* @param {string} params.file_id - The ID of the file to retrieve.
|
||||
* @param {string} params.basename - The basename of the file (if image); e.g., 'image.jpg'.
|
||||
* @param {string} [params.basename] - The basename of the file (if image); e.g., 'image.jpg'. `undefined` for `file_citation` annotations.
|
||||
* @param {boolean} [params.unknownType] - Whether the file type is unknown.
|
||||
* @returns {Promise<{file_id: string, filepath: string, source: string, bytes?: number, width?: number, height?: number} | null>}
|
||||
* - Returns null if `file_id` is not defined; else, the file metadata if successfully retrieved and processed.
|
||||
@@ -328,107 +425,69 @@ async function retrieveAndProcessFile({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (client.attachedFileIds?.has(file_id)) {
|
||||
return {
|
||||
file_id,
|
||||
// filepath: TODO: local source filepath?,
|
||||
source: FileSources.openai,
|
||||
};
|
||||
}
|
||||
|
||||
let basename = _basename;
|
||||
const downloadImages = isEnabled(GPTS_DOWNLOAD_IMAGES);
|
||||
const processArgs = { openai, file_id, filename: basename, userId: client.req.user.id };
|
||||
|
||||
/**
|
||||
* @param {string} file_id - The ID of the file to retrieve.
|
||||
* @param {boolean} [save] - Whether to save the file metadata to the database.
|
||||
*/
|
||||
const retrieveFile = async (file_id, save = false) => {
|
||||
const _file = await openai.files.retrieve(file_id);
|
||||
const filepath = `/api/files/download/${file_id}`;
|
||||
const file = {
|
||||
..._file,
|
||||
type: mime.getType(_file.filename),
|
||||
filepath,
|
||||
usage: 1,
|
||||
file_id,
|
||||
context: _file.purpose ?? FileContext.message_attachment,
|
||||
source: FileSources.openai,
|
||||
};
|
||||
|
||||
if (save) {
|
||||
await createFile(file, true);
|
||||
} else {
|
||||
try {
|
||||
await updateFileUsage({ file_id });
|
||||
} catch (error) {
|
||||
logger.error('Error updating file usage', error);
|
||||
}
|
||||
}
|
||||
|
||||
return file;
|
||||
};
|
||||
|
||||
// If image downloads are not enabled or no basename provided, return only the file metadata
|
||||
if (!downloadImages || (!basename && !downloadImages)) {
|
||||
return await retrieveFile(file_id, true);
|
||||
// If no basename provided, return only the file metadata
|
||||
if (!basename) {
|
||||
return await processOpenAIFile({ ...processArgs, saveFile: true });
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
const fileExt = path.extname(basename);
|
||||
if (client.attachedFileIds?.has(file_id) || client.processedFileIds?.has(file_id)) {
|
||||
return processOpenAIFile({ ...processArgs, updateUsage: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Buffer>} The file data buffer.
|
||||
*/
|
||||
const getDataBuffer = async () => {
|
||||
const response = await openai.files.content(file_id);
|
||||
data = await response.arrayBuffer();
|
||||
} catch (error) {
|
||||
logger.error('Error downloading file from OpenAI:', error);
|
||||
return await retrieveFile(file_id);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return await retrieveFile(file_id);
|
||||
}
|
||||
const dataBuffer = Buffer.from(data);
|
||||
|
||||
/**
|
||||
* @param {Buffer} dataBuffer
|
||||
* @param {string} fileExt
|
||||
*/
|
||||
const processAsImage = async (dataBuffer, fileExt) => {
|
||||
// Logic to process image files, convert to webp, etc.
|
||||
const _file = await convertToWebP(client.req, dataBuffer, 'high', `${file_id}${fileExt}`);
|
||||
const file = {
|
||||
..._file,
|
||||
type: 'image/webp',
|
||||
usage: 1,
|
||||
file_id,
|
||||
source: FileSources.openai,
|
||||
};
|
||||
createFile(file, true);
|
||||
return file;
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return Buffer.from(arrayBuffer);
|
||||
};
|
||||
|
||||
/** @param {Buffer} dataBuffer */
|
||||
const processOtherFileTypes = async (dataBuffer) => {
|
||||
// Logic to handle other file types
|
||||
logger.debug('[retrieveAndProcessFile] Non-image file type detected');
|
||||
return { filepath: `/api/files/download/${file_id}`, bytes: dataBuffer.length };
|
||||
};
|
||||
let dataBuffer;
|
||||
if (unknownType || !fileExt || imageExtRegex.test(basename)) {
|
||||
try {
|
||||
dataBuffer = await getDataBuffer();
|
||||
} catch (error) {
|
||||
logger.error('Error downloading file from OpenAI:', error);
|
||||
dataBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dataBuffer) {
|
||||
return await processOpenAIFile({ ...processArgs, saveFile: true });
|
||||
}
|
||||
|
||||
// If the filetype is unknown, inspect the file
|
||||
if (unknownType || !path.extname(basename)) {
|
||||
if (dataBuffer && (unknownType || !fileExt)) {
|
||||
const detectedExt = await determineFileType(dataBuffer);
|
||||
if (detectedExt && imageExtRegex.test('.' + detectedExt)) {
|
||||
return await processAsImage(dataBuffer, detectedExt);
|
||||
} else {
|
||||
return await processOtherFileTypes(dataBuffer);
|
||||
}
|
||||
}
|
||||
const isImageOutput = detectedExt && imageExtRegex.test('.' + detectedExt);
|
||||
|
||||
// Existing logic for processing known image types
|
||||
if (downloadImages && basename && path.extname(basename) && imageExtRegex.test(basename)) {
|
||||
return await processAsImage(dataBuffer, path.extname(basename));
|
||||
if (!isImageOutput) {
|
||||
return await processOpenAIFile({ ...processArgs, saveFile: true });
|
||||
}
|
||||
|
||||
return await processOpenAIImageOutput({
|
||||
file_id,
|
||||
req: client.req,
|
||||
buffer: dataBuffer,
|
||||
filename: basename,
|
||||
fileExt: detectedExt,
|
||||
});
|
||||
} else if (dataBuffer && imageExtRegex.test(basename)) {
|
||||
return await processOpenAIImageOutput({
|
||||
file_id,
|
||||
req: client.req,
|
||||
buffer: dataBuffer,
|
||||
filename: basename,
|
||||
fileExt,
|
||||
});
|
||||
} else {
|
||||
logger.debug('[retrieveAndProcessFile] Not an image or invalid extension: ', basename);
|
||||
return await processOtherFileTypes(dataBuffer);
|
||||
logger.debug(`[retrieveAndProcessFile] Non-image file type detected: ${basename}`);
|
||||
return await processOpenAIFile({ ...processArgs, saveFile: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const {
|
||||
saveBufferToFirebase,
|
||||
uploadImageToFirebase,
|
||||
processFirebaseAvatar,
|
||||
getFirebaseFileStream,
|
||||
} = require('./Firebase');
|
||||
const {
|
||||
getLocalFileURL,
|
||||
@@ -16,8 +17,9 @@ const {
|
||||
uploadLocalImage,
|
||||
prepareImagesLocal,
|
||||
processLocalAvatar,
|
||||
getLocalFileStream,
|
||||
} = require('./Local');
|
||||
const { uploadOpenAIFile, deleteOpenAIFile } = require('./OpenAI');
|
||||
const { uploadOpenAIFile, deleteOpenAIFile, getOpenAIFileStream } = require('./OpenAI');
|
||||
const { uploadVectors, deleteVectors } = require('./VectorDB');
|
||||
|
||||
/**
|
||||
@@ -35,6 +37,7 @@ const firebaseStrategy = () => ({
|
||||
prepareImagePayload: prepareImageURL,
|
||||
processAvatar: processFirebaseAvatar,
|
||||
handleImageUpload: uploadImageToFirebase,
|
||||
getDownloadStream: getFirebaseFileStream,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -51,6 +54,7 @@ const localStrategy = () => ({
|
||||
processAvatar: processLocalAvatar,
|
||||
handleImageUpload: uploadLocalImage,
|
||||
prepareImagePayload: prepareImagesLocal,
|
||||
getDownloadStream: getLocalFileStream,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -70,6 +74,8 @@ const vectorStrategy = () => ({
|
||||
handleImageUpload: null,
|
||||
/** @type {typeof prepareImagesLocal | null} */
|
||||
prepareImagePayload: null,
|
||||
/** @type {typeof getLocalFileStream | null} */
|
||||
getDownloadStream: null,
|
||||
handleFileUpload: uploadVectors,
|
||||
deleteFile: deleteVectors,
|
||||
});
|
||||
@@ -94,6 +100,7 @@ const openAIStrategy = () => ({
|
||||
prepareImagePayload: null,
|
||||
deleteFile: deleteOpenAIFile,
|
||||
handleFileUpload: uploadOpenAIFile,
|
||||
getDownloadStream: getOpenAIFileStream,
|
||||
});
|
||||
|
||||
// Strategy Selector
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
const axios = require('axios');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { EModelEndpoint, defaultModels, CacheKeys } = require('librechat-data-provider');
|
||||
const { extractBaseURL, inputSchema, processModelData } = require('~/utils');
|
||||
const { extractBaseURL, inputSchema, processModelData, logAxiosError } = require('~/utils');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
// const { getAzureCredentials, genAzureChatCompletion } = require('~/utils/');
|
||||
|
||||
const { openAIApiKey, userProvidedOpenAI } = require('./Config/EndpointService').config;
|
||||
|
||||
@@ -77,29 +74,7 @@ const fetchModels = async ({
|
||||
models = input.data.map((item) => item.id);
|
||||
} catch (error) {
|
||||
const logMessage = `Failed to fetch models from ${azure ? 'Azure ' : ''}${name} API`;
|
||||
if (error.response) {
|
||||
logger.error(
|
||||
`${logMessage} The request was made and the server responded with a status code that falls out of the range of 2xx: ${
|
||||
error.message ? error.message : ''
|
||||
}`,
|
||||
{
|
||||
headers: error.response.headers,
|
||||
status: error.response.status,
|
||||
data: error.response.data,
|
||||
},
|
||||
);
|
||||
} else if (error.request) {
|
||||
logger.error(
|
||||
`${logMessage} The request was made but no response was received: ${
|
||||
error.message ? error.message : ''
|
||||
}`,
|
||||
{
|
||||
request: error.request,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
logger.error(`${logMessage} Something happened in setting up the request`, error);
|
||||
}
|
||||
logAxiosError({ message: logMessage, error });
|
||||
}
|
||||
|
||||
return models;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const path = require('path');
|
||||
const {
|
||||
StepTypes,
|
||||
ContentTypes,
|
||||
@@ -102,18 +101,20 @@ class StreamRunManager {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async addContentData(data) {
|
||||
const { type, index } = data;
|
||||
this.finalMessage.content[index] = { type, [type]: data[type] };
|
||||
const { type, index, edited } = data;
|
||||
/** @type {ContentPart} */
|
||||
const contentPart = data[type];
|
||||
this.finalMessage.content[index] = { type, [type]: contentPart };
|
||||
|
||||
if (type === ContentTypes.TEXT) {
|
||||
this.text += data[type].value;
|
||||
if (type === ContentTypes.TEXT && !edited) {
|
||||
this.text += contentPart.value;
|
||||
return;
|
||||
}
|
||||
|
||||
const contentData = {
|
||||
index,
|
||||
type,
|
||||
[type]: data[type],
|
||||
[type]: contentPart,
|
||||
thread_id: this.thread_id,
|
||||
messageId: this.finalMessage.messageId,
|
||||
conversationId: this.finalMessage.conversationId,
|
||||
@@ -220,14 +221,9 @@ class StreamRunManager {
|
||||
file_id,
|
||||
basename: `${file_id}.png`,
|
||||
});
|
||||
// toolCall.asset_pointer = file.filepath;
|
||||
const prelimImage = {
|
||||
file_id,
|
||||
filename: path.basename(file.filepath),
|
||||
filepath: file.filepath,
|
||||
height: file.height,
|
||||
width: file.width,
|
||||
};
|
||||
|
||||
const prelimImage = file;
|
||||
|
||||
// check if every key has a value before adding to content
|
||||
const prelimImageKeys = Object.keys(prelimImage);
|
||||
const validImageFile = prelimImageKeys.every((key) => prelimImage[key]);
|
||||
@@ -593,7 +589,7 @@ class StreamRunManager {
|
||||
*/
|
||||
async handleMessageEvent(event) {
|
||||
if (event.event === AssistantStreamEvents.ThreadMessageCompleted) {
|
||||
this.messageCompleted(event);
|
||||
await this.messageCompleted(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,6 +609,7 @@ class StreamRunManager {
|
||||
this.addContentData({
|
||||
[ContentTypes.TEXT]: { value: result.text },
|
||||
type: ContentTypes.TEXT,
|
||||
edited: result.edited,
|
||||
index,
|
||||
});
|
||||
this.messages.push(message);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const axios = require('axios');
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const { logger } = require('~/config');
|
||||
const { logAxiosError } = require('~/utils');
|
||||
|
||||
/**
|
||||
* @typedef {Object} RetrieveOptions
|
||||
@@ -54,33 +54,8 @@ async function retrieveRun({ thread_id, run_id, timeout, openai }) {
|
||||
const response = await axios.get(url, axiosConfig);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const logMessage = '[retrieveRun] Failed to retrieve run data:';
|
||||
const timedOutMessage = 'Cannot read properties of undefined (reading \'status\')';
|
||||
if (error?.response && error?.response?.status) {
|
||||
logger.error(
|
||||
`${logMessage} The request was made and the server responded with a status code that falls out of the range of 2xx: ${
|
||||
error.message ? error.message : ''
|
||||
}`,
|
||||
{
|
||||
headers: error.response.headers,
|
||||
status: error.response.status,
|
||||
data: error.response.data,
|
||||
},
|
||||
);
|
||||
} else if (error.request) {
|
||||
logger.error(
|
||||
`${logMessage} The request was made but no response was received: ${
|
||||
error.message ? error.message : ''
|
||||
}`,
|
||||
{
|
||||
request: error.request,
|
||||
},
|
||||
);
|
||||
} else if (error?.message && !error?.message?.includes(timedOutMessage)) {
|
||||
logger.error(`${logMessage} Something happened in setting up the request`, {
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
const message = '[retrieveRun] Failed to retrieve run data:';
|
||||
logAxiosError({ message, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ const path = require('path');
|
||||
const { v4 } = require('uuid');
|
||||
const {
|
||||
Constants,
|
||||
FilePurpose,
|
||||
ContentTypes,
|
||||
imageExtRegex,
|
||||
EModelEndpoint,
|
||||
AnnotationTypes,
|
||||
defaultOrderQuery,
|
||||
} = require('librechat-data-provider');
|
||||
const { retrieveAndProcessFile } = require('~/server/services/Files/process');
|
||||
@@ -434,13 +433,15 @@ async function checkMessageGaps({ openai, latestMessageId, thread_id, run_id, co
|
||||
}
|
||||
|
||||
let addedCurrentMessage = false;
|
||||
const apiMessages = response.data.map((msg) => {
|
||||
if (msg.id === currentMessage.id) {
|
||||
addedCurrentMessage = true;
|
||||
return currentMessage;
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
const apiMessages = response.data
|
||||
.map((msg) => {
|
||||
if (msg.id === currentMessage.id) {
|
||||
addedCurrentMessage = true;
|
||||
return currentMessage;
|
||||
}
|
||||
return msg;
|
||||
})
|
||||
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||
|
||||
if (!addedCurrentMessage) {
|
||||
apiMessages.push(currentMessage);
|
||||
@@ -496,6 +497,44 @@ const recordUsage = async ({
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Safely replaces the annotated text within the specified range denoted by start_index and end_index,
|
||||
* after verifying that the text within that range matches the given annotation text.
|
||||
* Proceeds with the replacement even if a mismatch is found, but logs a warning.
|
||||
*
|
||||
* @param {string} originalText The original text content.
|
||||
* @param {number} start_index The starting index where replacement should begin.
|
||||
* @param {number} end_index The ending index where replacement should end.
|
||||
* @param {string} expectedText The text expected to be found in the specified range.
|
||||
* @param {string} replacementText The text to insert in place of the existing content.
|
||||
* @returns {string} The text with the replacement applied, regardless of text match.
|
||||
*/
|
||||
function replaceAnnotation(originalText, start_index, end_index, expectedText, replacementText) {
|
||||
if (start_index < 0 || end_index > originalText.length || start_index > end_index) {
|
||||
logger.warn(`Invalid range specified for annotation replacement.
|
||||
Attempting replacement with \`replace\` method instead...
|
||||
length: ${originalText.length}
|
||||
start_index: ${start_index}
|
||||
end_index: ${end_index}`);
|
||||
return originalText.replace(originalText, replacementText);
|
||||
}
|
||||
|
||||
const actualTextInRange = originalText.substring(start_index, end_index);
|
||||
|
||||
if (actualTextInRange !== expectedText) {
|
||||
logger.warn(`The text within the specified range does not match the expected annotation text.
|
||||
Attempting replacement with \`replace\` method instead...
|
||||
Expected: ${expectedText}
|
||||
Actual: ${actualTextInRange}`);
|
||||
|
||||
return originalText.replace(originalText, replacementText);
|
||||
}
|
||||
|
||||
const beforeText = originalText.substring(0, start_index);
|
||||
const afterText = originalText.substring(end_index);
|
||||
return beforeText + replacementText + afterText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts, processes, and flattens messages to a single string.
|
||||
*
|
||||
@@ -509,89 +548,101 @@ async function processMessages({ openai, client, messages = [] }) {
|
||||
const sorted = messages.sort((a, b) => a.created_at - b.created_at);
|
||||
|
||||
let text = '';
|
||||
let edited = false;
|
||||
const sources = [];
|
||||
for (const message of sorted) {
|
||||
message.files = [];
|
||||
for (const content of message.content) {
|
||||
const processImageFile =
|
||||
content.type === 'image_file' && !client.processedFileIds.has(content.image_file?.file_id);
|
||||
if (processImageFile) {
|
||||
const { file_id } = content.image_file;
|
||||
const type = content.type;
|
||||
const contentType = content[type];
|
||||
const currentFileId = contentType?.file_id;
|
||||
|
||||
if (type === ContentTypes.IMAGE_FILE && !client.processedFileIds.has(currentFileId)) {
|
||||
const file = await retrieveAndProcessFile({
|
||||
openai,
|
||||
client,
|
||||
file_id,
|
||||
basename: `${file_id}.png`,
|
||||
file_id: currentFileId,
|
||||
basename: `${currentFileId}.png`,
|
||||
});
|
||||
client.processedFileIds.add(file_id);
|
||||
|
||||
client.processedFileIds.add(currentFileId);
|
||||
message.files.push(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
text += (content.text?.value ?? '') + ' ';
|
||||
logger.debug('[processMessages] Processing message:', { value: text });
|
||||
let currentText = contentType?.value ?? '';
|
||||
|
||||
/** @type {{ annotations: Annotation[] }} */
|
||||
const { annotations } = contentType ?? {};
|
||||
|
||||
// Process annotations if they exist
|
||||
if (!content.text?.annotations?.length) {
|
||||
if (!annotations?.length) {
|
||||
text += currentText + ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug('[processMessages] Processing annotations:', content.text.annotations);
|
||||
for (const annotation of content.text.annotations) {
|
||||
logger.debug('Current annotation:', annotation);
|
||||
logger.debug('[processMessages] Processing annotations:', annotations);
|
||||
for (const annotation of annotations) {
|
||||
let file;
|
||||
const processFilePath =
|
||||
annotation.file_path && !client.processedFileIds.has(annotation.file_path?.file_id);
|
||||
const type = annotation.type;
|
||||
const annotationType = annotation[type];
|
||||
const file_id = annotationType?.file_id;
|
||||
const alreadyProcessed = client.processedFileIds.has(file_id);
|
||||
|
||||
if (processFilePath) {
|
||||
const basename = imageExtRegex.test(annotation.text)
|
||||
? path.basename(annotation.text)
|
||||
: null;
|
||||
const replaceCurrentAnnotation = (replacement = '') => {
|
||||
currentText = replaceAnnotation(
|
||||
currentText,
|
||||
annotation.start_index,
|
||||
annotation.end_index,
|
||||
annotation.text,
|
||||
replacement,
|
||||
);
|
||||
edited = true;
|
||||
};
|
||||
|
||||
if (alreadyProcessed) {
|
||||
const { file_id } = annotationType || {};
|
||||
file = await retrieveAndProcessFile({ openai, client, file_id, unknownType: true });
|
||||
} else if (type === AnnotationTypes.FILE_PATH) {
|
||||
const basename = path.basename(annotation.text);
|
||||
file = await retrieveAndProcessFile({
|
||||
openai,
|
||||
client,
|
||||
file_id: annotation.file_path.file_id,
|
||||
file_id,
|
||||
basename,
|
||||
});
|
||||
client.processedFileIds.add(annotation.file_path.file_id);
|
||||
}
|
||||
|
||||
const processFileCitation =
|
||||
annotation.file_citation &&
|
||||
!client.processedFileIds.has(annotation.file_citation?.file_id);
|
||||
|
||||
if (processFileCitation) {
|
||||
replaceCurrentAnnotation(file.filepath);
|
||||
} else if (type === AnnotationTypes.FILE_CITATION) {
|
||||
file = await retrieveAndProcessFile({
|
||||
openai,
|
||||
client,
|
||||
file_id: annotation.file_citation.file_id,
|
||||
file_id,
|
||||
unknownType: true,
|
||||
});
|
||||
client.processedFileIds.add(annotation.file_citation.file_id);
|
||||
sources.push(file.filename);
|
||||
replaceCurrentAnnotation(`^${sources.length}^`);
|
||||
}
|
||||
|
||||
if (!file && (annotation.file_path || annotation.file_citation)) {
|
||||
const { file_id } = annotation.file_citation || annotation.file_path || {};
|
||||
file = await retrieveAndProcessFile({ openai, client, file_id, unknownType: true });
|
||||
client.processedFileIds.add(file_id);
|
||||
}
|
||||
text += currentText + ' ';
|
||||
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.purpose && file.purpose === FilePurpose.Assistants) {
|
||||
text = text.replace(annotation.text, file.filename);
|
||||
} else if (file.filepath) {
|
||||
text = text.replace(annotation.text, file.filepath);
|
||||
}
|
||||
|
||||
client.processedFileIds.add(file_id);
|
||||
message.files.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { messages: sorted, text };
|
||||
if (sources.length) {
|
||||
text += '\n\n';
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
text += `^${i + 1}.^ ${sources[i]}${i === sources.length - 1 ? '' : '\n'}`;
|
||||
}
|
||||
}
|
||||
|
||||
return { messages: sorted, text, edited };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -12,8 +12,8 @@ const {
|
||||
openapiToFunction,
|
||||
validateAndParseOpenAPISpec,
|
||||
} = require('librechat-data-provider');
|
||||
const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process');
|
||||
const { loadActionSets, createActionTool, domainParser } = require('./ActionService');
|
||||
const { processFileURL } = require('~/server/services/Files/process');
|
||||
const { recordUsage } = require('~/server/services/Threads');
|
||||
const { loadTools } = require('~/app/clients/tools/util');
|
||||
const { redactMessage } = require('~/config/parsers');
|
||||
@@ -147,7 +147,7 @@ const processVisionRequest = async (client, currentAction) => {
|
||||
|
||||
/**
|
||||
* Processes return required actions from run.
|
||||
* @param {OpenAIClient} client - OpenAI or StreamRunManager Client.
|
||||
* @param {OpenAIClient | StreamRunManager} client - OpenAI (legacy) or StreamRunManager Client.
|
||||
* @param {RequiredAction[]} requiredActions - The required actions to submit outputs for.
|
||||
* @returns {Promise<ToolOutputs>} The outputs of the tools.
|
||||
*/
|
||||
@@ -164,6 +164,8 @@ async function processRequiredActions(client, requiredActions) {
|
||||
functions: true,
|
||||
options: {
|
||||
processFileURL,
|
||||
req: client.req,
|
||||
uploadImageBuffer,
|
||||
openAIApiKey: client.apiKey,
|
||||
fileStrategy: client.req.app.locals.fileStrategy,
|
||||
returnMetadata: true,
|
||||
|
||||
@@ -277,6 +277,12 @@
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports TEndpoint
|
||||
* @typedef {import('librechat-data-provider').TEndpoint} TEndpoint
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports TEndpointsConfig
|
||||
* @typedef {import('librechat-data-provider').TEndpointsConfig} TEndpointsConfig
|
||||
@@ -380,6 +386,18 @@
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports uploadImageBuffer
|
||||
* @typedef {import('~/server/services/Files/process').uploadImageBuffer} uploadImageBuffer
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports processFileURL
|
||||
* @typedef {import('~/server/services/Files/process').processFileURL} processFileURL
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports AssistantCreateParams
|
||||
* @typedef {import('librechat-data-provider').AssistantCreateParams} AssistantCreateParams
|
||||
@@ -436,7 +454,13 @@
|
||||
|
||||
/**
|
||||
* @exports ThreadMessage
|
||||
* @typedef {import('openai').OpenAI.Beta.Threads.ThreadMessage} ThreadMessage
|
||||
* @typedef {import('openai').OpenAI.Beta.Threads.Message} ThreadMessage
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports Annotation
|
||||
* @typedef {import('openai').OpenAI.Beta.Threads.Messages.Annotation} Annotation
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
const loadYaml = require('./loadYaml');
|
||||
const tokenHelpers = require('./tokens');
|
||||
const azureUtils = require('./azureUtils');
|
||||
const logAxiosError = require('./logAxiosError');
|
||||
const extractBaseURL = require('./extractBaseURL');
|
||||
const findMessageContent = require('./findMessageContent');
|
||||
|
||||
module.exports = {
|
||||
...azureUtils,
|
||||
loadYaml,
|
||||
...tokenHelpers,
|
||||
...azureUtils,
|
||||
logAxiosError,
|
||||
extractBaseURL,
|
||||
findMessageContent,
|
||||
loadYaml,
|
||||
};
|
||||
|
||||
45
api/utils/logAxiosError.js
Normal file
45
api/utils/logAxiosError.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Logs Axios errors based on the error object and a custom message.
|
||||
*
|
||||
* @param {Object} options - The options object.
|
||||
* @param {string} options.message - The custom message to be logged.
|
||||
* @param {Error} options.error - The Axios error object.
|
||||
*/
|
||||
const logAxiosError = ({ message, error }) => {
|
||||
const timedOutMessage = 'Cannot read properties of undefined (reading \'status\')';
|
||||
if (error.response) {
|
||||
logger.error(
|
||||
`${message} The request was made and the server responded with a status code that falls out of the range of 2xx: ${
|
||||
error.message ? error.message : ''
|
||||
}. Error response data:\n`,
|
||||
{
|
||||
headers: error.response?.headers,
|
||||
status: error.response?.status,
|
||||
data: error.response?.data,
|
||||
},
|
||||
);
|
||||
} else if (error.request) {
|
||||
logger.error(
|
||||
`${message} The request was made but no response was received: ${
|
||||
error.message ? error.message : ''
|
||||
}. Error Request:\n`,
|
||||
{
|
||||
request: error.request,
|
||||
},
|
||||
);
|
||||
} else if (error?.message?.includes(timedOutMessage)) {
|
||||
logger.error(
|
||||
`${message}\nThe request either timed out or was unsuccessful. Error message:\n`,
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
`${message}\nSomething happened in setting up the request. Error message:\n`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = logAxiosError;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/frontend",
|
||||
"version": "0.6.10",
|
||||
"version": "0.7.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -18,6 +18,8 @@ import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
|
||||
|
||||
export type LastSelectedModels = Record<EModelEndpoint, string>;
|
||||
|
||||
export type NavLink = {
|
||||
title: string;
|
||||
label?: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
@@ -38,7 +39,7 @@ export default function Footer() {
|
||||
) : (
|
||||
<>
|
||||
<a href="https://librechat.ai" target="_blank" rel="noreferrer" className="underline">
|
||||
{config?.appTitle || 'LibreChat'} v0.6.10
|
||||
{config?.appTitle || 'LibreChat'} {Constants.VERSION}
|
||||
</a>
|
||||
{' - '} {localize('com_ui_new_footer')}
|
||||
</>
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useForm } from 'react-hook-form';
|
||||
import { memo, useCallback, useRef, useMemo } from 'react';
|
||||
import {
|
||||
supportsFiles,
|
||||
EModelEndpoint,
|
||||
mergeFileConfig,
|
||||
fileConfig as defaultFileConfig,
|
||||
EModelEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
||||
import { useRequiresKey, useTextarea } from '~/hooks';
|
||||
@@ -43,9 +43,9 @@ const ChatForm = ({ index = 0 }) => {
|
||||
setFiles,
|
||||
conversation,
|
||||
isSubmitting,
|
||||
handleStopGenerating,
|
||||
filesLoading,
|
||||
setFilesLoading,
|
||||
handleStopGenerating,
|
||||
} = useChatContext();
|
||||
|
||||
const assistantMap = useAssistantsMapContext();
|
||||
@@ -57,7 +57,9 @@ const ChatForm = ({ index = 0 }) => {
|
||||
}
|
||||
ask({ text: data.text });
|
||||
methods.reset();
|
||||
textAreaRef.current?.setRangeText('', 0, data.text.length, 'end');
|
||||
if (textAreaRef.current) {
|
||||
textAreaRef.current.value = '';
|
||||
}
|
||||
},
|
||||
[ask, methods],
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import remarkMath from 'remark-math';
|
||||
@@ -9,9 +9,11 @@ import ReactMarkdown from 'react-markdown';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import type { PluggableList } from 'unified';
|
||||
import CodeBlock from '~/components/Messages/Content/CodeBlock';
|
||||
import { cn, langSubset, validateIframe, processLaTeX } from '~/utils';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import CodeBlock from '~/components/Messages/Content/CodeBlock';
|
||||
import { useChatContext, useToastContext } from '~/Providers';
|
||||
import { useFileDownload } from '~/data-provider';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import store from '~/store';
|
||||
|
||||
type TCodeProps = {
|
||||
@@ -37,6 +39,70 @@ export const code = memo(({ inline, className, children }: TCodeProps) => {
|
||||
}
|
||||
});
|
||||
|
||||
export const a = memo(({ href, children }: { href: string; children: React.ReactNode }) => {
|
||||
const user = useRecoilValue(store.user);
|
||||
const { showToast } = useToastContext();
|
||||
const localize = useLocalize();
|
||||
|
||||
const { filepath, filename } = useMemo(() => {
|
||||
const pattern = new RegExp(`(?:files|outputs)/${user?.id}/([^\\s]+)`);
|
||||
const match = href.match(pattern);
|
||||
if (match && match[0]) {
|
||||
const path = match[0];
|
||||
const name = path.split('/').pop();
|
||||
return { filepath: path, filename: name };
|
||||
}
|
||||
return { filepath: '', filename: '' };
|
||||
}, [user?.id, href]);
|
||||
|
||||
const { refetch: downloadFile } = useFileDownload(user?.id ?? '', filepath);
|
||||
const props: { target?: string; onClick?: React.MouseEventHandler } = { target: '_new' };
|
||||
|
||||
if (!filepath || !filename) {
|
||||
return (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const handleDownload = async (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const stream = await downloadFile();
|
||||
if (!stream.data) {
|
||||
console.error('Error downloading file: No data found');
|
||||
showToast({
|
||||
status: 'error',
|
||||
message: localize('com_ui_download_error'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const link = document.createElement('a');
|
||||
link.href = stream.data;
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(stream.data);
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
props.onClick = handleDownload;
|
||||
props.target = '_blank';
|
||||
|
||||
return (
|
||||
<a
|
||||
href={filepath.startsWith('files/') ? `/api/${filepath}` : `/api/files/${filepath}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
export const p = memo(({ children }: { children: React.ReactNode }) => {
|
||||
return <p className="mb-2 whitespace-pre-wrap">{children}</p>;
|
||||
});
|
||||
@@ -98,6 +164,7 @@ const Markdown = memo(({ content, message, showCursor }: TContentProps) => {
|
||||
components={
|
||||
{
|
||||
code,
|
||||
a,
|
||||
p,
|
||||
} as {
|
||||
[nodeType: string]: React.ElementType;
|
||||
|
||||
@@ -7,7 +7,7 @@ import ReactMarkdown from 'react-markdown';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import type { PluggableList } from 'unified';
|
||||
import { langSubset } from '~/utils';
|
||||
import { code, p } from './Markdown';
|
||||
import { code, a, p } from './Markdown';
|
||||
|
||||
const MarkdownLite = memo(({ content = '' }: { content?: string }) => {
|
||||
const rehypePlugins: PluggableList = [
|
||||
@@ -30,6 +30,7 @@ const MarkdownLite = memo(({ content = '' }: { content?: string }) => {
|
||||
components={
|
||||
{
|
||||
code,
|
||||
a,
|
||||
p,
|
||||
} as {
|
||||
[nodeType: string]: React.ElementType;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
@@ -12,7 +13,7 @@ export default function Footer() {
|
||||
) : (
|
||||
<>
|
||||
<a href="https://librechat.ai" target="_blank" rel="noreferrer" className="underline">
|
||||
{config?.appTitle || 'LibreChat'} v0.6.10
|
||||
{config?.appTitle || 'LibreChat'} {Constants.VERSION}
|
||||
</a>
|
||||
{' - '}. {localize('com_ui_pay_per_call')}
|
||||
</>
|
||||
|
||||
@@ -317,19 +317,28 @@ export default function AssistantPanel({
|
||||
<Controller
|
||||
name="model"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SelectDropDown
|
||||
emptyTitle={true}
|
||||
value={field.value}
|
||||
setValue={field.onChange}
|
||||
availableValues={modelsQuery.data?.[EModelEndpoint.assistants] ?? []}
|
||||
showAbove={false}
|
||||
showLabel={false}
|
||||
className={cn(
|
||||
cardStyle,
|
||||
'flex h-[40px] w-full flex-none items-center justify-center px-4 hover:cursor-pointer',
|
||||
rules={{ required: true, minLength: 1 }}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<>
|
||||
<SelectDropDown
|
||||
emptyTitle={true}
|
||||
value={field.value}
|
||||
setValue={field.onChange}
|
||||
availableValues={modelsQuery.data?.[EModelEndpoint.assistants] ?? []}
|
||||
showAbove={false}
|
||||
showLabel={false}
|
||||
className={cn(
|
||||
cardStyle,
|
||||
'flex h-[40px] w-full flex-none items-center justify-center px-4 hover:cursor-pointer',
|
||||
)}
|
||||
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')}
|
||||
/>
|
||||
{error && (
|
||||
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
|
||||
{localize('com_ui_field_required')}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,17 +4,24 @@ import {
|
||||
defaultAssistantFormValues,
|
||||
defaultOrderQuery,
|
||||
isImageVisionTool,
|
||||
EModelEndpoint,
|
||||
Capabilities,
|
||||
FileSources,
|
||||
} from 'librechat-data-provider';
|
||||
import type { UseFormReset } from 'react-hook-form';
|
||||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import type { Assistant, AssistantCreateParams } from 'librechat-data-provider';
|
||||
import type { AssistantForm, Actions, TAssistantOption, ExtendedFile } from '~/common';
|
||||
import type {
|
||||
AssistantForm,
|
||||
Actions,
|
||||
TAssistantOption,
|
||||
ExtendedFile,
|
||||
LastSelectedModels,
|
||||
} from '~/common';
|
||||
import SelectDropDown from '~/components/ui/SelectDropDown';
|
||||
import { useListAssistantsQuery } from '~/data-provider';
|
||||
import { useLocalize, useLocalStorage } from '~/hooks';
|
||||
import { useFileMapContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const keys = new Set(['name', 'id', 'description', 'instructions', 'model']);
|
||||
@@ -35,6 +42,10 @@ export default function AssistantSelect({
|
||||
const localize = useLocalize();
|
||||
const fileMap = useFileMapContext();
|
||||
const lastSelectedAssistant = useRef<string | null>(null);
|
||||
const [lastSelectedModels] = useLocalStorage<LastSelectedModels>(
|
||||
'lastSelectedModel',
|
||||
{} as LastSelectedModels,
|
||||
);
|
||||
|
||||
const assistants = useListAssistantsQuery(defaultOrderQuery, {
|
||||
select: (res) =>
|
||||
@@ -79,7 +90,10 @@ export default function AssistantSelect({
|
||||
createMutation.reset();
|
||||
if (!assistant) {
|
||||
setCurrentAssistantId(undefined);
|
||||
return reset(defaultAssistantFormValues);
|
||||
return reset({
|
||||
...defaultAssistantFormValues,
|
||||
model: lastSelectedModels?.[EModelEndpoint.assistants] ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
const update = {
|
||||
@@ -127,7 +141,7 @@ export default function AssistantSelect({
|
||||
reset(formValues);
|
||||
setCurrentAssistantId(assistant?.id);
|
||||
},
|
||||
[assistants.data, reset, setCurrentAssistantId, createMutation],
|
||||
[assistants.data, reset, setCurrentAssistantId, createMutation, lastSelectedModels],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -21,7 +21,7 @@ import type {
|
||||
TEndpointsConfig,
|
||||
TCheckUserKeyResponse,
|
||||
} from 'librechat-data-provider';
|
||||
import { findPageForConversation } from '~/utils';
|
||||
import { findPageForConversation, addFileToCache } from '~/utils';
|
||||
|
||||
export const useGetFiles = <TData = TFile[] | boolean>(
|
||||
config?: UseQueryOptions<TFile[], unknown, TData>,
|
||||
@@ -324,3 +324,35 @@ export const useGetAssistantDocsQuery = (
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useFileDownload = (userId: string, filepath: string): QueryObserverResult<string> => {
|
||||
const queryClient = useQueryClient();
|
||||
return useQuery(
|
||||
[QueryKeys.fileDownload, filepath],
|
||||
async () => {
|
||||
if (!userId) {
|
||||
console.warn('No user ID provided for file download');
|
||||
}
|
||||
const response = await dataService.getFileDownload(userId, filepath);
|
||||
const blob = response.data;
|
||||
const downloadURL = window.URL.createObjectURL(blob);
|
||||
try {
|
||||
const metadata: TFile | undefined = JSON.parse(response.headers['x-file-metadata']);
|
||||
if (!metadata) {
|
||||
console.warn('No metadata found for file download', response.headers);
|
||||
return downloadURL;
|
||||
}
|
||||
|
||||
addFileToCache(queryClient, metadata);
|
||||
} catch (e) {
|
||||
console.error('Error parsing file metadata, skipped updating file query cache', e);
|
||||
}
|
||||
|
||||
return downloadURL;
|
||||
},
|
||||
{
|
||||
enabled: false,
|
||||
retry: false,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -125,7 +125,7 @@ const useFileHandling = (params?: UseFileHandling) => {
|
||||
startUploadTimer(extendedFile.file_id, extendedFile.file?.name || 'File');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', extendedFile.file as File);
|
||||
formData.append('file', extendedFile.file as File, encodeURIComponent(extendedFile.file?.name || 'File'));
|
||||
formData.append('file_id', extendedFile.file_id);
|
||||
if (extendedFile.width) {
|
||||
formData.append('width', extendedFile.width?.toString());
|
||||
|
||||
@@ -4,6 +4,7 @@ import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { TEndpointOption } from 'librechat-data-provider';
|
||||
import type { UseFormSetValue } from 'react-hook-form';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import { forceResize, insertTextAtCursor, getAssistantName } from '~/utils';
|
||||
import { useAssistantsMapContext } from '~/Providers/AssistantsMapContext';
|
||||
import useGetSender from '~/hooks/Conversations/useGetSender';
|
||||
import useFileHandling from '~/hooks/Files/useFileHandling';
|
||||
@@ -12,58 +13,6 @@ import useLocalize from '~/hooks/useLocalize';
|
||||
|
||||
type KeyEvent = KeyboardEvent<HTMLTextAreaElement>;
|
||||
|
||||
function insertTextAtCursor(element: HTMLTextAreaElement, textToInsert: string) {
|
||||
element.focus();
|
||||
|
||||
// Use the browser's built-in undoable actions if possible
|
||||
if (window.getSelection() && document.queryCommandSupported('insertText')) {
|
||||
document.execCommand('insertText', false, textToInsert);
|
||||
} else {
|
||||
console.warn('insertTextAtCursor: document.execCommand is not supported');
|
||||
const startPos = element.selectionStart;
|
||||
const endPos = element.selectionEnd;
|
||||
const beforeText = element.value.substring(0, startPos);
|
||||
const afterText = element.value.substring(endPos);
|
||||
element.value = beforeText + textToInsert + afterText;
|
||||
element.selectionStart = element.selectionEnd = startPos + textToInsert.length;
|
||||
const event = new Event('input', { bubbles: true });
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Necessary resize helper for edge cases where paste doesn't update the container height.
|
||||
*
|
||||
1) Resetting the height to 'auto' forces the component to recalculate height based on its current content
|
||||
|
||||
2) Forcing a reflow. Accessing offsetHeight will cause a reflow of the page,
|
||||
ensuring that the reset height takes effect before resetting back to the scrollHeight.
|
||||
This step is necessary because changes to the DOM do not instantly cause reflows.
|
||||
|
||||
3) Reseting back to scrollHeight reads and applies the ideal height for the current content dynamically
|
||||
*/
|
||||
const forceResize = (textAreaRef: React.RefObject<HTMLTextAreaElement>) => {
|
||||
if (textAreaRef.current) {
|
||||
textAreaRef.current.style.height = 'auto';
|
||||
textAreaRef.current.offsetHeight;
|
||||
textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const getAssistantName = ({
|
||||
name,
|
||||
localize,
|
||||
}: {
|
||||
name?: string;
|
||||
localize: (phraseKey: string, ...values: string[]) => string;
|
||||
}) => {
|
||||
if (name && name.length > 0) {
|
||||
return name;
|
||||
} else {
|
||||
return localize('com_ui_assistant');
|
||||
}
|
||||
};
|
||||
|
||||
export default function useTextarea({
|
||||
textAreaRef,
|
||||
submitButtonRef,
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { ContentTypes } from 'librechat-data-provider';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import type {
|
||||
TSubmission,
|
||||
Text,
|
||||
TMessage,
|
||||
TContentData,
|
||||
ImageFile,
|
||||
TSubmission,
|
||||
ContentPart,
|
||||
PartMetadata,
|
||||
TContentData,
|
||||
TMessageContentParts,
|
||||
} from 'librechat-data-provider';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { addFileToCache } from '~/utils';
|
||||
|
||||
type TUseContentHandler = {
|
||||
setMessages: (messages: TMessage[]) => void;
|
||||
@@ -19,6 +25,7 @@ type TContentHandler = {
|
||||
};
|
||||
|
||||
export default function useContentHandler({ setMessages, getMessages }: TUseContentHandler) {
|
||||
const queryClient = useQueryClient();
|
||||
const messageMap = useMemo(() => new Map<string, TMessage>(), []);
|
||||
return useCallback(
|
||||
({ data, submission }: TContentHandler) => {
|
||||
@@ -46,9 +53,13 @@ export default function useContentHandler({ setMessages, getMessages }: TUseCont
|
||||
}
|
||||
|
||||
// TODO: handle streaming for non-text
|
||||
const part: ContentPart = data[ContentTypes.TEXT]
|
||||
? { value: data[ContentTypes.TEXT] }
|
||||
: data[type];
|
||||
const textPart: Text | string | undefined = data[ContentTypes.TEXT];
|
||||
const part: ContentPart =
|
||||
textPart && typeof textPart === 'string' ? { value: textPart } : data[type];
|
||||
|
||||
if (type === ContentTypes.IMAGE_FILE) {
|
||||
addFileToCache(queryClient, part as ImageFile & PartMetadata);
|
||||
}
|
||||
|
||||
/* spreading the content array to avoid mutation */
|
||||
response.content = [...(response.content ?? [])];
|
||||
@@ -67,6 +78,6 @@ export default function useContentHandler({ setMessages, getMessages }: TUseCont
|
||||
|
||||
setMessages([...messages, response]);
|
||||
},
|
||||
[getMessages, messageMap, setMessages],
|
||||
[queryClient, getMessages, messageMap, setMessages],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -251,7 +251,8 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||
let update = {} as TConversation;
|
||||
setConversation((prevState) => {
|
||||
let title = prevState?.title;
|
||||
if (parentMessageId !== Constants.NO_PARENT && title?.toLowerCase()?.includes('new chat')) {
|
||||
const parentId = isRegenerate ? message?.overrideParentMessageId : parentMessageId;
|
||||
if (parentId !== Constants.NO_PARENT && title?.toLowerCase()?.includes('new chat')) {
|
||||
const convos = queryClient.getQueryData<ConversationData>([QueryKeys.allConversations]);
|
||||
const cachedConvo = getConversationById(convos, conversationId);
|
||||
title = cachedConvo?.title;
|
||||
|
||||
37
client/src/localization/Translation.spec.ts
Normal file
37
client/src/localization/Translation.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { getTranslations, localize } from './Translation';
|
||||
import English from './languages/Eng';
|
||||
import Spanish from './languages/Es';
|
||||
import French from './languages/Fr';
|
||||
|
||||
describe('getTranslations', () => {
|
||||
it('should return the correct language object for a valid language code', () => {
|
||||
expect(getTranslations('en-US')).toEqual(English);
|
||||
expect(getTranslations('fr-FR')).toEqual(French);
|
||||
});
|
||||
|
||||
it('should return the correct language object for a language code without region', () => {
|
||||
expect(getTranslations('fr')).toEqual(French);
|
||||
expect(getTranslations('es')).toEqual(Spanish);
|
||||
});
|
||||
|
||||
it('should return the English language object for an invalid language code', () => {
|
||||
expect(getTranslations('invalid-code')).toEqual(English);
|
||||
});
|
||||
});
|
||||
|
||||
describe('localize', () => {
|
||||
it('should return the correct localized phrase for a valid language code and phrase key', () => {
|
||||
expect(localize('en-US', 'com_ui_examples')).toBe('Examples');
|
||||
expect(localize('fr-FR', 'com_ui_examples')).toBe('Exemples');
|
||||
});
|
||||
|
||||
it('should return the English phrase for an invalid language code or phrase key', () => {
|
||||
expect(localize('invalid-code', 'com_ui_examples')).toBe('Examples');
|
||||
expect(localize('en-US', 'invalid-key')).toBe('');
|
||||
});
|
||||
|
||||
it('should correctly format placeholders in the phrase', () => {
|
||||
expect(localize('en-US', 'com_endpoint_default_with_num', 'John')).toBe('default: John');
|
||||
expect(localize('fr-FR', 'com_endpoint_default_with_num', 'Marie')).toBe('par défaut : Marie');
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,9 @@ import Hebrew from './languages/He';
|
||||
|
||||
// === import additional language files here === //
|
||||
|
||||
const languageMap: { [key: string]: unknown } = {
|
||||
type Language = Record<string, string>;
|
||||
|
||||
const languageMap: Record<string, Language> = {
|
||||
'en-US': English,
|
||||
'ar-EG': Arabic,
|
||||
'zh-CN': Chinese,
|
||||
@@ -61,8 +63,20 @@ if (!String.prototype.format) {
|
||||
|
||||
// input: language code in string
|
||||
// returns an object of translated strings in the language
|
||||
export const getTranslations = (langCode: string) => {
|
||||
return languageMap[langCode] || English;
|
||||
export const getTranslations = (langCode: string): Language => {
|
||||
if (languageMap[langCode]) {
|
||||
return languageMap[langCode];
|
||||
}
|
||||
|
||||
const [langPart] = langCode.split('-');
|
||||
|
||||
const matchingLangCode = Object.keys(languageMap).find((key) => key.startsWith(langPart));
|
||||
|
||||
if (matchingLangCode) {
|
||||
return languageMap[matchingLangCode];
|
||||
}
|
||||
|
||||
return English;
|
||||
};
|
||||
|
||||
// input: language code in string & phrase key in string
|
||||
@@ -3,51 +3,81 @@
|
||||
// file deepcode ignore HardcodedNonCryptoSecret: No hardcoded secrets present in this file
|
||||
|
||||
export default {
|
||||
com_sidepanel_select_assistant: 'Selecione um Assistente',
|
||||
com_files_no_results: 'Nenhum resultado.',
|
||||
com_files_filter: 'Filtrar arquivos...',
|
||||
com_files_number_selected: '{0} de {1} arquivo(s) selecionado(s)',
|
||||
com_sidepanel_select_assistant: 'Selecionar um Assistente',
|
||||
com_sidepanel_assistant_builder: 'Construtor de Assistente',
|
||||
com_sidepanel_hide_panel: 'Ocultar Painel',
|
||||
com_sidepanel_attach_files: 'Anexar Arquivos',
|
||||
com_sidepanel_manage_files: 'Gerenciar Arquivos',
|
||||
com_assistants_capabilities: 'Capacidades',
|
||||
com_assistants_knowledge: 'Conhecimento',
|
||||
com_assistants_knowledge_info:
|
||||
'Se você carregar arquivos em Conhecimento, as conversas com seu Assistente podem incluir o conteúdo dos arquivos.',
|
||||
'Se você enviar arquivos de Conhecimento, as conversas com seu Assistente podem incluir o conteúdo dos arquivos.',
|
||||
com_assistants_knowledge_disabled:
|
||||
'O Assistente deve ser criado, e o Interpretador de Código ou Recuperação devem estar habilitados e salvos antes de carregar arquivos como Conhecimento.',
|
||||
'O Assistente deve ser criado, e o Interpretador de Código ou Recuperação devem ser habilitados e salvos antes de enviar arquivos como Conhecimento.',
|
||||
com_assistants_image_vision: 'Visão de Imagem',
|
||||
com_assistants_code_interpreter: 'Interpretador de Código',
|
||||
com_assistants_code_interpreter_files:
|
||||
'Os seguintes arquivos estão disponíveis apenas para o Interpretador de Código:',
|
||||
com_assistants_retrieval: 'Recuperação',
|
||||
com_assistants_tools_section: 'Ações, Ferramentas',
|
||||
com_assistants_search_name: 'Pesquisar assistentes por nome',
|
||||
com_assistants_tools: 'Ferramentas',
|
||||
com_assistants_actions: 'Ações',
|
||||
com_assistants_add_tools: 'Adicionar Ferramentas',
|
||||
com_assistants_add_actions: 'Adicionar Ações',
|
||||
com_assistants_available_actions: 'Ações Disponíveis',
|
||||
com_assistants_running_action: 'Executando ação',
|
||||
com_assistants_completed_action: 'Falou com {0}',
|
||||
com_assistants_completed_function: 'Executou {0}',
|
||||
com_assistants_function_use: 'O Assistente usou {0}',
|
||||
com_assistants_domain_info: 'O Assistente enviou esta informação para {0}',
|
||||
com_assistants_delete_actions_success: 'Ação excluída do Assistente com sucesso',
|
||||
com_assistants_update_actions_success: 'Ação criada ou atualizada com sucesso',
|
||||
com_assistants_update_actions_error: 'Ocorreu um erro ao criar ou atualizar a ação.',
|
||||
com_assistants_delete_actions_error: 'Ocorreu um erro ao excluir a ação.',
|
||||
com_assistants_actions_info:
|
||||
'Permita que seu Assistente recupere informações ou execute ações via APIs',
|
||||
com_assistants_name_placeholder: 'Opcional: O nome do assistente',
|
||||
com_assistants_instructions_placeholder: 'As instruções do sistema que o assistente usa',
|
||||
com_assistants_description_placeholder: 'Opcional: Descreva seu Assistente aqui',
|
||||
com_assistants_actions_disabled: 'Você precisa criar um assistente antes de adicionar ações.',
|
||||
com_assistants_update_success: 'Atualizado com sucesso',
|
||||
com_assistants_update_error: 'Houve um erro ao atualizar seu assistente.',
|
||||
com_assistants_update_error: 'Ocorreu um erro ao atualizar seu assistente.',
|
||||
com_assistants_create_success: 'Criado com sucesso',
|
||||
com_assistants_create_error: 'Houve um erro ao criar seu assistente.',
|
||||
com_ui_attach_error_type: 'Tipo de arquivo não suportado para o endpoint:',
|
||||
com_ui_attach_error_size: 'Limite de tamanho de arquivo excedido para o endpoint:',
|
||||
com_assistants_create_error: 'Ocorreu um erro ao criar seu assistente.',
|
||||
com_ui_attach_error_type: 'Tipo de arquivo não suportado para endpoint:',
|
||||
com_ui_attach_error_size: 'Limite de tamanho de arquivo excedido para endpoint:',
|
||||
com_ui_attach_error:
|
||||
'Não é possível anexar arquivo. Crie ou selecione uma conversa, ou tente atualizar a página.',
|
||||
'Não é possível anexar arquivo. Crie ou selecione uma conversa ou tente atualizar a página.',
|
||||
com_ui_examples: 'Exemplos',
|
||||
com_ui_new_chat: 'Nova Conversa',
|
||||
com_ui_happy_birthday: 'É meu primeiro aniversário!',
|
||||
com_ui_example_quantum_computing: 'Explique a computação quântica em termos simples',
|
||||
com_ui_example_10_year_old_b_day:
|
||||
'Tem alguma ideia criativa para o aniversário de uma criança de 10 anos?',
|
||||
com_ui_example_http_in_js: 'Como faço uma solicitação HTTP em Javascript?',
|
||||
com_ui_happy_birthday: 'É meu 1º aniversário!',
|
||||
com_ui_example_quantum_computing: 'Explique computação quântica em termos simples',
|
||||
com_ui_example_10_year_old_b_day: 'Tem alguma ideia criativa para o aniversário de 10 anos?',
|
||||
com_ui_example_http_in_js: 'Como faço uma requisição HTTP em Javascript?',
|
||||
com_ui_capabilities: 'Capacidades',
|
||||
com_ui_capability_remember: 'Lembra do que o usuário disse anteriormente na conversa',
|
||||
com_ui_capability_remember: 'Lembra o que o usuário disse anteriormente na conversa',
|
||||
com_ui_capability_correction: 'Permite que o usuário forneça correções de acompanhamento',
|
||||
com_ui_capability_decline_requests: 'Treinado para recusar solicitações inadequadas',
|
||||
com_ui_limitations: 'Limitações',
|
||||
com_ui_limitation_incorrect_info: 'Pode ocasionalmente gerar informações incorretas',
|
||||
com_ui_limitation_harmful_biased:
|
||||
'Pode ocasionalmente produzir instruções prejudiciais ou conteúdo tendencioso',
|
||||
'Pode ocasionalmente produzir instruções prejudiciais ou conteúdo enviesado',
|
||||
com_ui_limitation_limited_2021: 'Conhecimento limitado do mundo e eventos após 2021',
|
||||
com_ui_experimental: 'Recursos Experimentais',
|
||||
com_ui_ascending: 'Asc',
|
||||
com_ui_descending: 'Desc',
|
||||
com_ui_show_all: 'Mostrar Todos',
|
||||
com_ui_name: 'Nome',
|
||||
com_ui_date: 'Data',
|
||||
com_ui_storage: 'Armazenamento',
|
||||
com_ui_context: 'Contexto',
|
||||
com_ui_size: 'Tamanho',
|
||||
com_ui_host: 'Host',
|
||||
com_ui_update: 'Atualizar',
|
||||
com_ui_authentication: 'Autenticação',
|
||||
com_ui_instructions: 'Instruções',
|
||||
com_ui_description: 'Descrição',
|
||||
com_ui_error: 'Erro',
|
||||
@@ -55,10 +85,12 @@ export default {
|
||||
com_ui_input: 'Entrada',
|
||||
com_ui_close: 'Fechar',
|
||||
com_ui_model: 'Modelo',
|
||||
com_ui_select_model: 'Selecione um modelo',
|
||||
com_ui_use_prompt: 'Use o prompt',
|
||||
com_ui_prev: 'Anterior',
|
||||
com_ui_next: 'Próximo',
|
||||
com_ui_select_model: 'Selecionar um modelo',
|
||||
com_ui_select_search_model: 'Pesquisar modelo por nome',
|
||||
com_ui_select_search_plugin: 'Pesquisar plugin por nome',
|
||||
com_ui_use_prompt: 'Usar prompt',
|
||||
com_ui_prev: 'Ant',
|
||||
com_ui_next: 'Próx',
|
||||
com_ui_stop: 'Parar',
|
||||
com_ui_upload_files: 'Carregar arquivos',
|
||||
com_ui_prompt_templates: 'Modelos de Prompt',
|
||||
@@ -71,7 +103,7 @@ export default {
|
||||
com_ui_enter: 'Entrar',
|
||||
com_ui_submit: 'Enviar',
|
||||
com_ui_upload_success: 'Arquivo carregado com sucesso',
|
||||
com_ui_upload_error: 'Houve um erro ao carregar seu arquivo',
|
||||
com_ui_upload_error: 'Ocorreu um erro ao carregar seu arquivo',
|
||||
com_ui_upload_invalid: 'Arquivo inválido para upload. Deve ser uma imagem não excedendo 2 MB',
|
||||
com_ui_cancel: 'Cancelar',
|
||||
com_ui_save: 'Salvar',
|
||||
@@ -89,16 +121,27 @@ export default {
|
||||
com_ui_revoke_info: 'Revogar todas as credenciais fornecidas pelo usuário',
|
||||
com_ui_confirm_action: 'Confirmar Ação',
|
||||
com_ui_chats: 'conversas',
|
||||
com_ui_avatar: 'Avatar',
|
||||
com_ui_unknown: 'Desconhecido',
|
||||
com_ui_result: 'Resultado',
|
||||
com_ui_image_gen: 'Geração de Imagem',
|
||||
com_ui_assistant: 'Assistente',
|
||||
com_ui_assistants: 'Assistentes',
|
||||
com_ui_attachment: 'Anexo',
|
||||
com_ui_assistants_output: 'Saída dos Assistentes',
|
||||
com_ui_delete: 'Excluir',
|
||||
com_ui_create: 'Criar',
|
||||
com_ui_delete_conversation: 'Excluir conversa?',
|
||||
com_ui_delete_conversation_confirm: 'Isso irá excluir',
|
||||
com_ui_delete_conversation_confirm: 'Isso excluirá',
|
||||
com_ui_delete_assistant_confirm:
|
||||
'Tem certeza de que deseja excluir este Assistente? Isso não pode ser desfeito.',
|
||||
'Tem certeza de que deseja excluir este Assistente? Esta ação não pode ser desfeita.',
|
||||
com_ui_preview: 'Visualizar',
|
||||
com_ui_upload: 'Carregar',
|
||||
com_ui_connect: 'Conectar',
|
||||
com_ui_upload_delay:
|
||||
'O envio de "{0}" está levando mais tempo do que o esperado. Aguarde enquanto o arquivo é indexado para recuperação.',
|
||||
com_ui_privacy_policy: 'Política de privacidade',
|
||||
com_ui_terms_of_service: 'Termos de serviço',
|
||||
com_auth_error_login:
|
||||
'Não foi possível fazer login com as informações fornecidas. Por favor, verifique suas credenciais e tente novamente.',
|
||||
com_auth_error_login_rl:
|
||||
@@ -106,7 +149,7 @@ export default {
|
||||
com_auth_error_login_ban:
|
||||
'Sua conta foi temporariamente banida devido a violações de nosso serviço.',
|
||||
com_auth_error_login_server:
|
||||
'Houve um erro interno do servidor. Por favor, aguarde alguns momentos e tente novamente.',
|
||||
'Ocorreu um erro interno do servidor. Aguarde alguns instantes e tente novamente.',
|
||||
com_auth_no_account: 'Não tem uma conta?',
|
||||
com_auth_sign_up: 'Inscrever-se',
|
||||
com_auth_sign_in: 'Entrar',
|
||||
@@ -115,32 +158,33 @@ export default {
|
||||
com_auth_github_login: 'Continuar com o Github',
|
||||
com_auth_discord_login: 'Continuar com o Discord',
|
||||
com_auth_email: 'Email',
|
||||
com_auth_email_required: 'Email é obrigatório',
|
||||
com_auth_email_required: 'O email é obrigatório',
|
||||
com_auth_email_min_length: 'O email deve ter pelo menos 6 caracteres',
|
||||
com_auth_email_max_length: 'O email não deve ter mais de 120 caracteres',
|
||||
com_auth_email_pattern: 'Você deve inserir um endereço de email válido',
|
||||
com_auth_email_address: 'Endereço de email',
|
||||
com_auth_password: 'Senha',
|
||||
com_auth_password_required: 'Senha é obrigatória',
|
||||
com_auth_password_required: 'A senha é obrigatória',
|
||||
com_auth_password_min_length: 'A senha deve ter pelo menos 8 caracteres',
|
||||
com_auth_password_max_length: 'A senha deve ter menos de 128 caracteres',
|
||||
com_auth_password_forgot: 'Esqueceu a senha?',
|
||||
com_auth_password_forgot: 'Esqueceu a Senha?',
|
||||
com_auth_password_confirm: 'Confirmar senha',
|
||||
com_auth_password_not_match: 'As senhas não correspondem',
|
||||
com_auth_continue: 'Continuar',
|
||||
com_auth_create_account: 'Crie sua conta',
|
||||
com_auth_error_create: 'Houve um erro ao tentar registrar sua conta. Por favor, tente novamente.',
|
||||
com_auth_error_create:
|
||||
'Ocorreu um erro ao tentar registrar sua conta. Por favor, tente novamente.',
|
||||
com_auth_full_name: 'Nome completo',
|
||||
com_auth_name_required: 'Nome é obrigatório',
|
||||
com_auth_name_required: 'O nome é obrigatório',
|
||||
com_auth_name_min_length: 'O nome deve ter pelo menos 3 caracteres',
|
||||
com_auth_name_max_length: 'O nome deve ter menos de 80 caracteres',
|
||||
com_auth_username: 'Nome de usuário (opcional)',
|
||||
com_auth_username_required: 'Nome de usuário é obrigatório',
|
||||
com_auth_username_required: 'O nome de usuário é obrigatório',
|
||||
com_auth_username_min_length: 'O nome de usuário deve ter pelo menos 2 caracteres',
|
||||
com_auth_username_max_length: 'O nome de usuário deve ter menos de 20 caracteres',
|
||||
com_auth_already_have_account: 'Já tem uma conta?',
|
||||
com_auth_login: 'Login',
|
||||
com_auth_reset_password: 'Redefina sua senha',
|
||||
com_auth_login: 'Entrar',
|
||||
com_auth_reset_password: 'Redefinir sua senha',
|
||||
com_auth_click: 'Clique',
|
||||
com_auth_here: 'AQUI',
|
||||
com_auth_to_reset_your_password: 'para redefinir sua senha.',
|
||||
@@ -150,7 +194,7 @@ export default {
|
||||
com_auth_error_reset_password:
|
||||
'Houve um problema ao redefinir sua senha. Não foi encontrado nenhum usuário com o endereço de email fornecido. Por favor, tente novamente.',
|
||||
com_auth_reset_password_success: 'Redefinição de Senha Bem-sucedida',
|
||||
com_auth_login_with_new_password: 'Agora você pode fazer login com sua nova senha.',
|
||||
com_auth_login_with_new_password: 'Você pode agora fazer login com sua nova senha.',
|
||||
com_auth_error_invalid_reset_token: 'Este token de redefinição de senha não é mais válido.',
|
||||
com_auth_click_here: 'Clique aqui',
|
||||
com_auth_to_try_again: 'para tentar novamente.',
|
||||
@@ -162,9 +206,9 @@ export default {
|
||||
com_endpoint_bing_to_enable_sydney: 'Para habilitar Sydney',
|
||||
com_endpoint_bing_jailbreak: 'Jailbreak',
|
||||
com_endpoint_bing_context_placeholder:
|
||||
'O Bing pode usar até 7k tokens para \'contexto\', que ele pode referenciar para a conversa. O limite específico não é conhecido, mas pode ocorrer erros ao exceder 7k tokens',
|
||||
'O Bing pode usar até 7k tokens para \'contexto\', que ele pode referenciar para a conversa. O limite específico não é conhecido, mas pode causar erros ao exceder 7k tokens',
|
||||
com_endpoint_bing_system_message_placeholder:
|
||||
'AVISO: O uso indevido deste recurso pode fazer com que você seja BANIDO de usar o Bing! Clique em \'Mensagem do Sistema\' para obter instruções completas e a mensagem padrão se omitida, que é o preset \'Sydney\' que é considerado seguro.',
|
||||
'AVISO: O uso indevido deste recurso pode fazer com que você seja BANIDO de usar o Bing! Clique em \'Mensagem do Sistem\' para obter instruções completas e a mensagem padrão, caso omitida, que é a predefinição \'Sydney\', considerada segura.',
|
||||
com_endpoint_system_message: 'Mensagem do Sistema',
|
||||
com_endpoint_message: 'Conversar com',
|
||||
com_endpoint_message_not_appendable: 'Edite sua mensagem ou Regenere.',
|
||||
@@ -178,20 +222,20 @@ export default {
|
||||
com_endpoint_token_count: 'Contagem de Tokens',
|
||||
com_endpoint_output: 'Saída',
|
||||
com_endpoint_google_temp:
|
||||
'Valores mais altos = mais aleatório, enquanto valores mais baixos = mais focado e determinístico. Recomendamos alterar isso ou Top P, mas não ambos.',
|
||||
'Valores mais altos = mais aleatório, enquanto valores mais baixos = mais focado e determinístico. Recomendamos alterar este ou o Top P, mas não ambos.',
|
||||
com_endpoint_google_topp:
|
||||
'Top-p muda como o modelo seleciona tokens para saída. Os tokens são selecionados desde o mais K (veja o parâmetro topK) provável até o menos até que a soma de suas probabilidades seja igual ao valor top-p.',
|
||||
'O Top-p altera a forma como o modelo seleciona tokens para a saída. Os tokens são selecionados do mais provável K (veja o parâmetro topK) para o menos provável até que a soma de suas probabilidades seja igual ao valor top-p.',
|
||||
com_endpoint_google_topk:
|
||||
'Top-k muda como o modelo seleciona tokens para saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gananciosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).',
|
||||
'O Top-k altera a forma como o modelo seleciona tokens para a saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gulosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).',
|
||||
com_endpoint_google_maxoutputtokens:
|
||||
'Número máximo de tokens que podem ser gerados na resposta. Especifique um valor menor para respostas mais curtas e um valor maior para respostas mais longas.',
|
||||
com_endpoint_google_custom_name_placeholder: 'Defina um nome personalizado para o Google',
|
||||
com_endpoint_prompt_prefix_placeholder:
|
||||
'Defina instruções personalizadas ou contexto. Ignorado se vazio.',
|
||||
'Defina instruções ou contexto personalizados. Ignorado se vazio.',
|
||||
com_endpoint_instructions_assistants_placeholder:
|
||||
'Substitui as instruções do assistente. Isso é útil para modificar o comportamento em uma base por execução.',
|
||||
com_endpoint_prompt_prefix_assistants_placeholder:
|
||||
'Defina instruções adicionais ou contexto além das principais instruções do Assistente. Ignorado se vazio.',
|
||||
'Defina instruções ou contexto adicionais além das instruções principais do Assistente. Ignorado se vazio.',
|
||||
com_endpoint_custom_name: 'Nome Personalizado',
|
||||
com_endpoint_prompt_prefix: 'Instruções Personalizadas',
|
||||
com_endpoint_prompt_prefix_assistants: 'Instruções Adicionais',
|
||||
@@ -200,134 +244,143 @@ export default {
|
||||
com_endpoint_default: 'padrão',
|
||||
com_endpoint_top_p: 'Top P',
|
||||
com_endpoint_top_k: 'Top K',
|
||||
com_endpoint_max_output_tokens: 'Max Output Tokens',
|
||||
com_endpoint_max_output_tokens: 'Máx. Tokens de Saída',
|
||||
com_endpoint_openai_temp:
|
||||
'Valores mais altos = mais aleatório, enquanto valores mais baixos = mais focado e determinístico. Recomendamos alterar isso ou Top P, mas não ambos.',
|
||||
'Valores mais altos = mais aleatório, enquanto valores mais baixos = mais focado e determinístico. Recomendamos alterar este ou o Top P, mas não ambos.',
|
||||
com_endpoint_openai_max:
|
||||
'Os tokens máximos para gerar. O comprimento total dos tokens de entrada e dos tokens gerados é limitado pelo comprimento do contexto do modelo.',
|
||||
'O máximo de tokens a gerar. O comprimento total de tokens de entrada e tokens gerados é limitado pelo comprimento do contexto do modelo.',
|
||||
com_endpoint_openai_topp:
|
||||
'Uma alternativa para amostragem com temperatura, chamada amostragem de núcleo, onde o modelo considera os resultados dos tokens com massa de probabilidade top_p. Então 0.1 significa que apenas os tokens que compõem a massa de probabilidade dos 10% superiores são considerados. Recomendamos alterar isso ou a temperatura, mas não ambos.',
|
||||
'Uma alternativa à amostragem com temperatura, chamada amostragem de núcleo, onde o modelo considera os resultados dos tokens com massa de probabilidade top_p. Então, 0,1 significa que apenas os tokens que compõem os 10% superiores da massa de probabilidade são considerados. Recomendamos alterar este ou a temperatura, mas não ambos.',
|
||||
com_endpoint_openai_freq:
|
||||
'Número entre -2.0 e 2.0. Valores positivos penalizam novos tokens com base em sua frequência existente no texto até agora, diminuindo a probabilidade do modelo de repetir a mesma linha literalmente.',
|
||||
'Número entre -2,0 e 2,0. Valores positivos penalizam novos tokens com base em sua frequência existente no texto até agora, diminuindo a probabilidade do modelo repetir a mesma linha literalmente.',
|
||||
com_endpoint_openai_pres:
|
||||
'Número entre -2.0 e 2.0. Valores positivos penalizam novos tokens com base em se eles aparecem no texto até agora, aumentando a probabilidade do modelo de falar sobre novos tópicos.',
|
||||
'Número entre -2,0 e 2,0. Valores positivos penalizam novos tokens com base em sua aparição no texto até o momento, aumentando a probabilidade do modelo falar sobre novos tópicos.',
|
||||
com_endpoint_openai_resend:
|
||||
'Reenvie todas as imagens anexadas anteriormente. Nota: isso pode aumentar significativamente o custo do token e você pode experimentar erros com muitos anexos de imagem.',
|
||||
'Reenviar todas as imagens anexadas anteriormente. Nota: isso pode aumentar significativamente o custo de tokens e você pode encontrar erros com muitos anexos de imagem.',
|
||||
com_endpoint_openai_resend_files:
|
||||
'Reenviar todos os arquivos anexados anteriormente. Nota: isso aumentará o custo de tokens e você pode encontrar erros com muitos anexos.',
|
||||
com_endpoint_openai_detail:
|
||||
'A resolução para solicitações do Vision. "Low" é mais barato e rápido, "High" é mais detalhado e caro, e "Auto" escolherá automaticamente entre os dois com base na resolução da imagem.',
|
||||
com_endpoint_openai_custom_name_placeholder: 'Defina um nome personalizado para ChatGPT',
|
||||
'A resolução para solicitações de Visão. "Baixa" é mais barata e rápida, "Alta" é mais detalhada e cara, e "Automática" escolherá automaticamente entre as duas com base na resolução da imagem.',
|
||||
com_endpoint_openai_custom_name_placeholder: 'Defina um nome personalizado para o ChatGPT',
|
||||
com_endpoint_openai_prompt_prefix_placeholder:
|
||||
'Defina instruções personalizadas para incluir na Mensagem do Sistema. Padrão: nenhum',
|
||||
'Defina instruções personalizadas para incluir na Mensagem do Sistema. Padrão: nenhuma',
|
||||
com_endpoint_anthropic_temp:
|
||||
'Varia de 0 a 1. Use temp mais próximo de 0 para analítico / múltipla escolha, e mais próximo de 1 para tarefas criativas e gerativas. Recomendamos alterar isso ou Top P, mas não ambos.',
|
||||
'Varia de 0 a 1. Use temp mais próximo de 0 para tarefas analíticas/múltipla escolha e mais próximo de 1 para tarefas criativas e generativas. Recomendamos alterar este ou o Top P, mas não ambos.',
|
||||
com_endpoint_anthropic_topp:
|
||||
'Top-p muda como o modelo seleciona tokens para saída. Os tokens são selecionados desde o mais K (veja o parâmetro topK) provável até o menos até que a soma de suas probabilidades seja igual ao valor top-p.',
|
||||
'O Top-p altera a forma como o modelo seleciona tokens para a saída. Os tokens são selecionados do mais provável K (veja o parâmetro topK) para o menos provável até que a soma de suas probabilidades seja igual ao valor top-p.',
|
||||
com_endpoint_anthropic_topk:
|
||||
'Top-k muda como o modelo seleciona tokens para saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gananciosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).',
|
||||
'O Top-k altera a forma como o modelo seleciona tokens para a saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gulosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).',
|
||||
com_endpoint_anthropic_maxoutputtokens:
|
||||
'Número máximo de tokens que podem ser gerados na resposta. Especifique um valor menor para respostas mais curtas e um valor maior para respostas mais longas.',
|
||||
com_endpoint_anthropic_custom_name_placeholder: 'Defina um nome personalizado para Anthropic',
|
||||
com_endpoint_frequency_penalty: 'Penalidade de Frequência',
|
||||
com_endpoint_presence_penalty: 'Penalidade de Presença',
|
||||
com_endpoint_plug_use_functions: 'Use Funções',
|
||||
com_endpoint_plug_use_functions: 'Usar Funções',
|
||||
com_endpoint_plug_resend_files: 'Reenviar Arquivos',
|
||||
com_endpoint_plug_resend_images: 'Reenviar Imagens',
|
||||
com_endpoint_plug_image_detail: 'Detalhes da Imagem',
|
||||
com_endpoint_plug_skip_completion: 'Pular Conclusão',
|
||||
com_endpoint_disabled_with_tools: 'desativado com ferramentas',
|
||||
com_endpoint_disabled_with_tools_placeholder: 'Desativado com Ferramentas Selecionadas',
|
||||
com_endpoint_plug_image_detail: 'Detalhe da Imagem',
|
||||
com_endpoint_plug_skip_completion: 'Ignorar Conclusão',
|
||||
com_endpoint_disabled_with_tools: 'desabilitado com ferramentas',
|
||||
com_endpoint_disabled_with_tools_placeholder: 'Desabilitado com Ferramentas Selecionadas',
|
||||
com_endpoint_plug_set_custom_instructions_for_gpt_placeholder:
|
||||
'Defina instruções personalizadas para incluir na Mensagem do Sistema. Padrão: nenhum',
|
||||
'Defina instruções personalizadas para incluir na Mensagem do Sistema. Padrão: nenhuma',
|
||||
com_endpoint_import: 'Importar',
|
||||
com_endpoint_set_custom_name:
|
||||
'Defina um nome personalizado, caso você possa encontrar este preset',
|
||||
com_endpoint_preset_delete_confirm: 'Tem certeza de que deseja excluir este preset?',
|
||||
com_endpoint_preset_clear_all_confirm: 'Tem certeza de que deseja excluir todos os seus presets?',
|
||||
com_endpoint_preset_import: 'Preset Importado!',
|
||||
'Defina um nome personalizado, caso você encontre esta predefinição',
|
||||
com_endpoint_preset_delete_confirm: 'Tem certeza de que deseja excluir esta predefinição?',
|
||||
com_endpoint_preset_clear_all_confirm:
|
||||
'Tem certeza de que deseja excluir todas as suas predefinições?',
|
||||
com_endpoint_preset_import: 'Predefinição Importada!',
|
||||
com_endpoint_preset_import_error:
|
||||
'Houve um erro ao importar seu preset. Por favor, tente novamente.',
|
||||
com_endpoint_preset_save_error: 'Houve um erro ao salvar seu preset. Por favor, tente novamente.',
|
||||
'Ocorreu um erro ao importar sua predefinição. Por favor, tente novamente.',
|
||||
com_endpoint_preset_save_error:
|
||||
'Ocorreu um erro ao salvar sua predefinição. Por favor, tente novamente.',
|
||||
com_endpoint_preset_delete_error:
|
||||
'Houve um erro ao excluir seu preset. Por favor, tente novamente.',
|
||||
com_endpoint_preset_default_removed: 'não é mais o preset padrão.',
|
||||
'Ocorreu um erro ao excluir sua predefinição. Por favor, tente novamente.',
|
||||
com_endpoint_preset_default_removed: 'não é mais a predefinição padrão.',
|
||||
com_endpoint_preset_default_item: 'Padrão:',
|
||||
com_endpoint_preset_default_none: 'Nenhum preset padrão ativo.',
|
||||
com_endpoint_preset_title: 'Preset',
|
||||
com_endpoint_preset_default_none: 'Nenhuma predefinição padrão ativa.',
|
||||
com_endpoint_preset_title: 'Predefinição',
|
||||
com_endpoint_preset_saved: 'Salvo!',
|
||||
com_endpoint_preset_default: 'agora é o preset padrão.',
|
||||
com_endpoint_preset: 'preset',
|
||||
com_endpoint_presets: 'presets',
|
||||
com_endpoint_preset_selected: 'Preset Ativo!',
|
||||
com_endpoint_preset_selected_title: 'Ativo!',
|
||||
com_endpoint_preset_name: 'Nome do Preset',
|
||||
com_endpoint_preset_default: 'é agora a predefinição padrão.',
|
||||
com_endpoint_preset: 'predefinição',
|
||||
com_endpoint_presets: 'predefinições',
|
||||
com_endpoint_preset_selected: 'Predefinição Ativa!',
|
||||
com_endpoint_preset_selected_title: 'Ativa!',
|
||||
com_endpoint_preset_name: 'Nome da Predefinição',
|
||||
com_endpoint_new_topic: 'Novo Tópico',
|
||||
com_endpoint: 'Endpoint',
|
||||
com_endpoint_hide: 'Ocultar',
|
||||
com_endpoint_show: 'Mostrar',
|
||||
com_endpoint_examples: ' Presets',
|
||||
com_endpoint_examples: ' Predefinições',
|
||||
com_endpoint_completion: 'Conclusão',
|
||||
com_endpoint_agent: 'Agente',
|
||||
com_endpoint_show_what_settings: 'Mostrar {0} Configurações',
|
||||
com_endpoint_save: 'Salvar',
|
||||
com_endpoint_show_what_settings: 'Mostrar Configurações de {0}',
|
||||
com_endpoint_export: 'Exportar',
|
||||
com_endpoint_assistant: 'Assistente',
|
||||
com_endpoint_use_active_assistant: 'Use o Assistente Ativo',
|
||||
com_endpoint_assistant_model: 'Modelo do Assistente',
|
||||
com_endpoint_save_as_preset: 'Salvar Como Preset',
|
||||
com_endpoint_use_active_assistant: 'Usar Assistente Ativo',
|
||||
com_endpoint_assistant_model: 'Modelo de Assistente',
|
||||
com_endpoint_save_as_preset: 'Salvar como Predefinição',
|
||||
com_endpoint_presets_clear_warning:
|
||||
'Tem certeza de que deseja limpar todos os presets? Isso é irreversível.',
|
||||
'Tem certeza de que deseja limpar todas as predefinições? Isso é irreversível.',
|
||||
com_endpoint_not_implemented: 'Não implementado',
|
||||
com_endpoint_no_presets: 'Ainda não há presets, use o botão de configurações para criar um',
|
||||
com_endpoint_no_presets:
|
||||
'Nenhuma predefinição ainda, use o botão de configurações para criar uma',
|
||||
com_endpoint_not_available: 'Nenhum endpoint disponível',
|
||||
com_endpoint_view_options: 'Ver Opções',
|
||||
com_endpoint_save_convo_as_preset: 'Salvar Conversa como Preset',
|
||||
com_endpoint_my_preset: 'Meu Preset',
|
||||
com_endpoint_agent_model: 'Modelo do Agente (Recomendado: GPT-3.5)',
|
||||
com_endpoint_view_options: 'Opções de Visualização',
|
||||
com_endpoint_save_convo_as_preset: 'Salvar Conversa como Predefinição',
|
||||
com_endpoint_my_preset: 'Minha Predefinição',
|
||||
com_endpoint_agent_model: 'Modelo de Agente (Recomendado: GPT-3.5)',
|
||||
com_endpoint_completion_model: 'Modelo de Conclusão (Recomendado: GPT-4)',
|
||||
com_endpoint_func_hover: 'Habilitar o uso de Plugins como Funções OpenAI',
|
||||
com_endpoint_skip_hover:
|
||||
'Habilitar a etapa de conclusão de pulo, que revisa a resposta final e as etapas geradas',
|
||||
'Habilitar a ignorância da etapa de conclusão, que revisa a resposta final e as etapas geradas',
|
||||
com_endpoint_config_key: 'Definir Chave API',
|
||||
com_endpoint_assistant_placeholder:
|
||||
'Por favor, selecione um Assistente no Painel Lateral Direito',
|
||||
com_endpoint_config_placeholder: 'Defina sua Chave no menu Cabeçalho para conversar.',
|
||||
com_endpoint_config_key_for: 'Definir Chave API para',
|
||||
com_endpoint_config_key_name: 'Chave',
|
||||
com_endpoint_config_value: 'Inserir valor para',
|
||||
com_endpoint_config_value: 'Digite o valor para',
|
||||
com_endpoint_config_key_name_placeholder: 'Defina a chave API primeiro',
|
||||
com_endpoint_config_key_encryption: 'Sua chave será criptografada e excluída em',
|
||||
com_endpoint_config_key_expiry: 'o tempo de expiração',
|
||||
com_endpoint_config_click_here: 'Clique Aqui',
|
||||
com_endpoint_config_google_service_key: 'Chave da Conta de Serviço do Google',
|
||||
com_endpoint_config_google_cloud_platform: '(do Google Cloud Platform)',
|
||||
com_endpoint_config_google_service_key: 'Chave de Conta de Serviço do Google',
|
||||
com_endpoint_config_google_cloud_platform: '(da Google Cloud Platform)',
|
||||
com_endpoint_config_google_api_key: 'Chave API do Google',
|
||||
com_endpoint_config_google_gemini_api: '(API Gemini)',
|
||||
com_endpoint_config_google_api_info:
|
||||
'Para obter sua chave API de Linguagem Generativa (para Gemini),',
|
||||
'Para obter sua chave da API de Linguagem Generativa (para Gemini),',
|
||||
com_endpoint_config_key_import_json_key: 'Importar Chave JSON da Conta de Serviço.',
|
||||
com_endpoint_config_key_import_json_key_success:
|
||||
'Chave JSON da Conta de Serviço importada com sucesso',
|
||||
'Chave JSON da Conta de Serviço Importada com Sucesso',
|
||||
com_endpoint_config_key_import_json_key_invalid:
|
||||
'Chave JSON da Conta de Serviço inválida, você importou o arquivo correto?',
|
||||
com_endpoint_config_key_get_edge_key: 'Para obter seu token de acesso para o Bing, faça login em',
|
||||
'Chave JSON da Conta de Serviço Inválida, Você importou o arquivo correto?',
|
||||
com_endpoint_config_key_get_edge_key: 'Para obter seu Token de Acesso para o Bing, faça login em',
|
||||
com_endpoint_config_key_get_edge_key_dev_tool:
|
||||
'Use as ferramentas de desenvolvimento ou uma extensão enquanto estiver logado no site para copiar o conteúdo do cookie _U. Se isso falhar, siga estas',
|
||||
com_endpoint_config_key_edge_instructions: 'instruções',
|
||||
com_endpoint_config_key_edge_full_key_string: 'para fornecer as strings completas do cookie.',
|
||||
com_endpoint_config_key_edge_full_key_string: 'para fornecer as strings completas de cookies.',
|
||||
com_endpoint_config_key_chatgpt:
|
||||
'Para obter seu token de acesso para o ChatGPT \'Versão Gratuita\', faça login em',
|
||||
'Para obter seu Token de Acesso para o ChatGPT \'Versão Gratuita\', faça login em',
|
||||
com_endpoint_config_key_chatgpt_then_visit: 'então visite',
|
||||
com_endpoint_config_key_chatgpt_copy_token: 'Copie o token de acesso.',
|
||||
com_endpoint_config_key_chatgpt_copy_token: 'Copiar token de acesso.',
|
||||
com_endpoint_config_key_google_need_to: 'Você precisa',
|
||||
com_endpoint_config_key_google_vertex_ai: 'Habilitar Vertex AI',
|
||||
com_endpoint_config_key_google_vertex_ai: 'Habilitar o Vertex AI',
|
||||
com_endpoint_config_key_google_vertex_api: 'API no Google Cloud, então',
|
||||
com_endpoint_config_key_google_service_account: 'Crie uma Conta de Serviço',
|
||||
com_endpoint_config_key_google_service_account: 'Criar uma Conta de Serviço',
|
||||
com_endpoint_config_key_google_vertex_api_role:
|
||||
'Certifique-se de clicar em \'Criar e Continuar\' para dar pelo menos a função \'Usuário do Vertex AI\'. Por último, crie uma chave JSON para importar aqui.',
|
||||
com_nav_welcome_assistant: 'Por favor, Selecione um Assistente',
|
||||
com_nav_welcome_message: 'Como posso ajudar você hoje?',
|
||||
com_nav_auto_scroll: 'Auto-rolagem para o Mais Novo ao Abrir',
|
||||
com_nav_hide_panel: 'Ocultar Painel Mais à Direita',
|
||||
com_nav_modular_chat: 'Habilitar a troca de Endpoints durante a conversa',
|
||||
com_nav_latex_parsing: 'Análise de LaTeX em mensagens (pode afetar o desempenho)',
|
||||
com_nav_profile_picture: 'Foto do Perfil',
|
||||
com_nav_change_picture: 'Mudar foto',
|
||||
com_nav_auto_scroll: 'Auto-rolagem para o mais recente ao abrir',
|
||||
com_nav_hide_panel: 'Ocultar painel mais à direita',
|
||||
com_nav_enter_to_send: 'Enviar Mensagem com a Tecla Enter',
|
||||
com_nav_modular_chat: 'Habilitar troca de Endpoints durante a conversa',
|
||||
com_nav_latex_parsing: 'Analisando LaTeX nas mensagens (pode afetar o desempenho)',
|
||||
com_nav_profile_picture: 'Foto de perfil',
|
||||
com_nav_change_picture: 'Alterar foto',
|
||||
com_nav_plugin_store: 'Loja de plugins',
|
||||
com_nav_plugin_install: 'Instalar',
|
||||
com_nav_plugin_uninstall: 'Desinstalar',
|
||||
@@ -335,30 +388,32 @@ export default {
|
||||
com_nav_tool_remove: 'Remover',
|
||||
com_nav_tool_dialog: 'Ferramentas do Assistente',
|
||||
com_nav_tool_dialog_description:
|
||||
'O Assistente deve ser salvo para persistir as seleções de ferramentas.',
|
||||
com_show_agent_settings: 'Mostrar Configurações do Agente',
|
||||
com_show_completion_settings: 'Mostrar Configurações de Conclusão',
|
||||
com_hide_examples: 'Ocultar Exemplos',
|
||||
com_show_examples: 'Mostrar Exemplos',
|
||||
'O Assistente deve ser salvo para persistir nas seleções de ferramentas.',
|
||||
com_show_agent_settings: 'Mostrar configurações do agente',
|
||||
com_show_completion_settings: 'Mostrar configurações de conclusão',
|
||||
com_hide_examples: 'Ocultar exemplos',
|
||||
com_show_examples: 'Mostrar exemplos',
|
||||
com_nav_plugin_search: 'Pesquisar plugins',
|
||||
com_nav_tool_search: 'Pesquisar ferramentas',
|
||||
com_nav_plugin_auth_error:
|
||||
'Houve um erro ao tentar autenticar este plugin. Por favor, tente novamente.',
|
||||
com_nav_export_filename: 'Nome do arquivo',
|
||||
com_nav_export_filename_placeholder: 'Defina o nome do arquivo',
|
||||
com_nav_export_filename_placeholder: 'Definir o nome do arquivo',
|
||||
com_nav_export_type: 'Tipo',
|
||||
com_nav_export_include_endpoint_options: 'Incluir opções de endpoint',
|
||||
com_nav_enabled: 'Habilitado',
|
||||
com_nav_not_supported: 'Não Suportado',
|
||||
com_nav_export_all_message_branches: 'Exportar todos os ramos de mensagem',
|
||||
com_nav_export_all_message_branches: 'Exportar todos os ramos de mensagens',
|
||||
com_nav_export_recursive_or_sequential: 'Recursivo ou sequencial?',
|
||||
com_nav_export_recursive: 'Recursivo',
|
||||
com_nav_export_conversation: 'Exportar conversa',
|
||||
com_nav_my_files: 'Meus arquivos',
|
||||
com_nav_theme: 'Tema',
|
||||
com_nav_theme_system: 'Sistema',
|
||||
com_nav_theme_dark: 'Escuro',
|
||||
com_nav_theme_light: 'Claro',
|
||||
com_nav_user_name_display: 'Exibir nome de usuário nas mensagens',
|
||||
com_nav_show_code: 'Sempre mostrar código ao usar o interpretador de código',
|
||||
com_nav_clear_all_chats: 'Limpar todas as conversas',
|
||||
com_nav_confirm_clear: 'Confirmar Limpeza',
|
||||
com_nav_close_sidebar: 'Fechar barra lateral',
|
||||
|
||||
@@ -46,6 +46,8 @@ export default {
|
||||
com_assistants_update_error: 'There was an error updating your assistant.',
|
||||
com_assistants_create_success: 'Successfully created',
|
||||
com_assistants_create_error: 'There was an error creating your assistant.',
|
||||
com_ui_field_required: 'This field is required',
|
||||
com_ui_download_error: 'Error downloading file. The file may have been deleted.',
|
||||
com_ui_attach_error_type: 'Unsupported file type for endpoint:',
|
||||
com_ui_attach_error_size: 'File size limit exceeded for endpoint:',
|
||||
com_ui_attach_error:
|
||||
|
||||
@@ -3,320 +3,438 @@
|
||||
// file deepcode ignore HardcodedNonCryptoSecret: No hardcoded secrets present in this file
|
||||
|
||||
export default {
|
||||
com_files_no_results: 'Sin resultados.',
|
||||
com_files_filter: 'Filtrar archivos...',
|
||||
com_files_number_selected: '{0} de {1} archivo(s) seleccionado(s)',
|
||||
com_sidepanel_select_assistant: 'Seleccionar un Asistente',
|
||||
com_sidepanel_assistant_builder: 'Creador de Asistentes',
|
||||
com_sidepanel_hide_panel: 'Ocultar Panel',
|
||||
com_sidepanel_attach_files: 'Adjuntar Archivos',
|
||||
com_sidepanel_manage_files: 'Administrar Archivos',
|
||||
com_assistants_capabilities: 'Capacidades',
|
||||
com_assistants_knowledge: 'Conocimiento',
|
||||
com_assistants_knowledge_info:
|
||||
'Si sube archivos en Conocimiento, las conversaciones con su Asistente pueden incluir el contenido de los archivos.',
|
||||
com_assistants_knowledge_disabled:
|
||||
'El Asistente debe ser creado, y el Intérprete de Código o la Recuperación deben estar habilitados y guardados antes de subir archivos como Conocimiento.',
|
||||
com_assistants_image_vision: 'Visión de Imagen',
|
||||
com_assistants_code_interpreter: 'Intérprete de Código',
|
||||
com_assistants_code_interpreter_files:
|
||||
'Los siguientes archivos solo están disponibles para el Intérprete de Código:',
|
||||
com_assistants_retrieval: 'Recuperación',
|
||||
com_assistants_search_name: 'Buscar asistentes por nombre',
|
||||
com_assistants_tools: 'Herramientas',
|
||||
com_assistants_actions: 'Acciones',
|
||||
com_assistants_add_tools: 'Añadir Herramientas',
|
||||
com_assistants_add_actions: 'Añadir Acciones',
|
||||
com_assistants_available_actions: 'Acciones Disponibles',
|
||||
com_assistants_running_action: 'Ejecutando acción',
|
||||
com_assistants_completed_action: 'Hablé con {0}',
|
||||
com_assistants_completed_function: 'Ejecuté {0}',
|
||||
com_assistants_function_use: 'El Asistente usó {0}',
|
||||
com_assistants_domain_info: 'El Asistente envió esta información a {0}',
|
||||
com_assistants_delete_actions_success: 'Acción eliminada del Asistente con éxito',
|
||||
com_assistants_update_actions_success: 'Acción creada o actualizada con éxito',
|
||||
com_assistants_update_actions_error: 'Hubo un error al crear o actualizar la acción.',
|
||||
com_assistants_delete_actions_error: 'Hubo un error al eliminar la acción.',
|
||||
com_assistants_actions_info:
|
||||
'Permita que su Asistente recupere información o realice acciones a través de API\'s',
|
||||
com_assistants_name_placeholder: 'Opcional: El nombre del asistente',
|
||||
com_assistants_instructions_placeholder: 'Las instrucciones del sistema que utiliza el asistente',
|
||||
com_assistants_description_placeholder: 'Opcional: Describa su Asistente aquí',
|
||||
com_assistants_actions_disabled: 'Necesita crear un asistente antes de añadir acciones.',
|
||||
com_assistants_update_success: 'Actualizado con éxito',
|
||||
com_assistants_update_error: 'Hubo un error al actualizar su asistente.',
|
||||
com_assistants_create_success: 'Creado con éxito',
|
||||
com_assistants_create_error: 'Hubo un error al crear su asistente.',
|
||||
com_ui_attach_error_type: 'Tipo de archivo no admitido para el endpoint:',
|
||||
com_ui_attach_error_size: 'Se excedió el límite de tamaño de archivo para el endpoint:',
|
||||
com_ui_attach_error:
|
||||
'No se puede adjuntar el archivo. Cree o seleccione una conversación, o intente actualizar la página.',
|
||||
com_ui_examples: 'Ejemplos',
|
||||
com_ui_new_chat: 'Nuevo Chat',
|
||||
com_ui_happy_birthday: '¡Es mi primer cumpleaños!',
|
||||
com_ui_example_quantum_computing: 'Explicar la computación cuántica en términos simples',
|
||||
com_ui_example_quantum_computing: 'Explique la computación cuántica en términos sencillos',
|
||||
com_ui_example_10_year_old_b_day:
|
||||
'¿Tienes alguna idea creativa para el cumpleaños de un niño de 10 años?',
|
||||
'¿Tiene alguna idea creativa para el cumpleaños de un niño de 10 años?',
|
||||
com_ui_example_http_in_js: '¿Cómo hago una solicitud HTTP en Javascript?',
|
||||
com_ui_capabilities: 'Capacidades',
|
||||
com_ui_capability_remember: 'Recuerda lo que el usuario dijo anteriormente en la conversación',
|
||||
com_ui_capability_correction: 'Permite que el usuario proporcione correcciones de seguimiento',
|
||||
com_ui_capability_decline_requests: 'Entrenado para rechazar solicitudes inapropiadas',
|
||||
com_ui_limitations: 'Limitaciones',
|
||||
com_ui_limitation_incorrect_info: 'Puede generar ocasionalmente información incorrecta',
|
||||
com_ui_limitation_incorrect_info: 'Ocasionalmente puede generar información incorrecta',
|
||||
com_ui_limitation_harmful_biased:
|
||||
'Puede ocasionalmente producir instrucciones perjudiciales o contenido sesgado',
|
||||
com_ui_limitation_limited_2021: 'Conocimiento limitado del mundo y eventos después de 2021',
|
||||
com_ui_experimental: 'Experimental',
|
||||
'Ocasionalmente puede producir instrucciones dañinas o contenido sesgado',
|
||||
com_ui_limitation_limited_2021: 'Conocimiento limitado del mundo y eventos posteriores a 2021',
|
||||
com_ui_experimental: 'Funciones Experimentales',
|
||||
com_ui_ascending: 'Asc',
|
||||
com_ui_descending: 'Desc',
|
||||
com_ui_show_all: 'Mostrar Todo',
|
||||
com_ui_name: 'Nombre',
|
||||
com_ui_date: 'Fecha',
|
||||
com_ui_storage: 'Almacenamiento',
|
||||
com_ui_context: 'Contexto',
|
||||
com_ui_size: 'Tamaño',
|
||||
com_ui_host: 'Host',
|
||||
com_ui_update: 'Actualizar',
|
||||
com_ui_authentication: 'Autenticación',
|
||||
com_ui_instructions: 'Instrucciones',
|
||||
com_ui_description: 'Descripción',
|
||||
com_ui_error: 'Error',
|
||||
com_ui_select: 'Seleccionar',
|
||||
com_ui_input: 'Entrada',
|
||||
com_ui_close: 'Cerrar',
|
||||
com_ui_model: 'Modelo',
|
||||
com_ui_select_model: 'Selecciona un modelo',
|
||||
com_ui_use_prompt: 'Usar el prompt',
|
||||
com_ui_prev: 'Anterior',
|
||||
com_ui_next: 'Siguiente',
|
||||
com_ui_select_model: 'Seleccionar un modelo',
|
||||
com_ui_select_search_model: 'Buscar modelo por nombre',
|
||||
com_ui_select_search_plugin: 'Buscar plugin por nombre',
|
||||
com_ui_use_prompt: 'Usar prompt',
|
||||
com_ui_prev: 'Ant',
|
||||
com_ui_next: 'Sig',
|
||||
com_ui_stop: 'Detener',
|
||||
com_ui_upload_files: 'Subir archivos',
|
||||
com_ui_prompt_templates: 'Plantillas de Prompt',
|
||||
com_ui_hide_prompt_templates: 'Ocultar Plantillas de Prompt',
|
||||
com_ui_showing: 'Mostrando',
|
||||
com_ui_of: 'de',
|
||||
com_ui_entries: 'Entradas',
|
||||
com_ui_pay_per_call:
|
||||
'Todas las conversaciones de IA en un solo lugar. Paga por llamada y no por mes',
|
||||
com_ui_new_footer: 'Todas las conversaciones de IA en un solo lugar.',
|
||||
com_ui_enter: 'Entrar',
|
||||
'Todas las conversaciones de IA en un mismo lugar. Pague por llamada y no por mes',
|
||||
com_ui_new_footer: 'Todas las conversaciones de IA en un mismo lugar.',
|
||||
com_ui_enter: 'Intro',
|
||||
com_ui_submit: 'Enviar',
|
||||
com_ui_upload_success: 'Archivo cargado exitosamente',
|
||||
com_ui_upload_error: 'Hubo un error al cargar tu archivo',
|
||||
com_ui_upload_invalid:
|
||||
'Archivo no válido para cargar. Debe ser una imagen que no exceda los 2 MB',
|
||||
com_ui_upload_success: 'Archivo subido con éxito',
|
||||
com_ui_upload_error: 'Hubo un error al subir su archivo',
|
||||
com_ui_upload_invalid: 'Archivo inválido para subir. Debe ser una imagen que no exceda los 2 MB',
|
||||
com_ui_cancel: 'Cancelar',
|
||||
com_ui_save: 'Guardar',
|
||||
com_ui_save_submit: 'Guardar y Enviar',
|
||||
com_user_message: 'Tú',
|
||||
com_user_message: 'Usted',
|
||||
com_ui_copy_to_clipboard: 'Copiar al portapapeles',
|
||||
com_ui_copied_to_clipboard: 'Copiado al portapapeles',
|
||||
com_ui_regenerate: 'Regenerar',
|
||||
com_ui_continue: 'Continuar',
|
||||
com_ui_edit: 'Editar',
|
||||
com_ui_success: 'Éxito',
|
||||
com_ui_all: 'todos',
|
||||
com_ui_all: 'todas',
|
||||
com_ui_clear: 'Limpiar',
|
||||
com_ui_revoke: 'Revocar',
|
||||
com_ui_revoke_info: 'Revocar todas las credenciales proporcionadas por el usuario',
|
||||
com_ui_confirm_action: 'Confirmar Acción',
|
||||
com_ui_chats: 'chats',
|
||||
com_ui_chats: 'conversaciones',
|
||||
com_ui_avatar: 'Avatar',
|
||||
com_ui_unknown: 'Desconocido',
|
||||
com_ui_result: 'Resultado',
|
||||
com_ui_image_gen: 'Gen Imágenes',
|
||||
com_ui_assistant: 'Asistente',
|
||||
com_ui_assistants: 'Asistentes',
|
||||
com_ui_attachment: 'Adjunto',
|
||||
com_ui_assistants_output: 'Salida de Asistentes',
|
||||
com_ui_delete: 'Eliminar',
|
||||
com_ui_delete_conversation: '¿Eliminar chat?',
|
||||
com_ui_create: 'Crear',
|
||||
com_ui_delete_conversation: '¿Eliminar Chat?',
|
||||
com_ui_delete_conversation_confirm: 'Esto eliminará',
|
||||
com_ui_preview: 'Vista previa',
|
||||
com_ui_upload: 'Cargar',
|
||||
com_ui_delete_assistant_confirm:
|
||||
'¿Está seguro de que desea eliminar este Asistente? Esta acción no se puede deshacer.',
|
||||
com_ui_preview: 'Previsualizar',
|
||||
com_ui_upload: 'Subir',
|
||||
com_ui_connect: 'Conectar',
|
||||
com_ui_upload_delay:
|
||||
'La carga de "{0}" está tomando más tiempo del esperado. Espere mientras el archivo termina de indexarse para su recuperación.',
|
||||
com_ui_privacy_policy: 'Política de privacidad',
|
||||
com_ui_terms_of_service: 'Términos de servicio',
|
||||
com_auth_error_login:
|
||||
'No se pudo iniciar sesión con la información proporcionada. Verifica tus credenciales e inténtalo de nuevo.',
|
||||
'No se puede iniciar sesión con la información proporcionada. Verifique sus credenciales y vuelva a intentarlo.',
|
||||
com_auth_error_login_rl:
|
||||
'Demasiados intentos de inicio de sesión en un corto período de tiempo. Por favor, inténtalo nuevamente más tarde.',
|
||||
'Demasiados intentos de inicio de sesión en un corto período de tiempo. Inténtelo de nuevo más tarde.',
|
||||
com_auth_error_login_ban:
|
||||
'Tu cuenta ha sido temporalmente suspendida debido a violaciones de nuestro servicio.',
|
||||
'Su cuenta ha sido bloqueada temporalmente debido a violaciones de nuestro servicio.',
|
||||
com_auth_error_login_server:
|
||||
'Hubo un error interno del servidor. Por favor, espera unos momentos e inténtalo de nuevo.',
|
||||
com_auth_no_account: '¿No tienes una cuenta?',
|
||||
com_auth_sign_up: 'Registrarse',
|
||||
'Hubo un error interno del servidor. Espere unos momentos y vuelva a intentarlo.',
|
||||
com_auth_no_account: '¿No tiene una cuenta?',
|
||||
com_auth_sign_up: 'Regístrese',
|
||||
com_auth_sign_in: 'Iniciar sesión',
|
||||
com_auth_google_login: 'Iniciar sesión con Google',
|
||||
com_auth_facebook_login: 'Iniciar sesión con Facebook',
|
||||
com_auth_github_login: 'Iniciar sesión con Github',
|
||||
com_auth_discord_login: 'Iniciar sesión con Discord',
|
||||
com_auth_google_login: 'Continuar con Google',
|
||||
com_auth_facebook_login: 'Continuar con Facebook',
|
||||
com_auth_github_login: 'Continuar con Github',
|
||||
com_auth_discord_login: 'Continuar con Discord',
|
||||
com_auth_email: 'Correo electrónico',
|
||||
com_auth_email_required: 'El correo electrónico es obligatorio',
|
||||
com_auth_email_required: 'Se requiere correo electrónico',
|
||||
com_auth_email_min_length: 'El correo electrónico debe tener al menos 6 caracteres',
|
||||
com_auth_email_max_length: 'El correo electrónico no debe tener más de 120 caracteres',
|
||||
com_auth_email_pattern: 'Debes ingresar una dirección de correo electrónico válida',
|
||||
com_auth_email_pattern: 'Debe ingresar una dirección de correo electrónico válida',
|
||||
com_auth_email_address: 'Dirección de correo electrónico',
|
||||
com_auth_password: 'Contraseña',
|
||||
com_auth_password_required: 'La contraseña es obligatoria',
|
||||
com_auth_password_required: 'Se requiere contraseña',
|
||||
com_auth_password_min_length: 'La contraseña debe tener al menos 8 caracteres',
|
||||
com_auth_password_max_length: 'La contraseña debe tener menos de 128 caracteres',
|
||||
com_auth_password_forgot: '¿Olvidaste la contraseña?',
|
||||
com_auth_password_forgot: '¿Olvidó su contraseña?',
|
||||
com_auth_password_confirm: 'Confirmar contraseña',
|
||||
com_auth_password_not_match: 'Las contraseñas no coinciden',
|
||||
com_auth_continue: 'Continuar',
|
||||
com_auth_create_account: 'Crear tu cuenta',
|
||||
com_auth_error_create:
|
||||
'Hubo un error al intentar registrar tu cuenta. Por favor, inténtalo nuevamente.',
|
||||
com_auth_create_account: 'Crear su cuenta',
|
||||
com_auth_error_create: 'Hubo un error al intentar registrar su cuenta. Inténtelo de nuevo.',
|
||||
com_auth_full_name: 'Nombre completo',
|
||||
com_auth_name_required: 'El nombre es obligatorio',
|
||||
com_auth_name_required: 'Se requiere nombre',
|
||||
com_auth_name_min_length: 'El nombre debe tener al menos 3 caracteres',
|
||||
com_auth_name_max_length: 'El nombre debe tener menos de 80 caracteres',
|
||||
com_auth_username: 'Nombre de usuario (opcional)',
|
||||
com_auth_username_required: 'El nombre de usuario es obligatorio',
|
||||
com_auth_username_required: 'Se requiere nombre de usuario',
|
||||
com_auth_username_min_length: 'El nombre de usuario debe tener al menos 2 caracteres',
|
||||
com_auth_username_max_length: 'El nombre de usuario debe tener menos de 20 caracteres',
|
||||
com_auth_already_have_account: '¿Ya tienes una cuenta?',
|
||||
com_auth_already_have_account: '¿Ya tiene una cuenta?',
|
||||
com_auth_login: 'Iniciar sesión',
|
||||
com_auth_reset_password: 'Restablecer tu contraseña',
|
||||
com_auth_click: 'Haz clic',
|
||||
com_auth_reset_password: 'Restablecer su contraseña',
|
||||
com_auth_click: 'Haga clic',
|
||||
com_auth_here: 'AQUÍ',
|
||||
com_auth_to_reset_your_password: 'para restablecer tu contraseña.',
|
||||
com_auth_to_reset_your_password: 'para restablecer su contraseña.',
|
||||
com_auth_reset_password_link_sent: 'Correo electrónico enviado',
|
||||
com_auth_reset_password_email_sent:
|
||||
'Se ha enviado un correo electrónico con más instrucciones para restablecer tu contraseña.',
|
||||
'Se le ha enviado un correo electrónico con más instrucciones para restablecer su contraseña.',
|
||||
com_auth_error_reset_password:
|
||||
'Hubo un problema al restablecer tu contraseña. No se encontró ningún usuario con la dirección de correo electrónico proporcionada. Por favor, inténtalo de nuevo.',
|
||||
com_auth_reset_password_success: 'Restablecimiento de contraseña exitoso',
|
||||
com_auth_login_with_new_password: 'Ahora puedes iniciar sesión con tu nueva contraseña.',
|
||||
'Hubo un problema al restablecer su contraseña. No se encontró ningún usuario con la dirección de correo electrónico proporcionada. Inténtelo de nuevo.',
|
||||
com_auth_reset_password_success: 'Éxito al restablecer la contraseña',
|
||||
com_auth_login_with_new_password: 'Ahora puede iniciar sesión con su nueva contraseña.',
|
||||
com_auth_error_invalid_reset_token:
|
||||
'Este token de restablecimiento de contraseña ya no es válido.',
|
||||
com_auth_click_here: 'Haz clic aquí',
|
||||
com_auth_to_try_again: 'para intentarlo de nuevo.',
|
||||
com_auth_to_try_again: 'para intentar de nuevo.',
|
||||
com_auth_submit_registration: 'Enviar registro',
|
||||
com_auth_welcome_back: 'Bienvenido de nuevo',
|
||||
com_auth_back_to_login: 'Volver al inicio de sesión',
|
||||
com_endpoint_open_menu: 'Abrir menú',
|
||||
com_endpoint_bing_enable_sydney: 'Habilitar Sydney',
|
||||
com_endpoint_bing_to_enable_sydney: 'Para habilitar Sydney',
|
||||
com_endpoint_bing_jailbreak: 'Jailbreak',
|
||||
com_endpoint_bing_context_placeholder:
|
||||
'Bing puede usar hasta 7k tokens para "contexto", que puede referenciar en la conversación. El límite específico no se conoce, pero puede haber errores al superar los 7k tokens',
|
||||
'Bing puede utilizar hasta 7k tokens para el \'contexto\', al que puede hacer referencia en la conversación. El límite específico no se conoce, pero puede producir errores si se exceden los 7k tokens',
|
||||
com_endpoint_bing_system_message_placeholder:
|
||||
'ADVERTENCIA: El uso indebido de esta función puede resultar en una PROHIBICIÓN del uso de Bing. Haga clic en \'System Message\' para obtener instrucciones completas y el mensaje predeterminado si se omite, que es el ajuste preestablecido \'Sydney\', considerado seguro.',
|
||||
com_endpoint_system_message: 'Mensaje del Sistema',
|
||||
com_endpoint_message: 'Conversar con',
|
||||
com_endpoint_message_not_appendable: 'Edita tu mensaje o Regenera.',
|
||||
'ADVERTENCIA: El mal uso de esta función puede hacer que te PROHÍBAN el uso de Bing. Haz clic en \'Mensaje del sistema\' para ver las instrucciones completas y el mensaje predeterminado si se omite, que es la configuración preestablecida \'Sydney\' que se considera segura.',
|
||||
com_endpoint_system_message: 'Mensaje del sistema',
|
||||
com_endpoint_message: 'Mensaje',
|
||||
com_endpoint_message_not_appendable: 'Edita tu mensaje o regénera.',
|
||||
com_endpoint_default_blank: 'predeterminado: en blanco',
|
||||
com_endpoint_default_false: 'predeterminado: falso',
|
||||
com_endpoint_default_creative: 'predeterminado: creativo',
|
||||
com_endpoint_default_empty: 'predeterminado: vacío',
|
||||
com_endpoint_default_with_num: 'predeterminado: {0}',
|
||||
com_endpoint_context: 'Contexto',
|
||||
com_endpoint_tone_style: 'Estilo de Tono',
|
||||
com_endpoint_token_count: 'Conteo de Tokens',
|
||||
com_endpoint_tone_style: 'Estilo de tono',
|
||||
com_endpoint_token_count: 'Recuento de tokens',
|
||||
com_endpoint_output: 'Salida',
|
||||
com_endpoint_google_temp:
|
||||
'Valores más altos = más aleatorio, mientras que valores más bajos = más enfocado y determinista. Recomendamos cambiar esto o Top P, pero no ambos.',
|
||||
'Los valores más altos = más aleatorios, mientras que los valores más bajos = más enfocados y deterministas. Recomendamos alterar esto o Top P, pero no ambos.',
|
||||
com_endpoint_google_topp:
|
||||
'Top-p cambia cómo el modelo selecciona tokens para la salida. Los tokens se seleccionan desde el más K (ver el parámetro topK) probable hasta el menos, hasta que la suma de sus probabilidades sea igual al valor de top-p.',
|
||||
'Top-p cambia la forma en que el modelo selecciona tokens para la salida. Los tokens se seleccionan desde los más K (ver parámetro topK) probables hasta los menos probables hasta que la suma de sus probabilidades sea igual al valor top-p.',
|
||||
com_endpoint_google_topk:
|
||||
'Top-k cambia cómo el modelo selecciona tokens para la salida. Un top-k de 1 significa que el token seleccionado es el más probable entre todos los tokens en el vocabulario del modelo (también llamado decodificación codiciosa), mientras que un top-k de 3 significa que el próximo token se selecciona entre los 3 tokens más probables (usando temperatura).',
|
||||
'Top-k cambia la forma en que el modelo selecciona tokens para la salida. Un top-k de 1 significa que el token seleccionado es el más probable entre todos los tokens en el vocabulario del modelo (también llamado decodificación codiciosa), mientras que un top-k de 3 significa que el siguiente token se selecciona entre los 3 tokens más probables (usando temperatura).',
|
||||
com_endpoint_google_maxoutputtokens:
|
||||
'Número máximo de tokens que se pueden generar en la respuesta. Especifica un valor menor para respuestas más cortas y un valor mayor para respuestas más largas.',
|
||||
com_endpoint_google_custom_name_placeholder: 'Establece un nombre personalizado para Google',
|
||||
'Número máximo de tokens que se pueden generar en la respuesta. Especifique un valor más bajo para respuestas más cortas y un valor más alto para respuestas más largas.',
|
||||
com_endpoint_google_custom_name_placeholder: 'Establecer un nombre personalizado para Google',
|
||||
com_endpoint_prompt_prefix_placeholder:
|
||||
'Establece instrucciones personalizadas o contexto. Ignorado si está vacío.',
|
||||
com_endpoint_custom_name: 'Nombre Personalizado',
|
||||
com_endpoint_prompt_prefix: 'Prefijo del Prompt',
|
||||
'Configurar instrucciones personalizadas o contexto. Se ignora si está vacío.',
|
||||
com_endpoint_instructions_assistants_placeholder:
|
||||
'Anula las instrucciones del asistente. Esto es útil para modificar el comportamiento por ejecución.',
|
||||
com_endpoint_prompt_prefix_assistants_placeholder:
|
||||
'Establecer instrucciones o contexto adicionales además de las instrucciones principales del Asistente. Se ignora si está vacío.',
|
||||
com_endpoint_custom_name: 'Nombre personalizado',
|
||||
com_endpoint_prompt_prefix: 'Instrucciones personalizadas',
|
||||
com_endpoint_prompt_prefix_assistants: 'Instrucciones adicionales',
|
||||
com_endpoint_instructions_assistants: 'Anular instrucciones',
|
||||
com_endpoint_temperature: 'Temperatura',
|
||||
com_endpoint_default: 'predeterminado',
|
||||
com_endpoint_top_p: 'Top P',
|
||||
com_endpoint_top_k: 'Top K',
|
||||
com_endpoint_max_output_tokens: 'Máximo de Tokens de Salida',
|
||||
com_endpoint_max_output_tokens: 'Tokens de Salida Máximos',
|
||||
com_endpoint_openai_temp:
|
||||
'Valores más altos = más aleatorio, mientras que valores más bajos = más enfocado y determinista. Recomendamos cambiar esto o Top P, pero no ambos.',
|
||||
'Los valores más altos = más aleatorios, mientras que los valores más bajos = más enfocados y deterministas. Recomendamos alterar esto o Top P, pero no ambos.',
|
||||
com_endpoint_openai_max:
|
||||
'Tokens máximos para generar. La longitud total de los tokens de entrada y los tokens generados está limitada por la longitud del contexto del modelo.',
|
||||
'Los tokens máximos a generar. La longitud total de los tokens de entrada y los tokens generados está limitada por la longitud del contexto del modelo.',
|
||||
com_endpoint_openai_topp:
|
||||
'Una alternativa para el muestreo con temperatura, llamada muestreo de núcleo, donde el modelo considera los resultados de los tokens con masa de probabilidad top_p. Entonces, 0.1 significa que solo se consideran los tokens que comprenden la masa de probabilidad del 10% superior. Recomendamos cambiar esto o la temperatura, pero no ambos.',
|
||||
'Una alternativa al muestreo con temperatura, llamada muestreo de núcleo, donde el modelo considera los resultados de los tokens con la masa de probabilidad superior al top_p. Entonces, 0.1 significa que solo se consideran los tokens que comprenden la masa de probabilidad superior al 10%. Recomendamos alterar esto o la temperatura, pero no ambos.',
|
||||
com_endpoint_openai_freq:
|
||||
'Número entre -2.0 y 2.0. Los valores positivos penalizan nuevos tokens en función de su frecuencia existente en el texto hasta ahora, disminuyendo la probabilidad de que el modelo repita la misma línea literalmente.',
|
||||
'Número entre -2.0 y 2.0. Los valores positivos penalizan los nuevos tokens basados en su frecuencia existente en el texto hasta el momento, disminuyendo la probabilidad del modelo de repetir la misma línea textualmente.',
|
||||
com_endpoint_openai_pres:
|
||||
'Número entre -2.0 y 2.0. Los valores positivos penalizan nuevos tokens en función de si aparecen en el texto hasta ahora, aumentando la probabilidad de que el modelo hable sobre nuevos temas.',
|
||||
com_endpoint_openai_custom_name_placeholder: 'Establece un nombre personalizado para ChatGPT',
|
||||
'Número entre -2.0 y 2.0. Los valores positivos penalizan los nuevos tokens basados en si aparecen o no en el texto hasta el momento, aumentando la probabilidad del modelo de hablar sobre nuevos temas.',
|
||||
com_endpoint_openai_resend:
|
||||
'Reenviar todas las imágenes adjuntas previamente. Nota: esto puede aumentar significativamente el costo de tokens y puede experimentar errores con muchos archivos adjuntos de imágenes.',
|
||||
com_endpoint_openai_resend_files:
|
||||
'Reenviar todos los archivos adjuntos anteriormente. Nota: esto aumentará el costo de tokens y puede experimentar errores con muchos archivos adjuntos.',
|
||||
com_endpoint_openai_detail:
|
||||
'La resolución para las solicitudes de Vision. "Baja" es más económica y rápida, "Alta" es más detallada y costosa, y "Automática" elegirá automáticamente entre las dos en función de la resolución de la imagen.',
|
||||
com_endpoint_openai_custom_name_placeholder: 'Establecer un nombre personalizado para ChatGPT',
|
||||
com_endpoint_openai_prompt_prefix_placeholder:
|
||||
'Establece instrucciones personalizadas para incluir en el Mensaje del Sistema. Predeterminado: ninguno',
|
||||
'Establecer instrucciones personalizadas para incluir en el Mensaje del sistema. Predeterminado: ninguno',
|
||||
com_endpoint_anthropic_temp:
|
||||
'Varía de 0 a 1. Usa temp más cercano a 0 para analítico / opción múltiple, y más cercano a 1 para tareas creativas y generativas. Recomendamos cambiar esto o Top P, pero no ambos.',
|
||||
'Rango de 0 a 1. Utilice una temperatura más cercana a 0 para tareas analíticas/de opción múltiple y más cercana a 1 para tareas creativas y generativas. Recomendamos alterar esto o Top P, pero no ambos.',
|
||||
com_endpoint_anthropic_topp:
|
||||
'Top-p cambia cómo el modelo selecciona tokens para la salida. Los tokens se seleccionan desde el más K (ver el parámetro topK) probable hasta el menos, hasta que la suma de sus probabilidades sea igual al valor de top-p.',
|
||||
'Top-p cambia la forma en que el modelo selecciona tokens para la salida. Los tokens se seleccionan desde los más K (ver parámetro topK) probables hasta los menos probables hasta que la suma de sus probabilidades sea igual al valor top-p.',
|
||||
com_endpoint_anthropic_topk:
|
||||
'Top-k cambia cómo el modelo selecciona tokens para la salida. Un top-k de 1 significa que el token seleccionado es el más probable entre todos los tokens en el vocabulario del modelo (también llamado decodificación codiciosa), mientras que un top-k de 3 significa que el próximo token se selecciona entre los 3 tokens más probables (usando temperatura).',
|
||||
'Top-k cambia la forma en que el modelo selecciona tokens para la salida. Un top-k de 1 significa que el token seleccionado es el más probable entre todos los tokens en el vocabulario del modelo (también llamado decodificación codiciosa), mientras que un top-k de 3 significa que el siguiente token se selecciona entre los 3 tokens más probables (usando temperatura).',
|
||||
com_endpoint_anthropic_maxoutputtokens:
|
||||
'Número máximo de tokens que se pueden generar en la respuesta. Especifica un valor menor para respuestas más cortas y un valor mayor para respuestas más largas.',
|
||||
'Número máximo de tokens que se pueden generar en la respuesta. Especifique un valor más bajo para respuestas más cortas y un valor más alto para respuestas más largas.',
|
||||
com_endpoint_anthropic_custom_name_placeholder:
|
||||
'Establece un nombre personalizado para Anthropic',
|
||||
com_endpoint_frequency_penalty: 'Penalización de Frecuencia',
|
||||
com_endpoint_plug_use_functions: 'Usar Funciones',
|
||||
com_endpoint_plug_skip_completion: 'Omitir Completado',
|
||||
com_endpoint_disabled_with_tools: 'desactivado con herramientas',
|
||||
com_endpoint_disabled_with_tools_placeholder: 'Desactivado con Herramientas Seleccionadas',
|
||||
'Establecer un nombre personalizado para Anthropic',
|
||||
com_endpoint_frequency_penalty: 'Penalización de frecuencia',
|
||||
com_endpoint_presence_penalty: 'Penalización de presencia',
|
||||
com_endpoint_plug_use_functions: 'Utilizar funciones',
|
||||
com_endpoint_plug_resend_files: 'Reenviar archivos',
|
||||
com_endpoint_plug_resend_images: 'Reenviar imágenes',
|
||||
com_endpoint_plug_image_detail: 'Detalle de imagen',
|
||||
com_endpoint_plug_skip_completion: 'Omitir finalización',
|
||||
com_endpoint_disabled_with_tools: 'deshabilitado con herramientas',
|
||||
com_endpoint_disabled_with_tools_placeholder: 'Deshabilitado con herramientas seleccionadas',
|
||||
com_endpoint_plug_set_custom_instructions_for_gpt_placeholder:
|
||||
'Establecer instrucciones personalizadas para incluir en el Mensaje del Sistema. Predeterminado: ninguno',
|
||||
'Establecer instrucciones personalizadas para incluir en el Mensaje del sistema. Predeterminado: ninguno',
|
||||
com_endpoint_import: 'Importar',
|
||||
com_endpoint_set_custom_name:
|
||||
'Establecer un nombre personalizado, en caso de que quieras encontrar este preset',
|
||||
com_endpoint_preset_delete_confirm: '¿Estás seguro de que quieres eliminar este preset?',
|
||||
com_endpoint_preset_clear_all_confirm: '¿Estás seguro de que quieres eliminar todos tus presets?',
|
||||
com_endpoint_preset_import: 'Preset Importado!',
|
||||
'Establece un nombre personalizado, en caso de que puedas encontrar esta configuración preestablecida',
|
||||
com_endpoint_preset_delete_confirm:
|
||||
'¿Estás seguro de que quieres eliminar esta configuración preestablecida?',
|
||||
com_endpoint_preset_clear_all_confirm:
|
||||
'¿Estás seguro de que quieres eliminar todas tus configuraciones preestablecidas?',
|
||||
com_endpoint_preset_import: '¡Configuración preestablecida importada!',
|
||||
com_endpoint_preset_import_error:
|
||||
'Hubo un error al importar tu preset. Por favor, inténtalo de nuevo.',
|
||||
'Hubo un error al importar tu configuración preestablecida. Por favor, inténtalo de nuevo.',
|
||||
com_endpoint_preset_save_error:
|
||||
'Hubo un error al guardar tu preset. Por favor, inténtalo de nuevo.',
|
||||
'Hubo un error al guardar tu configuración preestablecida. Por favor, inténtalo de nuevo.',
|
||||
com_endpoint_preset_delete_error:
|
||||
'Hubo un error al eliminar tu preset. Por favor, inténtalo de nuevo.',
|
||||
com_endpoint_preset_default_removed: 'ya no es el preset predeterminado.',
|
||||
'Hubo un error al eliminar tu configuración preestablecida. Por favor, inténtalo de nuevo.',
|
||||
com_endpoint_preset_default_removed: 'ya no es la configuración preestablecida predeterminada.',
|
||||
com_endpoint_preset_default_item: 'Predeterminado:',
|
||||
com_endpoint_preset_default_none: 'Ningún preset predeterminado activo.',
|
||||
com_endpoint_preset_title: 'Preset',
|
||||
com_endpoint_preset_saved: 'Guardado!',
|
||||
com_endpoint_preset_default: 'es ahora el preset predeterminado.',
|
||||
com_endpoint_preset: 'preset',
|
||||
com_endpoint_presets: 'presets',
|
||||
com_endpoint_preset_selected: 'Preset Activo!',
|
||||
com_endpoint_preset_selected_title: 'Activo!',
|
||||
com_endpoint_preset_name: 'Nombre del Preset',
|
||||
com_endpoint_new_topic: 'Nuevo Tema',
|
||||
com_endpoint_preset_default_none: 'No hay configuración preestablecida predeterminada activa.',
|
||||
com_endpoint_preset_title: 'Configuración preestablecida',
|
||||
com_endpoint_preset_saved: '¡Guardado!',
|
||||
com_endpoint_preset_default: 'es ahora la configuración preestablecida predeterminada.',
|
||||
com_endpoint_preset: 'configuración preestablecida',
|
||||
com_endpoint_presets: 'configuraciones preestablecidas',
|
||||
com_endpoint_preset_selected: '¡Configuración preestablecida activa!',
|
||||
com_endpoint_preset_selected_title: '¡Activo!',
|
||||
com_endpoint_preset_name: 'Nombre de la configuración preestablecida',
|
||||
com_endpoint_new_topic: 'Nuevo tema',
|
||||
com_endpoint: 'Endpoint',
|
||||
com_endpoint_hide: 'Ocultar',
|
||||
com_endpoint_show: 'Mostrar',
|
||||
com_endpoint_examples: ' Presets',
|
||||
com_endpoint_completion: 'Completado',
|
||||
com_endpoint_examples: ' Configuraciones preestablecidas',
|
||||
com_endpoint_completion: 'Finalización',
|
||||
com_endpoint_agent: 'Agente',
|
||||
com_endpoint_show_what_settings: 'Mostrar Configuraciones de {0}',
|
||||
com_endpoint_save: 'Guardar',
|
||||
com_endpoint_show_what_settings: 'Mostrar configuración de {0}',
|
||||
com_endpoint_export: 'Exportar',
|
||||
com_endpoint_save_as_preset: 'Guardar como Preset',
|
||||
com_endpoint_assistant: 'Asistente',
|
||||
com_endpoint_use_active_assistant: 'Utilizar asistente activo',
|
||||
com_endpoint_assistant_model: 'Modelo de asistente',
|
||||
com_endpoint_save_as_preset: 'Guardar como configuración preestablecida',
|
||||
com_endpoint_presets_clear_warning:
|
||||
'¿Estás seguro de que quieres borrar todos los presets? Esto es irreversible.',
|
||||
'¿Estás seguro de que quieres borrar todas las configuraciones preestablecidas? Esto es irreversible.',
|
||||
com_endpoint_not_implemented: 'No implementado',
|
||||
com_endpoint_no_presets: 'Aún no hay presets, utiliza el botón de configuración para crear uno',
|
||||
com_endpoint_not_available: 'Ningún endpoint disponible',
|
||||
com_endpoint_view_options: 'Ver Opciones',
|
||||
com_endpoint_save_convo_as_preset: 'Guardar Conversación como Preset',
|
||||
com_endpoint_my_preset: 'Mi Preset',
|
||||
com_endpoint_agent_model: 'Modelo del Agente (Recomendado: GPT-3.5)',
|
||||
com_endpoint_completion_model: 'Modelo de Completado (Recomendado: GPT-4)',
|
||||
com_endpoint_func_hover: 'Habilitar uso de Plugins como Funciones OpenAI',
|
||||
com_endpoint_no_presets:
|
||||
'Aún no hay configuraciones preestablecidas, utiliza el botón de configuración para crear una',
|
||||
com_endpoint_not_available: 'No hay endpoint disponible',
|
||||
com_endpoint_view_options: 'Ver opciones',
|
||||
com_endpoint_save_convo_as_preset: 'Guardar conversación como configuración preestablecida',
|
||||
com_endpoint_my_preset: 'Mi configuración preestablecida',
|
||||
com_endpoint_agent_model: 'Modelo de agente (Recomendado: GPT-3.5)',
|
||||
com_endpoint_completion_model: 'Modelo de finalización (Recomendado: GPT-4)',
|
||||
com_endpoint_func_hover: 'Habilitar el uso de Plugins como funciones de OpenAI',
|
||||
com_endpoint_skip_hover:
|
||||
'Habilitar la etapa de salto de completado, que revisa la respuesta final y las etapas generadas',
|
||||
com_endpoint_config_key: 'Establecer Clave API',
|
||||
com_endpoint_config_placeholder: 'Establece tu Clave en el menú Cabecera para conversar.',
|
||||
com_endpoint_config_key_for: 'Establecer Clave API para',
|
||||
'Habilitar omitir el paso de finalización, que revisa la respuesta final y los pasos generados',
|
||||
com_endpoint_config_key: 'Establecer clave API',
|
||||
com_endpoint_assistant_placeholder:
|
||||
'Por favor, seleccione un Asistente desde el panel lateral derecho',
|
||||
com_endpoint_config_placeholder: 'Establezca su clave en el menú del encabezado para chatear.',
|
||||
com_endpoint_config_key_for: 'Establecer clave API para',
|
||||
com_endpoint_config_key_name: 'Clave',
|
||||
com_endpoint_config_value: 'Insertar valor para',
|
||||
com_endpoint_config_key_name_placeholder: 'Establece la clave API primero',
|
||||
com_endpoint_config_value: 'Ingresar valor para',
|
||||
com_endpoint_config_key_name_placeholder: 'Establezca primero la clave API',
|
||||
com_endpoint_config_key_encryption: 'Tu clave será encriptada y eliminada en',
|
||||
com_endpoint_config_key_expiry: 'el tiempo de expiración',
|
||||
com_endpoint_config_click_here: 'Haz clic Aquí',
|
||||
com_endpoint_config_google_service_key: 'Clave de la Cuenta de Servicio de Google',
|
||||
com_endpoint_config_click_here: 'Haz clic aquí',
|
||||
com_endpoint_config_google_service_key: 'Clave de cuenta de servicio de Google',
|
||||
com_endpoint_config_google_cloud_platform: '(de Google Cloud Platform)',
|
||||
com_endpoint_config_google_api_key: 'Clave API de Google',
|
||||
com_endpoint_config_google_gemini_api: '(API Gemini)',
|
||||
com_endpoint_config_google_api_info:
|
||||
'Para obtener tu clave API de Lenguaje Generativo (para Gemini),',
|
||||
com_endpoint_config_key_import_json_key: 'Importar Clave JSON de la Cuenta de Servicio.',
|
||||
'Para obtener tu clave de la API de Lenguaje Generativo (para Gemini),',
|
||||
com_endpoint_config_key_import_json_key: 'Importar clave JSON de cuenta de servicio.',
|
||||
com_endpoint_config_key_import_json_key_success:
|
||||
'Clave JSON de la Cuenta de Servicio importada con éxito',
|
||||
'Clave JSON de cuenta de servicio importada correctamente',
|
||||
com_endpoint_config_key_import_json_key_invalid:
|
||||
'Clave JSON de la Cuenta de Servicio inválida, ¿importaste el archivo correcto?',
|
||||
'Clave JSON de cuenta de servicio no válida, ¿importaste el archivo correcto?',
|
||||
com_endpoint_config_key_get_edge_key:
|
||||
'Para obtener tu token de acceso para Bing, inicia sesión en',
|
||||
com_endpoint_config_key_get_edge_key_dev_tool:
|
||||
'Utiliza las herramientas de desarrollo o una extensión mientras estás conectado al sitio para copiar el contenido de la cookie _U. Si esto falla, sigue estas',
|
||||
'Utiliza las herramientas de desarrollador o una extensión mientras estás conectado al sitio para copiar el contenido de la cookie _U. Si esto falla, sigue estas',
|
||||
com_endpoint_config_key_edge_instructions: 'instrucciones',
|
||||
com_endpoint_config_key_edge_full_key_string:
|
||||
'para proporcionar las cadenas completas de la cookie.',
|
||||
'para proporcionar las cadenas de cookies completas.',
|
||||
com_endpoint_config_key_chatgpt:
|
||||
'Para obtener tu token de acceso para ChatGPT \'Versión Gratuita\', inicia sesión en',
|
||||
com_endpoint_config_key_chatgpt_then_visit: 'luego visita',
|
||||
'Para obtener tu token de acceso para ChatGPT \'Versión gratuita\', inicia sesión en',
|
||||
com_endpoint_config_key_chatgpt_then_visit: 'y luego visita',
|
||||
com_endpoint_config_key_chatgpt_copy_token: 'Copia el token de acceso.',
|
||||
com_endpoint_config_key_google_need_to: 'Necesitas',
|
||||
com_endpoint_config_key_google_vertex_ai: 'Habilitar Vertex AI',
|
||||
com_endpoint_config_key_google_vertex_ai: 'Habilitar el Vertex AI',
|
||||
com_endpoint_config_key_google_vertex_api: 'API en Google Cloud, luego',
|
||||
com_endpoint_config_key_google_service_account: 'Crea una Cuenta de Servicio',
|
||||
com_endpoint_config_key_google_service_account: 'Crear una Cuenta de Servicio',
|
||||
com_endpoint_config_key_google_vertex_api_role:
|
||||
'Asegúrate de hacer clic en \'Crear y Continuar\' para dar al menos el rol \'Usuario de Vertex AI\'. Finalmente, crea una clave JSON para importar aquí.',
|
||||
com_nav_welcome_message: '¿Cómo puedo ayudarte hoy?',
|
||||
com_nav_auto_scroll: 'Desplazamiento Automático al Más Nuevo al Abrir',
|
||||
com_nav_modular_chat: 'Activar el cambio de Endpoints en medio de la conversación',
|
||||
com_nav_profile_picture: 'Foto de Perfil',
|
||||
com_nav_change_picture: 'Cambiar foto',
|
||||
'Asegúrate de hacer clic en \'Crear y continuar\' para otorgar al menos el rol de \'Usuario de Vertex AI\'. Por último, crea una clave JSON para importar aquí.',
|
||||
com_nav_welcome_assistant: 'Por favor, selecciona un asistente',
|
||||
com_nav_welcome_message: '¿En qué puedo ayudarte hoy?',
|
||||
com_nav_auto_scroll: 'Desplazamiento automático al más reciente al abrir',
|
||||
com_nav_hide_panel: 'Ocultar el panel lateral derecho',
|
||||
com_nav_enter_to_send: 'Enviar mensaje con la tecla Enter',
|
||||
com_nav_modular_chat: 'Habilitar el cambio de puntos finales en medio de una conversación',
|
||||
com_nav_latex_parsing: 'Analizar LaTeX en los mensajes (puede afectar el rendimiento)',
|
||||
com_nav_profile_picture: 'Imagen de perfil',
|
||||
com_nav_change_picture: 'Cambiar imagen',
|
||||
com_nav_plugin_store: 'Tienda de plugins',
|
||||
com_show_agent_settings: 'Mostrar Configuraciones del Agente',
|
||||
com_show_completion_settings: 'Mostrar Configuraciones de Completado',
|
||||
com_hide_examples: 'Ocultar Ejemplos',
|
||||
com_show_examples: 'Mostrar Ejemplos',
|
||||
com_nav_plugin_install: 'Instalar',
|
||||
com_nav_plugin_uninstall: 'Desinstalar',
|
||||
com_nav_tool_add: 'Agregar',
|
||||
com_nav_tool_remove: 'Eliminar',
|
||||
com_nav_tool_dialog: 'Herramientas del asistente',
|
||||
com_nav_tool_dialog_description:
|
||||
'El asistente debe guardarse para que las selecciones de herramientas persistan.',
|
||||
com_show_agent_settings: 'Mostrar configuración del agente',
|
||||
com_show_completion_settings: 'Mostrar configuración de completado',
|
||||
com_hide_examples: 'Ocultar ejemplos',
|
||||
com_show_examples: 'Mostrar ejemplos',
|
||||
com_nav_plugin_search: 'Buscar plugins',
|
||||
com_nav_tool_search: 'Buscar herramientas',
|
||||
com_nav_plugin_auth_error:
|
||||
'Hubo un error al intentar autenticar este plugin. Por favor, inténtalo de nuevo.',
|
||||
com_nav_export_filename: 'Nombre del archivo',
|
||||
com_nav_export_filename_placeholder: 'Establecer el nombre del archivo',
|
||||
com_nav_export_filename: 'Nombre de archivo',
|
||||
com_nav_export_filename_placeholder: 'Establecer el nombre de archivo',
|
||||
com_nav_export_type: 'Tipo',
|
||||
com_nav_export_include_endpoint_options: 'Incluir opciones de endpoint',
|
||||
com_nav_export_include_endpoint_options: 'Incluir opciones de punto final',
|
||||
com_nav_enabled: 'Habilitado',
|
||||
com_nav_not_supported: 'No soportado',
|
||||
com_nav_export_all_message_branches: 'Exportar todas las ramas de mensajes',
|
||||
com_nav_export_recursive_or_sequential: '¿Recursivo o secuencial?',
|
||||
com_nav_export_recursive: 'Recursivo',
|
||||
com_nav_export_conversation: 'Exportar conversación',
|
||||
com_nav_my_files: 'Mis archivos',
|
||||
com_nav_theme: 'Tema',
|
||||
com_nav_theme_system: 'Sistema',
|
||||
com_nav_theme_dark: 'Oscuro',
|
||||
com_nav_theme_light: 'Claro',
|
||||
com_nav_user_name_display: 'Mostrar nombre de usuario en los mensajes',
|
||||
com_nav_clear_all_chats: 'Limpiar todos los chats',
|
||||
com_nav_confirm_clear: 'Confirmar Limpieza',
|
||||
com_nav_show_code: 'Mostrar siempre el código cuando se use el intérprete de código',
|
||||
com_nav_clear_all_chats: 'Borrar todos los chats',
|
||||
com_nav_confirm_clear: 'Confirmar borrado',
|
||||
com_nav_close_sidebar: 'Cerrar barra lateral',
|
||||
com_nav_open_sidebar: 'Abrir barra lateral',
|
||||
com_nav_send_message: 'Enviar mensaje',
|
||||
com_nav_log_out: 'Cerrar sesión',
|
||||
com_nav_user: 'USUARIO',
|
||||
com_nav_clear_conversation: 'Limpiar conversaciones',
|
||||
com_nav_clear_conversation: 'Borrar conversaciones',
|
||||
com_nav_clear_conversation_confirm_message:
|
||||
'¿Estás seguro de que quieres limpiar todas las conversaciones? Esto es irreversible.',
|
||||
com_nav_help_faq: 'Ayuda y Preguntas Frecuentes',
|
||||
com_nav_settings: 'Configuraciones',
|
||||
'¿Estás seguro de que quieres borrar todas las conversaciones? Esta acción es irreversible.',
|
||||
com_nav_help_faq: 'Ayuda y preguntas frecuentes',
|
||||
com_nav_settings: 'Configuración',
|
||||
com_nav_search_placeholder: 'Buscar mensajes',
|
||||
com_nav_setting_general: 'General',
|
||||
com_nav_setting_beta: 'Funciones beta',
|
||||
com_nav_setting_data: 'Controles de datos',
|
||||
com_nav_setting_account: 'Cuenta',
|
||||
com_nav_language: 'Idioma',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
import { EModelEndpoint, ImageDetail } from 'librechat-data-provider';
|
||||
import type { ConversationData } from 'librechat-data-provider';
|
||||
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import { defaultEndpoints } from 'librechat-data-provider';
|
||||
import type { EModelEndpoint, TEndpointsConfig, TConfig } from 'librechat-data-provider';
|
||||
|
||||
export const getAssistantName = ({
|
||||
name,
|
||||
localize,
|
||||
}: {
|
||||
name?: string;
|
||||
localize: (phraseKey: string, ...values: string[]) => string;
|
||||
}) => {
|
||||
if (name && name.length > 0) {
|
||||
return name;
|
||||
} else {
|
||||
return localize('com_ui_assistant');
|
||||
}
|
||||
};
|
||||
|
||||
export const getEndpointsFilter = (endpointsConfig: TEndpointsConfig) => {
|
||||
const filter: Record<string, boolean> = {};
|
||||
if (!endpointsConfig) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { excelMimeTypes } from 'librechat-data-provider';
|
||||
import { excelMimeTypes, QueryKeys } from 'librechat-data-provider';
|
||||
import type { QueryClient } from '@tanstack/react-query';
|
||||
import type { TFile } from 'librechat-data-provider';
|
||||
import SheetPaths from '~/components/svg/Files/SheetPaths';
|
||||
import TextPaths from '~/components/svg/Files/TextPaths';
|
||||
import FilePaths from '~/components/svg/Files/FilePaths';
|
||||
@@ -128,3 +130,32 @@ export function formatDate(dateString) {
|
||||
|
||||
return `${day} ${month} ${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the query cache
|
||||
*/
|
||||
export function addFileToCache(queryClient: QueryClient, newfile: TFile) {
|
||||
const currentFiles = queryClient.getQueryData<TFile[]>([QueryKeys.files]);
|
||||
|
||||
if (!currentFiles) {
|
||||
console.warn('No current files found in cache, skipped updating file query cache');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileIndex = currentFiles.findIndex((file) => file.file_id === newfile.file_id);
|
||||
|
||||
if (fileIndex > -1) {
|
||||
console.warn('File already exists in cache, skipped updating file query cache');
|
||||
return;
|
||||
}
|
||||
|
||||
queryClient.setQueryData<TFile[]>(
|
||||
[QueryKeys.files],
|
||||
[
|
||||
{
|
||||
...newfile,
|
||||
},
|
||||
...currentFiles,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from './files';
|
||||
export * from './latex';
|
||||
export * from './convos';
|
||||
export * from './presets';
|
||||
export * from './textarea';
|
||||
export * from './languages';
|
||||
export * from './endpoints';
|
||||
export { default as cn } from './cn';
|
||||
|
||||
40
client/src/utils/textarea.ts
Normal file
40
client/src/utils/textarea.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Insert text at the cursor position in a textarea.
|
||||
*/
|
||||
export function insertTextAtCursor(element: HTMLTextAreaElement, textToInsert: string) {
|
||||
element.focus();
|
||||
|
||||
// Use the browser's built-in undoable actions if possible
|
||||
if (window.getSelection() && document.queryCommandSupported('insertText')) {
|
||||
document.execCommand('insertText', false, textToInsert);
|
||||
} else {
|
||||
console.warn('insertTextAtCursor: document.execCommand is not supported');
|
||||
const startPos = element.selectionStart;
|
||||
const endPos = element.selectionEnd;
|
||||
const beforeText = element.value.substring(0, startPos);
|
||||
const afterText = element.value.substring(endPos);
|
||||
element.value = beforeText + textToInsert + afterText;
|
||||
element.selectionStart = element.selectionEnd = startPos + textToInsert.length;
|
||||
const event = new Event('input', { bubbles: true });
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Necessary resize helper for edge cases where paste doesn't update the container height.
|
||||
*
|
||||
1) Resetting the height to 'auto' forces the component to recalculate height based on its current content
|
||||
|
||||
2) Forcing a reflow. Accessing offsetHeight will cause a reflow of the page,
|
||||
ensuring that the reset height takes effect before resetting back to the scrollHeight.
|
||||
This step is necessary because changes to the DOM do not instantly cause reflows.
|
||||
|
||||
3) Reseting back to scrollHeight reads and applies the ideal height for the current content dynamically
|
||||
*/
|
||||
export const forceResize = (textAreaRef: React.RefObject<HTMLTextAreaElement>) => {
|
||||
if (textAreaRef.current) {
|
||||
textAreaRef.current.style.height = 'auto';
|
||||
textAreaRef.current.offsetHeight;
|
||||
textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`;
|
||||
}
|
||||
};
|
||||
54
config/user-stats.js
Normal file
54
config/user-stats.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const path = require('path');
|
||||
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
|
||||
const { silentExit } = require('./helpers');
|
||||
const Conversation = require('~/models/schema/convoSchema');
|
||||
const Message = require('~/models/schema/messageSchema');
|
||||
const User = require('~/models/User');
|
||||
const connect = require('./connect');
|
||||
|
||||
(async () => {
|
||||
await connect();
|
||||
|
||||
/**
|
||||
* Show the welcome / help menu
|
||||
*/
|
||||
console.purple('-----------------------------');
|
||||
console.purple('Show the stats of all users');
|
||||
console.purple('-----------------------------');
|
||||
|
||||
let users = await User.find({});
|
||||
let userData = [];
|
||||
for (const user of users) {
|
||||
let conversationsCount = (await Conversation.count({ user: user._id })) ?? 0;
|
||||
let messagesCount = (await Message.count({ user: user._id })) ?? 0;
|
||||
|
||||
userData.push({
|
||||
User: user.name,
|
||||
Conversations: conversationsCount,
|
||||
Messages: messagesCount,
|
||||
});
|
||||
}
|
||||
|
||||
userData.sort((a, b) => {
|
||||
if (a.Conversations !== b.Conversations) {
|
||||
return b.Conversations - a.Conversations;
|
||||
}
|
||||
|
||||
return b.Messages - a.Messages;
|
||||
});
|
||||
|
||||
console.table(userData);
|
||||
|
||||
silentExit(0);
|
||||
})();
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
if (!err.message.includes('fetch failed')) {
|
||||
console.error('There was an uncaught error:');
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (!err.message.includes('fetch failed')) {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -11,6 +11,7 @@ services:
|
||||
- 3080:3080
|
||||
depends_on:
|
||||
- mongodb
|
||||
- rag_api
|
||||
restart: always
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
@@ -21,11 +22,14 @@ services:
|
||||
- NODE_ENV=production
|
||||
- MONGO_URI=mongodb://mongodb:27017/LibreChat
|
||||
- MEILI_HOST=http://meilisearch:7700
|
||||
- RAG_PORT=${RAG_PORT:-8000}
|
||||
- RAG_API_URL=http://rag_api:${RAG_PORT:-8000}
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./librechat.yaml
|
||||
target: /app/librechat.yaml
|
||||
- ./images:/app/client/public/images
|
||||
- ./librechat.yaml:/app/librechat.yaml
|
||||
- ./logs:/app/api/logs
|
||||
- ./uploads:/app/uploads
|
||||
client:
|
||||
build:
|
||||
context: .
|
||||
@@ -51,7 +55,7 @@ services:
|
||||
command: mongod --noauth
|
||||
meilisearch:
|
||||
container_name: chat-meilisearch
|
||||
image: getmeili/meilisearch:v1.0
|
||||
image: getmeili/meilisearch:v1.7.3
|
||||
# ports: # Uncomment this to access meilisearch from outside docker
|
||||
# - 7700:7700 # if exposing these ports, make sure your master key is not the default value
|
||||
env_file:
|
||||
@@ -60,4 +64,26 @@ services:
|
||||
- MEILI_HOST=http://meilisearch:7700
|
||||
- MEILI_NO_ANALYTICS=true
|
||||
volumes:
|
||||
- ./meili_data:/meili_data
|
||||
- ./meili_data_v1.7:/meili_data
|
||||
vectordb:
|
||||
image: ankane/pgvector:latest
|
||||
environment:
|
||||
POSTGRES_DB: mydatabase
|
||||
POSTGRES_USER: myuser
|
||||
POSTGRES_PASSWORD: mypassword
|
||||
restart: always
|
||||
volumes:
|
||||
- pgdata2:/var/lib/postgresql/data
|
||||
rag_api:
|
||||
image: ghcr.io/danny-avila/librechat-rag-api-dev-lite:latest
|
||||
environment:
|
||||
- DB_HOST=vectordb
|
||||
- RAG_PORT=${RAG_PORT:-8000}
|
||||
restart: always
|
||||
depends_on:
|
||||
- vectordb
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
volumes:
|
||||
pgdata2:
|
||||
|
||||
@@ -16,7 +16,9 @@ version: '3.4'
|
||||
# services:
|
||||
# api:
|
||||
# volumes:
|
||||
# - ./librechat.yaml:/app/librechat.yaml
|
||||
# - type: bind
|
||||
# source: ./librechat.yaml
|
||||
# target: /app/librechat.yaml
|
||||
# image: ghcr.io/danny-avila/librechat:latest
|
||||
|
||||
# ---------------------------------------------------
|
||||
@@ -26,7 +28,9 @@ version: '3.4'
|
||||
# # USE LIBRECHAT CONFIG FILE
|
||||
# api:
|
||||
# volumes:
|
||||
# - ./librechat.yaml:/app/librechat.yaml
|
||||
# - type: bind
|
||||
# source: ./librechat.yaml
|
||||
# target: /app/librechat.yaml
|
||||
|
||||
# # LOCAL BUILD
|
||||
# api:
|
||||
@@ -93,6 +97,10 @@ version: '3.4'
|
||||
# ports:
|
||||
# - 7700:7700
|
||||
|
||||
# # USE RAG API IMAGE WITH LOCAL EMBEDDINGS SUPPORT
|
||||
# rag_api:
|
||||
# image: ghcr.io/danny-avila/librechat-rag-api-dev:latest
|
||||
|
||||
# # ADD OLLAMA
|
||||
# ollama:
|
||||
# image: ollama/ollama:latest
|
||||
|
||||
@@ -10,6 +10,7 @@ services:
|
||||
- "${PORT}:${PORT}"
|
||||
depends_on:
|
||||
- mongodb
|
||||
- rag_api
|
||||
image: ghcr.io/danny-avila/librechat-dev:latest
|
||||
restart: always
|
||||
user: "${UID}:${GID}"
|
||||
@@ -19,10 +20,13 @@ services:
|
||||
- HOST=0.0.0.0
|
||||
- MONGO_URI=mongodb://mongodb:27017/LibreChat
|
||||
- MEILI_HOST=http://meilisearch:7700
|
||||
- RAG_PORT=${RAG_PORT:-8000}
|
||||
- RAG_API_URL=http://rag_api:${RAG_PORT:-8000}
|
||||
volumes:
|
||||
- ./.env:/app/.env
|
||||
- type: bind
|
||||
source: ./.env
|
||||
target: /app/.env
|
||||
- ./images:/app/client/public/images
|
||||
- ./uploads:/app/uploads
|
||||
- ./logs:/app/api/logs
|
||||
mongodb:
|
||||
container_name: chat-mongodb
|
||||
@@ -34,11 +38,33 @@ services:
|
||||
command: mongod --noauth
|
||||
meilisearch:
|
||||
container_name: chat-meilisearch
|
||||
image: getmeili/meilisearch:v1.6
|
||||
image: getmeili/meilisearch:v1.7.3
|
||||
restart: always
|
||||
user: "${UID}:${GID}"
|
||||
environment:
|
||||
- MEILI_HOST=http://meilisearch:7700
|
||||
- MEILI_NO_ANALYTICS=true
|
||||
volumes:
|
||||
- ./meili_data_v1.6:/meili_data
|
||||
- ./meili_data_v1.7:/meili_data
|
||||
vectordb:
|
||||
image: ankane/pgvector:latest
|
||||
environment:
|
||||
POSTGRES_DB: mydatabase
|
||||
POSTGRES_USER: myuser
|
||||
POSTGRES_PASSWORD: mypassword
|
||||
restart: always
|
||||
volumes:
|
||||
- pgdata2:/var/lib/postgresql/data
|
||||
rag_api:
|
||||
image: ghcr.io/danny-avila/librechat-rag-api-dev-lite:latest
|
||||
environment:
|
||||
- DB_HOST=vectordb
|
||||
- RAG_PORT=${RAG_PORT:-8000}
|
||||
restart: always
|
||||
depends_on:
|
||||
- vectordb
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
volumes:
|
||||
pgdata2:
|
||||
@@ -15,8 +15,8 @@ weight: -8
|
||||
### Preparation
|
||||
Fork the [LibreChat repository](https://github.librechat.ai) and download it using git clone. See: [Getting Started for Contributors - GitHub](./how_to_contribute.md#github)
|
||||
|
||||
### Add your language to `Translation.tsx`:
|
||||
- Navigate to the `client\src\localization` folder and open the `Translation.tsx` file
|
||||
### Add your language to `Translation.ts`:
|
||||
- Navigate to the `client\src\localization` folder and open the `Translation.ts` file
|
||||
|
||||
- At the beginning of the code, add your language below all the others in this format:
|
||||
|
||||
@@ -102,7 +102,7 @@ If you followed everything you should have ^^**one new file**^^ and ^^**3 modif
|
||||
```bash
|
||||
new file: client/src/localization/languages/**.tsx <-----new language
|
||||
modified: client/src/components/Nav/SettingsTabs/General.tsx
|
||||
modified: client/src/localization/Translation.tsx
|
||||
modified: client/src/localization/Translation.ts
|
||||
modified: client/src/localization/languages/Eng.tsx
|
||||
```
|
||||
!!! tip
|
||||
|
||||
@@ -9,6 +9,8 @@ weight: 2
|
||||
---
|
||||
|
||||
* 🤖[Custom Endpoints](../install/configuration/custom_config.md)
|
||||
* 🗃️ [RAG API (Chat with Files)](./rag_api.md)
|
||||
* 🔖 [Presets](./presets.md)
|
||||
* 🔌[Plugins](./plugins/index.md)
|
||||
* 🔌 [Introduction](./plugins/introduction.md)
|
||||
* 🛠️ [Make Your Own](./plugins/make_your_own.md)
|
||||
@@ -17,7 +19,6 @@ weight: 2
|
||||
* 🖌️ [Stable Diffusion](./plugins/stable_diffusion.md)
|
||||
* 🧠 [Wolfram|Alpha](./plugins/wolfram.md)
|
||||
* ⚡ [Azure AI Search](./plugins/azure_ai_search.md)
|
||||
* 🔖 [Presets](./presets.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: 🔨 Automated Moderation
|
||||
description: The Automated Moderation System uses a scoring mechanism to track user violations. As users commit actions like excessive logins, registrations, or messaging, they accumulate violation scores. Upon reaching a set threshold, the user and their IP are temporarily banned. This system ensures platform security by monitoring and penalizing rapid or suspicious activities.
|
||||
weight: -8
|
||||
weight: -7
|
||||
---
|
||||
## Automated Moderation System (optional)
|
||||
The Automated Moderation System uses a scoring mechanism to track user violations. As users commit actions like excessive logins, registrations, or messaging, they accumulate violation scores. Upon reaching a set threshold, the user and their IP are temporarily banned. This system ensures platform security by monitoring and penalizing rapid or suspicious activities.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Plugins
|
||||
description: 🔌 All about plugins, how to make them, how use the official ChatGPT plugins, and how to configure custom plugins.
|
||||
weight: -10
|
||||
weight: -9
|
||||
---
|
||||
|
||||
# Plugins
|
||||
|
||||
148
docs/features/rag_api.md
Normal file
148
docs/features/rag_api.md
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
title: 🗃️ RAG API (Chat with Files)
|
||||
description: Retrieval-Augmented Generation (RAG) API for document indexing and retrieval using Langchain and FastAPI. This API integrates with LibreChat to provide context-aware responses based on user-uploaded files.
|
||||
weight: -10
|
||||
---
|
||||
|
||||
# RAG API
|
||||
|
||||
The **RAG (Retrieval-Augmented Generation) API** is a powerful tool that integrates with LibreChat to provide context-aware responses based on user-uploaded files.
|
||||
|
||||
It leverages LangChain, PostgresQL + PGVector, and Python FastAPI to index and retrieve relevant documents, enhancing the conversational experience.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Currently, this feature is available to all Custom Endpoints, OpenAI, Azure OpenAi, Anthropic, and Google.**
|
||||
|
||||
OpenAI Assistants have their own implementation of RAG through the "Retrieval" capability. Learn more about it [here.](https://platform.openai.com/docs/assistants/tools/knowledge-retrieval)
|
||||
|
||||
It will still be useful to implement usage of the RAG API with the Assistants API since OpenAI charges for both file storage, and use of "Retrieval," and will be introduced in a future update.
|
||||
|
||||
Plugins support is not enabled as the whole "plugin/tool" framework will get a complete rework soon, making tools available to most endpoints (ETA Summer 2024).
|
||||
|
||||
**Still confused about RAG?** [Read the section I wrote below](#what-is-rag) explaining the general concept in more detail with a link to a helpful video.
|
||||
|
||||
## Features
|
||||
|
||||
- **Document Indexing**: The RAG API indexes user-uploaded files, creating embeddings for efficient retrieval.
|
||||
- **Semantic Search**: It performs semantic search over the indexed documents to find the most relevant information based on the user's input.
|
||||
- **Context-Aware Responses**: By augmenting the user's prompt with retrieved information, the API enables LibreChat to generate more accurate and contextually relevant responses.
|
||||
- **Asynchronous Processing**: The API supports asynchronous operations for improved performance and scalability.
|
||||
- **Flexible Configuration**: It allows customization of various parameters such as chunk size, overlap, and embedding models.
|
||||
|
||||
## Setup
|
||||
|
||||
To set up the RAG API with LibreChat, follow these steps:
|
||||
|
||||
### Docker Setup
|
||||
|
||||
For Docker, the setup is configured for you in both the default `docker-compose.yml` and `deploy-compose.yml` files, and you will just need to make sure you are using the latest docker image and compose files. Make sure to read the [Updating LibreChat guide for Docker](../install/installation/docker_compose_install.md#updating-librechat) if you are unsure how to update your Docker instance.
|
||||
|
||||
Docker uses the "lite" image of the RAG API by default, which only supports remote embeddings, leveraging embeddings proccesses from OpenAI or a remote service you have configured for HuggingFace/Ollama.
|
||||
|
||||
Local embeddings are supported by changing the image used by the default compose file, from `ghcr.io/danny-avila/librechat-rag-api-dev-lite:latest` to `ghcr.io/danny-avila/librechat-rag-api-dev:latest`.
|
||||
|
||||
As always, make these changes in your [Docker Compose Override File](../install/configuration/docker_override.md). You can find an example for exactly how to change the image in `docker-compose.override.yml.example` at the root of the project.
|
||||
|
||||
If you wish to see an example of a compose file that only includes the PostgresQL + PGVector database and the Python API, see `rag.yml` file at the root of the project.
|
||||
|
||||
**Important:** When using the default docker setup, the .env file, where configuration options can be set for the RAG API, is shared between LibreChat and the RAG API.
|
||||
|
||||
### Local Setup
|
||||
|
||||
Local, non-container setup is more hands-on, and for this you can refer to the [RAG API repo.](https://github.com/danny-avila/rag_api/)
|
||||
|
||||
In a local setup, you will need to manually set the `RAG_API_URL` in your LibreChat `.env` file to where it's available from your setup.
|
||||
|
||||
This contrasts Docker, where is already set in the default `docker-compose.yml` file.
|
||||
|
||||
## Configuration
|
||||
|
||||
The RAG API provides several configuration options that can be set using environment variables from an `.env` file accessible to the API. Most of them are optional, asides from the credentials/paths necessary for the provider you configured. In the default setup, only OPENAI_API_KEY is required.
|
||||
|
||||
**Important:** When using the default docker setup, the .env file is shared between LibreChat and the RAG API.
|
||||
|
||||
Here are some notable configurations:
|
||||
|
||||
- `OPENAI_API_KEY`: The API key for OpenAI API Embeddings (if using default settings).
|
||||
- `RAG_PORT`: The port number where the API server will run. Defaults to port 8000.
|
||||
- `RAG_HOST`: The hostname or IP address where the API server will run. Defaults to "0.0.0.0"
|
||||
- `COLLECTION_NAME`: The name of the collection in the vector store. Default is "testcollection".
|
||||
- `CHUNK_SIZE`: The size of the chunks for text processing. Default is "1500".
|
||||
- `CHUNK_OVERLAP`: The overlap between chunks during text processing. Default is "100".
|
||||
- `EMBEDDINGS_PROVIDER`: The embeddings provider to use. Options are "openai", "azure", "huggingface", "huggingfacetei", or "ollama". Default is "openai".
|
||||
- `EMBEDDINGS_MODEL`: The specific embeddings model to use from the configured provider. Default is dependent on the provider; for "openai", the model is "text-embedding-3-small".
|
||||
|
||||
There are several more configuration options.
|
||||
|
||||
For a complete list and their descriptions, please refer to the [RAG API repo.](https://github.com/danny-avila/rag_api/)
|
||||
|
||||
## Usage
|
||||
|
||||
Once the RAG API is set up and running, it seamlessly integrates with LibreChat. When a user uploads files to a conversation, the RAG API indexes those files and uses them to provide context-aware responses.
|
||||
|
||||
**To utilize the RAG API effectively:**
|
||||
|
||||
1. Ensure that the necessary files are uploaded to the conversation in LibreChat. If `RAG_API_URL` is not configured, or is not reachable, the file upload will fail.
|
||||
2. As the user interacts with the chatbot, the RAG API will automatically retrieve relevant information from the indexed files based on the user's input.
|
||||
3. The retrieved information will be used to augment the user's prompt, enabling LibreChat to generate more accurate and contextually relevant responses.
|
||||
4. Craft your prompts carefully when you attach files as the default behavior is to query the vector store upon every new message to a conversation with a file attached.
|
||||
- You can disable the default behavior by toggling the "Resend Files" option to an "off" state, found in the conversation settings.
|
||||
- Doing so allows for targeted file queries, making it so that the "retrieval" will only be done when files are explicitly attached to a message.
|
||||
- 
|
||||
5. You only have to upload a file once to use it multiple times for RAG.
|
||||
- You can attach uploaded/indexed files to any new message or conversation using the Side Panel:
|
||||
- 
|
||||
- Note: The files must be in the "Host" storage, as "OpenAI" files are treated differently and exclusive to Assistants. In other words, they must not have been uploaded when the Assistants endpoint was selected and active. You can view and manage your files by clicking here from the Side Panel.
|
||||
- 
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any issues while setting up or using the RAG API, consider the following:
|
||||
|
||||
- Double-check that all the required environment variables are correctly set in your `.env` file.
|
||||
- Ensure that the vector database is properly configured and accessible.
|
||||
- Verify that the OpenAI API key or other necessary credentials are valid.
|
||||
- Check both the LibreChat and RAG API logs for any error messages or warnings.
|
||||
|
||||
If the problem persists, please refer to the RAG API documentation or seek assistance from the LibreChat community on GitHub Discussions or Discord.
|
||||
|
||||
## What is RAG?
|
||||
|
||||
RAG, or Retrieval-Augmented Generation, is an AI framework designed to improve the quality and accuracy of responses generated by large language models (LLMs). It achieves this by grounding the LLM on external sources of knowledge, supplementing the model's internal representation of information.
|
||||
|
||||
### Key Benefits of RAG
|
||||
|
||||
1. **Access to up-to-date and reliable facts**: RAG ensures that the LLM has access to the most current and reliable information by retrieving relevant facts from an external knowledge base.
|
||||
2. **Transparency and trust**: Users can access the model's sources, allowing them to verify the accuracy of the generated responses and build trust in the system.
|
||||
3. **Reduced data leakage and hallucinations**: By grounding the LLM on a set of external, verifiable facts, RAG reduces the chances of the model leaking sensitive data or generating incorrect or misleading information.
|
||||
4. **Lower computational and financial costs**: RAG reduces the need for continuous training and updating of the model's parameters, potentially lowering the computational and financial costs of running LLM-powered chatbots in an enterprise setting.
|
||||
|
||||
### How RAG Works
|
||||
|
||||
RAG consists of two main phases: retrieval and content generation.
|
||||
|
||||
1. **Retrieval Phase**: Algorithms search for and retrieve snippets of information relevant to the user's prompt or question from an external knowledge base. In an open-domain, consumer setting, these facts can come from indexed documents on the internet. In a closed-domain, enterprise setting, a narrower set of sources are typically used for added security and reliability.
|
||||
2. **Generative Phase**: The retrieved external knowledge is appended to the user's prompt and passed to the LLM. The LLM then draws from the augmented prompt and its internal representation of its training data to synthesize a tailored, engaging answer for the user. The answer can be passed to a chatbot with links to its sources.
|
||||
|
||||
### Challenges and Ongoing Research
|
||||
|
||||
While RAG is currently the best-known tool for grounding LLMs on the latest, verifiable information and lowering the costs of constant retraining and updating, it is not perfect. Some challenges include:
|
||||
|
||||
1. **Recognizing unanswerable questions**: LLMs need to be explicitly trained to recognize questions they can't answer based on the available information. This may require fine-tuning on thousands of examples of answerable and unanswerable questions.
|
||||
2. **Improving retrieval and generation**: Ongoing research focuses on innovating at both ends of the RAG process: improving the retrieval of the most relevant information possible to feed the LLM, and optimizing the structure of that information to obtain the richest responses from the LLM.
|
||||
|
||||
In summary, RAG is a powerful framework that enhances the capabilities of LLMs by grounding them on external, verifiable knowledge. It helps to ensure more accurate, up-to-date, and trustworthy responses while reducing the costs associated with continuous model retraining. As research in this area progresses, we can expect further improvements in the quality and efficiency of LLM-powered conversational AI systems.
|
||||
|
||||
For a more detailed explanation of RAG, you can watch this informative video by IBM on Youtube:
|
||||
|
||||
[](https://www.youtube.com/watch?v=T-D1OfcDW1M)
|
||||
|
||||
## Conclusion
|
||||
|
||||
The RAG API is a powerful addition to LibreChat, enabling context-aware responses based on user-uploaded files. By leveraging Langchain and FastAPI, it provides efficient document indexing, retrieval, and generation capabilities. With its flexible configuration options and seamless integration, the RAG API enhances the conversational experience in LibreChat.
|
||||
|
||||
For more detailed information on the RAG API, including API endpoints, request/response formats, and advanced configuration, please refer to the official RAG API documentation.
|
||||
@@ -5,261 +5,316 @@ weight: -10
|
||||
---
|
||||
# ⚠️ Breaking Changes
|
||||
|
||||
> **Note:**
|
||||
**If you experience any issues after updating, we recommend clearing your browser cache and cookies.**
|
||||
Certain changes in the updates may impact cookies, leading to unexpected behaviors if not cleared properly.
|
||||
!!! warning
|
||||
|
||||
**If you experience any issues after updating, we recommend clearing your browser cache and cookies.**
|
||||
Certain changes in the updates may impact cookies, leading to unexpected behaviors if not cleared properly.
|
||||
|
||||
---
|
||||
|
||||
## 🔎Meilisearch v1.6
|
||||
## v0.7.0+
|
||||
|
||||
- **Meilisearch Update**: Following the recent update to Meilisearch, an unused folder named `meili_data_v1.5` may be present in your root directory. This folder is no longer required and **can be safely deleted** to free up space.
|
||||
- **New Indexing Data Location**: With the current Meilisearch version `1.6`, the new indexing data location folder will be `meili_data_v1.6`.
|
||||
!!! info "🗃️ RAG API (Chat with Files)"
|
||||
|
||||
- **RAG API Update**: The default Docker compose files now include a Python API and Vector Database for RAG (Retrieval-Augmented Generation). Read more about this in the [RAG API page](../features/rag_api.md)
|
||||
|
||||
## v0.6.10+ (-dev build)
|
||||
|
||||
!!! info "🔎Meilisearch v1.7"
|
||||
|
||||
- **Meilisearch Update**: Following the recent update to Meilisearch, an unused folder named `meili_data_v1.6` may be present in your root directory. This folder is no longer required and **can be safely deleted** to free up space.
|
||||
- **New Indexing Data Location**: With the current Meilisearch version `1.7.3`, the new indexing data location folder will be `meili_data_v1.7`.
|
||||
|
||||
!!! info "🔎Meilisearch v1.6"
|
||||
|
||||
- **Meilisearch Update**: Following the recent update to Meilisearch, an unused folder named `meili_data_v1.5` may be present in your root directory. This folder is no longer required and **can be safely deleted** to free up space.
|
||||
- **New Indexing Data Location**: With the current Meilisearch version `1.6`, the new indexing data location folder will be `meili_data_v1.6`.
|
||||
|
||||
!!! failure "🥷🪦 Ninja"
|
||||
|
||||
- Following to the shut down of "Ninja", the ChatGPTbrowser endpoint is no longer available in LibreChat.
|
||||
|
||||
!!! warning "🐋 `docker-compose.yml` Update"
|
||||
|
||||
We have made changes to the `docker-compose.yml` file to enhance the default behavior. Starting now, the file uses the pre-built image by default. If you prefer to build the image yourself, you'll need to utilize the override file to specify your custom build configuration.
|
||||
|
||||
Here's an example of the `docker-compose.override.yml`:
|
||||
|
||||
```yaml
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
api:
|
||||
image: librechat
|
||||
build:
|
||||
context: .
|
||||
target: node
|
||||
```
|
||||
|
||||
For more detailed information on using the `docker-compose.override.yaml`, please refer to our documentation: [docker_override](https://docs.librechat.ai/install/configuration/docker_override.html)
|
||||
|
||||
---
|
||||
|
||||
## 🥷🪦 Ninja - March 4, 2024
|
||||
- Since Ninja has shut down, the ChatGPTbrowser endpoint is no longer available in LibreChat.
|
||||
## v0.6.10
|
||||
|
||||
!!! danger "Söhne Font Licensing Issue"
|
||||
|
||||
During a recent license review, it was discovered that the Söhne fonts used in LibreChat require proper licensing for legal use. These fonts were added early in the project by a community contribution to mirror ChatGPT's aesthetic, but it was an oversight to allow them without proper knowledge.
|
||||
|
||||
To address this issue, the Söhne fonts have been removed from the project and replaced with open-source alternatives, effective immediately in the latest version of the repository on GitHub. The relevant font foundry has been contacted to resolve the matter.
|
||||
|
||||
All users and those who have forked LibreChat are required to update to the latest version to comply with font licensing laws. If you prefer to continue using the fonts, please follow the instructions provided [here](https://gist.github.com/danny-avila/e1d623e51b24cf0989865197bb788102).
|
||||
|
||||
LibreChat remains committed to ensuring compliance, accessibility, and continuous improvement. The effort to match OpenAI's ChatGPT styling was well-intentioned but poorly executed, and moving forward, all aspects of the project will meet legal and permissive standards.
|
||||
|
||||
We appreciate your understanding and cooperation in making these necessary adjustments. For updates or guidance on implementing these changes, please reach out.
|
||||
|
||||
Thank you for your continued support of LibreChat.
|
||||
|
||||
---
|
||||
|
||||
## 🐋 docker-compose.yml - February 22nd, 2024
|
||||
## v0.6.9
|
||||
|
||||
### Update to `docker-compose.yml`
|
||||
!!! info "⚙️ Environment Variables - v0.6.6 -> v0.6.9"
|
||||
|
||||
We have made changes to the `docker-compose.yml` file to enhance the default behavior. Starting now, the file uses the pre-built image by default. If you prefer to build the image yourself, you'll need to utilize the override file to specify your custom build configuration.
|
||||
see [⚙️ Environment Variables](../install/configuration/dotenv.md) for more info
|
||||
|
||||
Here's an example of the `docker-compose.override.yml`:
|
||||
!!! abstract "Endpoints"
|
||||
|
||||
```yaml
|
||||
version: '3.4'
|
||||
```sh
|
||||
# ENDPOINTS=openAI,assistants,azureOpenAI,bingAI,chatGPTBrowser,google,gptPlugins,anthropic
|
||||
```
|
||||
|
||||
services:
|
||||
api:
|
||||
image: librechat
|
||||
build:
|
||||
context: .
|
||||
target: node
|
||||
```
|
||||
!!! abstract "OpenAI models"
|
||||
|
||||
For more detailed information on using the `docker-compose.override.yaml`, please refer to our documentation: [docker_override](https://docs.librechat.ai/install/configuration/docker_override.html)
|
||||
```sh
|
||||
# OPENAI_MODELS=gpt-3.5-turbo-0125,gpt-3.5-turbo-0301,gpt-3.5-turbo,gpt-4,gpt-4-0613,gpt-4-vision-preview,gpt-3.5-turbo-0613,gpt-3.5-turbo-16k-0613,gpt-4-0125-preview,gpt-4-turbo-preview,gpt-4-1106-preview,gpt-3.5-turbo-1106,gpt-3.5-turbo-instruct,gpt-3.5-turbo-instruct-0914,gpt-3.5-turbo-16k
|
||||
```
|
||||
|
||||
---
|
||||
!!! abstract "Assistants API"
|
||||
|
||||
## **.env** changes v0.6.6 -> v0.6.9
|
||||
see [⚙️ Environment Variables](../install/configuration/dotenv.md) for more info
|
||||
```sh
|
||||
#====================#
|
||||
# Assistants API #
|
||||
#====================#
|
||||
|
||||
- Assistants added to the list
|
||||
```sh
|
||||
# ENDPOINTS=openAI,assistants,azureOpenAI,bingAI,chatGPTBrowser,google,gptPlugins,anthropic
|
||||
```
|
||||
- Updated OpenAI models
|
||||
```sh
|
||||
# OPENAI_MODELS=gpt-3.5-turbo-0125,gpt-3.5-turbo-0301,gpt-3.5-turbo,gpt-4,gpt-4-0613,gpt-4-vision-preview,gpt-3.5-turbo-0613,gpt-3.5-turbo-16k-0613,gpt-4-0125-preview,gpt-4-turbo-preview,gpt-4-1106-preview,gpt-3.5-turbo-1106,gpt-3.5-turbo-instruct,gpt-3.5-turbo-instruct-0914,gpt-3.5-turbo-16k
|
||||
```
|
||||
- Assistants configuration
|
||||
```sh
|
||||
#====================#
|
||||
# Assistants API #
|
||||
#====================#
|
||||
ASSISTANTS_API_KEY=user_provided
|
||||
# ASSISTANTS_BASE_URL=
|
||||
# ASSISTANTS_MODELS=gpt-3.5-turbo-0125,gpt-3.5-turbo-16k-0613,gpt-3.5-turbo-16k,gpt-3.5-turbo,gpt-4,gpt-4-0314,gpt-4-32k-0314,gpt-4-0613,gpt-3.5-turbo-0613,gpt-3.5-turbo-1106,gpt-4-0125-preview,gpt-4-turbo-preview,gpt-4-1106-preview
|
||||
```
|
||||
|
||||
# ASSISTANTS_API_KEY=
|
||||
# ASSISTANTS_BASE_URL=
|
||||
# ASSISTANTS_MODELS=gpt-3.5-turbo-0125,gpt-3.5-turbo-16k-0613,gpt-3.5-turbo-16k,gpt-3.5-turbo,gpt-4,gpt-4-0314,gpt-4-32k-0314,gpt-4-0613,gpt-3.5-turbo-0613,gpt-3.5-turbo-1106,gpt-4-0125-preview,gpt-4-turbo-preview,gpt-4-1106-preview
|
||||
```
|
||||
- Updated Plugin models
|
||||
```sh
|
||||
# PLUGIN_MODELS=gpt-4,gpt-4-turbo-preview,gpt-4-0125-preview,gpt-4-1106-preview,gpt-4-0613,gpt-3.5-turbo,gpt-3.5-turbo-0125,gpt-3.5-turbo-1106,gpt-3.5-turbo-0613
|
||||
```
|
||||
- Birthday hat
|
||||
```sh
|
||||
# SHOW_BIRTHDAY_ICON=true
|
||||
```
|
||||
### Previous changes:
|
||||
- DALL-E
|
||||
```sh
|
||||
# DALL·E
|
||||
#----------------
|
||||
# DALLE_API_KEY=
|
||||
# DALLE3_API_KEY=
|
||||
# DALLE2_API_KEY=
|
||||
# DALLE3_SYSTEM_PROMPT=
|
||||
# DALLE2_SYSTEM_PROMPT=
|
||||
# DALLE_REVERSE_PROXY=
|
||||
# DALLE3_BASEURL=
|
||||
# DALLE2_BASEURL=
|
||||
!!! abstract "Plugin models"
|
||||
|
||||
# DALL·E (via Azure OpenAI)
|
||||
# Note: requires some of the variables above to be set
|
||||
#----------------
|
||||
# DALLE3_AZURE_API_VERSION=
|
||||
# DALLE2_AZURE_API_VERSION=
|
||||
```
|
||||
```sh
|
||||
# PLUGIN_MODELS=gpt-4,gpt-4-turbo-preview,gpt-4-0125-preview,gpt-4-1106-preview,gpt-4-0613,gpt-3.5-turbo,gpt-3.5-turbo-0125,gpt-3.5-turbo-1106,gpt-3.5-turbo-0613
|
||||
```
|
||||
|
||||
---
|
||||
!!! abstract "Birthday Hat"
|
||||
|
||||
## January 31th 2024
|
||||
- A new method to use the ChatGPT endpoint is now documented. It uses "Ninja"
|
||||
- For more info:
|
||||
- ~~[Ninja Deployment Guide](../general_info/breaking_changes.md)~~
|
||||
- [Ninja GitHub repo](https://github.com/gngpp/ninja/tree/main)
|
||||
```sh
|
||||
# SHOW_BIRTHDAY_ICON=true
|
||||
```
|
||||
|
||||
---
|
||||
!!! abstract "DALL·E"
|
||||
|
||||
## January 30th 2024
|
||||
- Since PandoraNext has shut down, the ChatGPTbrowser endpoint is no longer available in LibreChat.
|
||||
- For more info:
|
||||
- [https://github.com/danny-avila/LibreChat/discussions/1663](https://github.com/danny-avila/LibreChat/discussions/1663#discussioncomment-8314025)
|
||||
- [https://linux.do/t/topic/1051](https://linux.do/t/topic/1051)
|
||||
```sh
|
||||
# DALL·E
|
||||
#----------------
|
||||
# DALLE_API_KEY=
|
||||
# DALLE3_API_KEY=
|
||||
# DALLE2_API_KEY=
|
||||
# DALLE3_SYSTEM_PROMPT=
|
||||
# DALLE2_SYSTEM_PROMPT=
|
||||
# DALLE_REVERSE_PROXY=
|
||||
# DALLE3_BASEURL=
|
||||
# DALLE2_BASEURL=
|
||||
|
||||
# DALL·E (via Azure OpenAI)
|
||||
# Note: requires some of the variables above to be set
|
||||
#----------------
|
||||
# DALLE3_AZURE_API_VERSION=
|
||||
# DALLE2_AZURE_API_VERSION=
|
||||
```
|
||||
|
||||
!!! success "🥷 Ninja"
|
||||
|
||||
- A new method to use the ChatGPT endpoint is now documented. It uses "Ninja"
|
||||
- For more info:
|
||||
- ~~[Ninja Deployment Guide](../general_info/breaking_changes.md)~~
|
||||
- [Ninja GitHub repo](https://github.com/gngpp/ninja/tree/main)
|
||||
|
||||
!!! failure "🪦 PandoraNext"
|
||||
|
||||
- Since PandoraNext has shut down, the ChatGPTbrowser endpoint is no longer available in LibreChat.
|
||||
- For more info:
|
||||
- [https://github.com/danny-avila/LibreChat/discussions/1663](https://github.com/danny-avila/LibreChat/discussions/1663#discussioncomment-8314025)
|
||||
- [https://linux.do/t/topic/1051](https://linux.do/t/topic/1051)
|
||||
|
||||
---
|
||||
|
||||
## v0.6.6
|
||||
|
||||
- **DALL-E Update**: user-provided keys for DALL-E are now specific to each DALL-E version, i.e.: `DALLE3_API_KEY` and `DALLE2_API_KEY`
|
||||
- Note: `DALLE_API_KEY` will work for both DALL-E-3 and DALL-E-2 when the admin provides the credential; in other words, this may only affect your users if DALLE_API_KEY is not set in the `.env` file. In this case, they will simply have to "uninstall" the plugin, and provide their API key again.
|
||||
!!! abstract "v0.6.6"
|
||||
|
||||
- **DALL-E Update**: user-provided keys for DALL-E are now specific to each DALL-E version, i.e.: `DALLE3_API_KEY` and `DALLE2_API_KEY`
|
||||
- Note: `DALLE_API_KEY` will work for both DALL-E-3 and DALL-E-2 when the admin provides the credential; in other words, this may only affect your users if DALLE_API_KEY is not set in the `.env` file. In this case, they will simply have to "uninstall" the plugin, and provide their API key again.
|
||||
|
||||
---
|
||||
|
||||
## v0.6.x
|
||||
|
||||
- **Meilisearch Update**: Following the recent update to Meilisearch, an unused folder named `meili_data` may be present in your root directory. This folder is no longer required and can be **safely deleted** to free up space.
|
||||
- **New Indexing Data Location**: The indexing data has been relocated. It will now be stored in a new folder named `meili_data_v1.x`, where `1.x` represents the version of Meilisearch. For instance, with the current Meilisearch version `1.5`, the folder will be `meili_data_v1.5`.
|
||||
!!! info "Meilisearch"
|
||||
|
||||
- **Meilisearch Update**: Following the recent update to Meilisearch, an unused folder named `meili_data` may be present in your root directory. This folder is no longer required and can be **safely deleted** to free up space.
|
||||
- **New Indexing Data Location**: The indexing data has been relocated. It will now be stored in a new folder named `meili_data_v1.x`, where `1.x` represents the version of Meilisearch. For instance, with the current Meilisearch version `1.5`, the folder will be `meili_data_v1.5`.
|
||||
|
||||
---
|
||||
|
||||
## v0.5.9
|
||||
|
||||
- It's now required to set a **JWT_REFRESH_SECRET** in your .env file as of [#927](https://github.com/danny-avila/LibreChat/pull/927)
|
||||
- It's also recommended you update your `SESSION_EXPIRY` to a lower value and set `REFRESH_TOKEN_EXPIRY`
|
||||
!!! warning "JWT Secret"
|
||||
|
||||
- Default values: session expiry: 15 minutes, refresh token expiry: 7 days
|
||||
- It's now required to set a **JWT_REFRESH_SECRET** in your .env file as of [#927](https://github.com/danny-avila/LibreChat/pull/927)
|
||||
- It's also recommended you update your `SESSION_EXPIRY` to a lower value and set `REFRESH_TOKEN_EXPIRY`
|
||||
|
||||
- *See **[.env.example](https://github.com/danny-avila/LibreChat/blob/1378eb5097b666a4add27923e47be73919957e5b/.env.example#L314)** for exact values in millisecond calculation*
|
||||
- Default values: session expiry: 15 minutes, refresh token expiry: 7 days
|
||||
|
||||
- *See **[.env.example](https://github.com/danny-avila/LibreChat/blob/1378eb5097b666a4add27923e47be73919957e5b/.env.example#L314)** for exact values in millisecond calculation*
|
||||
|
||||
---
|
||||
|
||||
## v0.5.8
|
||||
|
||||
- It's now required to name manifest JSON files (for [ChatGPT Plugins](../features/plugins/chatgpt_plugins_openapi.md)) in the `api\app\clients\tools\.well-known` directory after their `name_for_model` property should you add one yourself.
|
||||
- This was a recommended convention before, but is now required.
|
||||
!!! info "manifest JSON files"
|
||||
|
||||
- It's now required to name manifest JSON files (for [ChatGPT Plugins](../features/plugins/chatgpt_plugins_openapi.md)) in the `api\app\clients\tools\.well-known` directory after their `name_for_model` property should you add one yourself.
|
||||
- This was a recommended convention before, but is now required.
|
||||
|
||||
---
|
||||
|
||||
## v0.5.7
|
||||
|
||||
Now, we have an easier and safer way to update LibreChat. You can simply run `npm run update` from the project directory for a clean update.
|
||||
If you want to skip the prompt you can use
|
||||
!!! tip "Update LibreChat"
|
||||
|
||||
for a docker install:
|
||||
- `npm run update:docker`
|
||||
Now, we have an easier and safer way to update LibreChat. You can simply run `npm run update` from the project directory for a clean update.
|
||||
If you want to skip the prompt you can use
|
||||
|
||||
for a local install:
|
||||
- `npm run update:local`
|
||||
for a docker install:
|
||||
- `npm run update:docker`
|
||||
|
||||
for a local install:
|
||||
- `npm run update:local`
|
||||
|
||||
---
|
||||
|
||||
## v0.5.5
|
||||
Some users have reported an error after updating their docker containers.
|
||||
|
||||

|
||||
!!! warning "Possible Error and Solution"
|
||||
|
||||
- To fix this error, you need to:
|
||||
- Delete the LibreChat image in docker 🗑️
|
||||
Some users have reported an error after updating their docker containers.
|
||||
|
||||
**(leave mongo intact to preserve your profiles and history)**
|
||||

|
||||
- Repeat the docker update process: 🚀
|
||||
- `docker compose build`
|
||||
- `docker compose up -d`
|
||||

|
||||
|
||||
- To fix this error, you need to:
|
||||
- Delete the LibreChat image in docker 🗑️
|
||||
|
||||
**(leave mongo intact to preserve your profiles and history)**
|
||||

|
||||
- Repeat the docker update process: 🚀
|
||||
- `docker compose build`
|
||||
- `docker compose up -d`
|
||||
|
||||
---
|
||||
|
||||
## v0.5.4
|
||||
Some changes were made in the .env file
|
||||
**Look at the .env.example for reference.**
|
||||
|
||||
- If you previously used social login, you need to:
|
||||
- Add this to your .env file: 👇
|
||||
!!! abstract ".env file"
|
||||
|
||||
```env
|
||||
##########################
|
||||
# User System:
|
||||
##########################
|
||||
Some changes were made in the .env file
|
||||
**Look at the .env.example for reference.**
|
||||
|
||||
# Allow Public Registration
|
||||
ALLOW_REGISTRATION=true
|
||||
- If you previously used social login, you need to:
|
||||
- Add this to your .env file: 👇
|
||||
|
||||
# Allow Social Registration
|
||||
ALLOW_SOCIAL_LOGIN=false
|
||||
```
|
||||
```env
|
||||
##########################
|
||||
# User System:
|
||||
##########################
|
||||
|
||||
- Set ALLOW_SOCIAL_LOGIN to true if you want to enable social login 🔥
|
||||
# Allow Public Registration
|
||||
ALLOW_REGISTRATION=true
|
||||
|
||||
- If you want to enable the Anthropic Endpoint (Claude), you need to:
|
||||
- Add this part in your .env file: 👇
|
||||
# Allow Social Registration
|
||||
ALLOW_SOCIAL_LOGIN=false
|
||||
```
|
||||
|
||||
```env
|
||||
##########################
|
||||
# Anthropic Endpoint:
|
||||
##########################
|
||||
# Access key from https://console.anthropic.com/
|
||||
# Leave it blank to disable this feature.
|
||||
# Set to "user_provided" to allow the user to provide their API key from the UI.
|
||||
# Note that access to claude-1 may potentially become unavailable with the release of claude-2.
|
||||
ANTHROPIC_API_KEY="user_provided"
|
||||
ANTHROPIC_MODELS=claude-1,claude-instant-1,claude-2
|
||||
```
|
||||
- Set ALLOW_SOCIAL_LOGIN to true if you want to enable social login 🔥
|
||||
|
||||
- Choose from ANTHROPIC_MODELS which models you want to enable 🤖
|
||||
- If you want to enable the Anthropic Endpoint (Claude), you need to:
|
||||
- Add this part in your .env file: 👇
|
||||
|
||||
```env
|
||||
##########################
|
||||
# Anthropic Endpoint:
|
||||
##########################
|
||||
# Access key from https://console.anthropic.com/
|
||||
# Leave it blank to disable this feature.
|
||||
# Set to "user_provided" to allow the user to provide their API key from the UI.
|
||||
# Note that access to claude-1 may potentially become unavailable with the release of claude-2.
|
||||
ANTHROPIC_API_KEY="user_provided"
|
||||
ANTHROPIC_MODELS=claude-1,claude-instant-1,claude-2
|
||||
```
|
||||
|
||||
- Choose from ANTHROPIC_MODELS which models you want to enable 🤖
|
||||
|
||||
---
|
||||
|
||||
## v0.5.3
|
||||
|
||||
Changed **AZURE_OPENAI_API_KEY** to **AZURE_API_KEY**:
|
||||
!!! warning "Azure API Key variable"
|
||||
|
||||
I had to change the environment variable from AZURE_OPENAI_API_KEY to AZURE_API_KEY, because the former would be read by langchain and cause issues when a user has both Azure and OpenAI keys set. This is a [known issue in the langchain library](https://github.com/hwchase17/langchainjs/issues/1687)
|
||||
Changed **AZURE_OPENAI_API_KEY** to **AZURE_API_KEY**:
|
||||
|
||||
I had to change the environment variable from AZURE_OPENAI_API_KEY to AZURE_API_KEY, because the former would be read by langchain and cause issues when a user has both Azure and OpenAI keys set. This is a [known issue in the langchain library](https://github.com/hwchase17/langchainjs/issues/1687)
|
||||
|
||||
---
|
||||
|
||||
## v0.5.0
|
||||
|
||||
**Note: These changes only apply to users who are updating from a previous version of the app.**
|
||||
!!! warning "Summary"
|
||||
**Note: These changes only apply to users who are updating from a previous version of the app.**
|
||||
|
||||
### Summary
|
||||
- In this version, we have simplified the configuration process, improved the security of your credentials, and updated the docker instructions. 🚀
|
||||
- Please read the following sections carefully to learn how to upgrade your app and avoid any issues. 🙏
|
||||
- **Note:** If you're having trouble, before creating a new issue, please search for similar ones on our [#issues thread on our discord](https://discord.librechat.ai) or our [troubleshooting discussion](https://github.com/danny-avila/LibreChat/discussions/new?category=troubleshooting) on our Discussions page. If you don't find a relevant issue, feel free to create a new one and provide as much detail as possible.
|
||||
- In this version, we have simplified the configuration process, improved the security of your credentials, and updated the docker instructions. 🚀
|
||||
- Please read the following sections carefully to learn how to upgrade your app and avoid any issues. 🙏
|
||||
- **Note:** If you're having trouble, before creating a new issue, please search for similar ones on our [#issues thread on our discord](https://discord.librechat.ai) or our [troubleshooting discussion](https://github.com/danny-avila/LibreChat/discussions/new?category=troubleshooting) on our Discussions page. If you don't find a relevant issue, feel free to create a new one and provide as much detail as possible.
|
||||
|
||||
---
|
||||
|
||||
### Configuration
|
||||
- We have simplified the configuration process by using a single `.env` file in the root folder instead of separate `/api/.env` and `/client/.env` files.
|
||||
- We have renamed the `OPENAI_KEY` variable to `OPENAI_API_KEY` to match the official documentation. The upgrade script should do this automatically for you, but please double-check that your key is correct in the new `.env` file.
|
||||
- We have removed the `VITE_SHOW_GOOGLE_LOGIN_OPTION` variable, since it is no longer needed. The app will automatically enable Google Login if you provide the `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` variables. 🔑
|
||||
- We have changed the variable name for setting the app title from `VITE_APP_TITLE` to `APP_TITLE`. If you had set a custom app title before, you need to update the variable name in the `.env` file to keep it. Otherwise, the app might revert to the default title.
|
||||
- For enhanced security, we are now asking for crypto keys for securely storing credentials in the `.env` file. Crypto keys are used to encrypt and decrypt sensitive data such as passwords and access keys. If you don't set them, the app will crash on startup. 🔒
|
||||
- You need to fill the following variables in the `.env` file with 32-byte (64 characters in hex) or 16-byte (32 characters in hex) values:
|
||||
- `CREDS_KEY` (32-byte)
|
||||
- `CREDS_IV` (16-byte)
|
||||
- `JWT_SECRET` (32-byte) optional but recommended
|
||||
- The upgrade script will do it for you, otherwise you can use this replit to generate some crypto keys quickly: https://replit.com/@daavila/crypto#index.js
|
||||
- Make sure you keep your crypto keys safe and don't share them with anyone. 🙊
|
||||
!!! info "Configuration"
|
||||
|
||||
---
|
||||
- We have simplified the configuration process by using a single `.env` file in the root folder instead of separate `/api/.env` and `/client/.env` files.
|
||||
- We have renamed the `OPENAI_KEY` variable to `OPENAI_API_KEY` to match the official documentation. The upgrade script should do this automatically for you, but please double-check that your key is correct in the new `.env` file.
|
||||
- We have removed the `VITE_SHOW_GOOGLE_LOGIN_OPTION` variable, since it is no longer needed. The app will automatically enable Google Login if you provide the `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` variables. 🔑
|
||||
- We have changed the variable name for setting the app title from `VITE_APP_TITLE` to `APP_TITLE`. If you had set a custom app title before, you need to update the variable name in the `.env` file to keep it. Otherwise, the app might revert to the default title.
|
||||
- For enhanced security, we are now asking for crypto keys for securely storing credentials in the `.env` file. Crypto keys are used to encrypt and decrypt sensitive data such as passwords and access keys. If you don't set them, the app will crash on startup. 🔒
|
||||
- You need to fill the following variables in the `.env` file with 32-byte (64 characters in hex) or 16-byte (32 characters in hex) values:
|
||||
- `CREDS_KEY` (32-byte)
|
||||
- `CREDS_IV` (16-byte)
|
||||
- `JWT_SECRET` (32-byte) optional but recommended
|
||||
- The upgrade script will do it for you, otherwise you can use this replit to generate some crypto keys quickly: https://replit.com/@daavila/crypto#index.js
|
||||
- Make sure you keep your crypto keys safe and don't share them with anyone. 🙊
|
||||
|
||||
### Docker
|
||||
- The docker-compose file had some change. Review the [new docker instructions](../install/installation/docker_compose_install.md) to make sure you are setup properly. This is still the simplest and most effective method.
|
||||
|
||||
---
|
||||
!!! info "docker"
|
||||
|
||||
- The docker-compose file had some change. Review the [new docker instructions](../install/installation/docker_compose_install.md) to make sure you are setup properly. This is still the simplest and most effective method.
|
||||
|
||||
!!! info "Local Install"
|
||||
|
||||
- If you had installed a previous version, you can run `npm run upgrade` to automatically copy the content of both files to the new `.env` file and backup the old ones in the root dir.
|
||||
- If you are installing the project for the first time, it's recommend you run the installation script `npm run ci` to guide your local setup (otherwise continue to use docker)
|
||||
- The upgrade script requires both `/api/.env` and `/client/.env` files to run properly. If you get an error about a missing client env file, just rename the `/client/.env.example` file to `/client/.env` and run the script again.
|
||||
- After running the upgrade script, the `OPENAI_API_KEY` variable might be placed in a different section in the new `.env` file than before. This does not affect the functionality of the app, but if you want to keep it organized, you can look for it near the bottom of the file and move it to its usual section.
|
||||
|
||||
### Local Install
|
||||
- If you had installed a previous version, you can run `npm run upgrade` to automatically copy the content of both files to the new `.env` file and backup the old ones in the root dir.
|
||||
- If you are installing the project for the first time, it's recommend you run the installation script `npm run ci` to guide your local setup (otherwise continue to use docker)
|
||||
- The upgrade script requires both `/api/.env` and `/client/.env` files to run properly. If you get an error about a missing client env file, just rename the `/client/.env.example` file to `/client/.env` and run the script again.
|
||||
- After running the upgrade script, the `OPENAI_API_KEY` variable might be placed in a different section in the new `.env` file than before. This does not affect the functionality of the app, but if you want to keep it organized, you can look for it near the bottom of the file and move it to its usual section.
|
||||
|
||||
---
|
||||
|
||||
We apologize for any inconvenience caused by these changes. We hope you enjoy the new and improved version of our app!
|
||||
|
||||
@@ -37,8 +37,11 @@ weight: -10
|
||||
## 🪶 Features
|
||||
- 🖥️ UI matching ChatGPT, including Dark mode, Streaming, and 11-2023 updates
|
||||
- 💬 Multimodal Chat:
|
||||
- Upload and analyze images with GPT-4 and Gemini Vision 📸
|
||||
- More filetypes and Assistants API integration in Active Development 🚧
|
||||
- Upload and analyze images with Claude 3, GPT-4, and Gemini Vision 📸
|
||||
- Chat with Files using Custom Endpoints, OpenAI, Azure, Anthropic, & Google. 🗃️
|
||||
- Advanced Agents with Files, Code Interpreter, Tools, and API Actions 🔦
|
||||
- Available through the [OpenAI Assistants API](https://platform.openai.com/docs/assistants/overview) 🌤️
|
||||
- Non-OpenAI Agents in Active Development 🚧
|
||||
- 🌎 Multilingual UI:
|
||||
- English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro, Русский
|
||||
- 日本語, Svenska, 한국어, Tiếng Việt, 繁體中文, العربية, Türkçe, Nederlands
|
||||
@@ -49,7 +52,9 @@ weight: -10
|
||||
- 🔍 Search all messages/conversations
|
||||
- 🔌 Plugins, including web access, image generation with DALL-E-3 and more
|
||||
- 👥 Multi-User, Secure Authentication with Moderation and Token spend tools
|
||||
- ⚙️ Configure Proxy, Reverse Proxy, Docker, many Deployment options, and completely Open-Source
|
||||
- ⚙️ Configure Proxy, Reverse Proxy, Docker, & many Deployment options
|
||||
- 📖 Completely Open-Source & Built in Public
|
||||
- 🧑🤝🧑 Community-driven development, support, and feedback
|
||||
|
||||
## 📃 All-In-One AI Conversations with LibreChat
|
||||
LibreChat brings together the future of assistant AIs with the revolutionary technology of OpenAI's ChatGPT. Celebrating the original styling, LibreChat gives you the ability to integrate multiple AI models. It also integrates and enhances original client features such as conversation and message search, prompt templates and plugins.
|
||||
|
||||
@@ -76,6 +76,14 @@ NO_INDEX=true
|
||||
|
||||
> ❗**Note:** This method is not guaranteed to work for all search engines, and some search engines may still index your website or web page for other purposes, such as caching or archiving. Therefore, you should not rely solely on this method to protect sensitive or confidential information on your website or web page.
|
||||
|
||||
### JSON Logging
|
||||
|
||||
When handling console logs in cloud deployments (such as GCP or AWS), enabling this will duump the logs with a UTC timestamp and format them as JSON. See: [feat: Add CONSOLE_JSON](https://github.com/danny-avila/LibreChat/pull/2146)
|
||||
|
||||
```
|
||||
CONSOLE_JSON=false
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
LibreChat has built-in central logging, see [Logging System](../../features/logging_system.md) for more info.
|
||||
@@ -86,16 +94,27 @@ LibreChat has built-in central logging, see [Logging System](../../features/logg
|
||||
- Keep debug logs active by default or disable them by setting `DEBUG_LOGGING=false` in the environment variable.
|
||||
- For more information about this feature, read our docs: **[Logging System](../../features/logging_system.md)**
|
||||
|
||||
- Enable verbose file logs with `DEBUG_LOGGING=TRUE`.
|
||||
- Note: can be used with either `DEBUG_CONSOLE` or `CONSOLE_JSON` but not both.
|
||||
|
||||
```bash
|
||||
DEBUG_LOGGING=true
|
||||
```
|
||||
|
||||
- Enable verbose server output in the console with `DEBUG_CONSOLE=TRUE`, though it's not recommended due to high verbosity.
|
||||
- Enable verbose console/stdout logs with `DEBUG_CONSOLE=TRUE` in the same format as file debug logs.
|
||||
- Note: can be used in conjunction with `DEBUG_LOGGING` but not `CONSOLE_JSON`.
|
||||
|
||||
```bash
|
||||
DEBUG_CONSOLE=false
|
||||
```
|
||||
|
||||
- Enable verbose JSON console/stdout logs suitable for cloud deployments like GCP/AWS
|
||||
- Note: can be used in conjunction with `DEBUG_LOGGING` but not `DEBUG_CONSOLE`.
|
||||
|
||||
```bash
|
||||
CONSOLE_JSON=false
|
||||
```
|
||||
|
||||
This is not recommend, however, as the outputs can be quite verbose, and so it's disabled by default.
|
||||
|
||||
### Permission
|
||||
|
||||
@@ -51,7 +51,12 @@ Once you have completed all the setup, you can start the LibreChat application b
|
||||
That's it! If you need more detailed information on configuring your compose file, see my notes below.
|
||||
|
||||
## Updating LibreChat
|
||||
The following commands will fetch the latest code of LibreChat and build a new docker image.
|
||||
|
||||
As of v0.7.0+, Docker installations transitioned from building images locally to using prebuilt images [hosted on Github Container registry](https://github.com/danny-avila?tab=packages&repo_name=LibreChat).
|
||||
|
||||
You can still build the image locally, as shown in the commented commands below. More info on building the image locally in the [Docker Compose Override Section](../configuration/docker_override.md).
|
||||
|
||||
The following commands will fetch the latest LibreChat project changes, including any necessary changes to the docker compose files, as well as the latest prebuilt images.
|
||||
|
||||
```bash
|
||||
# Stop the running container(s)
|
||||
@@ -63,13 +68,16 @@ git pull
|
||||
# Pull the latest LibreChat image (default setup)
|
||||
docker compose pull
|
||||
|
||||
# If building the LibreChat image Locally, build without cache (legacy setup)
|
||||
# docker compose build --no-cache
|
||||
|
||||
# Start LibreChat
|
||||
docker compose up
|
||||
```
|
||||
|
||||
If you're having issues running the above commands, you can try a comprehensive approach:
|
||||
If you're having issues running the above commands, you can try a comprehensive approach instead:
|
||||
|
||||
Prefix commands with `sudo` according to your environment permissions.
|
||||
Note: you may need to prefix commands with `sudo` according to your environment permissions.
|
||||
|
||||
```bash
|
||||
# Stop the container (if running)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// v0.6.10
|
||||
// v0.7.0
|
||||
// See .env.test.example for an example of the '.env.test' file.
|
||||
require('dotenv').config({ path: './e2e/.env.test' });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- v0.6.10 -->
|
||||
<!-- v0.7.0 -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
@@ -12,7 +12,6 @@ use_directory_urls: false
|
||||
|
||||
theme:
|
||||
name: material
|
||||
custom_dir: overrides
|
||||
logo: assets/LibreChat.svg
|
||||
favicon: assets/favicon_package/favicon-32x32.png
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
<!-- Custom front matter -->
|
||||
{% block extrahead %}
|
||||
<!-- Extra style sheets (can't be set in mkdocs.yml due to content hash) -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{ 'assets/stylesheets/custom.css' | url }}"
|
||||
/>
|
||||
{% endblock %}
|
||||
|
||||
<!-- Announcement bar -->
|
||||
{% block announce %}
|
||||
For updates, check out our
|
||||
<a rel="me" href="https://librechat.ai/blog">
|
||||
<strong>Blog</strong>
|
||||
</a>
|
||||
and
|
||||
<a href="https://twitter.com/LibreChatAI">
|
||||
<span class="twemoji twitter">
|
||||
{% include ".icons/fontawesome/brands/twitter.svg" %}
|
||||
</span>
|
||||
<strong>Twitter</strong>
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
<!-- Theme-related JavaScript -->
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
|
||||
<!-- Extra JavaScript (can't be set in mkdocs.yml due to content hash) -->
|
||||
<script src="{{ 'assets/javascripts/custom.js' | url }}"></script>
|
||||
|
||||
<!-- Crisp Chat Script -->
|
||||
<script type="text/javascript">
|
||||
window.$crisp=[];window.CRISP_WEBSITE_ID="5421b199-fca2-4c83-9394-be6eca8b12dd";
|
||||
(function(){d=document;s=d.createElement("script");s.src="https://client.crisp.chat/l.js";s.async=1;d.getElementsByTagName("head")[0].appendChild(s);})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "LibreChat",
|
||||
"version": "0.6.10",
|
||||
"version": "0.7.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "LibreChat",
|
||||
"version": "0.6.10",
|
||||
"version": "0.7.0",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"api",
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"api": {
|
||||
"name": "@librechat/backend",
|
||||
"version": "0.6.10",
|
||||
"version": "0.7.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.16.1",
|
||||
@@ -73,7 +73,7 @@
|
||||
"langchain": "^0.0.214",
|
||||
"librechat-data-provider": "*",
|
||||
"lodash": "^4.17.21",
|
||||
"meilisearch": "^0.37.0",
|
||||
"meilisearch": "^0.38.0",
|
||||
"mime": "^3.0.0",
|
||||
"module-alias": "^2.2.3",
|
||||
"mongoose": "^7.1.1",
|
||||
@@ -135,7 +135,7 @@
|
||||
},
|
||||
"client": {
|
||||
"name": "@librechat/frontend",
|
||||
"version": "0.6.10",
|
||||
"version": "0.7.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@dicebear/collection": "^7.0.4",
|
||||
@@ -19214,9 +19214,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/meilisearch": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.37.0.tgz",
|
||||
"integrity": "sha512-LdbK6JmRghCawrmWKJSEQF0OiE82md+YqJGE/U2JcCD8ROwlhTx0KM6NX4rQt0u0VpV0QZVG9umYiu3CSSIJAQ==",
|
||||
"version": "0.38.0",
|
||||
"resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.38.0.tgz",
|
||||
"integrity": "sha512-bHaq8nYxSKw9/Qslq1Zes5g9tHgFkxy/I9o8942wv2PqlNOT0CzptIkh/x98N52GikoSZOXSQkgt6oMjtf5uZw==",
|
||||
"dependencies": {
|
||||
"cross-fetch": "^3.1.6"
|
||||
}
|
||||
@@ -27994,7 +27994,7 @@
|
||||
},
|
||||
"packages/data-provider": {
|
||||
"name": "librechat-data-provider",
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "LibreChat",
|
||||
"version": "0.6.10",
|
||||
"version": "0.7.0",
|
||||
"description": "",
|
||||
"workspaces": [
|
||||
"api",
|
||||
@@ -11,6 +11,7 @@
|
||||
"update": "node config/update.js",
|
||||
"add-balance": "node config/add-balance.js",
|
||||
"list-balances": "node config/list-balances.js",
|
||||
"user-stats": "node config/user-stats.js",
|
||||
"rebuild:package-lock": "node config/packages",
|
||||
"reinstall": "node config/update.js -l -g",
|
||||
"b:reinstall": "bun config/update.js -b -l -g",
|
||||
|
||||
@@ -150,6 +150,8 @@ export const endpointSchema = z.object({
|
||||
customOrder: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TEndpoint = z.infer<typeof endpointSchema>;
|
||||
|
||||
export const azureEndpointSchema = z
|
||||
.object({
|
||||
groups: azureGroupConfigsSchema,
|
||||
@@ -519,7 +521,7 @@ export enum Constants {
|
||||
/**
|
||||
* Key for the app's version.
|
||||
*/
|
||||
VERSION = 'v0.6.10',
|
||||
VERSION = 'v0.7.0',
|
||||
/**
|
||||
* Key for the Custom Config's version (librechat.yaml).
|
||||
*/
|
||||
|
||||
@@ -6,10 +6,7 @@ import * as t from './types';
|
||||
import * as s from './schemas';
|
||||
import request from './request';
|
||||
import * as endpoints from './api-endpoints';
|
||||
|
||||
export function getConversations(pageNumber: string): Promise<t.TGetConversationsResponse> {
|
||||
return request.get(endpoints.conversations(pageNumber));
|
||||
}
|
||||
import type { AxiosResponse } from 'axios';
|
||||
|
||||
export function abortRequestWithMessage(
|
||||
endpoint: string,
|
||||
@@ -19,15 +16,6 @@ export function abortRequestWithMessage(
|
||||
return request.post(endpoints.abortRequest(endpoint), { arg: { abortKey, message } });
|
||||
}
|
||||
|
||||
export function deleteConversation(payload: t.TDeleteConversationRequest) {
|
||||
//todo: this should be a DELETE request
|
||||
return request.post(endpoints.deleteConversation(), { arg: payload });
|
||||
}
|
||||
|
||||
export function clearAllConversations(): Promise<unknown> {
|
||||
return request.post(endpoints.deleteConversation(), { arg: {} });
|
||||
}
|
||||
|
||||
export function revokeUserKey(name: string): Promise<unknown> {
|
||||
return request.delete(endpoints.revokeUserKey(name));
|
||||
}
|
||||
@@ -43,20 +31,6 @@ export function getMessagesByConvoId(conversationId: string): Promise<s.TMessage
|
||||
return request.get(endpoints.messages(conversationId));
|
||||
}
|
||||
|
||||
export function getConversationById(id: string): Promise<s.TConversation> {
|
||||
return request.get(endpoints.conversationById(id));
|
||||
}
|
||||
|
||||
export function updateConversation(
|
||||
payload: t.TUpdateConversationRequest,
|
||||
): Promise<t.TUpdateConversationResponse> {
|
||||
return request.post(endpoints.updateConversation(), { arg: payload });
|
||||
}
|
||||
|
||||
export function genTitle(payload: m.TGenTitleRequest): Promise<m.TGenTitleResponse> {
|
||||
return request.post(endpoints.genTitle(), payload);
|
||||
}
|
||||
|
||||
export function updateMessage(payload: t.TUpdateMessageRequest): Promise<unknown> {
|
||||
const { conversationId, messageId, text } = payload;
|
||||
if (!conversationId) {
|
||||
@@ -103,13 +77,6 @@ export function getUserBalance(): Promise<string> {
|
||||
return request.get(endpoints.balance());
|
||||
}
|
||||
|
||||
export const searchConversations = async (
|
||||
q: string,
|
||||
pageNumber: string,
|
||||
): Promise<t.TSearchResults> => {
|
||||
return request.get(endpoints.search(q, pageNumber));
|
||||
};
|
||||
|
||||
export const updateTokenCount = (text: string) => {
|
||||
return request.post(endpoints.tokenizer(), { arg: text });
|
||||
};
|
||||
@@ -196,6 +163,10 @@ export const listAssistants = (
|
||||
return request.get(endpoints.assistants(), { params });
|
||||
};
|
||||
|
||||
export function getAssistantDocs(): Promise<a.AssistantDocument[]> {
|
||||
return request.get(endpoints.assistants('documents'));
|
||||
}
|
||||
|
||||
/* Tools */
|
||||
|
||||
export const getAvailableTools = (): Promise<s.TPlugin[]> => {
|
||||
@@ -231,19 +202,13 @@ export const uploadAssistantAvatar = (data: m.AssistantAvatarVariables): Promise
|
||||
);
|
||||
};
|
||||
|
||||
export const updateAction = (data: m.UpdateActionVariables): Promise<m.UpdateActionResponse> => {
|
||||
const { assistant_id, ...body } = data;
|
||||
return request.post(endpoints.assistants(`actions/${assistant_id}`), body);
|
||||
export const getFileDownload = async (userId: string, filepath: string): Promise<AxiosResponse> => {
|
||||
const encodedFilePath = encodeURIComponent(filepath);
|
||||
return request.getResponse(`${endpoints.files()}/download/${userId}/${encodedFilePath}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
};
|
||||
|
||||
export function getActions(): Promise<a.Action[]> {
|
||||
return request.get(endpoints.assistants('actions'));
|
||||
}
|
||||
|
||||
export function getAssistantDocs(): Promise<a.AssistantDocument[]> {
|
||||
return request.get(endpoints.assistants('documents'));
|
||||
}
|
||||
|
||||
export const deleteFiles = async (
|
||||
files: f.BatchFile[],
|
||||
assistant_id?: string,
|
||||
@@ -252,8 +217,35 @@ export const deleteFiles = async (
|
||||
data: { files, assistant_id },
|
||||
});
|
||||
|
||||
/* actions */
|
||||
|
||||
export const updateAction = (data: m.UpdateActionVariables): Promise<m.UpdateActionResponse> => {
|
||||
const { assistant_id, ...body } = data;
|
||||
return request.post(endpoints.assistants(`actions/${assistant_id}`), body);
|
||||
};
|
||||
|
||||
export function getActions(): Promise<a.Action[]> {
|
||||
return request.get(endpoints.assistants('actions'));
|
||||
}
|
||||
|
||||
export const deleteAction = async (
|
||||
assistant_id: string,
|
||||
action_id: string,
|
||||
model: string,
|
||||
): Promise<void> =>
|
||||
request.delete(endpoints.assistants(`actions/${assistant_id}/${action_id}/${model}`));
|
||||
|
||||
/* conversations */
|
||||
|
||||
export function deleteConversation(payload: t.TDeleteConversationRequest) {
|
||||
//todo: this should be a DELETE request
|
||||
return request.post(endpoints.deleteConversation(), { arg: payload });
|
||||
}
|
||||
|
||||
export function clearAllConversations(): Promise<unknown> {
|
||||
return request.post(endpoints.deleteConversation(), { arg: {} });
|
||||
}
|
||||
|
||||
export const listConversations = (
|
||||
params?: q.ConversationListParams,
|
||||
): Promise<q.ConversationListResponse> => {
|
||||
@@ -275,9 +267,27 @@ export const listConversationsByQuery = (
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAction = async (
|
||||
assistant_id: string,
|
||||
action_id: string,
|
||||
model: string,
|
||||
): Promise<void> =>
|
||||
request.delete(endpoints.assistants(`actions/${assistant_id}/${action_id}/${model}`));
|
||||
export const searchConversations = async (
|
||||
q: string,
|
||||
pageNumber: string,
|
||||
): Promise<t.TSearchResults> => {
|
||||
return request.get(endpoints.search(q, pageNumber));
|
||||
};
|
||||
|
||||
export function getConversations(pageNumber: string): Promise<t.TGetConversationsResponse> {
|
||||
return request.get(endpoints.conversations(pageNumber));
|
||||
}
|
||||
|
||||
export function getConversationById(id: string): Promise<s.TConversation> {
|
||||
return request.get(endpoints.conversationById(id));
|
||||
}
|
||||
|
||||
export function updateConversation(
|
||||
payload: t.TUpdateConversationRequest,
|
||||
): Promise<t.TUpdateConversationResponse> {
|
||||
return request.post(endpoints.updateConversation(), { arg: payload });
|
||||
}
|
||||
|
||||
export function genTitle(payload: m.TGenTitleRequest): Promise<m.TGenTitleResponse> {
|
||||
return request.post(endpoints.genTitle(), payload);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export enum QueryKeys {
|
||||
tools = 'tools',
|
||||
actions = 'actions',
|
||||
assistantDocs = 'assistantDocs',
|
||||
fileDownload = 'fileDownload',
|
||||
}
|
||||
|
||||
export enum MutationKeys {
|
||||
|
||||
@@ -8,6 +8,10 @@ async function _get<T>(url: string, options?: AxiosRequestConfig): Promise<T> {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function _getResponse<T>(url: string, options?: AxiosRequestConfig): Promise<T> {
|
||||
return await axios.get(url, { ...options });
|
||||
}
|
||||
|
||||
async function _post(url: string, data?: any) {
|
||||
const response = await axios.post(url, JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -114,6 +118,7 @@ axios.interceptors.response.use(
|
||||
|
||||
export default {
|
||||
get: _get,
|
||||
getResponse: _getResponse,
|
||||
post: _post,
|
||||
postMultiPart: _postMultiPart,
|
||||
put: _put,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OpenAPIV3 } from 'openapi-types';
|
||||
import type { TFile } from './files';
|
||||
|
||||
export type Schema = OpenAPIV3.SchemaObject & { description?: string };
|
||||
export type Reference = OpenAPIV3.ReferenceObject & { description?: string };
|
||||
@@ -131,7 +132,7 @@ export type ToolCallsStepDetails = {
|
||||
type: 'tool_calls'; // Always 'tool_calls'.
|
||||
};
|
||||
|
||||
export type ImageFile = {
|
||||
export type ImageFile = TFile & {
|
||||
/**
|
||||
* The [File](https://platform.openai.com/docs/api-reference/files) ID of the image
|
||||
* in the message content.
|
||||
@@ -182,6 +183,11 @@ export type Text = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export enum AnnotationTypes {
|
||||
FILE_CITATION = 'file_citation',
|
||||
FILE_PATH = 'file_path',
|
||||
}
|
||||
|
||||
export enum ContentTypes {
|
||||
TEXT = 'text',
|
||||
TOOL_CALL = 'tool_call',
|
||||
@@ -246,7 +252,10 @@ export type TMessageContentParts =
|
||||
| { type: ContentTypes.IMAGE_FILE; image_file: ImageFile & PartMetadata };
|
||||
|
||||
export type StreamContentData = TMessageContentParts & {
|
||||
/** The index of the current content part */
|
||||
index: number;
|
||||
/** The current text content was already served but edited to replace elements therein */
|
||||
edited?: boolean;
|
||||
};
|
||||
|
||||
export type TContentData = StreamContentData & {
|
||||
@@ -259,6 +268,8 @@ export type TContentData = StreamContentData & {
|
||||
|
||||
export const actionDelimiter = '_action_';
|
||||
export const actionDomainSeparator = '---';
|
||||
export const hostImageIdSuffix = '_host_copy';
|
||||
export const hostImageNamePrefix = 'host_copy_';
|
||||
|
||||
export enum AuthTypeEnum {
|
||||
ServiceHttp = 'service_http',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// v0.6.10
|
||||
// v0.7.0
|
||||
module.exports = {
|
||||
tailwindConfig: './client/tailwind.config.cjs',
|
||||
printWidth: 100,
|
||||
|
||||
2
rag.yml
2
rag.yml
@@ -21,7 +21,7 @@ services:
|
||||
- POSTGRES_USER=myuser
|
||||
- POSTGRES_PASSWORD=mypassword
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "${RAG_PORT}:${RAG_PORT}"
|
||||
depends_on:
|
||||
- vectordb
|
||||
env_file:
|
||||
|
||||
21
utils/docker/docker-build.sh
Executable file
21
utils/docker/docker-build.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
[ "$1" = -x ] && shift && set -x
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
cd ${DIR}/../..
|
||||
|
||||
TAG=$1
|
||||
|
||||
if [[ -z "${TAG}" ]]; then
|
||||
TAG=${LIBRE_CHAT_DOCKER_TAG}
|
||||
fi
|
||||
|
||||
if [[ -z "${TAG}" ]]; then
|
||||
TAG=latest
|
||||
fi
|
||||
|
||||
LOCAL_DOCKER_IMG=librechat:${TAG}
|
||||
|
||||
set -e
|
||||
|
||||
docker build -t ${LOCAL_DOCKER_IMG} .
|
||||
31
utils/docker/docker-push.sh
Executable file
31
utils/docker/docker-push.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
[ "$1" = -x ] && shift && set -x
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
cd ${DIR}/../..
|
||||
|
||||
TAG=$1
|
||||
|
||||
if [[ -z "${TAG}" ]]; then
|
||||
TAG=${LIBRE_CHAT_DOCKER_TAG}
|
||||
fi
|
||||
|
||||
if [[ -z "${TAG}" ]]; then
|
||||
TAG=latest
|
||||
fi
|
||||
|
||||
LOCAL_DOCKER_IMG=librechat:${TAG}
|
||||
|
||||
if [[ -z "${DOCKER_REMOTE_REGISTRY}" ]]; then
|
||||
echo "DOCKER_REMOTE_REGISTRY is not set" >&2
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REMOTE_DOCKER_IMG=${DOCKER_REMOTE_REGISTRY}/${LOCAL_DOCKER_IMG}
|
||||
|
||||
set -e
|
||||
|
||||
docker tag ${LOCAL_DOCKER_IMG} ${REMOTE_DOCKER_IMG}
|
||||
|
||||
docker push ${REMOTE_DOCKER_IMG}
|
||||
Reference in New Issue
Block a user