Compare commits

..
35 Commits
Author SHA1 Message Date
Hanzo Dev 00871642a4 chore: bump version to v0.7.9 2025-06-27 14:44:31 -04:00
Hanzo Dev 83bfac37ee fix: Update logo SVGs to support dark mode with proper theme switching
- Replace hardcoded colors with CSS classes that respond to prefers-color-scheme
- Update hanzo-logo.svg and logo.svg with .logo-main and .logo-shadow classes
- Remove theme-based logo switching logic from UnknownIcon component
- Simplify AuthLayout to remove unnecessary text color classes on logo
2025-06-27 10:33:50 -04:00
Hanzo Dev 10b0f4be3a Update logos 2025-06-27 02:02:29 -04:00
Hanzo Dev a1cf2e5395 fix: Fix Jest configuration and module path mapping
- Fix syntax error in Jest config (unclosed string)
- Update @hanzochat/data-provider module path mapping
- Point to correct location in packages/data-provider
- Remove duplicate configuration entries
2025-06-27 01:20:18 -04:00
Hanzo DevandClaude 35052e95d2 feat: Rename Vendor Mode to Brand Mode
- Renamed all VENDOR_* environment variables to BRAND_*
- Updated VendorService to BrandService
- Updated vendorMode middleware to brandMode
- Updated useVendorMode hook to useBrandMode
- Renamed vendor-related files to brand-related
- Updated all references throughout the codebase
- Created comprehensive Brand Mode documentation
- Fixed all remaining vendor references in BrandService.js
- Updated Jest config for module resolution

This change makes the white-label deployment feature more appealing
and better reflects its purpose as a branding customization tool.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-26 20:44:51 -04:00
Hanzo Dev 5a690460cd Simple demo 2025-06-26 18:57:25 -04:00
Hanzo Dev 0882ef6d1c Update compose 2025-06-26 18:47:46 -04:00
Hanzo DevandClaude ca6f2d7a2a feat: Add comprehensive CI/CD with GitHub Actions and test fixes
- Add docker-build.yml for automatic image building on push
- Add ci-cd.yml for complete CI/CD pipeline with testing
- Add docker-cache.yml for efficient caching strategy
- Add release.yml for automated releases with changelogs
- Configure multi-platform builds (amd64/arm64)
- Set up security scanning with Trivy
- Enable SBOM generation for supply chain security
- Add git-cliff configuration for changelog generation

This enables automatic building and publishing of Docker images
to GitHub Container Registry with proper versioning and caching.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-26 17:56:47 -04:00
Hanzo DevandClaude bfb4cb3f66 fix: Use direct node command instead of npm script in production
The production container doesn't have cross-env installed, so we use
the node command directly to start the server.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-26 17:53:17 -04:00
Hanzo DevandClaude 30fd72ec1b feat: Add vendor mode for white-label deployments
Implement comprehensive vendor mode that allows white-labeling the chat platform
with custom branding and model names while internally routing to any backend.

Key features:
- Enable vendor mode via VENDOR_MODE=true environment variable
- Custom branding: name, logo, colors, app title
- Fixed model names (e.g., "Hanzo Zen-1") that map to actual backend models
- Hide UI elements: model switchers, endpoint menus, API key inputs
- Support for multiple backends: OpenAI, Anthropic, custom endpoints
- Default configuration for Hanzo AI with Zen-1 models

Changes:
- Add VendorService to handle all vendor mode logic
- Create /api/vendor/config endpoint for frontend configuration
- Add useVendorMode React hook for accessing vendor settings
- Update ModelSelector to respect vendor mode restrictions
- Modify Makefile with vendor mode commands and setup
- Set Hanzo AI as default vendor in .env.example
- Add comprehensive documentation in docs/VENDOR_MODE.md

This enables easy white-label deployments while maintaining full backend flexibility.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-26 17:33:10 -04:00
Hanzo DevandClaude cd9624b434 feat: Add comprehensive CI/CD with GitHub Actions and test fixes
- Set up GitHub Actions workflows for CI, PR checks, and releases
- Fix all critical test failures (99.7% tests now passing)
- Add Husky pre-commit hooks with lint-staged
- Configure Jest for better test reporting
- Fix AnthropicClient and OpenAIClient streaming issues
- Fix Winston logger null reference errors
- Add test environment configuration
- Update package.json with new test and lint scripts
- Add branch protection documentation
- Set up automated testing, linting, and security scanning
- Add Docker build and NPM publishing to release workflow

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-26 16:36:32 -04:00
Hanzo Dev 6048f68105 fix: Update all .github references from LibreChat to HanzoChat
- Update Discord links to hanzoai
- Update all workflow image names to hanzochat
- Update npm package references to @hanzochat
- Update documentation links to hanzo.ai
- Fix all repository references
2025-06-26 12:30:20 -04:00
Hanzo Dev b8605e89dc fix: Update pnpm lockfile 2025-06-26 12:25:40 -04:00
Hanzo Dev 86f827494d feat: Convert to black and white theme
- Remove all color variables (green, red, amber) from CSS
- Update Tailwind config to use only grays
- Convert all SVG icons to grayscale
- Update component color classes to use grays
- Replace colored UI elements with black/white/gray alternatives
- Maintain accessibility with sufficient contrast ratios
2025-06-26 12:12:57 -04:00
Hanzo Dev 15aace307b chore: Complete rebranding from LibreChat to HanzoChat
- Update all @librechat/agents imports to @hanzochat/agents
- Add author information (Hanzo Industries Inc <dev@hanzo.ai>) to all package.json files
- Create @hanzochat/agents package with required exports and types
- Update VSCode launch configuration
- Remove LibreChat TODO comments
- Publish packages: @hanzochat/data-schemas@0.0.11, @hanzochat/data-provider@0.7.88, @hanzochat/api@1.2.6
2025-06-26 11:56:08 -04:00
Hanzo Dev 89a499febb fix: Update package names and dependencies for @hanzochat namespace
- Renamed hanzo-data-provider to @hanzochat/data-provider
- Updated all imports from chat-data-provider to @hanzochat/data-provider
- Fixed @chat/* imports to use @hanzochat/* namespace
- Updated @hanzochat/agents to @librechat/agents
- Fixed MCPIcon import path in client
- Added missing @redis/client dependency
- Rebuilt all packages with correct dependencies
- All packages now build successfully
2025-06-26 11:11:25 -04:00
Hanzo Dev 317e24a032 fix: Resolve merge conflicts from rebranding update
- Fixed merge conflicts in 6 translation files (es, ja, ko, nl, pt-BR, zh-Hans)
- Fixed merge conflict in CHANGELOG.md
- Updated branding to use 'Chat' and 'Hanzo' consistently
- Preserved all recent changes from HEAD version
2025-06-26 03:31:58 -04:00
Hanzo Dev 4d67f7d81b Update imports 2025-06-26 03:15:25 -04:00
Hanzo Dev 071bcf6f4d Update gitignore 2025-06-26 02:40:40 -04:00
Hanzo DevandClaude f599632f57 chore: Update branding references in documentation and configs
- Update GitHub templates and workflows
- Update Docker compose examples
- Update Helm chart values
- Update package.json files
- Sync CHANGELOG formatting

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-26 02:37:06 -04:00
Hanzo DevandClaude c4e841c2b9 feat: Add RAG API Docker image build and publish
- Create rag.Dockerfile to rebrand LibreChat RAG API
- Update GitHub Actions to build both main and RAG API images
- Both images now published to ghcr.io/hanzoai/

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-25 23:36:24 -04:00
Hanzo Dev 46657ba812 fix: Update Docker build workflow with manual trigger 2025-06-25 23:21:48 -04:00
Hanzo DevandClaude e6737188c1 feat: Complete Hanzo Chat rebranding
- Replace all LibreChat references with Hanzo Chat
- Update package namespace from @librechat to @hanzochat
- Update Docker images to use ghcr.io/hanzoai/chat
- Update configuration files and environment variables
- Update translation keys and UI references
- Rename helm charts to hanzo-chat
- Update MongoDB database name to HanzoChat
- Update all documentation links to hanzo.ai

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-25 23:11:58 -04:00
Hanzo DevandClaude 2c832aa8b9 refactor: Rename configuration file from librechat.yaml to chat.yaml
- Renamed librechat.yaml to chat.yaml for brand consistency
- Renamed librechat.example.yaml to chat.example.yaml
- Updated all references across the codebase:
  - Source files (JS/TS)
  - Translation files (all locales)
  - Docker compose files
  - Helm charts
  - Documentation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-25 22:52:55 -04:00
Hanzo DevandClaude fd3d031cc7 feat: Add GitHub Actions workflow for Docker image publishing
- Add workflow to build and push Docker images to ghcr.io on push to main
- Update docker-compose.yml to use ghcr.io registry instead of Docker Hub
- Support multi-platform builds (amd64 and arm64)
- Automatic tagging based on branch/version/SHA

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-25 22:46:29 -04:00
Hanzo DevandClaude 7e6c10299f feat: Complete Hanzo Chat rebranding
- Update footer to show "Hanzo Chat v0.7.8 - AI Infrastructure for Everyone"
- Replace all LibreChat references with Hanzo Chat
- Update README with Hanzo Chat branding and clean badges
- Remove all remaining green color styles (replaced with grayscale)
- Update package.json descriptions and metadata
- Update HTML meta tags and window title
- Add ALLOW_REGISTRATION=true to .env for user signup

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-25 18:29:16 -04:00
Hanzo DevandClaude 0f8eea1e3a chore: Update Makefile with branding command and sync with upstream
- Add 'make update-branding' command to update logo in running container
- Merge latest upstream LibreChat changes (486c1314a)
- Includes: configurable retention for temp chats, Bedrock MCP support,
  thinking budget for Gemini 2.5, client-side image resizing, and more
- Ensure all Hanzo customizations are preserved

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-25 17:58:53 -04:00
Hanzo Dev 751b8122ab Merge latest upstream LibreChat changes with Hanzo customizations 2025-06-25 17:50:20 -04:00
Hanzo DevandClaude 32d3485663 feat: Customize Hanzo Chat with white/black theme and Hanzo logo
- Replace LibreChat logo with Hanzo geometric logo
- Update theme to use white buttons with black text throughout
- Remove green color scheme in favor of white/black styling
- Add comprehensive Makefile for local development
- Update docker-compose.override.yml for asset mounting
- Configure all auth components, panels, and UI elements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-25 17:27:03 -04:00
Hanzo Dev 7b05ada0ca Merge upstream LibreChat updates: Index sync, MeiliSearch optimization, and agent packages bump
# Conflicts:
#	package-lock.json
2025-06-23 17:47:44 -04:00
Hanzo Dev 88cd45da4b Fix package.json exports for CommonJS compatibility 2025-06-23 17:46:49 -04:00
Hanzo Dev d8b20d61e9 Fix all merge conflicts and update dependencies 2025-06-20 14:07:53 -04:00
Hanzo Dev 91e9711e6b Fix merge conflict in package.json - update to v0.7.8 2025-06-20 13:52:49 -04:00
Hanzo Dev d0b64340fe Fix MCP and add example 2025-06-20 13:43:44 -04:00
Hanzo Dev fb7787c87a Update brand 2025-06-20 13:43:27 -04:00
3679 changed files with 77095 additions and 622704 deletions
-15
View File
@@ -1,15 +0,0 @@
# used by CI/CD testing
openai==1.81.0
python-dotenv
tiktoken
importlib_metadata
cohere
redis==5.2.1
redisvl==0.4.1
anthropic
orjson==3.10.12 # fast /embedding responses
pydantic==2.10.2
google-cloud-aiplatform==1.43.0
fastapi-sso==0.16.0
uvloop==0.21.0
mcp==1.9.3 # for MCP server
-62
View File
@@ -1,62 +0,0 @@
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
# restart: always
links:
- mongodb
- meilisearch
# ports:
# - 3080:3080 # Change it to 9000:3080 to use nginx
extra_hosts: # if you are running APIs on docker you need access to, you will need to uncomment this line and next
- "host.docker.internal:host-gateway"
volumes:
# This is where VS Code should expect to find your project's source code and the value of "workspaceFolder" in .devcontainer/devcontainer.json
- ..:/workspaces:cached
# Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details.
# - /var/run/docker.sock:/var/run/docker.sock
environment:
- HOST=0.0.0.0
- MONGO_URI=mongodb://mongodb:27017/HanzoChat
# - OPENAI_REVERSE_PROXY=http://host.docker.internal:8070/v1
- MEILI_HOST=http://meilisearch:7700
# Runs app on the same network as the service container, allows "forwardPorts" in devcontainer.json function.
# network_mode: service:another-service
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
# Use a non-root user for all processes - See https://aka.ms/vscode-remote/containers/non-root for details.
user: vscode
# Overrides default command so things don't shut down after the process ends.
command: /bin/sh -c "while sleep 1000; do :; done"
mongodb:
container_name: chat-mongodb
expose:
- 27017
# ports:
# - 27018:27017
image: mongo
# restart: always
volumes:
- ./data-node:/data/db
command: mongod --noauth
meilisearch:
container_name: chat-meilisearch
image: getmeili/meilisearch:v1.5
# restart: always
expose:
- 7700
# Uncomment this to access meilisearch from outside docker
# ports:
# - 7700:7700 # if exposing these ports, make sure your master key is not the default value
environment:
- MEILI_NO_ANALYTICS=true
- MEILI_MASTER_KEY=5c71cf56d672d009e36070b5bc5e47b743535ae55c818ae3b735bb6ebfb4ba63
volumes:
- ./meili_data_v1.5:/meili_data
+17 -51
View File
@@ -1,52 +1,18 @@
{
"name": "Python 3.11",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/python:3.11-bookworm",
// https://github.com/devcontainers/images/tree/main/src/python
// https://mcr.microsoft.com/en-us/product/devcontainers/python/tags
// "build": {
// "dockerfile": "Dockerfile",
// "context": ".."
// },
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
"settings": {},
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"GitHub.copilot",
"GitHub.copilot-chat",
"ms-python.autopep8"
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [4000],
"containerEnv": {
"LITELLM_LOG": "DEBUG"
},
// Use 'portsAttributes' to set default properties for specific forwarded ports.
// More info: https://containers.dev/implementors/json_reference/#port-attributes
"portsAttributes": {
"4000": {
"label": "LiteLLM Server",
"onAutoForward": "notify"
}
},
// More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "litellm",
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "pipx install poetry && poetry install -E extra_proxy -E proxy"
}
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces",
"customizations": {
"vscode": {
"extensions": [],
"settings": {
"terminal.integrated.profiles.linux": {
"bash": null
}
}
}
},
"postCreateCommand": "",
"features": { "ghcr.io/devcontainers/features/git:1": {} },
"remoteUser": "vscode"
}
+3 -2
View File
@@ -19,8 +19,9 @@ services:
# - /var/run/docker.sock:/var/run/docker.sock
environment:
- HOST=0.0.0.0
- MONGO_URI=mongodb://mongodb:27017/LibreChat
# - OPENAI_REVERSE_PROXY=http://host.docker.internal:8070/v1
- MONGO_URI=mongodb://mongodb:27017/HanzoChat
# - CHATGPT_REVERSE_PROXY=http://host.docker.internal:8080/api/conversation # if you are hosting your own chatgpt reverse proxy with docker
# - OPENAI_REVERSE_PROXY=http://host.docker.internal:8070/v1/chat/completions # if you are hosting your own chatgpt reverse proxy with docker
- MEILI_HOST=http://meilisearch:7700
# Runs app on the same network as the service container, allows "forwardPorts" in devcontainer.json function.
-25
View File
@@ -1,25 +0,0 @@
# Caddy reverse proxy with bearer token auth and automatic HTTPS.
# The domain is supplied via environment variable GITNEXUS_DOMAIN,
# and the auth token via API_TOKEN. Both are set in docker-compose.yml.
{$GITNEXUS_DOMAIN} {
# Health check — unauthenticated so monitoring can probe it
@health path /health
handle @health {
reverse_proxy gitnexus:4747 {
rewrite /api/info
}
}
# All other routes require bearer token
@authed {
header Authorization "Bearer {$API_TOKEN}"
}
handle @authed {
reverse_proxy gitnexus:4747
}
# Reject unauthenticated requests
respond "Unauthorized" 401
}
-45
View File
@@ -1,45 +0,0 @@
# Long-lived GitNexus image for DigitalOcean droplet deployment.
#
# This image does NOT bake in the index data. Indexes are mounted from
# the host at /indexes/<repo>/.gitnexus/ and registered at container
# startup. A fresh index only requires rsync + container restart — no
# image rebuild on every push.
FROM node:24.16.0-slim
ARG GITNEXUS_VERSION=1.5.3
# 1. Build native addons with Bookworm toolchain, then remove build tools.
# curl stays for the docker healthcheck; Caddy lives in its own container.
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ curl \
&& npm install -g gitnexus@${GITNEXUS_VERSION} \
&& apt-get purge -y --auto-remove python3 make g++ \
&& rm -rf /var/lib/apt/lists/* /root/.npm
# 2. Upgrade libstdc++ from Trixie — @ladybugdb/core prebuilt binary needs
# GLIBCXX_3.4.32 which Bookworm (3.4.31) doesn't ship.
RUN echo "deb http://deb.debian.org/debian trixie main" > /etc/apt/sources.list.d/trixie.list \
&& apt-get update \
&& apt-get install -y -t trixie libstdc++6 \
&& rm /etc/apt/sources.list.d/trixie.list \
&& rm -rf /var/lib/apt/lists/*
# 3. Pre-install LadybugDB FTS + vector extensions so ~/.kuzu/extension/
# is baked into the image. Workaround for upstream GitNexus 1.5.3 bug.
COPY install-extensions.js /tmp/install-extensions.js
RUN node /tmp/install-extensions.js && rm -rf /tmp/install-extensions.js /tmp/lbug-ext-install
# 4. Patch lbug-adapter.js to also LOAD EXTENSION vector after FTS.
RUN LBUG_ADAPTER=/usr/local/lib/node_modules/gitnexus/dist/mcp/core/lbug-adapter.js \
&& grep -q "LOAD EXTENSION fts" "$LBUG_ADAPTER" \
&& sed -i "s|await available\[0\]\.query('LOAD EXTENSION fts');|await available[0].query('LOAD EXTENSION fts'); try { await available[0].query('LOAD EXTENSION vector'); } catch (e) { /* vector extension may not be installed */ }|g" "$LBUG_ADAPTER" \
&& grep -c "LOAD EXTENSION vector" "$LBUG_ADAPTER" \
&& echo "lbug-adapter.js patched to load vector extension"
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 4747
ENTRYPOINT ["/entrypoint.sh"]
-87
View File
@@ -1,87 +0,0 @@
# GitNexus stack for the DigitalOcean droplet.
#
# Two services: the gitnexus server (bound to an internal network only)
# and a Caddy reverse proxy that handles TLS + auth.
#
# Index data lives on the host at /opt/gitnexus/indexes/ and is
# bind-mounted read-write into the gitnexus container. The deploy
# workflow rsyncs fresh indexes into that directory and restarts
# only the gitnexus container — Caddy keeps running undisturbed.
#
# Break-glass: if gitnexus is stuck unhealthy and you need to restart
# just Caddy (e.g. to push an emergency Caddyfile fix), the
# `depends_on: condition: service_healthy` would block:
# docker compose up -d caddy
# Use --no-deps to bypass the dependency check:
# docker compose up -d --no-deps caddy
name: gitnexus
# Shared logging defaults applied to both services so the droplet's
# disk doesn't fill up with unbounded json-file logs.
x-logging: &default-logging
driver: json-file
options:
max-size: '50m'
max-file: '3'
services:
gitnexus:
# Override via GITNEXUS_IMAGE in /opt/gitnexus/.env to use a fork or
# a pinned version tag like :v1.5.3 for reproducible rollbacks.
image: ${GITNEXUS_IMAGE:-ghcr.io/danny-avila/librechat-gitnexus:latest}
container_name: gitnexus
restart: unless-stopped
networks:
- gitnexus-net
volumes:
- /opt/gitnexus/indexes:/indexes
# memswap_limit equal to mem_limit disables swap for this container.
# Without it, Docker lets the process silently swap onto host disk,
# turning sub-second graph queries into multi-second ones. Hard
# OOM-kill is preferable — the container restarts via unless-stopped,
# the deploy health poll catches it, and the failure is explicit.
mem_limit: 1792m
memswap_limit: 1792m
logging: *default-logging
healthcheck:
test: ['CMD', 'curl', '-fsS', 'http://127.0.0.1:4747/api/info']
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
caddy:
image: caddy:2-alpine
container_name: gitnexus-caddy
restart: unless-stopped
# service_healthy (not just service_started) ensures Caddy doesn't
# start routing traffic until gitnexus passes its initial healthcheck
# on a cold `compose up`. This only governs initial startup ordering —
# during force-recreates of gitnexus, Caddy stays up and may briefly
# return 502 while the new gitnexus container binds its port. The
# deploy workflow's health poll catches any sustained failure.
depends_on:
gitnexus:
condition: service_healthy
ports:
- '80:80'
- '443:443'
networks:
- gitnexus-net
volumes:
- /opt/gitnexus/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
logging: *default-logging
environment:
GITNEXUS_DOMAIN: ${GITNEXUS_DOMAIN}
API_TOKEN: ${API_TOKEN}
networks:
gitnexus-net:
driver: bridge
volumes:
caddy-data:
caddy-config:
-48
View File
@@ -1,48 +0,0 @@
#!/bin/sh
set -e
# Cap Node heap below the container's cgroup limit (1792m in compose),
# leaving room for @ladybugdb/core's C++ heap and OS overhead. Native
# allocations happen outside V8's view, so a slim V8 budget is the only
# thing between a heavy query and a cgroup OOM-kill. Without this cap,
# gitnexus defaults to --max-old-space-size=8192 and reserves memory
# the container doesn't have.
export NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=1280}"
# Register every index mounted under /indexes/<name>/.gitnexus/.
# This is idempotent — re-registering an existing repo updates the
# metadata pointer without touching the index data.
#
# Registration failure handling:
# - main (LibreChat) and dev (LibreChat-dev) are critical. If either
# fails to register, exit 1 so docker marks the container unhealthy
# and the deploy workflow's readiness check surfaces the error.
# - PR indexes (LibreChat-pr-*) are best-effort. A corrupt PR index
# shouldn't take the whole server down.
if [ -d /indexes ]; then
for dir in /indexes/*/; do
[ -d "$dir" ] || continue
name=$(basename "$dir")
[ -d "$dir.gitnexus" ] || continue
echo "Registering index: $name"
if ! gitnexus index "$dir" --allow-non-git; then
case "$name" in
LibreChat|LibreChat-dev)
echo "ERROR: failed to register critical index $name" >&2
exit 1
;;
*)
echo "WARN: failed to register PR index $name — skipping" >&2
;;
esac
fi
done
else
echo "WARN: /indexes directory not mounted" >&2
fi
# Bind 0.0.0.0 inside the container so Caddy (in a separate container
# on the same docker network) can reach gitnexus at gitnexus:4747.
# docker-compose.yml intentionally does NOT expose port 4747 on the
# host — only Caddy's 80/443 are published.
exec gitnexus serve --host 0.0.0.0 --port 4747
-46
View File
@@ -1,46 +0,0 @@
/**
* Pre-install LadybugDB extensions (FTS + vector) into the Docker image's
* extension cache (~/.kuzu/extension/). Without this, gitnexus serve's
* lbug-adapter calls LOAD EXTENSION fts at runtime but fails silently
* because the extension was never installed, causing all BM25 and
* semantic queries via the query() tool to return empty.
*
* Workaround for upstream GitNexus 1.5.3 bug where the CI-produced
* .gitnexus/ artifact doesn't include the extension cache.
*/
const path = require('path');
const fs = require('fs');
// @ladybugdb/core lives under the globally-installed gitnexus package.
// This path is stable across gitnexus versions because npm always nests
// transitive deps under the installed package's node_modules.
const lbugPath = '/usr/local/lib/node_modules/gitnexus/node_modules/@ladybugdb/core';
const lbug = require(lbugPath);
const tmpDir = '/tmp/lbug-ext-install';
fs.mkdirSync(tmpDir, { recursive: true });
// Open a throwaway database just to run INSTALL against. The extension
// cache persists in ~/.kuzu/extension/ regardless of which database was
// used to install it, so the throwaway db and tmpDir are deleted in the
// Dockerfile after this script finishes.
const db = new lbug.Database(path.join(tmpDir, 'db'), 0, false, false);
const conn = new lbug.Connection(db);
(async () => {
try {
await conn.query('INSTALL fts');
console.log('FTS extension installed');
} catch (err) {
console.error('FTS install failed:', err.message);
process.exit(1);
}
try {
await conn.query('INSTALL vector');
console.log('Vector extension installed');
} catch (err) {
console.error('Vector install failed:', err.message);
process.exit(1);
}
})();
+16 -16
View File
@@ -1,17 +1,17 @@
docs
cookbook
.circleci
.github
tests
.git
.github
.circleci
.devcontainer
*.tgz
log.txt
docker/Dockerfile.*
node_modules
**/.circleci
**/.editorconfig
**/.dockerignore
**/.git
**/.DS_Store
**/.vscode
**/node_modules
.env
.env.*
!.env.static
# Specific patterns to ignore
data-node
meili_data*
librechat*
Dockerfile*
docs
# Ignore all hidden files
.*
-10
View File
@@ -1,10 +0,0 @@
PORT=3080
RAG_PORT=8000
MEILI_MASTER_KEY=masterkey
ENDPOINTS=openAI,anthropic,assistants,custom
# Security keys (generate random strings)
CREDS_KEY=a_random_32_character_string_for_creds
CREDS_IV=a_random_16_character_string_for_creds
JWT_SECRET=a_random_secret_for_jwt_tokens
JWT_REFRESH_SECRET=another_random_secret_for_refresh_tokens
-1
View File
@@ -1 +0,0 @@
ENDPOINTS=openAI,anthropic,assistants,custom
-660
View File
@@ -1,660 +0,0 @@
#=====================================================================#
# Hanzo Chat Configuration #
#=====================================================================#
# Please refer to the reference documentation for assistance #
# with configuring your Hanzo Chat environment. #
# #
# https://docs.hanzo.ai/chat/configuration/dotenv #
#=====================================================================#
#==================================================#
# Server Configuration #
#==================================================#
HOST=localhost
PORT=3080
MONGO_URI=mongodb://127.0.0.1:27017/HanzoChat
DOMAIN_CLIENT=http://localhost:3080
DOMAIN_SERVER=http://localhost:3080
NO_INDEX=true
# Use the address that is at most n number of hops away from the Express application.
# req.socket.remoteAddress is the first hop, and the rest are looked for in the X-Forwarded-For header from right to left.
# A value of 0 means that the first untrusted address would be req.socket.remoteAddress, i.e. there is no reverse proxy.
# Defaulted to 1.
TRUST_PROXY=1
#===============#
# JSON Logging #
#===============#
# Use when process console logs in cloud deployment like GCP/AWS
CONSOLE_JSON=false
#===============#
# Debug Logging #
#===============#
DEBUG_LOGGING=true
DEBUG_CONSOLE=false
#=============#
# Permissions #
#=============#
# UID=1000
# GID=1000
#===============#
# Configuration #
#===============#
# Use an absolute path, a relative path, or a URL
# CONFIG_PATH="/alternative/path/to/chat.yaml"
#===================================================#
# Endpoints #
#===================================================#
# ENDPOINTS=openAI,assistants,azureOpenAI,google,anthropic
PROXY=
#===================================#
# Known Endpoints - chat.yaml #
#===================================#
# https://docs.hanzo.ai/chat/configuration/ai_endpoints
# ANYSCALE_API_KEY=
# APIPIE_API_KEY=
# COHERE_API_KEY=
# DEEPSEEK_API_KEY=
# DATABRICKS_API_KEY=
# FIREWORKS_API_KEY=
# GROQ_API_KEY=
# HUGGINGFACE_TOKEN=
# MISTRAL_API_KEY=
# OPENROUTER_KEY=
# PERPLEXITY_API_KEY=
# SHUTTLEAI_API_KEY=
# TOGETHERAI_API_KEY=
# UNIFY_API_KEY=
# XAI_API_KEY=
#============#
# Anthropic #
#============#
ANTHROPIC_API_KEY=user_provided
# ANTHROPIC_MODELS=claude-opus-4-20250514,claude-sonnet-4-20250514,claude-3-7-sonnet-20250219,claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307
# ANTHROPIC_REVERSE_PROXY=
#============#
# Azure #
#============#
# Note: these variables are DEPRECATED
# Use the `chat.yaml` configuration for `azureOpenAI` instead
# You may also continue to use them if you opt out of using the `chat.yaml` configuration
# AZURE_OPENAI_DEFAULT_MODEL=gpt-3.5-turbo # Deprecated
# AZURE_OPENAI_MODELS=gpt-3.5-turbo,gpt-4 # Deprecated
# AZURE_USE_MODEL_AS_DEPLOYMENT_NAME=TRUE # Deprecated
# AZURE_API_KEY= # Deprecated
# AZURE_OPENAI_API_INSTANCE_NAME= # Deprecated
# AZURE_OPENAI_API_DEPLOYMENT_NAME= # Deprecated
# AZURE_OPENAI_API_VERSION= # Deprecated
# AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME= # Deprecated
# AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME= # Deprecated
# PLUGINS_USE_AZURE="true" # Deprecated
#=================#
# AWS Bedrock #
#=================#
# BEDROCK_AWS_DEFAULT_REGION=us-east-1 # A default region must be provided
# BEDROCK_AWS_ACCESS_KEY_ID=someAccessKey
# BEDROCK_AWS_SECRET_ACCESS_KEY=someSecretAccessKey
# BEDROCK_AWS_SESSION_TOKEN=someSessionToken
# Note: This example list is not meant to be exhaustive. If omitted, all known, supported model IDs will be included for you.
# BEDROCK_AWS_MODELS=anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0
# See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns
# Notes on specific models:
# The following models are not support due to not supporting streaming:
# ai21.j2-mid-v1
# The following models are not support due to not supporting conversation history:
# ai21.j2-ultra-v1, cohere.command-text-v14, cohere.command-light-text-v14
#============#
# Google #
#============#
GOOGLE_KEY=user_provided
# GOOGLE_REVERSE_PROXY=
# Some reverse proxies do not support the X-goog-api-key header, uncomment to pass the API key in Authorization header instead.
# GOOGLE_AUTH_HEADER=true
# Gemini API (AI Studio)
# GOOGLE_MODELS=gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite-preview-06-17,gemini-2.0-flash,gemini-2.0-flash-lite
# Vertex AI
# GOOGLE_MODELS=gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite-preview-06-17,gemini-2.0-flash-001,gemini-2.0-flash-lite-001
# GOOGLE_TITLE_MODEL=gemini-2.0-flash-lite-001
# GOOGLE_LOC=us-central1
# Google Safety Settings
# NOTE: These settings apply to both Vertex AI and Gemini API (AI Studio)
#
# For Vertex AI:
# To use the BLOCK_NONE setting, you need either:
# (a) Access through an allowlist via your Google account team, or
# (b) Switch to monthly invoiced billing: https://cloud.google.com/billing/docs/how-to/invoiced-billing
#
# For Gemini API (AI Studio):
# BLOCK_NONE is available by default, no special account requirements.
#
# Available options: BLOCK_NONE, BLOCK_ONLY_HIGH, BLOCK_MEDIUM_AND_ABOVE, BLOCK_LOW_AND_ABOVE
#
# GOOGLE_SAFETY_SEXUALLY_EXPLICIT=BLOCK_ONLY_HIGH
# GOOGLE_SAFETY_HATE_SPEECH=BLOCK_ONLY_HIGH
# GOOGLE_SAFETY_HARASSMENT=BLOCK_ONLY_HIGH
# GOOGLE_SAFETY_DANGEROUS_CONTENT=BLOCK_ONLY_HIGH
# GOOGLE_SAFETY_CIVIC_INTEGRITY=BLOCK_ONLY_HIGH
#============#
# OpenAI #
#============#
OPENAI_API_KEY=user_provided
# OPENAI_MODELS=o1,o1-mini,o1-preview,gpt-4o,gpt-4.5-preview,chatgpt-4o-latest,gpt-4o-mini,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
DEBUG_OPENAI=false
# TITLE_CONVO=false
# OPENAI_TITLE_MODEL=gpt-4o-mini
# OPENAI_SUMMARIZE=true
# OPENAI_SUMMARY_MODEL=gpt-4o-mini
# OPENAI_FORCE_PROMPT=true
# OPENAI_REVERSE_PROXY=
# OPENAI_ORGANIZATION=
#====================#
# Assistants API #
#====================#
ASSISTANTS_API_KEY=user_provided
# ASSISTANTS_BASE_URL=
# ASSISTANTS_MODELS=gpt-4o,gpt-4o-mini,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
#==========================#
# Azure Assistants API #
#==========================#
# Note: You should map your credentials with custom variables according to your Azure OpenAI Configuration
# The models for Azure Assistants are also determined by your Azure OpenAI configuration.
# More info, including how to enable use of Assistants with Azure here:
# https://docs.hanzo.ai/chat/configuration/ai_endpoints/azure#using-assistants-with-azure
#============#
# Plugins #
#============#
# PLUGIN_MODELS=gpt-4o,gpt-4o-mini,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
DEBUG_PLUGINS=true
CREDS_KEY=f34be427ebb29de8d88c107a71546019685ed8b241d8f2ed00c3df97ad2566f0
CREDS_IV=e2341419ec3dd3d19b13a1a87fafcbfb
# Azure AI Search
#-----------------
AZURE_AI_SEARCH_SERVICE_ENDPOINT=
AZURE_AI_SEARCH_INDEX_NAME=
AZURE_AI_SEARCH_API_KEY=
AZURE_AI_SEARCH_API_VERSION=
AZURE_AI_SEARCH_SEARCH_OPTION_QUERY_TYPE=
AZURE_AI_SEARCH_SEARCH_OPTION_TOP=
AZURE_AI_SEARCH_SEARCH_OPTION_SELECT=
# OpenAI Image Tools Customization
#----------------
# IMAGE_GEN_OAI_DESCRIPTION_WITH_FILES=Custom description for image generation tool when files are present
# IMAGE_GEN_OAI_DESCRIPTION_NO_FILES=Custom description for image generation tool when no files are present
# IMAGE_EDIT_OAI_DESCRIPTION=Custom description for image editing tool
# IMAGE_GEN_OAI_PROMPT_DESCRIPTION=Custom prompt description for image generation tool
# IMAGE_EDIT_OAI_PROMPT_DESCRIPTION=Custom prompt description for image editing tool
# 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=
# Flux
#-----------------
FLUX_API_BASE_URL=https://api.us1.bfl.ai
# FLUX_API_BASE_URL = 'https://api.bfl.ml';
# Get your API key at https://api.us1.bfl.ai/auth/profile
# FLUX_API_KEY=
# Google
#-----------------
GOOGLE_SEARCH_API_KEY=
GOOGLE_CSE_ID=
# YOUTUBE
#-----------------
YOUTUBE_API_KEY=
# SerpAPI
#-----------------
SERPAPI_API_KEY=
# Stable Diffusion
#-----------------
SD_WEBUI_URL=http://host.docker.internal:7860
# Tavily
#-----------------
TAVILY_API_KEY=
# Traversaal
#-----------------
TRAVERSAAL_API_KEY=
# WolframAlpha
#-----------------
WOLFRAM_APP_ID=
# Zapier
#-----------------
ZAPIER_NLA_API_KEY=
#==================================================#
# Search #
#==================================================#
SEARCH=true
MEILI_NO_ANALYTICS=true
MEILI_HOST=http://localhost:7700
MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt
# Optional: Disable indexing, useful in a multi-node setup
# where only one instance should perform an index sync.
# MEILI_NO_SYNC=true
#==================================================#
# Speech to Text & Text to Speech #
#==================================================#
STT_API_KEY=
TTS_API_KEY=
#==================================================#
# RAG #
#==================================================#
# More info: https://docs.hanzo.ai/chat/configuration/rag_api
# RAG_OPENAI_BASEURL=
# RAG_OPENAI_API_KEY=
# RAG_USE_FULL_CONTEXT=
# EMBEDDINGS_PROVIDER=openai
# EMBEDDINGS_MODEL=text-embedding-3-small
#===================================================#
# User System #
#===================================================#
#========================#
# Moderation #
#========================#
OPENAI_MODERATION=false
OPENAI_MODERATION_API_KEY=
# OPENAI_MODERATION_REVERSE_PROXY=
BAN_VIOLATIONS=true
BAN_DURATION=1000 * 60 * 60 * 2
BAN_INTERVAL=20
LOGIN_VIOLATION_SCORE=1
REGISTRATION_VIOLATION_SCORE=1
CONCURRENT_VIOLATION_SCORE=1
MESSAGE_VIOLATION_SCORE=1
NON_BROWSER_VIOLATION_SCORE=20
LOGIN_MAX=7
LOGIN_WINDOW=5
REGISTER_MAX=5
REGISTER_WINDOW=60
LIMIT_CONCURRENT_MESSAGES=true
CONCURRENT_MESSAGE_MAX=2
LIMIT_MESSAGE_IP=true
MESSAGE_IP_MAX=40
MESSAGE_IP_WINDOW=1
LIMIT_MESSAGE_USER=false
MESSAGE_USER_MAX=40
MESSAGE_USER_WINDOW=1
ILLEGAL_MODEL_REQ_SCORE=5
#========================#
# Balance #
#========================#
# CHECK_BALANCE=false
# START_BALANCE=20000 # note: the number of tokens that will be credited after registration.
#========================#
# Registration and Login #
#========================#
ALLOW_EMAIL_LOGIN=true
ALLOW_REGISTRATION=true
ALLOW_SOCIAL_LOGIN=false
ALLOW_SOCIAL_REGISTRATION=false
ALLOW_PASSWORD_RESET=false
# ALLOW_ACCOUNT_DELETION=true # note: enabled by default if omitted/commented out
ALLOW_UNVERIFIED_EMAIL_LOGIN=true
SESSION_EXPIRY=1000 * 60 * 15
REFRESH_TOKEN_EXPIRY=(1000 * 60 * 60 * 24) * 7
JWT_SECRET=16f8c0ef4a5d391b26034086c628469d3f9f497f08163ab9b40137092f2909ef
JWT_REFRESH_SECRET=eaa5191f2914e30b9387fd84e254e4ba6fc51b4654968a9b0803b456a54b8418
# Discord
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
DISCORD_CALLBACK_URL=/oauth/discord/callback
# Facebook
FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
FACEBOOK_CALLBACK_URL=/oauth/facebook/callback
# GitHub
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_CALLBACK_URL=/oauth/github/callback
# GitHub Enterprise
# GITHUB_ENTERPRISE_BASE_URL=
# GITHUB_ENTERPRISE_USER_AGENT=
# Google
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=/oauth/google/callback
# Apple
APPLE_CLIENT_ID=
APPLE_TEAM_ID=
APPLE_KEY_ID=
APPLE_PRIVATE_KEY_PATH=
APPLE_CALLBACK_URL=/oauth/apple/callback
# OpenID
OPENID_CLIENT_ID=
OPENID_CLIENT_SECRET=
OPENID_ISSUER=
OPENID_SESSION_SECRET=
OPENID_SCOPE="openid profile email"
OPENID_CALLBACK_URL=/oauth/openid/callback
OPENID_REQUIRED_ROLE=
OPENID_REQUIRED_ROLE_TOKEN_KIND=
OPENID_REQUIRED_ROLE_PARAMETER_PATH=
# Set to determine which user info property returned from OpenID Provider to store as the User's username
OPENID_USERNAME_CLAIM=
# Set to determine which user info property returned from OpenID Provider to store as the User's name
OPENID_NAME_CLAIM=
OPENID_BUTTON_LABEL=
OPENID_IMAGE_URL=
# Set to true to automatically redirect to the OpenID provider when a user visits the login page
# This will bypass the login form completely for users, only use this if OpenID is your only authentication method
OPENID_AUTO_REDIRECT=false
# Set to true to use PKCE (Proof Key for Code Exchange) for OpenID authentication
OPENID_USE_PKCE=false
#Set to true to reuse openid tokens for authentication management instead of using the mongodb session and the custom refresh token.
OPENID_REUSE_TOKENS=
#By default, signing key verification results are cached in order to prevent excessive HTTP requests to the JWKS endpoint.
#If a signing key matching the kid is found, this will be cached and the next time this kid is requested the signing key will be served from the cache.
#Default is true.
OPENID_JWKS_URL_CACHE_ENABLED=
OPENID_JWKS_URL_CACHE_TIME= # 600000 ms eq to 10 minutes leave empty to disable caching
#Set to true to trigger token exchange flow to acquire access token for the userinfo endpoint.
OPENID_ON_BEHALF_FLOW_FOR_USERINFRO_REQUIRED=
OPENID_ON_BEHALF_FLOW_USERINFRO_SCOPE = "user.read" # example for Scope Needed for Microsoft Graph API
# Set to true to use the OpenID Connect end session endpoint for logout
OPENID_USE_END_SESSION_ENDPOINT=
# SAML
# Note: If OpenID is enabled, SAML authentication will be automatically disabled.
SAML_ENTRY_POINT=
SAML_ISSUER=
SAML_CERT=
SAML_CALLBACK_URL=/oauth/saml/callback
SAML_SESSION_SECRET=
# Attribute mappings (optional)
SAML_EMAIL_CLAIM=
SAML_USERNAME_CLAIM=
SAML_GIVEN_NAME_CLAIM=
SAML_FAMILY_NAME_CLAIM=
SAML_PICTURE_CLAIM=
SAML_NAME_CLAIM=
# Logint buttion settings (optional)
SAML_BUTTON_LABEL=
SAML_IMAGE_URL=
# Whether the SAML Response should be signed.
# - If "true", the entire `SAML Response` will be signed.
# - If "false" or unset, only the `SAML Assertion` will be signed (default behavior).
# SAML_USE_AUTHN_RESPONSE_SIGNED=
# LDAP
LDAP_URL=
LDAP_BIND_DN=
LDAP_BIND_CREDENTIALS=
LDAP_USER_SEARCH_BASE=
#LDAP_SEARCH_FILTER="mail="
LDAP_CA_CERT_PATH=
# LDAP_TLS_REJECT_UNAUTHORIZED=
# LDAP_STARTTLS=
# LDAP_LOGIN_USES_USERNAME=true
# LDAP_ID=
# LDAP_USERNAME=
# LDAP_EMAIL=
# LDAP_FULL_NAME=
#========================#
# 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_FROM=noreply@hanzo.ai
#========================#
# Mailgun API #
#========================#
# MAILGUN_API_KEY=your-mailgun-api-key
# MAILGUN_DOMAIN=mg.yourdomain.com
# EMAIL_FROM=noreply@yourdomain.com
# EMAIL_FROM_NAME="Hanzo Chat"
# # Optional: For EU region
# MAILGUN_HOST=https://api.eu.mailgun.net
#========================#
# Firebase CDN #
#========================#
FIREBASE_API_KEY=
FIREBASE_AUTH_DOMAIN=
FIREBASE_PROJECT_ID=
FIREBASE_STORAGE_BUCKET=
FIREBASE_MESSAGING_SENDER_ID=
FIREBASE_APP_ID=
#========================#
# S3 AWS Bucket #
#========================#
AWS_ENDPOINT_URL=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
AWS_BUCKET_NAME=
#========================#
# Azure Blob Storage #
#========================#
AZURE_STORAGE_CONNECTION_STRING=
AZURE_STORAGE_PUBLIC_ACCESS=false
AZURE_CONTAINER_NAME=files
#========================#
# Shared Links #
#========================#
ALLOW_SHARED_LINKS=true
ALLOW_SHARED_LINKS_PUBLIC=true
#==============================#
# Static File Cache Control #
#==============================#
# Leave commented out to use defaults: 1 day (86400 seconds) for s-maxage and 2 days (172800 seconds) for max-age
# NODE_ENV must be set to production for these to take effect
# STATIC_CACHE_MAX_AGE=172800
# STATIC_CACHE_S_MAX_AGE=86400
# If you have another service in front of your Hanzo Chat doing compression, disable express based compression here
# DISABLE_COMPRESSION=true
#===================================================#
# UI #
#===================================================#
APP_TITLE=Hanzo Chat
# CUSTOM_FOOTER="My custom footer"
HELP_AND_FAQ_URL=https://hanzo.ai
# SHOW_BIRTHDAY_ICON=true
# Google tag manager id
#ANALYTICS_GTM_ID=user provided google tag manager id
#===============#
# REDIS Options #
#===============#
# REDIS_URI=10.10.10.10:6379
# USE_REDIS=true
# USE_REDIS_CLUSTER=true
# REDIS_CA=/path/to/ca.crt
#==================================================#
# Others #
#==================================================#
# You should leave the following commented out #
# NODE_ENV=
# E2E_USER_EMAIL=
# E2E_USER_PASSWORD=
#=====================================================#
# Cache Headers #
#=====================================================#
# Headers that control caching of the index.html #
# Default configuration prevents caching to ensure #
# users always get the latest version. Customize #
# only if you understand caching implications. #
# INDEX_CACHE_CONTROL=no-cache, no-store, must-revalidate
# INDEX_PRAGMA=no-cache
# INDEX_EXPIRES=0
# no-cache: Forces validation with server before using cached version
# no-store: Prevents storing the response entirely
# must-revalidate: Prevents using stale content when offline
#=====================================================#
# OpenWeather #
#=====================================================#
OPENWEATHER_API_KEY=
#====================================#
# Hanzo Chat Code Interpreter API #
#====================================#
# https://code.hanzo.ai
# HANZO_CODE_API_KEY=your-key
#======================#
# Web Search #
#======================#
# Note: All of the following variable names can be customized.
# Omit values to allow user to provide them.
# For more information on configuration values, see:
# https://docs.hanzo.ai/chat/features/web_search
# Search Provider (Required)
# SERPER_API_KEY=your_serper_api_key
# Scraper (Required)
# FIRECRAWL_API_KEY=your_firecrawl_api_key
# Optional: Custom Firecrawl API URL
# FIRECRAWL_API_URL=your_firecrawl_api_url
# Reranker (Required)
# JINA_API_KEY=your_jina_api_key
# or
# COHERE_API_KEY=your_cohere_api_key
-95
View File
@@ -1,95 +0,0 @@
# ==================================
# BRAND MODE EXAMPLE CONFIGURATION
# ==================================
# This example shows how to configure Hanzo Chat in brand mode
# for white-label deployments
# Copy the main .env.example first, then override with these settings:
# cp .env.example .env
# Then add these brand mode settings
# ==================================
# Enable Brand Mode
# ==================================
BRAND_MODE=true
# ==================================
# Brand Configuration
# ==================================
BRAND_NAME=ACME AI
BRAND_MODEL_DEFAULT=ACME Assistant
BRAND_MODEL_PRO=ACME Assistant Pro
# Optional branding
BRAND_COLOR=#FF6B6B
BRAND_LOGO_URL=/assets/acme-logo.svg
APP_TITLE=ACME AI Chat
# ==================================
# Backend Configuration
# ==================================
# Configure which actual AI provider to use behind the scenes
# Example 1: Using Anthropic Claude
BRAND_BACKEND_ENDPOINT=anthropic
BRAND_BACKEND_API_KEY=sk-ant-api03-YOUR-KEY-HERE
BRAND_BACKEND_MODEL_DEFAULT=claude-3-5-sonnet-20241022
BRAND_BACKEND_MODEL_PRO=claude-3-5-opus-latest
# Example 2: Using OpenAI
# BRAND_BACKEND_ENDPOINT=openAI
# BRAND_BACKEND_API_KEY=sk-YOUR-OPENAI-KEY
# BRAND_BACKEND_MODEL_DEFAULT=gpt-4o-mini
# BRAND_BACKEND_MODEL_PRO=gpt-4o
# Example 3: Using Hanzo AI API (recommended)
# BRAND_BACKEND_ENDPOINT=hanzoai
# BRAND_BACKEND_API_KEY=hsk-YOUR-HANZO-KEY
# BRAND_BACKEND_BASE_URL=https://api.hanzo.ai/v1
# BRAND_BACKEND_MODEL_DEFAULT=hanzo-zen-1
# BRAND_BACKEND_MODEL_PRO=hanzo-zen-1-pro
# ==================================
# Other Required Settings
# ==================================
# Don't forget to set these from the main .env.example:
# Server
HOST=0.0.0.0
PORT=3080
# Database
MONGO_URI=mongodb://mongodb:27017/BrandChat
# Security (generate your own!)
JWT_SECRET=your-super-secret-jwt-key-change-this
JWT_REFRESH_SECRET=your-refresh-secret-key-change-this
CREDS_KEY=your-32-character-encryption-key
CREDS_IV=16-char-creds-iv
# Search (optional)
MEILI_HOST=http://meilisearch:7700
MEILI_MASTER_KEY=your-master-key-minimum-16-bytes
# ==================================
# What Brand Mode Does
# ==================================
# When BRAND_MODE=true:
#
# 1. Only shows your branded models (ACME Assistant, ACME Assistant Pro)
# 2. Hides all endpoint/provider selection UI
# 3. Hides API key configuration options
# 4. Routes all requests through your configured backend
# 5. Users never see the actual AI provider being used
# 6. Perfect for white-label SaaS deployments
#
# Your users will only see:
# - Your brand name (ACME AI)
# - Your model names (ACME Assistant)
# - Your logo and colors
# - No configuration options
#
# Behind the scenes, you control:
# - Which AI provider to use
# - Which actual models to use
# - All API keys and routing
-13
View File
@@ -1,13 +0,0 @@
# Docker Registry Configuration
# Uncomment one of the following to choose your registry:
# For Docker Hub (default):
DOCKER_REGISTRY=docker.io
DOCKER_IMAGE_PREFIX=
# For GitHub Container Registry:
# DOCKER_REGISTRY=ghcr.io
# DOCKER_IMAGE_PREFIX=ghcr.io/
# Organization name
DOCKER_ORG=hanzoai
+97 -264
View File
@@ -2,37 +2,51 @@
# Hanzo Chat Configuration #
#=====================================================================#
# Please refer to the reference documentation for assistance #
# with configuring your Hanzo Chat environment. #
# with configuring your Hanzo Chat environment. #
# #
# https://hanzo.ai/docs/chat/configuration/dotenv #
# https://docs.hanzo.ai/chat/configuration/dotenv #
#=====================================================================#
#==================================================#
# Server Configuration #
#==================================================#
HOST=localhost
HOST=0.0.0.0
PORT=3080
MONGO_URI=mongodb://127.0.0.1:27017/HanzoChat
#The maximum number of connections in the connection pool. */
MONGO_MAX_POOL_SIZE=
#The minimum number of connections in the connection pool. */
MONGO_MIN_POOL_SIZE=
#The maximum number of connections that may be in the process of being established concurrently by the connection pool. */
MONGO_MAX_CONNECTING=
#The maximum number of milliseconds that a connection can remain idle in the pool before being removed and closed. */
MONGO_MAX_IDLE_TIME_MS=
#The maximum time in milliseconds that a thread can wait for a connection to become available. */
MONGO_WAIT_QUEUE_TIMEOUT_MS=
# Set to false to disable automatic index creation for all models associated with this connection. */
MONGO_AUTO_INDEX=
# Set to `false` to disable Mongoose automatically calling `createCollection()` on every model created on this connection. */
MONGO_AUTO_CREATE=
# For Docker: mongodb://mongodb:27017/Hanzo
# For Local: mongodb://127.0.0.1:27017/HanzoChat
MONGO_URI=mongodb://mongodb:27017/Hanzo
DOMAIN_CLIENT=http://localhost:3080
DOMAIN_SERVER=http://localhost:3080
#==================================================#
# Brand Mode Configuration #
#==================================================#
# Enable brand mode for white-label deployments
BRAND_MODE=true
# Vendor display name (shown in UI)
BRAND_NAME=Hanzo AI
# Fixed model names for brand mode
BRAND_MODEL_DEFAULT=Hanzo Zen-1
BRAND_MODEL_PRO=Hanzo Zen-1 Pro
# Branding
APP_TITLE=Hanzo AI Chat
BRAND_COLOR=#000000
BRAND_LOGO_URL=/assets/hanzo-logo.svg
# Backend routing for brand mode
# The actual API endpoint and key used internally
BRAND_BACKEND_ENDPOINT=anthropic # Options: openAI, anthropic, hanzoai, custom
BRAND_BACKEND_API_KEY=
BRAND_BACKEND_BASE_URL=https://api.hanzo.ai/v1
BRAND_BACKEND_MODEL_DEFAULT=claude-3-5-sonnet-20241022 # Actual model used
BRAND_BACKEND_MODEL_PRO=claude-3-opus-20240229 # Actual pro model used
NO_INDEX=true
# Use the address that is at most n number of hops away from the Express application.
# req.socket.remoteAddress is the first hop, and the rest are looked for in the X-Forwarded-For header from right to left.
@@ -40,17 +54,6 @@ NO_INDEX=true
# Defaulted to 1.
TRUST_PROXY=1
# Minimum password length for user authentication
# Default: 8
# Note: When using LDAP authentication, you may want to set this to 1
# to bypass local password validation, as LDAP servers handle their own
# password policies.
# MIN_PASSWORD_LENGTH=8
# When enabled, the app will continue running after encountering uncaught exceptions
# instead of exiting the process. Not recommended for production unless necessary.
# CONTINUE_ON_UNCAUGHT_EXCEPTION=false
#===============#
# JSON Logging #
#===============#
@@ -72,18 +75,6 @@ DEBUG_CONSOLE=false
# UID=1000
# GID=1000
#==============#
# Node Options #
#==============#
# NOTE: NODE_MAX_OLD_SPACE_SIZE is NOT recognized by Node.js directly.
# This variable is used as a build argument for Docker or CI/CD workflows,
# and is NOT used by Node.js to set the heap size at runtime.
# To configure Node.js memory, use NODE_OPTIONS, e.g.:
# NODE_OPTIONS="--max-old-space-size=6144"
# See: https://nodejs.org/api/cli.html#--max-old-space-sizesize-in-mib
NODE_MAX_OLD_SPACE_SIZE=6144
#===============#
# Configuration #
#===============#
@@ -91,16 +82,6 @@ NODE_MAX_OLD_SPACE_SIZE=6144
# CONFIG_PATH="/alternative/path/to/chat.yaml"
#==================#
# Langfuse Tracing #
#==================#
# Get Langfuse API keys for your project from the project settings page: https://cloud.langfuse.com
# LANGFUSE_PUBLIC_KEY=
# LANGFUSE_SECRET_KEY=
# LANGFUSE_BASE_URL=
#===================================================#
# Endpoints #
#===================================================#
@@ -109,18 +90,17 @@ NODE_MAX_OLD_SPACE_SIZE=6144
PROXY=
#===============================#
# Hanzo API (Zen models) #
#===============================#
# Publishable key for unauthenticated users (rate-limited).
# Authenticated users get their real API key after IAM login (hanzo.id).
# Endpoint: https://api.hanzo.ai/v1
HANZO_API_KEY=
#============#
# Hanzo API #
#============#
# Unified API Gateway for all LLMs
# When set, all models will route through Hanzo API unless user provides their own keys
# HANZO_API_KEY=hsk_...
#===================================#
# Known Endpoints - chat.yaml #
# Known Endpoints - chat.yaml #
#===================================#
# https://hanzo.ai/docs/chat/configuration/ai_endpoints
# https://docs.hanzo.ai/chat/configuration/ai_endpoints
# ANYSCALE_API_KEY=
# APIPIE_API_KEY=
@@ -143,13 +123,9 @@ HANZO_API_KEY=
#============#
ANTHROPIC_API_KEY=user_provided
# ANTHROPIC_MODELS=claude-sonnet-4-6,claude-opus-4-6,claude-opus-4-20250514,claude-sonnet-4-20250514,claude-3-7-sonnet-20250219,claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307
# ANTHROPIC_MODELS=claude-opus-4-20250514,claude-sonnet-4-20250514,claude-3-7-sonnet-20250219,claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307
# ANTHROPIC_REVERSE_PROXY=
# Set to true to use Anthropic models through Google Vertex AI instead of direct API
# ANTHROPIC_USE_VERTEX=
# ANTHROPIC_VERTEX_REGION=us-east5
#============#
# Azure #
#============#
@@ -167,6 +143,7 @@ ANTHROPIC_API_KEY=user_provided
# AZURE_OPENAI_API_VERSION= # Deprecated
# AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME= # Deprecated
# AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME= # Deprecated
# PLUGINS_USE_AZURE="true" # Deprecated
#=================#
# AWS Bedrock #
@@ -178,8 +155,7 @@ ANTHROPIC_API_KEY=user_provided
# BEDROCK_AWS_SESSION_TOKEN=someSessionToken
# Note: This example list is not meant to be exhaustive. If omitted, all known, supported model IDs will be included for you.
# BEDROCK_AWS_MODELS=anthropic.claude-sonnet-4-6,anthropic.claude-opus-4-6-v1,anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0
# Cross-region inference model IDs: us.anthropic.claude-sonnet-4-6,us.anthropic.claude-opus-4-6-v1,global.anthropic.claude-opus-4-6-v1
# BEDROCK_AWS_MODELS=anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0
# See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns
@@ -201,23 +177,15 @@ GOOGLE_KEY=user_provided
# GOOGLE_AUTH_HEADER=true
# Gemini API (AI Studio)
# GOOGLE_MODELS=gemini-3.1-pro-preview,gemini-3.1-pro-preview-customtools,gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash,gemini-2.0-flash-lite
# GOOGLE_MODELS=gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite-preview-06-17,gemini-2.0-flash,gemini-2.0-flash-lite
# Vertex AI
# GOOGLE_MODELS=gemini-3.1-pro-preview,gemini-3.1-pro-preview-customtools,gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash-001,gemini-2.0-flash-lite-001
# GOOGLE_MODELS=gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite-preview-06-17,gemini-2.0-flash-001,gemini-2.0-flash-lite-001
# GOOGLE_TITLE_MODEL=gemini-2.0-flash-lite-001
# Google Cloud region for Vertex AI (used by both chat and image generation)
# GOOGLE_LOC=us-central1
# Alternative region env var for Gemini Image Generation
# GOOGLE_CLOUD_LOCATION=global
# Vertex AI Service Account Configuration
# Path to your Google Cloud service account JSON file
# GOOGLE_SERVICE_KEY_FILE=/path/to/service-account.json
# Google Safety Settings
# NOTE: These settings apply to both Vertex AI and Gemini API (AI Studio)
#
@@ -237,33 +205,12 @@ GOOGLE_KEY=user_provided
# GOOGLE_SAFETY_DANGEROUS_CONTENT=BLOCK_ONLY_HIGH
# GOOGLE_SAFETY_CIVIC_INTEGRITY=BLOCK_ONLY_HIGH
#========================#
# Gemini Image Generation #
#========================#
# Gemini Image Generation Tool (for Agents)
# Supports multiple authentication methods in priority order:
# 1. User-provided API key (via GUI)
# 2. GEMINI_API_KEY env var (admin-configured)
# 3. GOOGLE_KEY env var (shared with Google chat endpoint)
# 4. Vertex AI service account (via GOOGLE_SERVICE_KEY_FILE)
# Option A: Use dedicated Gemini API key for image generation
# GEMINI_API_KEY=your-gemini-api-key
# Option B: Use Vertex AI (no API key needed, uses service account)
# Set this to enable Vertex AI and allow tool without requiring API keys
# GEMINI_VERTEX_ENABLED=true
# Vertex AI model for image generation (defaults to gemini-2.5-flash-image)
# GEMINI_IMAGE_MODEL=gemini-2.5-flash-image
#============#
# OpenAI #
#============#
OPENAI_API_KEY=user_provided
# OPENAI_MODELS=gpt-5,gpt-5-codex,gpt-5-mini,gpt-5-nano,o3-pro,o3,o4-mini,gpt-4.1,gpt-4.1-mini,gpt-4.1-nano,o3-mini,o1-pro,o1,gpt-4o,gpt-4o-mini
# OPENAI_MODELS=o1,o1-mini,o1-preview,gpt-4o,gpt-4.5-preview,chatgpt-4o-latest,gpt-4o-mini,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
DEBUG_OPENAI=false
@@ -295,7 +242,18 @@ ASSISTANTS_API_KEY=user_provided
# The models for Azure Assistants are also determined by your Azure OpenAI configuration.
# More info, including how to enable use of Assistants with Azure here:
# https://hanzo.ai/docs/chat/configuration/ai_endpoints/azure#using-assistants-with-azure
# https://docs.hanzo.ai/chat/configuration/ai_endpoints/azure#using-assistants-with-azure
#============#
# Plugins #
#============#
# PLUGIN_MODELS=gpt-4o,gpt-4o-mini,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
DEBUG_PLUGINS=true
CREDS_KEY=f34be427ebb29de8d88c107a71546019685ed8b241d8f2ed00c3df97ad2566f0
CREDS_IV=e2341419ec3dd3d19b13a1a87fafcbfb
# Azure AI Search
#-----------------
@@ -310,11 +268,6 @@ AZURE_AI_SEARCH_SEARCH_OPTION_SELECT=
# OpenAI Image Tools Customization
#----------------
# IMAGE_GEN_OAI_API_KEY= # Create or reuse OpenAI API key for image generation tool
# IMAGE_GEN_OAI_BASEURL= # Custom OpenAI base URL for image generation tool
# IMAGE_GEN_OAI_AZURE_API_VERSION= # Custom Azure OpenAI deployments
# IMAGE_GEN_OAI_MODEL=gpt-image-1 # OpenAI image model (e.g., gpt-image-1, gpt-image-1.5)
# IMAGE_GEN_OAI_DESCRIPTION=
# IMAGE_GEN_OAI_DESCRIPTION_WITH_FILES=Custom description for image generation tool when files are present
# IMAGE_GEN_OAI_DESCRIPTION_NO_FILES=Custom description for image generation tool when no files are present
# IMAGE_EDIT_OAI_DESCRIPTION=Custom description for image editing tool
@@ -351,6 +304,14 @@ FLUX_API_BASE_URL=https://api.us1.bfl.ai
GOOGLE_SEARCH_API_KEY=
GOOGLE_CSE_ID=
# YOUTUBE
#-----------------
YOUTUBE_API_KEY=
# SerpAPI
#-----------------
SERPAPI_API_KEY=
# Stable Diffusion
#-----------------
SD_WEBUI_URL=http://host.docker.internal:7860
@@ -377,8 +338,10 @@ ZAPIER_NLA_API_KEY=
SEARCH=true
MEILI_NO_ANALYTICS=true
MEILI_HOST=http://0.0.0.0:7700
MEILI_MASTER_KEY=
# For Docker: http://meilisearch:7700
# For Local: http://0.0.0.0:7700
MEILI_HOST=http://meilisearch:7700
MEILI_MASTER_KEY=your-master-key-minimum-16-bytes
# Optional: Disable indexing, useful in a multi-node setup
# where only one instance should perform an index sync.
@@ -394,7 +357,7 @@ TTS_API_KEY=
#==================================================#
# RAG #
#==================================================#
# More info: https://hanzo.ai/docs/chat/configuration/rag_api
# More info: https://docs.hanzo.ai/chat/configuration/rag_api
# RAG_OPENAI_BASEURL=
# RAG_OPENAI_API_KEY=
@@ -423,11 +386,6 @@ REGISTRATION_VIOLATION_SCORE=1
CONCURRENT_VIOLATION_SCORE=1
MESSAGE_VIOLATION_SCORE=1
NON_BROWSER_VIOLATION_SCORE=20
TTS_VIOLATION_SCORE=0
STT_VIOLATION_SCORE=0
FORK_VIOLATION_SCORE=0
IMPORT_VIOLATION_SCORE=0
FILE_UPLOAD_VIOLATION_SCORE=0
LOGIN_MAX=7
LOGIN_WINDOW=5
@@ -463,29 +421,14 @@ ALLOW_REGISTRATION=true
ALLOW_SOCIAL_LOGIN=false
ALLOW_SOCIAL_REGISTRATION=false
ALLOW_PASSWORD_RESET=false
#=====================================================#
# Guest Chat (anonymous preview) #
#=====================================================#
# Off by default. When enabled, unauthenticated visitors get a small free
# quota on the free Zen model routed through api.hanzo.ai. After the quota is
# spent the UI surfaces the existing OpenID/hanzo.id login. Guests are scoped
# server-side to the free model only and rejected from every other route.
ALLOW_GUEST_CHAT=false
# GUEST_MESSAGE_MAX=3 # free messages per IP before login is required
# GUEST_ENDPOINT=Hanzo # custom endpoint name from librechat.yaml (api.hanzo.ai)
# GUEST_MODEL=zen3-nano # free Zen model id guests are pinned to
# GUEST_TOKEN_EXPIRY=3600000 # guest JWT lifetime in ms (default 1h)
# GUEST_TOKEN_MAX=20 # max guest sessions issued per IP per window
# GUEST_TOKEN_WINDOW=60 # guest-session issuance window in minutes
# ALLOW_ACCOUNT_DELETION=true # note: enabled by default if omitted/commented out
ALLOW_UNVERIFIED_EMAIL_LOGIN=true
SESSION_EXPIRY=1000 * 60 * 15
REFRESH_TOKEN_EXPIRY=(1000 * 60 * 60 * 24) * 7
JWT_SECRET=
JWT_REFRESH_SECRET=
JWT_SECRET=16f8c0ef4a5d391b26034086c628469d3f9f497f08163ab9b40137092f2909ef
JWT_REFRESH_SECRET=eaa5191f2914e30b9387fd84e254e4ba6fc51b4654968a9b0803b456a54b8418
# Discord
DISCORD_CLIENT_ID=
@@ -527,15 +470,10 @@ OPENID_CALLBACK_URL=/oauth/openid/callback
OPENID_REQUIRED_ROLE=
OPENID_REQUIRED_ROLE_TOKEN_KIND=
OPENID_REQUIRED_ROLE_PARAMETER_PATH=
OPENID_ADMIN_ROLE=
OPENID_ADMIN_ROLE_PARAMETER_PATH=
OPENID_ADMIN_ROLE_TOKEN_KIND=
# Set to determine which user info property returned from OpenID Provider to store as the User's username
OPENID_USERNAME_CLAIM=
# Set to determine which user info property returned from OpenID Provider to store as the User's name
OPENID_NAME_CLAIM=
# Optional audience parameter for OpenID authorization requests
OPENID_AUDIENCE=
OPENID_BUTTON_LABEL=
OPENID_IMAGE_URL=
@@ -552,28 +490,11 @@ OPENID_REUSE_TOKENS=
OPENID_JWKS_URL_CACHE_ENABLED=
OPENID_JWKS_URL_CACHE_TIME= # 600000 ms eq to 10 minutes leave empty to disable caching
#Set to true to trigger token exchange flow to acquire access token for the userinfo endpoint.
OPENID_ON_BEHALF_FLOW_FOR_USERINFO_REQUIRED=
OPENID_ON_BEHALF_FLOW_USERINFO_SCOPE="user.read" # example for Scope Needed for Microsoft Graph API
OPENID_ON_BEHALF_FLOW_FOR_USERINFRO_REQUIRED=
OPENID_ON_BEHALF_FLOW_USERINFRO_SCOPE = "user.read" # example for Scope Needed for Microsoft Graph API
# Set to true to use the OpenID Connect end session endpoint for logout
OPENID_USE_END_SESSION_ENDPOINT=
# URL to redirect to after OpenID logout (defaults to ${DOMAIN_CLIENT}/login)
OPENID_POST_LOGOUT_REDIRECT_URI=
#========================#
# SharePoint Integration #
#========================#
# Requires Entra ID (OpenID) authentication to be configured
# Enable SharePoint file picker in chat and agent panels
# ENABLE_SHAREPOINT_FILEPICKER=true
# SharePoint tenant base URL (e.g., https://yourtenant.sharepoint.com)
# SHAREPOINT_BASE_URL=https://yourtenant.sharepoint.com
# Microsoft Graph API And SharePoint scopes for file picker
# SHAREPOINT_PICKER_SHAREPOINT_SCOPE==https://yourtenant.sharepoint.com/AllSites.Read
# SHAREPOINT_PICKER_GRAPH_SCOPE=Files.Read.All
#========================#
# SAML
# Note: If OpenID is enabled, SAML authentication will be automatically disabled.
@@ -601,21 +522,6 @@ SAML_IMAGE_URL=
# SAML_USE_AUTHN_RESPONSE_SIGNED=
#===============================================#
# Microsoft Graph API / Entra ID Integration #
#===============================================#
# Enable Entra ID people search integration in permissions/sharing system
# When enabled, the people picker will search both local database and Entra ID
USE_ENTRA_ID_FOR_PEOPLE_SEARCH=false
# When enabled, entra id groups owners will be considered as members of the group
ENTRA_ID_INCLUDE_OWNERS_AS_MEMBERS=false
# Microsoft Graph API scopes needed for people/group search
# Default scopes provide access to user profiles and group memberships
OPENID_GRAPH_SCOPES=User.Read,People.Read,GroupMember.Read.All
# LDAP
LDAP_URL=
LDAP_BIND_DN=
@@ -678,9 +584,6 @@ AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
AWS_BUCKET_NAME=
# Required for path-style S3-compatible providers (MinIO, Hetzner, Backblaze B2, etc.)
# that don't support virtual-hosted-style URLs (bucket.endpoint). Not needed for AWS S3.
# AWS_FORCE_PATH_STYLE=false
#========================#
# Azure Blob Storage #
@@ -709,86 +612,28 @@ ALLOW_SHARED_LINKS_PUBLIC=true
# If you have another service in front of your Hanzo Chat doing compression, disable express based compression here
# DISABLE_COMPRESSION=true
# If you have gzipped version of uploaded image images in the same folder, this will enable gzip scan and serving of these images
# Note: The images folder will be scanned on startup and a ma kept in memory. Be careful for large number of images.
# ENABLE_IMAGE_OUTPUT_GZIP_SCAN=true
#===================================================#
# UI #
#===================================================#
APP_TITLE=Hanzo Chat
# APP_TITLE is set in brand mode section above
# CUSTOM_FOOTER="My custom footer"
HELP_AND_FAQ_URL=https://hanzo.ai/chat
HELP_AND_FAQ_URL=https://hanzo.ai
# SHOW_BIRTHDAY_ICON=true
# Google tag manager id
#ANALYTICS_GTM_ID=user provided google tag manager id
# limit conversation file imports to a certain number of bytes in size to avoid the container
# maxing out memory limitations by unremarking this line and supplying a file size in bytes
# such as the below example of 250 mib
# CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES=262144000
#===============#
# REDIS Options #
#===============#
# Enable Redis for caching and session storage
# REDIS_URI=10.10.10.10:6379
# USE_REDIS=true
# Enable Redis for resumable LLM streams (defaults to USE_REDIS value if not set)
# Set to false to use in-memory storage for streams while keeping Redis for other caches
# USE_REDIS_STREAMS=true
# Single Redis instance
# REDIS_URI=redis://127.0.0.1:6379
# Redis cluster (multiple nodes)
# REDIS_URI=redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003
# Redis with TLS/SSL encryption and CA certificate
# REDIS_URI=rediss://127.0.0.1:6380
# REDIS_CA=/path/to/ca-cert.pem
# Elasticache may need to use an alternate dnsLookup for TLS connections. see "Special Note: Aws Elasticache Clusters with TLS" on this webpage: https://www.npmjs.com/package/ioredis
# Enable alternative dnsLookup for redis
# REDIS_USE_ALTERNATIVE_DNS_LOOKUP=true
# Redis authentication (if required)
# REDIS_USERNAME=your_redis_username
# REDIS_PASSWORD=your_redis_password
# Redis key prefix configuration
# Use environment variable name for dynamic prefix (recommended for cloud deployments)
# REDIS_KEY_PREFIX_VAR=K_REVISION
# Or use static prefix directly
# REDIS_KEY_PREFIX=librechat
# Redis connection limits
# REDIS_MAX_LISTENERS=40
# Redis ping interval in seconds (0 = disabled, >0 = enabled)
# When set to a positive integer, Redis clients will ping the server at this interval to keep connections alive
# When unset or 0, no pinging is performed (recommended for most use cases)
# REDIS_PING_INTERVAL=300
# Force specific cache namespaces to use in-memory storage even when Redis is enabled
# Comma-separated list of CacheKeys
# Defaults to CONFIG_STORE,APP_CONFIG so YAML-derived config stays per-container (safe for blue/green deployments)
# Set to empty string to force all namespaces through Redis: FORCED_IN_MEMORY_CACHE_NAMESPACES=
# FORCED_IN_MEMORY_CACHE_NAMESPACES=CONFIG_STORE,APP_CONFIG
# Leader Election Configuration (for multi-instance deployments with Redis)
# Duration in seconds that the leader lease is valid before it expires (default: 25)
# LEADER_LEASE_DURATION=25
# Interval in seconds at which the leader renews its lease (default: 10)
# LEADER_RENEW_INTERVAL=10
# Maximum number of retry attempts when renewing the lease fails (default: 3)
# LEADER_RENEW_ATTEMPTS=3
# Delay in seconds between retry attempts when renewing the lease (default: 0.5)
# LEADER_RENEW_RETRY_DELAY=0.5
# USE_REDIS_CLUSTER=true
# REDIS_CA=/path/to/ca.crt
#==================================================#
# Others #
@@ -822,43 +667,31 @@ HELP_AND_FAQ_URL=https://hanzo.ai/chat
OPENWEATHER_API_KEY=
#====================================#
# Hanzo Chat Code Interpreter API #
# Hanzo Chat Code Interpreter API #
#====================================#
# "Run Code" routes through the Hanzo unified backend (api.hanzo.ai/v1/exec),
# which executes in a Hanzo sandbox. execute_code POSTs {BASEURL}/exec with
# X-API-Key. https://hanzo.ai/docs/chat/code-interpreter
# LIBRECHAT_CODE_BASEURL=https://api.hanzo.ai/v1
# LIBRECHAT_CODE_API_KEY=your-key # prod: KMS chat-secrets/LIBRECHAT_CODE_API_KEY
# https://code.hanzo.ai
# HANZO_CODE_API_KEY=your-key
#======================#
# Web Search #
#======================#
# Web Search routes through the Hanzo unified backend ONLY — no external SaaS.
# The searxng provider + firecrawl scraper both point at api.hanzo.ai/v1/websearch
# (Hanzo metasearch + Hanzo Crawl). See librechat.yaml `webSearch`.
# https://hanzo.ai/docs/chat/features/web_search
# SEARXNG_INSTANCE_URL=https://api.hanzo.ai/v1/websearch
# FIRECRAWL_API_URL=https://api.hanzo.ai/v1/websearch
# WEBSEARCH_API_KEY=your-key # prod: KMS chat-secrets/WEBSEARCH_API_KEY
#
# Do NOT set SERPER_API_KEY / TAVILY_API_KEY / JINA_API_KEY / COHERE_API_KEY:
# external providers are intentionally disabled (one way, unified backend).
# Note: All of the following variable names can be customized.
# Omit values to allow user to provide them.
#======================#
# MCP Configuration #
#======================#
# For more information on configuration values, see:
# https://docs.hanzo.ai/chat/features/web_search
# Treat 401/403 responses as OAuth requirement when no oauth metadata found
# MCP_OAUTH_ON_AUTH_ERROR=true
# Search Provider (Required)
# SERPER_API_KEY=your_serper_api_key
# Timeout for OAuth detection requests in milliseconds
# MCP_OAUTH_DETECTION_TIMEOUT=5000
# Scraper (Required)
# FIRECRAWL_API_KEY=your_firecrawl_api_key
# Optional: Custom Firecrawl API URL
# FIRECRAWL_API_URL=your_firecrawl_api_url
# Cache connection status checks for this many milliseconds to avoid expensive verification
# MCP_CONNECTION_CHECK_TTL=60000
# Skip code challenge method validation (e.g., for AWS Cognito that supports S256 but doesn't advertise it)
# When set to true, forces S256 code challenge even if not advertised in .well-known/openid-configuration
# MCP_SKIP_CODE_CHALLENGE_CHECK=false
# Reranker (Required)
# JINA_API_KEY=your_jina_api_key
# or
# COHERE_API_KEY=your_cohere_api_key
-660
View File
@@ -1,660 +0,0 @@
#=====================================================================#
# LibreChat Configuration #
#=====================================================================#
# Please refer to the reference documentation for assistance #
# with configuring your LibreChat environment. #
# #
# https://www.librechat.ai/docs/configuration/dotenv #
#=====================================================================#
#==================================================#
# Server Configuration #
#==================================================#
HOST=localhost
PORT=3080
MONGO_URI=mongodb://127.0.0.1:27017/LibreChat
DOMAIN_CLIENT=http://localhost:3080
DOMAIN_SERVER=http://localhost:3080
NO_INDEX=true
# Use the address that is at most n number of hops away from the Express application.
# req.socket.remoteAddress is the first hop, and the rest are looked for in the X-Forwarded-For header from right to left.
# A value of 0 means that the first untrusted address would be req.socket.remoteAddress, i.e. there is no reverse proxy.
# Defaulted to 1.
TRUST_PROXY=1
#===============#
# JSON Logging #
#===============#
# Use when process console logs in cloud deployment like GCP/AWS
CONSOLE_JSON=false
#===============#
# Debug Logging #
#===============#
DEBUG_LOGGING=true
DEBUG_CONSOLE=false
#=============#
# Permissions #
#=============#
# UID=1000
# GID=1000
#===============#
# Configuration #
#===============#
# Use an absolute path, a relative path, or a URL
# CONFIG_PATH="/alternative/path/to/librechat.yaml"
#===================================================#
# Endpoints #
#===================================================#
# ENDPOINTS=openAI,assistants,azureOpenAI,google,anthropic
PROXY=
#===================================#
# Known Endpoints - librechat.yaml #
#===================================#
# https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints
# ANYSCALE_API_KEY=
# APIPIE_API_KEY=
# COHERE_API_KEY=
# DEEPSEEK_API_KEY=
# DATABRICKS_API_KEY=
# FIREWORKS_API_KEY=
# GROQ_API_KEY=
# HUGGINGFACE_TOKEN=
# MISTRAL_API_KEY=
# OPENROUTER_KEY=
# PERPLEXITY_API_KEY=
# SHUTTLEAI_API_KEY=
# TOGETHERAI_API_KEY=
# UNIFY_API_KEY=
# XAI_API_KEY=
#============#
# Anthropic #
#============#
ANTHROPIC_API_KEY=user_provided
# ANTHROPIC_MODELS=claude-opus-4-20250514,claude-sonnet-4-20250514,claude-3-7-sonnet-20250219,claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307
# ANTHROPIC_REVERSE_PROXY=
#============#
# Azure #
#============#
# Note: these variables are DEPRECATED
# Use the `librechat.yaml` configuration for `azureOpenAI` instead
# You may also continue to use them if you opt out of using the `librechat.yaml` configuration
# AZURE_OPENAI_DEFAULT_MODEL=gpt-3.5-turbo # Deprecated
# AZURE_OPENAI_MODELS=gpt-3.5-turbo,gpt-4 # Deprecated
# AZURE_USE_MODEL_AS_DEPLOYMENT_NAME=TRUE # Deprecated
# AZURE_API_KEY= # Deprecated
# AZURE_OPENAI_API_INSTANCE_NAME= # Deprecated
# AZURE_OPENAI_API_DEPLOYMENT_NAME= # Deprecated
# AZURE_OPENAI_API_VERSION= # Deprecated
# AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME= # Deprecated
# AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME= # Deprecated
# PLUGINS_USE_AZURE="true" # Deprecated
#=================#
# AWS Bedrock #
#=================#
# BEDROCK_AWS_DEFAULT_REGION=us-east-1 # A default region must be provided
# BEDROCK_AWS_ACCESS_KEY_ID=someAccessKey
# BEDROCK_AWS_SECRET_ACCESS_KEY=someSecretAccessKey
# BEDROCK_AWS_SESSION_TOKEN=someSessionToken
# Note: This example list is not meant to be exhaustive. If omitted, all known, supported model IDs will be included for you.
# BEDROCK_AWS_MODELS=anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0
# See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns
# Notes on specific models:
# The following models are not support due to not supporting streaming:
# ai21.j2-mid-v1
# The following models are not support due to not supporting conversation history:
# ai21.j2-ultra-v1, cohere.command-text-v14, cohere.command-light-text-v14
#============#
# Google #
#============#
GOOGLE_KEY=user_provided
# GOOGLE_REVERSE_PROXY=
# Some reverse proxies do not support the X-goog-api-key header, uncomment to pass the API key in Authorization header instead.
# GOOGLE_AUTH_HEADER=true
# Gemini API (AI Studio)
# GOOGLE_MODELS=gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite-preview-06-17,gemini-2.0-flash,gemini-2.0-flash-lite
# Vertex AI
# GOOGLE_MODELS=gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite-preview-06-17,gemini-2.0-flash-001,gemini-2.0-flash-lite-001
# GOOGLE_TITLE_MODEL=gemini-2.0-flash-lite-001
# GOOGLE_LOC=us-central1
# Google Safety Settings
# NOTE: These settings apply to both Vertex AI and Gemini API (AI Studio)
#
# For Vertex AI:
# To use the BLOCK_NONE setting, you need either:
# (a) Access through an allowlist via your Google account team, or
# (b) Switch to monthly invoiced billing: https://cloud.google.com/billing/docs/how-to/invoiced-billing
#
# For Gemini API (AI Studio):
# BLOCK_NONE is available by default, no special account requirements.
#
# Available options: BLOCK_NONE, BLOCK_ONLY_HIGH, BLOCK_MEDIUM_AND_ABOVE, BLOCK_LOW_AND_ABOVE
#
# GOOGLE_SAFETY_SEXUALLY_EXPLICIT=BLOCK_ONLY_HIGH
# GOOGLE_SAFETY_HATE_SPEECH=BLOCK_ONLY_HIGH
# GOOGLE_SAFETY_HARASSMENT=BLOCK_ONLY_HIGH
# GOOGLE_SAFETY_DANGEROUS_CONTENT=BLOCK_ONLY_HIGH
# GOOGLE_SAFETY_CIVIC_INTEGRITY=BLOCK_ONLY_HIGH
#============#
# OpenAI #
#============#
OPENAI_API_KEY=user_provided
# OPENAI_MODELS=o1,o1-mini,o1-preview,gpt-4o,gpt-4.5-preview,chatgpt-4o-latest,gpt-4o-mini,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
DEBUG_OPENAI=false
# TITLE_CONVO=false
# OPENAI_TITLE_MODEL=gpt-4o-mini
# OPENAI_SUMMARIZE=true
# OPENAI_SUMMARY_MODEL=gpt-4o-mini
# OPENAI_FORCE_PROMPT=true
# OPENAI_REVERSE_PROXY=
# OPENAI_ORGANIZATION=
#====================#
# Assistants API #
#====================#
ASSISTANTS_API_KEY=user_provided
# ASSISTANTS_BASE_URL=
# ASSISTANTS_MODELS=gpt-4o,gpt-4o-mini,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
#==========================#
# Azure Assistants API #
#==========================#
# Note: You should map your credentials with custom variables according to your Azure OpenAI Configuration
# The models for Azure Assistants are also determined by your Azure OpenAI configuration.
# More info, including how to enable use of Assistants with Azure here:
# https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints/azure#using-assistants-with-azure
#============#
# Plugins #
#============#
# PLUGIN_MODELS=gpt-4o,gpt-4o-mini,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
DEBUG_PLUGINS=true
CREDS_KEY=f34be427ebb29de8d88c107a71546019685ed8b241d8f2ed00c3df97ad2566f0
CREDS_IV=e2341419ec3dd3d19b13a1a87fafcbfb
# Azure AI Search
#-----------------
AZURE_AI_SEARCH_SERVICE_ENDPOINT=
AZURE_AI_SEARCH_INDEX_NAME=
AZURE_AI_SEARCH_API_KEY=
AZURE_AI_SEARCH_API_VERSION=
AZURE_AI_SEARCH_SEARCH_OPTION_QUERY_TYPE=
AZURE_AI_SEARCH_SEARCH_OPTION_TOP=
AZURE_AI_SEARCH_SEARCH_OPTION_SELECT=
# OpenAI Image Tools Customization
#----------------
# IMAGE_GEN_OAI_DESCRIPTION_WITH_FILES=Custom description for image generation tool when files are present
# IMAGE_GEN_OAI_DESCRIPTION_NO_FILES=Custom description for image generation tool when no files are present
# IMAGE_EDIT_OAI_DESCRIPTION=Custom description for image editing tool
# IMAGE_GEN_OAI_PROMPT_DESCRIPTION=Custom prompt description for image generation tool
# IMAGE_EDIT_OAI_PROMPT_DESCRIPTION=Custom prompt description for image editing tool
# 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=
# Flux
#-----------------
FLUX_API_BASE_URL=https://api.us1.bfl.ai
# FLUX_API_BASE_URL = 'https://api.bfl.ml';
# Get your API key at https://api.us1.bfl.ai/auth/profile
# FLUX_API_KEY=
# Google
#-----------------
GOOGLE_SEARCH_API_KEY=
GOOGLE_CSE_ID=
# YOUTUBE
#-----------------
YOUTUBE_API_KEY=
# SerpAPI
#-----------------
SERPAPI_API_KEY=
# Stable Diffusion
#-----------------
SD_WEBUI_URL=http://host.docker.internal:7860
# Tavily
#-----------------
TAVILY_API_KEY=
# Traversaal
#-----------------
TRAVERSAAL_API_KEY=
# WolframAlpha
#-----------------
WOLFRAM_APP_ID=
# Zapier
#-----------------
ZAPIER_NLA_API_KEY=
#==================================================#
# Search #
#==================================================#
SEARCH=true
MEILI_NO_ANALYTICS=true
MEILI_HOST=http://0.0.0.0:7700
MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt
# Optional: Disable indexing, useful in a multi-node setup
# where only one instance should perform an index sync.
# MEILI_NO_SYNC=true
#==================================================#
# Speech to Text & Text to Speech #
#==================================================#
STT_API_KEY=
TTS_API_KEY=
#==================================================#
# RAG #
#==================================================#
# More info: https://www.librechat.ai/docs/configuration/rag_api
# RAG_OPENAI_BASEURL=
# RAG_OPENAI_API_KEY=
# RAG_USE_FULL_CONTEXT=
# EMBEDDINGS_PROVIDER=openai
# EMBEDDINGS_MODEL=text-embedding-3-small
#===================================================#
# User System #
#===================================================#
#========================#
# Moderation #
#========================#
OPENAI_MODERATION=false
OPENAI_MODERATION_API_KEY=
# OPENAI_MODERATION_REVERSE_PROXY=
BAN_VIOLATIONS=true
BAN_DURATION=1000 * 60 * 60 * 2
BAN_INTERVAL=20
LOGIN_VIOLATION_SCORE=1
REGISTRATION_VIOLATION_SCORE=1
CONCURRENT_VIOLATION_SCORE=1
MESSAGE_VIOLATION_SCORE=1
NON_BROWSER_VIOLATION_SCORE=20
LOGIN_MAX=7
LOGIN_WINDOW=5
REGISTER_MAX=5
REGISTER_WINDOW=60
LIMIT_CONCURRENT_MESSAGES=true
CONCURRENT_MESSAGE_MAX=2
LIMIT_MESSAGE_IP=true
MESSAGE_IP_MAX=40
MESSAGE_IP_WINDOW=1
LIMIT_MESSAGE_USER=false
MESSAGE_USER_MAX=40
MESSAGE_USER_WINDOW=1
ILLEGAL_MODEL_REQ_SCORE=5
#========================#
# Balance #
#========================#
# CHECK_BALANCE=false
# START_BALANCE=20000 # note: the number of tokens that will be credited after registration.
#========================#
# Registration and Login #
#========================#
ALLOW_EMAIL_LOGIN=true
ALLOW_REGISTRATION=true
ALLOW_SOCIAL_LOGIN=false
ALLOW_SOCIAL_REGISTRATION=false
ALLOW_PASSWORD_RESET=false
# ALLOW_ACCOUNT_DELETION=true # note: enabled by default if omitted/commented out
ALLOW_UNVERIFIED_EMAIL_LOGIN=true
SESSION_EXPIRY=1000 * 60 * 15
REFRESH_TOKEN_EXPIRY=(1000 * 60 * 60 * 24) * 7
JWT_SECRET=16f8c0ef4a5d391b26034086c628469d3f9f497f08163ab9b40137092f2909ef
JWT_REFRESH_SECRET=eaa5191f2914e30b9387fd84e254e4ba6fc51b4654968a9b0803b456a54b8418
# Discord
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
DISCORD_CALLBACK_URL=/oauth/discord/callback
# Facebook
FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
FACEBOOK_CALLBACK_URL=/oauth/facebook/callback
# GitHub
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_CALLBACK_URL=/oauth/github/callback
# GitHub Enterprise
# GITHUB_ENTERPRISE_BASE_URL=
# GITHUB_ENTERPRISE_USER_AGENT=
# Google
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=/oauth/google/callback
# Apple
APPLE_CLIENT_ID=
APPLE_TEAM_ID=
APPLE_KEY_ID=
APPLE_PRIVATE_KEY_PATH=
APPLE_CALLBACK_URL=/oauth/apple/callback
# OpenID
OPENID_CLIENT_ID=
OPENID_CLIENT_SECRET=
OPENID_ISSUER=
OPENID_SESSION_SECRET=
OPENID_SCOPE="openid profile email"
OPENID_CALLBACK_URL=/oauth/openid/callback
OPENID_REQUIRED_ROLE=
OPENID_REQUIRED_ROLE_TOKEN_KIND=
OPENID_REQUIRED_ROLE_PARAMETER_PATH=
# Set to determine which user info property returned from OpenID Provider to store as the User's username
OPENID_USERNAME_CLAIM=
# Set to determine which user info property returned from OpenID Provider to store as the User's name
OPENID_NAME_CLAIM=
OPENID_BUTTON_LABEL=
OPENID_IMAGE_URL=
# Set to true to automatically redirect to the OpenID provider when a user visits the login page
# This will bypass the login form completely for users, only use this if OpenID is your only authentication method
OPENID_AUTO_REDIRECT=false
# Set to true to use PKCE (Proof Key for Code Exchange) for OpenID authentication
OPENID_USE_PKCE=false
#Set to true to reuse openid tokens for authentication management instead of using the mongodb session and the custom refresh token.
OPENID_REUSE_TOKENS=
#By default, signing key verification results are cached in order to prevent excessive HTTP requests to the JWKS endpoint.
#If a signing key matching the kid is found, this will be cached and the next time this kid is requested the signing key will be served from the cache.
#Default is true.
OPENID_JWKS_URL_CACHE_ENABLED=
OPENID_JWKS_URL_CACHE_TIME= # 600000 ms eq to 10 minutes leave empty to disable caching
#Set to true to trigger token exchange flow to acquire access token for the userinfo endpoint.
OPENID_ON_BEHALF_FLOW_FOR_USERINFRO_REQUIRED=
OPENID_ON_BEHALF_FLOW_USERINFRO_SCOPE = "user.read" # example for Scope Needed for Microsoft Graph API
# Set to true to use the OpenID Connect end session endpoint for logout
OPENID_USE_END_SESSION_ENDPOINT=
# SAML
# Note: If OpenID is enabled, SAML authentication will be automatically disabled.
SAML_ENTRY_POINT=
SAML_ISSUER=
SAML_CERT=
SAML_CALLBACK_URL=/oauth/saml/callback
SAML_SESSION_SECRET=
# Attribute mappings (optional)
SAML_EMAIL_CLAIM=
SAML_USERNAME_CLAIM=
SAML_GIVEN_NAME_CLAIM=
SAML_FAMILY_NAME_CLAIM=
SAML_PICTURE_CLAIM=
SAML_NAME_CLAIM=
# Logint buttion settings (optional)
SAML_BUTTON_LABEL=
SAML_IMAGE_URL=
# Whether the SAML Response should be signed.
# - If "true", the entire `SAML Response` will be signed.
# - If "false" or unset, only the `SAML Assertion` will be signed (default behavior).
# SAML_USE_AUTHN_RESPONSE_SIGNED=
# LDAP
LDAP_URL=
LDAP_BIND_DN=
LDAP_BIND_CREDENTIALS=
LDAP_USER_SEARCH_BASE=
#LDAP_SEARCH_FILTER="mail="
LDAP_CA_CERT_PATH=
# LDAP_TLS_REJECT_UNAUTHORIZED=
# LDAP_STARTTLS=
# LDAP_LOGIN_USES_USERNAME=true
# LDAP_ID=
# LDAP_USERNAME=
# LDAP_EMAIL=
# LDAP_FULL_NAME=
#========================#
# 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_FROM=noreply@librechat.ai
#========================#
# Mailgun API #
#========================#
# MAILGUN_API_KEY=your-mailgun-api-key
# MAILGUN_DOMAIN=mg.yourdomain.com
# EMAIL_FROM=noreply@yourdomain.com
# EMAIL_FROM_NAME="LibreChat"
# # Optional: For EU region
# MAILGUN_HOST=https://api.eu.mailgun.net
#========================#
# Firebase CDN #
#========================#
FIREBASE_API_KEY=
FIREBASE_AUTH_DOMAIN=
FIREBASE_PROJECT_ID=
FIREBASE_STORAGE_BUCKET=
FIREBASE_MESSAGING_SENDER_ID=
FIREBASE_APP_ID=
#========================#
# S3 AWS Bucket #
#========================#
AWS_ENDPOINT_URL=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
AWS_BUCKET_NAME=
#========================#
# Azure Blob Storage #
#========================#
AZURE_STORAGE_CONNECTION_STRING=
AZURE_STORAGE_PUBLIC_ACCESS=false
AZURE_CONTAINER_NAME=files
#========================#
# Shared Links #
#========================#
ALLOW_SHARED_LINKS=true
ALLOW_SHARED_LINKS_PUBLIC=true
#==============================#
# Static File Cache Control #
#==============================#
# Leave commented out to use defaults: 1 day (86400 seconds) for s-maxage and 2 days (172800 seconds) for max-age
# NODE_ENV must be set to production for these to take effect
# STATIC_CACHE_MAX_AGE=172800
# STATIC_CACHE_S_MAX_AGE=86400
# If you have another service in front of your LibreChat doing compression, disable express based compression here
# DISABLE_COMPRESSION=true
#===================================================#
# UI #
#===================================================#
APP_TITLE=LibreChat
# CUSTOM_FOOTER="My custom footer"
HELP_AND_FAQ_URL=https://librechat.ai
# SHOW_BIRTHDAY_ICON=true
# Google tag manager id
#ANALYTICS_GTM_ID=user provided google tag manager id
#===============#
# REDIS Options #
#===============#
# REDIS_URI=10.10.10.10:6379
# USE_REDIS=true
# USE_REDIS_CLUSTER=true
# REDIS_CA=/path/to/ca.crt
#==================================================#
# Others #
#==================================================#
# You should leave the following commented out #
# NODE_ENV=
# E2E_USER_EMAIL=
# E2E_USER_PASSWORD=
#=====================================================#
# Cache Headers #
#=====================================================#
# Headers that control caching of the index.html #
# Default configuration prevents caching to ensure #
# users always get the latest version. Customize #
# only if you understand caching implications. #
# INDEX_CACHE_CONTROL=no-cache, no-store, must-revalidate
# INDEX_PRAGMA=no-cache
# INDEX_EXPIRES=0
# no-cache: Forces validation with server before using cached version
# no-store: Prevents storing the response entirely
# must-revalidate: Prevents using stale content when offline
#=====================================================#
# OpenWeather #
#=====================================================#
OPENWEATHER_API_KEY=
#====================================#
# LibreChat Code Interpreter API #
#====================================#
# https://code.librechat.ai
# LIBRECHAT_CODE_API_KEY=your-key
#======================#
# Web Search #
#======================#
# Note: All of the following variable names can be customized.
# Omit values to allow user to provide them.
# For more information on configuration values, see:
# https://librechat.ai/docs/features/web_search
# Search Provider (Required)
# SERPER_API_KEY=your_serper_api_key
# Scraper (Required)
# FIRECRAWL_API_KEY=your_firecrawl_api_key
# Optional: Custom Firecrawl API URL
# FIRECRAWL_API_URL=your_firecrawl_api_url
# Reranker (Required)
# JINA_API_KEY=your_jina_api_key
# or
# COHERE_API_KEY=your_cohere_api_key
-89
View File
@@ -1,89 +0,0 @@
# Hanzo Chat - Cloud Deployment Configuration
# All AI models routed through api.hanzo.ai (zen4 series + standard models)
# ====== HANZO CLOUD API ======
OPENAI_API_KEY=sk-hanzo-your-api-key-here
OPENAI_BASE_URL=https://api.hanzo.ai/v1
# Code Interpreter ("Run Code") — Hanzo unified backend.
# LibreChat's execute_code tool POSTs {LIBRECHAT_CODE_BASEURL}/exec with header
# X-API-Key. cloud-api serves /v1/exec (-> sandboxed executor). The key is
# KMS-sourced (chat-secrets/LIBRECHAT_CODE_API_KEY). These are the ONLY vars
# LibreChat reads — the old CODE_EXECUTION_ENDPOINT/RUNTIME_API_KEY were dead.
LIBRECHAT_CODE_BASEURL=https://api.hanzo.ai/v1
LIBRECHAT_CODE_API_KEY=set-in-KMS-chat-secrets
# Web Search — Hanzo unified backend (searxng + firecrawl compat over Hanzo
# search + crawl). See librechat.yaml `webSearch`. One shared service key.
SEARXNG_INSTANCE_URL=https://api.hanzo.ai/v1/websearch
FIRECRAWL_API_URL=https://api.hanzo.ai/v1/websearch
WEBSEARCH_API_KEY=set-in-KMS-chat-secrets
# ====== BRANDING ======
APP_TITLE=Hanzo Chat
CUSTOM_FOOTER=Powered by Hanzo AI
# ====== LOCAL SERVICES ======
# MongoDB for chat history
MONGO_URI=mongodb://hanzo:hanzo123@localhost:27017/HanzoChat?authSource=admin
MONGO_PASSWORD=hanzo123
# Meilisearch for full-text search
MEILI_HOST=http://localhost:7700
MEILI_MASTER_KEY=HanzoMeiliMasterKey
# ====== APPLICATION SETTINGS ======
DOMAIN_CLIENT=https://hanzo.chat
DOMAIN_SERVER=https://hanzo.chat
# ====== SECURITY (CHANGE IN PRODUCTION) ======
JWT_SECRET=hanzo-jwt-secret-change-in-production
JWT_REFRESH_SECRET=hanzo-refresh-secret-change-in-production
# ====== HANZO IAM (hanzo.id) OIDC SSO ======
OPENID_CLIENT_ID=chat-app
OPENID_CLIENT_SECRET=chat-app-client-secret
OPENID_ISSUER=https://hanzo.id
OPENID_SESSION_SECRET=hanzo-openid-session-change-in-production
OPENID_SCOPE=openid profile email
OPENID_CALLBACK_URL=/oauth/openid/callback
OPENID_BUTTON_LABEL=Log in with Hanzo
OPENID_IMAGE_URL=https://hanzo.ai/logo/icon.svg
OPENID_USERNAME_CLAIM=name
OPENID_NAME_CLAIM=name
OPENID_USE_PKCE=true
# ====== FEATURES ======
ALLOW_REGISTRATION=true
ALLOW_UNVERIFIED_EMAIL_LOGIN=true
ALLOW_SOCIAL_LOGIN=true
ALLOW_SOCIAL_REGISTRATION=true
# MCP Tools
MCP_ENABLED=true
# ====== AVAILABLE MODELS ======
# zen4 series (default, all via api.hanzo.ai):
# zen4 - General purpose
# zen4-pro - High capability
# zen4-max - Extended context
# zen4-mini - Fast & efficient
# zen4-ultra - Maximum reasoning
# zen4-coder - Code specialist
# zen4-thinking - Deep reasoning
# zen4-coder-pro - Premium coding
#
# Standard models (via api.hanzo.ai):
# gpt-4o, gpt-4o-mini
# claude-sonnet-4-5, claude-haiku-4-5
# deepseek-r1-distill-70b
# qwen3-32b
# Debug
DEBUG_LOGGING=false
# ====== NOTES ======
# 1. Get your Hanzo API key from https://hanzo.ai/dashboard
# 2. All AI providers are accessed through api.hanzo.ai -- no individual keys needed
# 3. Model list is configured in librechat.yaml under endpoints.custom (Hanzo endpoint)
# 4. zen4 is the default model for new conversations
-41
View File
@@ -1,41 +0,0 @@
# Hanzo IAM Integration Configuration
# Copy this file to .env and update with your values
# Hanzo IAM SSO Configuration
HANZO_IAM_CLIENT_ID=chat-local
HANZO_IAM_CLIENT_SECRET=your-client-secret-here
HANZO_IAM_SERVER_URL=http://localhost:9003
# For local development with Hanzo Stack
# HANZO_IAM_SERVER_URL=http://iam:3000 # Use this when running in Docker
# Hanzo Router Configuration
ROUTER_MASTER_KEY=your-master-key-here
LITELLM_MASTER_KEY=${ROUTER_MASTER_KEY} # For backward compatibility
PROXY_BASE_URL=http://localhost:3081
# Development Mode Demo Credentials (only works in development)
# These are only used when NODE_ENV=development or DEV_MODE=true
DEMO_USERNAME=demo@hanzo.ai
DEMO_PASSWORD=hanzo123
# Choose Authentication Method:
# Option 1: Built-in Auth (default)
UI_USERNAME=admin
# UI_PASSWORD=<your-password> # If not set, uses ROUTER_MASTER_KEY
# Option 2: Hanzo IAM SSO
# Comment out UI_USERNAME above and set HANZO_IAM_CLIENT_ID/SECRET
# Option 3: Other SSO Providers (requires Hanzo Router Enterprise)
# GOOGLE_CLIENT_ID=your-google-client-id
# GOOGLE_CLIENT_SECRET=your-google-client-secret
# MICROSOFT_CLIENT_ID=your-microsoft-client-id
# MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret
# MICROSOFT_TENANT=your-tenant-id
# Database Configuration
DATABASE_URL=mongodb://localhost:27017/hanzo-router
# Enable Development Mode Features
DEV_MODE=false # Set to true for demo credentials
-67
View File
@@ -1,67 +0,0 @@
# Hanzo AI Chat Stack Environment Variables
# Copy this file to .env and update with your values
# ==================================
# LLM API Keys (at least one required)
# ==================================
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
TOGETHER_API_KEY=...
# ==================================
# Database Configuration
# ==================================
POSTGRES_PASSWORD=hanzo123
MONGO_PASSWORD=hanzo123
REDIS_PASSWORD=hanzosecret
# ==================================
# Service URLs
# ==================================
DOMAIN_CLIENT=http://localhost:3081
DOMAIN_SERVER=http://localhost:3081
# ==================================
# Security Keys (CHANGE IN PRODUCTION!)
# ==================================
ROUTER_MASTER_KEY=sk-hanzo-master-key
JWT_SECRET=hanzo-jwt-secret-change-in-production
JWT_REFRESH_SECRET=hanzo-refresh-secret-change-in-production
CREDS_KEY=hanzo-creds-key-change-in-production
CREDS_IV=hanzo-creds-iv-change
MEILI_MASTER_KEY=HanzoMeiliMasterKey
# ==================================
# Feature Flags
# ==================================
ALLOW_REGISTRATION=true
ALLOW_UNVERIFIED_EMAIL_LOGIN=false
DEBUG=false
# ==================================
# Performance Tuning
# ==================================
ROUTER_WORKERS=4
# ==================================
# Optional: Additional LLM Providers
# ==================================
# AZURE_API_KEY=...
# AZURE_API_BASE=https://your-resource.openai.azure.com/
# GROQ_API_KEY=...
# COHERE_API_KEY=...
# ==================================
# Optional: Observability
# ==================================
# LANGFUSE_PUBLIC_KEY=...
# LANGFUSE_SECRET_KEY=...
# LANGFUSE_HOST=https://cloud.langfuse.com
# ==================================
# Optional: Authentication Providers
# ==================================
# GOOGLE_CLIENT_ID=...
# GOOGLE_CLIENT_SECRET=...
# GITHUB_CLIENT_ID=...
# GITHUB_CLIENT_SECRET=...
-1
View File
@@ -1 +0,0 @@
ENDPOINTS=openAI,anthropic,custom
-37
View File
@@ -1,37 +0,0 @@
# Test Environment Configuration
NODE_ENV=test
# MongoDB for tests (use in-memory or test container)
MONGO_URI=mongodb://localhost:27017/HanzoChat_test
# Disable external services in tests
MEILI_HOST=
RAG_API_URL=
# Test user configuration
ALLOW_REGISTRATION=true
ALLOW_UNVERIFIED_EMAIL_LOGIN=true
# Disable rate limiting in tests
RATE_LIMIT_ENABLED=false
# Use test keys
CREDS_KEY=test-creds-key-32-chars-long-string
CREDS_IV=test-creds-iv-16
JWT_SECRET=test-jwt-secret-very-secure
JWT_REFRESH_SECRET=test-jwt-refresh-secret
# Disable analytics in tests
GOOGLE_ANALYTICS_ID=
SENTRY_DSN=
# Test domain
DOMAIN_CLIENT=http://localhost:3080
DOMAIN_SERVER=http://localhost:3080
# Disable SSL in tests
TRUST_PROXY=0
# Silent logging
DEBUG_LOGGING=false
CONSOLE_JSON=false
-46
View File
@@ -1,46 +0,0 @@
[flake8]
ignore =
# The following ignores can be removed when formatting using black
W191,W291,W292,W293,W391,W504
E101,E111,E114,E116,E117,E121,E122,E123,E124,E125,E126,E127,E128,E129,E131,
E201,E202,E221,E222,E225,E226,E231,E241,E251,E252,E261,E265,E271,E272,E275,
E301,E302,E303,E305,E306,
# line break before binary operator
W503,
# inline comment should start with '# '
E262,
# too many leading '#' for block comment
E266,
# multiple imports on one line
E401,
# module level import not at top of file
E402,
# Line too long (82 > 79 characters)
E501,
# comparison to None should be 'if cond is None:'
E711,
# comparison to True should be 'if cond is True:' or 'if cond:'
E712,
# do not compare types, for exact checks use `is` / `is not`, for instance checks use `isinstance()`
E721,
# do not use bare 'except'
E722,
# x is imported but unused
F401,
# 'from . import *' used; unable to detect undefined names
F403,
# x may be undefined, or defined from star imports:
F405,
# f-string is missing placeholders
F541,
# dictionary key '' repeated with different values
F601,
# redefinition of unused x from line 123
F811,
# undefined name x
F821,
# local variable x is assigned to but never used
F841,
# https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8
extend-ignore = E203
-10
View File
@@ -1,10 +0,0 @@
# Add the commit hash of any commit you want to ignore in `git blame` here.
# One commit hash per line.
#
# The GitHub Blame UI will use this file automatically!
#
# Run this command to always ignore formatting commits in `git blame`
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# Update pydantic code to fix warnings (GH-3600)
876840e9957bc7e9f7d6a2b58c4d7c53dad16481
-1
View File
@@ -1 +0,0 @@
*.ipynb linguist-vendored
+106
View File
@@ -0,0 +1,106 @@
# GitHub Branch Protection Rules
This document outlines the recommended branch protection rules for the Hanzo Chat repository.
## Main Branch Protection
Go to Settings → Branches → Add rule for `main`:
### Required Status Checks
- [x] **Require status checks to pass before merging**
- [x] Test Suite
- [x] Code Quality
- [x] Build Check
- [x] Security Scan
### Required Reviews
- [x] **Require pull request reviews before merging**
- Required approving reviews: 1
- [x] Dismiss stale pull request approvals when new commits are pushed
- [x] Require review from CODEOWNERS
### Additional Settings
- [x] **Require branches to be up to date before merging**
- [x] **Include administrators**
- [x] **Restrict who can push to matching branches**
- Add teams/users who can merge to main
## Develop Branch Protection
Go to Settings → Branches → Add rule for `develop`:
### Required Status Checks
- [x] **Require status checks to pass before merging**
- [x] Test Suite
- [x] Code Quality
### Required Reviews
- [x] **Require pull request reviews before merging**
- Required approving reviews: 1
## GitHub Actions Secrets
Add these secrets in Settings → Secrets and variables → Actions:
1. **CODECOV_TOKEN** - For coverage reporting (optional)
2. **SNYK_TOKEN** - For security scanning (optional)
3. **OPENAI_API_KEY** - For tests that require API access
4. **ANTHROPIC_API_KEY** - For tests that require API access
## Recommended `.github/CODEOWNERS` file
```
# Global owners
* @hanzoai/core-team
# Frontend
/client/ @hanzoai/frontend-team
/packages/data-provider/ @hanzoai/frontend-team
# Backend
/api/ @hanzoai/backend-team
/packages/data-schemas/ @hanzoai/backend-team
# CI/CD
/.github/ @hanzoai/devops-team
```
## PR Merge Strategy
In Settings → General → Pull Requests:
- [x] Allow squash merging
- [ ] Allow merge commits
- [ ] Allow rebase merging
- [x] Automatically delete head branches
## Status Checks Configuration
The following GitHub Actions workflows provide status checks:
1. **ci.yml** - Main CI pipeline
- `Test Suite` - Runs all tests
- `Code Quality` - Linting and formatting
- `Security Scan` - Dependency scanning
- `Build Check` - Ensures code builds
2. **pr-checks.yml** - PR-specific checks
- `Client Tests` - Runs only on client changes
- `API Tests` - Runs only on API changes
- `Bundle Size Check` - Monitors client bundle size
- `TypeScript Check` - Type checking
## Monitoring
Set up notifications for:
- Failed CI runs on main branch
- Security vulnerabilities
- Dependency updates
## Best Practices
1. **Never push directly to main** - Always use pull requests
2. **Keep PRs small and focused** - Easier to review and test
3. **Write descriptive PR titles** - They become part of the git history
4. **Update tests** - All new features should include tests
5. **Monitor CI times** - Keep test suites fast (< 10 minutes)
+1 -1
View File
@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement here on GitHub or
on the official GitHub repository.
on the official [Discord Server](https://discord.gg/hanzoai).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
+46 -39
View File
@@ -4,11 +4,11 @@ Thank you to all the contributors who have helped make this project possible! We
## Contributing Guidelines
If the feature you would like to contribute has not already received prior approval from the project maintainers, please submit a request in the [Feature Requests & Suggestions category](https://github.com/hanzoai/chat/discussions/new?category=feature-requests-suggestions) of the discussions board before beginning work on it. The requests should include specific implementation details, including areas of the application that will be affected by the change (including designs if applicable), and any other relevant information that might be required for a speedy review. However, proposals are not required for small changes, bug fixes, or documentation improvements. Small changes and bug fixes should be tied to an [issue](https://github.com/hanzoai/chat/issues) and included in the corresponding pull request for tracking purposes.
If the feature you would like to contribute has not already received prior approval from the project maintainers (i.e., the feature is currently on the [roadmap](https://github.com/users/hanzoai/projects/2)), please submit a request in the [Feature Requests & Suggestions category](https://github.com/hanzoai/chat/discussions/new?category=feature-requests-suggestions) of the discussions board before beginning work on it. The requests should include specific implementation details, including areas of the application that will be affected by the change (including designs if applicable), and any other relevant information that might be required for a speedy review. However, proposals are not required for small changes, bug fixes, or documentation improvements. Small changes and bug fixes should be tied to an [issue](https://github.com/hanzoai/chat/issues) and included in the corresponding pull request for tracking purposes.
Please note that a pull request involving a feature that has not been reviewed and approved by the project maintainers may be rejected. We appreciate your understanding and cooperation.
If you would like to discuss the changes you wish to make, open an issue or discussion on [GitHub](https://github.com/hanzoai/chat), where you can engage with other contributors and seek guidance from the community.
If you would like to discuss the changes you wish to make, join our [Discord community](https://discord.gg/hanzoai), where you can engage with other contributors and seek guidance from the community.
## Our Standards
@@ -26,29 +26,33 @@ Project maintainers have the right and responsibility to remove, edit, or reject
## 1. Development Setup
1. Use Node.js v20.19.0+ or ^22.12.0 or >= 23.0.0.
2. Run `npm run smart-reinstall` to install dependencies (uses Turborepo). Use `npm run reinstall` for a clean install, or `npm ci` for a fresh lockfile-based install.
3. Build all compiled code: `npm run build`.
4. Setup and run unit tests:
1. Use Node.JS 20.x.
2. Install typescript globally: `npm i -g typescript`.
3. Run `npm ci` to install dependencies.
4. Build the data provider: `npm run build:data-provider`.
5. Build data schemas: `npm run build:data-schemas`.
6. Build API methods: `npm run build:api`.
7. Setup and run unit tests:
- Copy `.env.test`: `cp api/test/.env.test.example api/test/.env.test`.
- Run backend unit tests: `npm run test:api`.
- Run frontend unit tests: `npm run test:client`.
5. Setup and run integration tests:
8. Setup and run integration tests:
- Build client: `cd client && npm run build`.
- Create `.env`: `cp .env.example .env`.
- Install [MongoDB Community Edition](https://www.mongodb.com/docs/manual/administration/install-community/), ensure that `mongosh` connects to your local instance.
- Run: `npx install playwright`, then `npx playwright install`.
- Copy `config.local`: `cp e2e/config.local.example.ts e2e/config.local.ts`.
- Copy `librechat.yaml`: `cp librechat.example.yaml librechat.yaml`.
- Copy `chat.yaml`: `cp chat.example.yaml chat.yaml`.
- Run: `npm run e2e`.
## 2. Development Notes
1. Before starting work, make sure your main branch has the latest commits with `npm run update`.
2. Run linting command to find errors: `npm run lint`. Alternatively, ensure husky pre-commit checks are functioning.
3. Run linting command to find errors: `npm run lint`. Alternatively, ensure husky pre-commit checks are functioning.
3. After your changes, reinstall packages in your current branch using `npm run reinstall` and ensure everything still works.
- Restart the ESLint server ("ESLint: Restart ESLint Server" in VS Code command bar) and your IDE after reinstalling or updating.
4. Clear web app localStorage and cookies before and after changes.
5. To check for introduced errors, build all compiled code: `npm run build`.
5. For frontend changes, compile typescript before and after changes to check for introduced errors: `cd client && npm run build`.
6. Run backend unit tests: `npm run test:api`.
7. Run frontend unit tests: `npm run test:client`.
8. Run integration tests: `npm run e2e`.
@@ -114,45 +118,48 @@ Apply the following naming conventions to branches, labels, and other Git-relate
- **JS/TS:** Directories and file names: Descriptive and camelCase. First letter uppercased for React files (e.g., `helperFunction.ts, ReactComponent.tsx`).
- **Docs:** Directories and file names: Descriptive and snake_case (e.g., `config_files.md`).
## 7. Coding Standards
For detailed coding conventions, workspace boundaries, and architecture guidance, refer to the [`AGENTS.md`](../AGENTS.md) file at the project root. It covers code style, type safety, import ordering, iteration/performance expectations, frontend rules, testing, and development commands.
## 8. TypeScript Conversion
## 7. TypeScript Conversion
1. **Original State**: The project was initially developed entirely in JavaScript (JS).
2. **Frontend**: Fully transitioned to TypeScript.
2. **Frontend Transition**:
- We are in the process of transitioning the frontend from JS to TypeScript (TS).
- The transition is nearing completion.
- This conversion is feasible due to React's capability to intermix JS and TS prior to code compilation. It's standard practice to compile/bundle the code in such scenarios.
3. **Backend**:
- The legacy Express.js server remains in `/api` as JavaScript.
- All new backend code is written in TypeScript under `/packages/api`, which is compiled and consumed by `/api`.
- Shared database logic lives in `/packages/data-schemas` (TypeScript).
- Shared frontend/backend API types and services live in `/packages/data-provider` (TypeScript).
- Minimize direct changes to `/api`; prefer adding TypeScript code to `/packages/api` and importing it.
3. **Backend Considerations**:
- Transitioning the backend to TypeScript would be a more intricate process, especially for an established Express.js server.
- **Options for Transition**:
- **Single Phase Overhaul**: This involves converting the entire backend to TypeScript in one go. It's the most straightforward approach but can be disruptive, especially for larger codebases.
- **Incremental Transition**: Convert parts of the backend progressively. This can be done by:
- Maintaining a separate directory for TypeScript files.
- Gradually migrating and testing individual modules or routes.
- Using a build tool like `tsc` to compile TypeScript files independently until the entire transition is complete.
- **Compilation Considerations**:
- Introducing a compilation step for the server is an option. This would involve using tools like `ts-node` for development and `tsc` for production builds.
- However, this is not a conventional approach for Express.js servers and could introduce added complexity, especially in terms of build and deployment processes.
- **Current Stance**: At present, this backend transition is of lower priority and might not be pursued.
## 9. Module Import Conventions
## 8. Module Import Conventions
Imports are organized into three sections (in order):
- `npm` packages first,
- from shortest line (top) to longest (bottom)
1. **Package imports** — sorted from shortest to longest line length.
- `react` is always the first import.
- Multi-line (stacked) imports count their total character length across all lines for sorting.
- Followed by typescript types (pertains to data-provider and client workspaces)
- longest line (top) to shortest (bottom)
- types from package come first
2. **`import type` imports** — sorted from longest to shortest line length.
- Package type imports come first, then local type imports.
- Line length sorting resets between the package and local sub-groups.
- Lastly, local imports
- longest line (top) to shortest (bottom)
- imports with alias `~` treated the same as relative import with respect to line length
3. **Local/project imports** — sorted from longest to shortest line length.
- Multi-line (stacked) imports count their total character length across all lines for sorting.
- Imports with alias `~` are treated the same as relative imports with respect to line length.
---
- Consolidate value imports from the same module as much as possible.
- Always use standalone `import type { ... }` for type imports; never use inline `type` keyword inside value imports (e.g., `import { Foo, type Bar }` is wrong).
**Note:** ESLint will automatically enforce these import conventions when you run `npm run lint --fix` or through pre-commit hooks.
For the full set of coding standards, see [`AGENTS.md`](../AGENTS.md).
Please ensure that you adapt this summary to fit the specific context and nuances of your project.
---
+2 -2
View File
@@ -1,6 +1,6 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
github: [hanzoai]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
@@ -10,4 +10,4 @@ liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: https://buy.stripe.com/9AQ03Kd3P91o0Q8bIS
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+1 -1
View File
@@ -29,7 +29,7 @@ body:
description: |
If using Docker, please run and provide the output of:
```bash
docker images | grep hanzo
docker images | grep hanzochat
```
If running from source, please run and provide the output of:
@@ -1,15 +1,17 @@
name: Locize Translation Access Request
description: Request access to an additional language in Locize for Hanzo Chat translations.
description: Request access to an additional language in Locize for chat translations.
title: "Locize Access Request: "
labels: ["🌍 i18n", "🔑 access request"]
body:
- type: markdown
attributes:
value: |
Thank you for your interest in contributing to Hanzo Chat translations!
Thank you for your interest in contributing to chat translations!
Please fill out the form below to request access to an additional language in **Locize**.
**Note:** Ensure that the requested language is supported before submitting your request.
**🔗 Available Languages:** [View the list here](https://docs.hanzo.ai/chat/translation)
**📌 Note:** Ensure that the requested language is supported before submitting your request.
- type: input
id: account_name
attributes:
@@ -23,8 +25,10 @@ body:
attributes:
label: Language Code (ISO 639-1)
description: |
Enter the **ISO 639-1** language code for the language you want to translate into.
Enter the **ISO 639-1** language code for the language you want to translate into.
Example: `es` for Spanish, `zh-Hant` for Traditional Chinese.
**🔗 Reference:** [Available Languages](https://docs.hanzo.ai/chat/translation)
placeholder: e.g., es
validations:
required: true
@@ -34,5 +38,5 @@ body:
label: Agreement
description: By submitting this request, you confirm that you will contribute responsibly and adhere to the project guidelines.
options:
- label: I agree to use my access solely for contributing to Hanzo Chat translations.
required: true
- label: I agree to use my access solely for contributing to chat translations.
required: true
@@ -1,5 +1,5 @@
name: New Language Request
description: Request to add a new language for Hanzo Chat translations.
description: Request to add a new language for chat translations.
title: "New Language Request: "
labels: ["✨ enhancement", "🌍 i18n"]
body:
-49
View File
@@ -1,49 +0,0 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: dropdown
id: ml-ops-team
attributes:
label: Are you a ML Ops Team?
description: This helps us prioritize your requests correctly
options:
- "No"
- "Yes"
validations:
required: true
- type: input
id: version
attributes:
label: What LiteLLM version are you on ?
placeholder: v1.53.1
validations:
required: true
- type: input
id: contact
attributes:
label: Twitter / LinkedIn details
description: We announce new features on Twitter + LinkedIn. If this issue leads to an announcement, and you'd like a mention, we'll gladly shout you out!
placeholder: ex. @krrish_dh / https://www.linkedin.com/in/krish-d/
validations:
required: false
-8
View File
@@ -1,8 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Schedule Demo
url: https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat
about: Speak directly with Krrish and Ishaan, the founders, to discuss issues, share feedback, or explore improvements for LiteLLM
- name: Discord
url: https://discord.com/invite/wuPM9dRgDw
about: Join 250+ LiteLLM community members!
@@ -1,42 +0,0 @@
name: 🚀 Feature Request
description: Submit a proposal/request for a new LiteLLM feature.
title: "[Feature]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for making LiteLLM better!
- type: textarea
id: the-feature
attributes:
label: The Feature
description: A clear and concise description of the feature proposal
placeholder: Tell us what you want!
validations:
required: true
- type: textarea
id: motivation
attributes:
label: Motivation, pitch
description: Please outline the motivation for the proposal. Is your feature request related to a specific problem? e.g., "I'm working on X and would like Y to be possible". If this is related to another GitHub issue, please link here too.
validations:
required: true
- type: dropdown
id: hiring-interest
attributes:
label: LiteLLM is hiring a founding backend engineer, are you interested in joining us and shipping to all our users?
description: If yes, apply here - https://www.ycombinator.com/companies/litellm/jobs/6uvoBp3-founding-backend-engineer
options:
- "No"
- "Yes"
validations:
required: true
- type: input
id: contact
attributes:
label: Twitter / LinkedIn details
description: We announce new features on Twitter + LinkedIn. When this is announced, and you'd like a mention, we'll gladly shout you out!
placeholder: ex. @krrish_dh / https://www.linkedin.com/in/krish-d/
validations:
required: false
+10 -8
View File
@@ -1,8 +1,8 @@
# Security Policy
At Hanzo, we prioritize the security of our project and value the contributions of security researchers in helping us improve the security of our codebase. If you discover a security vulnerability within our project, we appreciate your responsible disclosure. Please follow the guidelines below to report any vulnerabilities to us:
At chat, we prioritize the security of our project and value the contributions of security researchers in helping us improve the security of our codebase. If you discover a security vulnerability within our project, we appreciate your responsible disclosure. Please follow the guidelines below to report any vulnerabilities to us:
**Note: Only report sensitive vulnerability details via the appropriate private communication channels mentioned below. Public channels, such as GitHub issues, should be used for initiating contact and establishing private communication channels.**
**Note: Only report sensitive vulnerability details via the appropriate private communication channels mentioned below. Public channels, such as GitHub issues and Discord, should be used for initiating contact and establishing private communication channels.**
## Communication Channels
@@ -12,7 +12,7 @@ When reporting a security vulnerability, you have the following options to reach
- **Option 2: GitHub Issues**: You can initiate first contact via GitHub Issues. However, please note that initial contact through GitHub Issues should not include any sensitive details.
- **Option 3: Email**: You can reach us at security@hanzo.ai for security-related reports.
- **Option 3: Discord Server**: You can join our [Discord community](https://discord.gg/hanzoai) and initiate first contact in the `#issues` channel. However, please ensure that initial contact through Discord does not include any sensitive details.
_After the initial contact, we will establish a private communication channel for further discussion._
@@ -26,10 +26,10 @@ We strive to acknowledge vulnerability reports within 72 hours and will keep you
## Security Updates and Patching
We are committed to maintaining the security of our open-source project, Hanzo Chat, and promptly addressing any identified vulnerabilities. To ensure the security of our project, we adhere to the following practices:
We are committed to maintaining the security of our open-source project, chat, and promptly addressing any identified vulnerabilities. To ensure the security of our project, we adhere to the following practices:
- We prioritize security updates for the current major release of our software.
- We actively monitor the GitHub Security Advisory system for any vulnerability reports.
- We actively monitor the GitHub Security Advisory system and the `#issues` channel on Discord for any vulnerability reports.
- We promptly review and validate reported vulnerabilities and take appropriate actions to address them.
- We release security patches and updates in a timely manner to mitigate any identified vulnerabilities.
@@ -39,11 +39,11 @@ Please note that as a security-conscious community, we may not always disclose d
This security policy applies to the following GitHub repository:
- Repository: [Hanzo Chat](https://github.com/hanzoai/chat)
- Repository: [chat](https://github.com/hanzoai/chat)
## Contact
If you have any questions or concerns regarding the security of our project, please reach out to us at security@hanzo.ai or by [opening an issue](https://github.com/hanzoai/chat/issues/new) on GitHub. Please note that the response time may vary depending on the nature and severity of the inquiry.
If you have any questions or concerns regarding the security of our project, please join our [Discord community](https://discord.gg/hanzoai) and report them in the appropriate channel. You can also reach out to us by [opening an issue](https://github.com/hanzoai/chat/issues/new) on GitHub. Please note that the response time may vary depending on the nature and severity of the inquiry.
## Acknowledgments
@@ -51,7 +51,9 @@ We would like to express our gratitude to the security researchers and community
## Bug Bounty Program
We currently do not have a bug bounty program in place. However, we welcome and appreciate any security-related contributions through pull requests (PRs) that address vulnerabilities in our codebase. We believe in the power of collaboration to improve the security of our project and invite you to join us in making it more robust.
We currently do not have a bug bounty program in place. However, we welcome and appreciate any
security-related contributions through pull requests (PRs) that address vulnerabilities in our codebase. We believe in the power of collaboration to improve the security of our project and invite you to join us in making it more robust.
**Reference**
- https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html
@@ -1,77 +0,0 @@
name: Helm OCI Chart Releaser
description: Push Helm charts to OCI-based (Docker) registries
author: sergeyshaykhullin
branding:
color: yellow
icon: upload-cloud
inputs:
name:
required: true
description: Chart name
repository:
required: true
description: Chart repository name
tag:
required: true
description: Chart version
app_version:
required: true
description: App version
path:
required: false
description: Chart path (Default 'charts/{name}')
registry:
required: true
description: OCI registry
registry_username:
required: true
description: OCI registry username
registry_password:
required: true
description: OCI registry password
update_dependencies:
required: false
default: 'false'
description: Update chart dependencies before packaging (Default 'false')
outputs:
image:
value: ${{ steps.output.outputs.image }}
description: Chart image (Default '{registry}/{repository}/{image}:{tag}')
runs:
using: composite
steps:
- name: Helm | Login
shell: bash
run: echo ${{ inputs.registry_password }} | helm registry login -u ${{ inputs.registry_username }} --password-stdin ${{ inputs.registry }}
env:
HELM_EXPERIMENTAL_OCI: '1'
- name: Helm | Dependency
if: inputs.update_dependencies == 'true'
shell: bash
run: helm dependency update ${{ inputs.path == null && format('{0}/{1}', 'charts', inputs.name) || inputs.path }}
env:
HELM_EXPERIMENTAL_OCI: '1'
- name: Helm | Package
shell: bash
run: helm package ${{ inputs.path == null && format('{0}/{1}', 'charts', inputs.name) || inputs.path }} --version ${{ inputs.tag }} --app-version ${{ inputs.app_version }}
env:
HELM_EXPERIMENTAL_OCI: '1'
- name: Helm | Push
shell: bash
run: helm push ${{ inputs.name }}-${{ inputs.tag }}.tgz oci://${{ inputs.registry }}/${{ inputs.repository }}
env:
HELM_EXPERIMENTAL_OCI: '1'
- name: Helm | Logout
shell: bash
run: helm registry logout ${{ inputs.registry }}
env:
HELM_EXPERIMENTAL_OCI: '1'
- name: Helm | Output
id: output
shell: bash
run: echo "image=${{ inputs.registry }}/${{ inputs.repository }}/${{ inputs.name }}:${{ inputs.tag }}" >> $GITHUB_OUTPUT
+51
View File
@@ -0,0 +1,51 @@
# git-cliff configuration for changelog generation
[changelog]
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
{% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message | upper_first }}
{%- endfor %}
{% endfor %}\n
"""
footer = """
<!-- generated by git-cliff -->
"""
trim = true
[git]
conventional_commits = true
filter_unconventional = true
split_commits = false
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->⛰️ Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps.*\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
]
protect_breaking_commits = false
filter_commits = false
topo_order = false
sort_commits = "oldest"
-10
View File
@@ -1,10 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
groups:
github-actions:
patterns:
- "*"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

-9
View File
@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-label="chat">
<rect width="1280" height="640" fill="#0A0A0A"/>
<svg x="96" y="215" width="210" height="210" viewBox="0 0 67 67"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="#fff"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#fff"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="#fff"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#fff"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#fff"/></svg>
<text x="378" y="276" font-family="Inter,system-ui,-apple-system,sans-serif" font-size="78" font-weight="800" letter-spacing="-2" fill="#ffffff">chat</text>
<text x="378" y="322" font-family="Inter,system-ui,sans-serif" font-size="30" fill="#ffffff" opacity=".66">AI chat with MCP integration and multi-provider support</text>
<rect x="378" y="338" width="806" height="3" rx="1.5" fill="#ffffff" opacity=".9"/>
<text x="378" y="390" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">github.com/hanzoai</text>
<text x="1184" y="390" text-anchor="end" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">hanzo.ai</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

+41 -33
View File
@@ -1,33 +1,41 @@
## Title
<!-- e.g. "Implement user authentication feature" -->
## Relevant issues
<!-- e.g. "Fixes #000" -->
## Pre-Submission checklist
**Please complete all items before asking a LiteLLM maintainer to review your PR**
- [ ] I have Added testing in the [`tests/litellm/`](https://github.com/BerriAI/litellm/tree/main/tests/litellm) directory, **Adding at least 1 test is a hard requirement** - [see details](https://docs.litellm.ai/docs/extras/contributing_code)
- [ ] I have added a screenshot of my new test passing locally
- [ ] My PR passes all unit tests on [`make test-unit`](https://docs.litellm.ai/docs/extras/contributing_code)
- [ ] My PR's scope is as isolated as possible, it only solves 1 specific problem
## Type
<!-- Select the type of Pull Request -->
<!-- Keep only the necessary ones -->
🆕 New Feature
🐛 Bug Fix
🧹 Refactoring
📖 Documentation
🚄 Infrastructure
✅ Test
## Changes
# Pull Request Template
⚠️ Before Submitting a PR, Please Review:
- Please ensure that you have thoroughly read and understood the [Contributing Docs](https://github.com/hanzoai/chat/blob/main/.github/CONTRIBUTING.md) before submitting your Pull Request.
⚠️ Documentation Updates Notice:
- Kindly note that documentation updates are managed in this repository: [hanzo.ai](https://github.com/hanzoai/docs)
## Summary
Please provide a brief summary of your changes and the related issue. Include any motivation and context that is relevant to your changes. If there are any dependencies necessary for your changes, please list them here.
## Change Type
Please delete any irrelevant options.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- [ ] Translation update
## Testing
Please describe your test process and include instructions so that we can reproduce your test. If there are any important variables for your testing configuration, list them here.
### **Test Configuration**:
## Checklist
Please delete any irrelevant options.
- [ ] My code adheres to this project's style guidelines
- [ ] I have performed a self-review of my own code
- [ ] I have commented in any complex areas of my code
- [ ] I have made pertinent documentation changes
- [ ] My changes do not introduce new warnings
- [ ] I have written tests demonstrating that my changes are effective or that my feature works
- [ ] Local unit tests pass with my changes
- [ ] Any changes dependent on mine have been merged and published in downstream modules.
- [ ] A pull request for updating the documentation has been submitted.
-237
View File
@@ -1,237 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
CHART_PATH="${CHART_PATH:-helm/librechat/Chart.yaml}"
DEFAULT_BRANCH="${DEFAULT_BRANCH:-main}"
BASE_REF="${BASE_REF:-refs/remotes/origin/${DEFAULT_BRANCH}}"
BACKFILL_FROM_VERSION="${BACKFILL_FROM_VERSION:-1.9.0}"
PUSH_TAGS="${PUSH_TAGS:-false}"
TAG_PREFIX="${TAG_PREFIX:-chart-}"
GITHUB_SERVER_URL="${GITHUB_SERVER_URL:-https://github.com}"
DISPATCH_WORKFLOW="${DISPATCH_WORKFLOW:-}"
RELEASE_EXISTING_TAG="${RELEASE_EXISTING_TAG:-}"
SEMVER_REGEX='^(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)(-[0-9A-Za-z-]+([.][0-9A-Za-z-]+)*)?([+][0-9A-Za-z-]+([.][0-9A-Za-z-]+)*)?$'
fail() {
printf '::error::%s\n' "$1" >&2
exit 1
}
git_auth_header() {
token="$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')"
printf 'AUTHORIZATION: basic %s' "$token"
}
git_with_auth() {
if [ -n "${GITHUB_TOKEN:-}" ]; then
git -c "http.extraheader=$(git_auth_header)" "$@"
return
fi
git "$@"
}
dispatch_release() {
tag="$1"
if [ -z "$DISPATCH_WORKFLOW" ]; then
return
fi
if [ -z "${GITHUB_REPOSITORY:-}" ]; then
fail "GITHUB_REPOSITORY is required to dispatch ${DISPATCH_WORKFLOW}"
fi
if [[ ! "$GITHUB_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
fail "Unexpected repository name: ${GITHUB_REPOSITORY}"
fi
if [[ ! "$DISPATCH_WORKFLOW" =~ ^[A-Za-z0-9_.-]+[.]ya?ml$ ]]; then
fail "Unexpected workflow file: ${DISPATCH_WORKFLOW}"
fi
token="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
if [ -z "$token" ]; then
fail "GH_TOKEN or GITHUB_TOKEN is required to dispatch ${DISPATCH_WORKFLOW}"
fi
command -v gh >/dev/null ||
fail "GitHub CLI is required to dispatch ${DISPATCH_WORKFLOW}"
GH_TOKEN="$token" gh workflow run "$DISPATCH_WORKFLOW" \
--repo "$GITHUB_REPOSITORY" \
--ref "$DEFAULT_BRANCH" \
-f "chart_tag=${tag}"
}
version_less_than() {
left="${1%%[-+]*}"
right="${2%%[-+]*}"
IFS=. read -r left_major left_minor left_patch <<<"$left"
IFS=. read -r right_major right_minor right_patch <<<"$right"
if (( left_major != right_major )); then
(( left_major < right_major ))
return
fi
if (( left_minor != right_minor )); then
(( left_minor < right_minor ))
return
fi
(( left_patch < right_patch ))
}
validate_chart_tag() {
tag="$1"
version="${tag#${TAG_PREFIX}}"
git check-ref-format "refs/tags/${tag}" >/dev/null ||
fail "Refusing to use invalid tag ${tag}"
if [[ "$tag" != "${TAG_PREFIX}"* || ! "$version" =~ $SEMVER_REGEX ]]; then
fail "Chart tags must use the form ${TAG_PREFIX}<semver>, for example ${TAG_PREFIX}2.0.5"
fi
}
dispatch_existing_tag() {
tag="$1"
if [ -z "$tag" ]; then
return
fi
validate_chart_tag "$tag"
if [ "$PUSH_TAGS" != "true" ]; then
printf 'Would dispatch release workflow for existing %s.\n' "$tag"
return
fi
if ! git_with_auth ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
fail "Remote tag ${tag} does not exist"
fi
printf 'Dispatching release workflow for existing %s.\n' "$tag"
dispatch_release "$tag"
}
chart_version_at() {
git show "${1}:${CHART_PATH}" 2>/dev/null | awk '
/^version:[[:space:]]*/ {
value = $0
sub(/^version:[[:space:]]*/, "", value)
sub(/[[:space:]]*#.*/, "", value)
gsub(/^[[:space:]"'\''"]+|[[:space:]"'\''"]+$/, "", value)
print value
exit
}
'
}
case "$PUSH_TAGS" in
true | false) ;;
*) fail "PUSH_TAGS must be true or false" ;;
esac
if [[ ! "$BACKFILL_FROM_VERSION" =~ $SEMVER_REGEX ]]; then
fail "BACKFILL_FROM_VERSION must be a valid SemVer value"
fi
git rev-parse --verify "${BASE_REF}^{commit}" >/dev/null ||
fail "Unable to resolve ${BASE_REF}; fetch ${DEFAULT_BRANCH} before running this script"
history_file="$(mktemp)"
versions_file="$(mktemp)"
seen_file="$(mktemp)"
missing_file="$(mktemp)"
cleanup() {
rm -f "$history_file" "$versions_file" "$seen_file" "$missing_file"
}
trap cleanup EXIT
git log --first-parent --reverse --format=%H "$BASE_REF" -- "$CHART_PATH" >"$history_file"
if [ ! -s "$history_file" ]; then
fail "No history found for ${CHART_PATH} on ${BASE_REF}"
fi
while IFS= read -r commit; do
version="$(chart_version_at "$commit")"
if [ -z "$version" ]; then
continue
fi
if [[ ! "$version" =~ $SEMVER_REGEX ]]; then
fail "${CHART_PATH} has invalid SemVer '${version}' at ${commit}"
fi
if version_less_than "$version" "$BACKFILL_FROM_VERSION"; then
continue
fi
if grep -Fqx "$version" "$seen_file"; then
continue
fi
printf '%s\n' "$version" >>"$seen_file"
printf '%s\t%s\n' "$version" "$commit" >>"$versions_file"
done <"$history_file"
if [ ! -s "$versions_file" ]; then
fail "No chart versions found in ${CHART_PATH}"
fi
while IFS="$(printf '\t')" read -r version commit; do
tag="${TAG_PREFIX}${version}"
validate_chart_tag "$tag"
if git rev-parse --quiet --verify "refs/tags/${tag}" >/dev/null; then
continue
fi
printf '%s\t%s\n' "$tag" "$commit" >>"$missing_file"
done <"$versions_file"
if [ ! -s "$missing_file" ]; then
printf 'All chart versions on %s already have %s tags.\n' "$BASE_REF" "$TAG_PREFIX"
dispatch_existing_tag "$RELEASE_EXISTING_TAG"
exit 0
fi
while IFS="$(printf '\t')" read -r tag commit; do
short_commit="$(git rev-parse --short "$commit")"
if [ "$PUSH_TAGS" != "true" ]; then
printf 'Would create %s at %s.\n' "$tag" "$short_commit"
continue
fi
if git_with_auth ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
printf 'Remote tag %s already exists; dispatching release workflow.\n' "$tag"
dispatch_release "$tag"
continue
fi
git tag "$tag" "$commit"
if git_with_auth push origin "refs/tags/${tag}"; then
printf 'Created %s at %s.\n' "$tag" "$short_commit"
dispatch_release "$tag"
continue
fi
if git_with_auth ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
printf 'Remote tag %s was created concurrently; dispatching release workflow.\n' "$tag"
dispatch_release "$tag"
continue
fi
fail "Failed to push ${tag}"
done <"$missing_file"
dispatch_existing_tag "$RELEASE_EXISTING_TAG"
-94
View File
@@ -1,94 +0,0 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
llmlite-service
SAM Template for llmlite-service
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 600
MemorySize: 128
Environment:
Variables:
WORKER_CONFIG: !Ref WorkerConfigParameter
Parameters:
AliasParameter:
Type: String
Default: live
WorkerConfigParameter:
Type: String
Description: Sample environment variable
Default: '{"model": null, "alias": null, "api_base": null, "api_version": "2023-07-01-preview", "debug": false, "temperature": null, "max_tokens": null, "request_timeout": 600, "max_budget": null, "telemetry": true, "drop_params": false, "add_function_to_prompt": false, "headers": null, "save": false, "config": null, "use_queue": false}'
Resources:
MyUrlFunctionPermissions:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref URL
Action: lambda:InvokeFunctionUrl
Principal: "*"
FunctionUrlAuthType: NONE
Function:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub "${AWS::StackName}-function"
CodeUri: "./litellm"
Handler: proxy/lambda.handler
Runtime: python3.11
AutoPublishAlias: !Ref AliasParameter
Architectures:
- x86_64
DeploymentPreference:
Type: AllAtOnce
Alarms:
- !Ref NewVersionErrorMetricGreaterThanZeroAlarm
NewVersionErrorMetricGreaterThanZeroAlarm:
Type: "AWS::CloudWatch::Alarm"
Properties:
AlarmDescription: Lambda Function Error > 0
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: Resource
Value: !Sub "${Function}:live"
- Name: FunctionName
Value: !Ref Function
- Name: ExecutedVersion
Value: !GetAtt Function.Version.Version
EvaluationPeriods: 1
Unit: Count
MetricName: Errors
Namespace: AWS/Lambda
Period: 60
Statistic: Sum
Threshold: 0
URL:
Type: AWS::Lambda::Url
DependsOn: FunctionAliaslive
Properties:
AuthType: NONE
Qualifier: live
TargetFunctionArn: !GetAtt Function.Arn
Outputs:
FunctionARN:
Description: "Lambda Function ARN"
Value: !GetAtt Function.Arn
FunctionUrl:
Description: "Lambda Function URL Endpoint"
Value:
Fn::GetAtt: URL.FunctionUrl
FunctionVersion:
Description: "Lambda Function Version"
Value: !GetAtt Function.Version.Version
FunctionNewAlarmARN:
Description: "Lambda Function New Alarm ARN"
Value: !GetAtt NewVersionErrorMetricGreaterThanZeroAlarm.Arn
+2 -2
View File
@@ -13,13 +13,13 @@ on:
jobs:
axe-linter:
runs-on: hanzo-build-linux-amd64
runs-on: ubuntu-latest
if: >
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'hanzoai/chat') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_workflow == 'true')
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: dequelabs/axe-linter-action@v1
with:
api_key: ${{ secrets.AXE_LINTER_API_KEY }}
@@ -1,28 +0,0 @@
name: Updates model_prices_and_context_window.json and Create Pull Request
on:
schedule:
- cron: "0 0 * * 0" # Run every Sundays at midnight
#- cron: "0 0 * * *" # Run daily at midnight
jobs:
auto_update_price_and_context_window:
runs-on: hanzo-build-linux-amd64
steps:
- uses: actions/checkout@v6
- name: Install Dependencies
run: |
pip install aiohttp
- name: Update JSON Data
run: |
python ".github/workflows/auto_update_price_and_context_window_file.py"
- name: Create Pull Request
run: |
git add model_prices_and_context_window.json
git commit -m "Update model_prices_and_context_window.json file: $(date +'%Y-%m-%d')"
gh pr create --title "Update model_prices_and_context_window.json file" \
--body "Automated update for model_prices_and_context_window.json" \
--head auto-update-price-and-context-window-$(date +'%Y-%m-%d') \
--base main
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
@@ -1,121 +0,0 @@
import asyncio
import aiohttp
import json
# Asynchronously fetch data from a given URL
async def fetch_data(url):
try:
# Create an asynchronous session
async with aiohttp.ClientSession() as session:
# Send a GET request to the URL
async with session.get(url) as resp:
# Raise an error if the response status is not OK
resp.raise_for_status()
# Parse the response JSON
resp_json = await resp.json()
print("Fetch the data from URL.")
# Return the 'data' field from the JSON response
return resp_json['data']
except Exception as e:
# Print an error message if fetching data fails
print("Error fetching data from URL:", e)
return None
# Synchronize local data with remote data
def sync_local_data_with_remote(local_data, remote_data):
# Update existing keys in local_data with values from remote_data
for key in (set(local_data) & set(remote_data)):
local_data[key].update(remote_data[key])
# Add new keys from remote_data to local_data
for key in (set(remote_data) - set(local_data)):
local_data[key] = remote_data[key]
# Write data to the json file
def write_to_file(file_path, data):
try:
# Open the file in write mode
with open(file_path, "w") as file:
# Dump the data as JSON into the file
json.dump(data, file, indent=4)
print("Values updated successfully.")
except Exception as e:
# Print an error message if writing to file fails
print("Error updating JSON file:", e)
# Update the existing models and add the missing models
def transform_remote_data(data):
transformed = {}
for row in data:
# Add the fields 'max_tokens' and 'input_cost_per_token'
obj = {
"max_tokens": row["context_length"],
"input_cost_per_token": float(row["pricing"]["prompt"]),
}
# Add 'max_output_tokens' as a field if it is not None
if "top_provider" in row and "max_completion_tokens" in row["top_provider"] and row["top_provider"]["max_completion_tokens"] is not None:
obj['max_output_tokens'] = int(row["top_provider"]["max_completion_tokens"])
# Add the field 'output_cost_per_token'
obj.update({
"output_cost_per_token": float(row["pricing"]["completion"]),
})
# Add field 'input_cost_per_image' if it exists and is non-zero
if "pricing" in row and "image" in row["pricing"] and float(row["pricing"]["image"]) != 0.0:
obj['input_cost_per_image'] = float(row["pricing"]["image"])
# Add the fields 'litellm_provider' and 'mode'
obj.update({
"litellm_provider": "openrouter",
"mode": "chat"
})
# Add the 'supports_vision' field if the modality is 'multimodal'
if row.get('architecture', {}).get('modality') == 'multimodal':
obj['supports_vision'] = True
# Use a composite key to store the transformed object
transformed[f'openrouter/{row["id"]}'] = obj
return transformed
# Load local data from a specified file
def load_local_data(file_path):
try:
# Open the file in read mode
with open(file_path, "r") as file:
# Load and return the JSON data
return json.load(file)
except FileNotFoundError:
# Print an error message if the file is not found
print("File not found:", file_path)
return None
except json.JSONDecodeError as e:
# Print an error message if JSON decoding fails
print("Error decoding JSON:", e)
return None
def main():
local_file_path = "model_prices_and_context_window.json" # Path to the local data file
url = "https://openrouter.ai/api/v1/models" # URL to fetch remote data
# Load local data from file
local_data = load_local_data(local_file_path)
# Fetch remote data asynchronously
remote_data = asyncio.run(fetch_data(url))
# Transform the fetched remote data
remote_data = transform_remote_data(remote_data)
# If both local and remote data are available, synchronize and save
if local_data and remote_data:
sync_local_data_with_remote(local_data, remote_data)
write_to_file(local_file_path, local_data)
else:
print("Failed to fetch model data from either local file or URL.")
# Entry point of the script
if __name__ == "__main__":
main()
+17 -20
View File
@@ -4,16 +4,15 @@ on:
branches:
- main
- dev
- dev-staging
- release/*
paths:
- 'api/**'
- 'packages/**'
- 'packages/api/**'
jobs:
tests_Backend:
name: Run Backend unit tests
timeout-minutes: 60
runs-on: hanzo-build-linux-amd64
runs-on: ubuntu-latest
env:
MONGO_URI: ${{ secrets.MONGO_URI }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
@@ -24,27 +23,25 @@ jobs:
BAN_DURATION: ${{ secrets.BAN_DURATION }}
BAN_INTERVAL: ${{ secrets.BAN_INTERVAL }}
NODE_ENV: CI
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache: 'npm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: npm ci
- name: Install Data Provider Package
run: pnpm run build:data-provider
run: npm run build:data-provider
- name: Install Data Schemas Package
run: pnpm run build:data-schemas
run: npm run build:data-schemas
- name: Install API Package
run: pnpm run build:api
run: npm run build:api
- name: Create empty auth.json file
run: |
@@ -54,7 +51,7 @@ jobs:
- name: Check for Circular dependency in rollup
working-directory: ./packages/data-provider
run: |
output=$(pnpm run rollup:api)
output=$(npm run rollup:api)
echo "$output"
if echo "$output" | grep -q "Circular dependency"; then
echo "Error: Circular dependency detected!"
@@ -65,13 +62,13 @@ jobs:
run: cp api/test/.env.test.example api/test/.env.test
- name: Run unit tests
run: cd api && pnpm run test:ci
run: cd api && npm run test:ci
- name: Run librechat-data-provider unit tests
run: cd packages/data-provider && pnpm run test:ci
- name: Run hanzochat-data-provider unit tests
run: cd packages/data-provider && npm run test:ci
- name: Run @librechat/data-schemas unit tests
run: cd packages/data-schemas && pnpm run test:ci
- name: Run @hanzochat/data-schemas unit tests
run: cd packages/data-schemas && npm run test:ci
- name: Run @librechat/api unit tests
run: cd packages/api && pnpm run test:ci
- name: Run @hanzochat/api unit tests
run: cd packages/api && npm run test:ci
+4 -4
View File
@@ -8,19 +8,19 @@ env:
jobs:
build-and-push:
runs-on: hanzo-build-linux-amd64
runs-on: ubuntu-latest
steps:
# checkout the repo
- name: 'Checkout GitHub Action'
uses: actions/checkout@main
- name: 'Login via Azure CLI'
uses: azure/login@v2
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: 'Build GitHub Runner container image'
uses: azure/docker-login@v2
uses: azure/docker-login@v1
with:
login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
username: ${{ secrets.REGISTRY_USERNAME }}
@@ -29,7 +29,7 @@ jobs:
docker build --build-arg RUNNER_VERSION=${{ env.RUNNER_VERSION }} -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/pwd9000-github-runner-lin:${{ env.RUNNER_VERSION }} .
- name: 'Push container image to ACR'
uses: azure/docker-login@v2
uses: azure/docker-login@v1
with:
login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
username: ${{ secrets.REGISTRY_USERNAME }}
@@ -1,93 +0,0 @@
name: Cache Integration Tests
on:
pull_request:
branches:
- main
- dev
- dev-staging
- release/*
paths:
- 'packages/api/src/cache/**'
- 'packages/api/src/cluster/**'
- 'packages/api/src/mcp/**'
- 'packages/api/src/stream/**'
- 'redis-config/**'
- '.github/workflows/cache-integration-tests.yml'
jobs:
cache_integration_tests:
name: Integration Tests that use actual Redis Cache
timeout-minutes: 30
runs-on: hanzo-build-linux-amd64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install Redis tools
run: |
sudo apt-get update
sudo apt-get install -y redis-server redis-tools
- name: Start Single Redis Instance
run: |
redis-server --daemonize yes --port 6379
sleep 2
# Verify single Redis is running
redis-cli -p 6379 ping || exit 1
- name: Start Redis Cluster
working-directory: redis-config
run: |
chmod +x start-cluster.sh stop-cluster.sh
./start-cluster.sh
sleep 10
# Verify cluster is running
redis-cli -p 7001 cluster info || exit 1
redis-cli -p 7002 cluster info || exit 1
redis-cli -p 7003 cluster info || exit 1
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: |
pnpm run build:data-provider
pnpm run build:data-schemas
pnpm run build:api
- name: Run all cache integration tests (Single Redis Node)
working-directory: packages/api
env:
NODE_ENV: test
USE_REDIS: true
USE_REDIS_CLUSTER: false
REDIS_URI: redis://127.0.0.1:6379
run: pnpm run test:cache-integration
- name: Run all cache integration tests (Redis Cluster)
working-directory: packages/api
env:
NODE_ENV: test
USE_REDIS: true
USE_REDIS_CLUSTER: true
REDIS_URI: redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003
run: pnpm run test:cache-integration
- name: Stop Redis Cluster
if: always()
working-directory: redis-config
run: ./stop-cluster.sh || true
- name: Stop Single Redis Instance
if: always()
run: redis-cli -p 6379 shutdown || true
+244
View File
@@ -0,0 +1,244 @@
name: CI/CD Pipeline
on:
push:
branches:
- main
- develop
- 'release/**'
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
inputs:
deploy_environment:
description: 'Environment to deploy to'
required: false
default: 'staging'
type: choice
options:
- staging
- production
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
NODE_VERSION: '20'
jobs:
test:
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
test-type: [api, client, packages]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build packages
if: matrix.test-type != 'packages'
run: |
npm run build:data-provider
npm run build:data-schemas
npm run build:api
- name: Run tests
run: |
if [ "${{ matrix.test-type }}" = "api" ]; then
npm run test:api
elif [ "${{ matrix.test-type }}" = "client" ]; then
npm run test:client
else
cd packages/data-provider && npm test
cd ../data-schemas && npm test
cd ../api && npm test
fi
- name: Upload coverage
if: always()
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: ${{ matrix.test-type }}
lint:
name: Lint & Format Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Check formatting
run: npm run format:check
build-docker:
name: Build Docker Image
needs: [test, lint]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
outputs:
image-digest: ${{ steps.build.outputs.digest }}
image-tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=vendor-mode,enable={{is_default_branch}}
type=sha,prefix={{branch}}-
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILD_DATE=${{ github.event.head_commit.timestamp }}
VCS_REF=${{ github.sha }}
VERSION=${{ github.ref_name }}
- name: Generate SBOM
if: github.event_name != 'pull_request'
uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
format: spdx-json
output-file: sbom.spdx.json
- name: Upload SBOM
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json
security-scan:
name: Security Scan
needs: build-docker
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
security-events: write
steps:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-docker.outputs.image-digest }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy scan results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
deploy-staging:
name: Deploy to Staging
needs: [build-docker, security-scan]
if: github.ref == 'refs/heads/develop' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.hanzo.ai
steps:
- name: Deploy to staging
run: |
echo "Deploying ${{ needs.build-docker.outputs.image-digest }} to staging"
# Add your deployment logic here
# Example: kubectl set image deployment/chat chat=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-docker.outputs.image-digest }}
deploy-production:
name: Deploy to Production
needs: [build-docker, security-scan]
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
environment:
name: production
url: https://chat.hanzo.ai
steps:
- name: Deploy to production
run: |
echo "Deploying ${{ needs.build-docker.outputs.image-digest }} to production"
# Add your deployment logic here
# Example: kubectl set image deployment/chat chat=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-docker.outputs.image-digest }}
- name: Create deployment record
uses: actions/github-script@v7
with:
script: |
await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha,
environment: 'production',
required_contexts: [],
auto_merge: false,
production_environment: true,
});
+220
View File
@@ -0,0 +1,220 @@
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
env:
NODE_ENV: test
jobs:
test:
name: Test Suite
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
mongodb-version: ['7.0']
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.10.0
with:
mongodb-version: ${{ matrix.mongodb-version }}
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
*/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Cache build artifacts
uses: actions/cache@v4
with:
path: |
packages/*/dist
api/lib
client/dist
key: ${{ runner.os }}-build-${{ hashFiles('packages/**/*.ts', 'packages/**/*.js') }}
restore-keys: |
${{ runner.os }}-build-
- name: Install dependencies
run: npm ci
- name: Build packages
run: |
npm run build:data-provider
npm run build:data-schemas
npm run build:api
- name: Create test environment
run: |
cat > .env.test << EOF
NODE_ENV=test
MONGO_URI=mongodb://localhost:27017/HanzoChat_test
MEILI_HOST=
RAG_API_URL=
ALLOW_REGISTRATION=true
ALLOW_UNVERIFIED_EMAIL_LOGIN=true
RATE_LIMIT_ENABLED=false
CREDS_KEY=test-creds-key-32-chars-long-string
CREDS_IV=test-creds-iv-16
JWT_SECRET=test-jwt-secret-very-secure
JWT_REFRESH_SECRET=test-jwt-refresh-secret
GOOGLE_ANALYTICS_ID=
SENTRY_DSN=
DOMAIN_CLIENT=http://localhost:3080
DOMAIN_SERVER=http://localhost:3080
TRUST_PROXY=0
DEBUG_LOGGING=false
CONSOLE_JSON=false
EOF
- name: Run Client Tests
run: NODE_ENV=test npm run test:client -- --coverage --coverageReporters=json
continue-on-error: false
- name: Run API Tests
run: NODE_ENV=test npm run test:api -- --coverage --coverageReporters=json --maxWorkers=2
continue-on-error: false
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
client/coverage/
api/coverage/
- name: Test Report
uses: dorny/test-reporter@v1
if: always()
with:
name: Jest Tests
path: '**/junit.xml'
reporter: jest-junit
- name: Coverage Report
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./client/coverage/coverage-final.json,./api/coverage/coverage-final.json
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
lint:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
*/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run Prettier Check
run: npm run format:check || true
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run npm audit
run: npm audit --production || true
- name: Run Snyk Security Scan
uses: snyk/actions/node@master
continue-on-error: true
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
build:
name: Build Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
*/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
- name: Build all packages
run: |
npm run build:data-provider
npm run build:data-schemas
npm run build:api
- name: Build client
run: npm run build:client
env:
DOMAIN_CLIENT: https://chat.hanzo.ai
DOMAIN_SERVER: https://chat.hanzo.ai
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: |
client/dist/
api/lib/
packages/*/dist/
-67
View File
@@ -1,67 +0,0 @@
name: Publish `@librechat/client` to NPM
on:
push:
branches:
- main
paths:
- 'packages/client/package.json'
workflow_dispatch:
inputs:
reason:
description: 'Reason for manual trigger'
required: false
default: 'Manual publish requested'
permissions:
id-token: write # Required for OIDC trusted publishing
contents: read
jobs:
build-and-publish:
runs-on: hanzo-build-linux-amd64
environment: publish # Must match npm trusted publisher config
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: '20.x'
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- name: Update npm for OIDC support
run: npm install -g npm@latest # Must be 11.5.1+ for provenance
- name: Install client dependencies
run: cd packages/client && pnpm install --frozen-lockfile
- name: Build client
run: cd packages/client && pnpm run build
- name: Check version change
id: check
working-directory: packages/client
run: |
PACKAGE_VERSION=$(node -p "require('./package.json').version")
PUBLISHED_VERSION=$(npm view @librechat/client version 2>/dev/null || echo "0.0.0")
if [ "$PACKAGE_VERSION" = "$PUBLISHED_VERSION" ]; then
echo "No version change, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "Version changed, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Pack package
if: steps.check.outputs.skip != 'true'
working-directory: packages/client
run: npm pack
- name: Publish
if: steps.check.outputs.skip != 'true'
working-directory: packages/client
run: npm publish *.tgz --access public --provenance
+16 -33
View File
@@ -1,4 +1,4 @@
name: Publish `librechat-data-provider` to NPM
name: Node.js Package
on:
push:
@@ -6,46 +6,29 @@ on:
- main
paths:
- 'packages/data-provider/package.json'
workflow_dispatch:
inputs:
reason:
description: 'Reason for manual trigger'
required: false
default: 'Manual publish requested'
permissions:
id-token: write # Required for OIDC trusted publishing
contents: read
jobs:
build:
runs-on: hanzo-build-linux-amd64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: cd packages/data-provider && pnpm install --frozen-lockfile
- run: cd packages/data-provider && pnpm run build
node-version: 16
- run: cd packages/data-provider && npm ci
- run: cd packages/data-provider && npm run build
publish-npm:
needs: build
runs-on: hanzo-build-linux-amd64
environment: publish # Must match npm trusted publisher config
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
node-version: 16
registry-url: 'https://registry.npmjs.org'
- name: Update npm for OIDC support
run: npm install -g npm@latest # Must be 11.5.1+ for provenance
- run: cd packages/data-provider && pnpm install --frozen-lockfile
- run: cd packages/data-provider && pnpm run build
- run: cd packages/data-provider && npm publish --provenance
- run: cd packages/data-provider && npm ci
- run: cd packages/data-provider && npm run build
- run: cd packages/data-provider && npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
+15 -24
View File
@@ -1,4 +1,4 @@
name: Publish `@librechat/data-schemas` to NPM
name: Publish `@hanzochat/data-schemas` to NPM
on:
push:
@@ -13,41 +13,32 @@ on:
required: false
default: 'Manual publish requested'
permissions:
id-token: write # Required for OIDC trusted publishing
contents: read
jobs:
build-and-publish:
runs-on: hanzo-build-linux-amd64
environment: publish # Must match npm trusted publisher config
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- name: Update npm for OIDC support
run: npm install -g npm@latest # Must be 11.5.1+ for provenance
node-version: '18.x'
- name: Install dependencies
run: cd packages/data-schemas && pnpm install --frozen-lockfile
run: cd packages/data-schemas && npm ci
- name: Build
run: cd packages/data-schemas && pnpm run build
run: cd packages/data-schemas && npm run build
- name: Set up npm authentication
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.PUBLISH_NPM_TOKEN }}" > ~/.npmrc
- name: Check version change
id: check
working-directory: packages/data-schemas
run: |
PACKAGE_VERSION=$(node -p "require('./package.json').version")
PUBLISHED_VERSION=$(npm view @librechat/data-schemas version 2>/dev/null || echo "0.0.0")
PUBLISHED_VERSION=$(npm view @hanzochat/data-schemas version 2>/dev/null || echo "0.0.0")
if [ "$PACKAGE_VERSION" = "$PUBLISHED_VERSION" ]; then
echo "No version change, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
@@ -64,4 +55,4 @@ jobs:
- name: Publish
if: steps.check.outputs.skip != 'true'
working-directory: packages/data-schemas
run: npm publish *.tgz --access public --provenance
run: npm publish *.tgz --access public
+5 -8
View File
@@ -7,14 +7,11 @@ on:
- completed
workflow_dispatch:
permissions:
contents: read
jobs:
deploy:
runs-on: hanzo-deploy-linux-amd64
runs-on: ubuntu-latest
if: |
github.repository == 'danny-avila/LibreChat' &&
github.repository == 'hanzoai/chat' &&
(github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'dev'))
steps:
@@ -32,12 +29,12 @@ jobs:
DO_HOST: ${{ secrets.DO_HOST }}
DO_USER: ${{ secrets.DO_USER }}
run: |
ssh ${DO_USER}@${DO_HOST} << EOF
ssh -o StrictHostKeyChecking=no ${DO_USER}@${DO_HOST} << EOF
sudo -i -u danny bash << 'EEOF'
cd ~/LibreChat && \
cd ~/chat && \
git fetch origin main && \
sudo npm run stop:deployed && \
sudo docker images --format "{{.Repository}}:{{.ID}}" | grep -E "lc-dev|librechat" | cut -d: -f2 | xargs -r sudo docker rmi -f || true && \
sudo docker images --format "{{.Repository}}:{{.ID}}" | grep -E "lc-dev|hanzochat" | cut -d: -f2 | xargs -r sudo docker rmi -f || true && \
sudo npm run update:deployed && \
git checkout dev && \
git pull origin dev && \
+3 -3
View File
@@ -13,14 +13,14 @@ env:
jobs:
deploy-gh-runner-aci:
runs-on: hanzo-deploy-linux-amd64
runs-on: ubuntu-latest
steps:
# checkout the repo
- name: 'Checkout GitHub Action'
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: 'Login via Azure CLI'
uses: azure/login@v2
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
+3 -26
View File
@@ -9,23 +9,10 @@ on:
- 'api/**'
- 'client/**'
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- 'Dockerfile'
- 'Dockerfile.multi'
permissions:
contents: read
packages: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: hanzo-build-linux-amd64
timeout-minutes: 130
runs-on: ubuntu-latest
strategy:
matrix:
include:
@@ -51,7 +38,7 @@ jobs:
# Log in to GitHub Container Registry
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -69,12 +56,6 @@ jobs:
run: |
cp .env.example .env
- name: Compute build metadata
run: |
echo "BUILD_COMMIT=${{ github.sha }}" >> $GITHUB_ENV
echo "BUILD_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
# Build and push Docker images for each target
- name: Build and push Docker images
uses: docker/build-push-action@v5
@@ -88,8 +69,4 @@ jobs:
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ github.sha }}
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
platforms: linux/amd64,linux/arm64
target: ${{ matrix.target }}
build-args: |
BUILD_COMMIT=${{ env.BUILD_COMMIT }}
BUILD_BRANCH=${{ env.BUILD_BRANCH }}
BUILD_DATE=${{ env.BUILD_DATE }}
target: ${{ matrix.target }}
+4 -23
View File
@@ -9,28 +9,19 @@ on:
- 'api/**'
- 'client/**'
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- 'Dockerfile'
- 'Dockerfile.multi'
permissions:
contents: read
packages: write
jobs:
build:
runs-on: hanzo-build-linux-amd64
timeout-minutes: 130
runs-on: ubuntu-latest
strategy:
matrix:
include:
- target: api-build
file: Dockerfile.multi
image_name: librechat-dev-api
image_name: hanzochat-dev-api
- target: node
file: Dockerfile
image_name: librechat-dev
image_name: hanzochat-dev
steps:
# Check out the repository
@@ -47,7 +38,7 @@ jobs:
# Log in to GitHub Container Registry
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -65,12 +56,6 @@ jobs:
run: |
cp .env.example .env
- name: Compute build metadata
run: |
echo "BUILD_COMMIT=${{ github.sha }}" >> $GITHUB_ENV
echo "BUILD_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
# Build and push Docker images for each target
- name: Build and push Docker images
uses: docker/build-push-action@v5
@@ -85,7 +70,3 @@ jobs:
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
platforms: linux/amd64,linux/arm64
target: ${{ matrix.target }}
build-args: |
BUILD_COMMIT=${{ env.BUILD_COMMIT }}
BUILD_BRANCH=${{ env.BUILD_BRANCH }}
BUILD_DATE=${{ env.BUILD_DATE }}
-79
View File
@@ -1,79 +0,0 @@
name: Docker Dev Staging Images Build
on:
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build:
runs-on: hanzo-build-linux-amd64
strategy:
matrix:
include:
- target: api-build
file: Dockerfile.multi
image_name: lc-dev-staging-api
- target: node
file: Dockerfile
image_name: lc-dev-staging
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@v3
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
- name: Compute build metadata
run: |
echo "BUILD_COMMIT=${{ github.sha }}" >> $GITHUB_ENV
echo "BUILD_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_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.sha }}
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ github.sha }}
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
platforms: linux/amd64,linux/arm64
target: ${{ matrix.target }}
build-args: |
BUILD_COMMIT=${{ env.BUILD_COMMIT }}
BUILD_BRANCH=${{ env.BUILD_BRANCH }}
BUILD_DATE=${{ env.BUILD_DATE }}
+109
View File
@@ -0,0 +1,109 @@
name: Build and Push Docker Images
on:
push:
branches:
- main
tags:
- 'v*'
pull_request:
branches:
- main
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
COMMIT_SHA=${{ github.sha }}
BUILD_DATE=${{ steps.meta.outputs.created }}
build-and-push-rag:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for RAG API
id: meta-rag
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/hanzoai/chat-rag-api
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push RAG API Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./rag.Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta-rag.outputs.tags }}
labels: ${{ steps.meta-rag.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+107
View File
@@ -0,0 +1,107 @@
name: Build and Push Docker Image
on:
push:
branches:
- main
- develop
tags:
- 'v*'
pull_request:
branches:
- main
- develop
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# For main branch, tag as 'latest'
type=raw,value=latest,enable={{is_default_branch}}
# For develop branch, tag as 'develop'
type=raw,value=develop,enable=${{ github.ref == 'refs/heads/develop' }}
# For tags, use the tag name
type=ref,event=tag
# For PRs, use pr-NUMBER
type=ref,event=pr
# SHA short
type=sha,prefix={{branch}}-
# Vendor mode tag for main
type=raw,value=vendor-mode,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
- name: Create Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v1
with:
body: |
## Docker Image
🐳 Docker image is available at:
```
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
```
### Vendor Mode Usage
To run with Hanzo AI branding (default):
```bash
docker run -d \
-p 3080:3080 \
--env-file .env \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
```
To run with custom branding:
```bash
docker run -d \
-p 3080:3080 \
-e VENDOR_MODE=true \
-e VENDOR_NAME="Your Company" \
-e VENDOR_MODEL_DEFAULT="Your Model" \
-e VENDOR_MODEL_PRO="Your Model Pro" \
-e APP_TITLE="Your Platform" \
-e VENDOR_BACKEND_ENDPOINT=anthropic \
-e VENDOR_BACKEND_API_KEY=$YOUR_API_KEY \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
```
generate_release_notes: true
+56
View File
@@ -0,0 +1,56 @@
name: Docker Build Cache
on:
schedule:
# Run every Monday at 3 AM UTC to refresh caches
- cron: '0 3 * * 1'
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-cache:
name: Build and Cache Base Images
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
include:
- target: builder
cache-tag: cache-builder
- target: stage-1
cache-tag: cache-deps
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push cache image
uses: docker/build-push-action@v5
with:
context: .
target: ${{ matrix.target }}
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.cache-tag }}
cache-from: |
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.cache-tag }}
type=gha
cache-to: |
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.cache-tag }},mode=max
type=gha,mode=max
platforms: linux/amd64,linux/arm64
-71
View File
@@ -1,71 +0,0 @@
# Hanzo Chat — image build (amd64, self-hosted hanzoai pool).
#
# chat is a PUBLIC repo, so it cannot reuse the private hanzoai/.github
# reusable workflow (GitHub blocks public->private workflow reuse).
# This is therefore self-contained.
#
# Builds ghcr.io/hanzoai/chat on:
# - push to main -> immutable sha-<sha7> tag
# - v* tags -> semver image tag (e.g. v0.7.10 -> 0.7.10)
# No :latest, no floating tags (semver-only policy). amd64 only
# (DOKS is all amd64 — no arm64 runners).
name: Docker Release
on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:
concurrency:
group: docker-release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
packages: write
jobs:
build-amd64:
# hanzoai org pool — ARC routes by runnerScaleSetName, not labels.
runs-on: hanzo-build-linux-amd64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
driver-opts: network=host
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/hanzoai/chat
tags: |
type=semver,pattern={{version}}
type=sha,prefix=sha-,format=short
flavor: |
latest=false
- name: Build and push amd64 image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=ghcr.io/hanzoai/chat-cache:amd64
cache-to: type=registry,ref=ghcr.io/hanzoai/chat-cache:amd64,mode=max
provenance: false
-36
View File
@@ -1,36 +0,0 @@
name: Docker Build Smoke Tests
on:
workflow_dispatch:
pull_request:
paths:
- '.github/workflows/docker-smoke.yml'
- '.dockerignore'
- 'Dockerfile.multi'
- 'package.json'
- 'package-lock.json'
- 'packages/client/**'
- 'packages/data-provider/**'
permissions:
contents: read
jobs:
client-package-target:
name: Build Docker client package target
runs-on: hanzo-build-linux-amd64
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build client package target
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile.multi
platforms: linux/amd64
push: false
target: client-package-build
-71
View File
@@ -1,71 +0,0 @@
name: E2E Tests
on:
pull_request:
branches: [main]
paths:
- 'client/**'
- 'api/**'
- 'packages/**'
- 'e2e/**'
concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true
jobs:
e2e:
name: Playwright E2E
if: github.event.pull_request.head.repo.full_name == 'hanzoai/chat'
timeout-minutes: 30
runs-on: hanzo-build-linux-amd64
env:
CI: true
NODE_ENV: CI
SEARCH: false
MONGO_URI: mongodb://localhost:27017/librechat-test
JWT_SECRET: e2e-test-jwt-secret
JWT_REFRESH_SECRET: e2e-test-jwt-refresh-secret
CREDS_KEY: e2e-test-creds-key-32chars00000000
CREDS_IV: e2e-test-creds-iv-16c
DOMAIN_CLIENT: http://localhost:3080
DOMAIN_SERVER: http://localhost:3080
ALLOW_REGISTRATION: true
TITLE_CONVO: false
services:
mongodb:
image: mongo:7
ports:
- 27017:27017
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build client
run: pnpm run frontend
- name: Install Playwright
run: |
npx playwright install --with-deps chromium
- name: Run E2E tests
run: pnpm run e2e:ci
- name: Upload report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: e2e/playwright-report/
retention-days: 14
+18 -7
View File
@@ -5,7 +5,6 @@ on:
branches:
- main
- dev
- dev-staging
- release/*
paths:
- 'api/**'
@@ -14,7 +13,7 @@ on:
jobs:
eslint_checks:
name: Run ESLint Linting
runs-on: hanzo-build-linux-amd64
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
@@ -25,19 +24,19 @@ jobs:
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- name: Set up Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
cache: npm
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: npm ci
# Run ESLint on changed files within the api/ and client/ directories.
- name: Run ESLint on changed files
env:
SARIF_ESLINT_IGNORE_SUPPRESSED: "true"
run: |
# Extract the base commit SHA from the pull_request event payload.
BASE_SHA=$(jq --raw-output .pull_request.base.sha "$GITHUB_EVENT_PATH")
@@ -53,10 +52,22 @@ jobs:
# Ensure there are files to lint before running ESLint
if [[ -z "$CHANGED_FILES" ]]; then
echo "No matching files changed. Skipping ESLint."
echo "UPLOAD_SARIF=false" >> $GITHUB_ENV
exit 0
fi
# Set variable to allow SARIF upload
echo "UPLOAD_SARIF=true" >> $GITHUB_ENV
# Run ESLint
npx eslint --no-error-on-unmatched-pattern \
--config eslint.config.mjs \
$CHANGED_FILES
--format @microsoft/eslint-formatter-sarif \
--output-file eslint-results.sarif $CHANGED_FILES || true
- name: Upload analysis results to GitHub
if: env.UPLOAD_SARIF == 'true'
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: eslint-results.sarif
wait-for-processing: true
+14 -21
View File
@@ -5,59 +5,52 @@ on:
branches:
- main
- dev
- dev-staging
- release/*
paths:
- 'client/**'
- 'packages/data-provider/**'
- 'packages/**'
jobs:
tests_frontend_ubuntu:
name: Run frontend unit tests on Ubuntu
timeout-minutes: 60
runs-on: hanzo-build-linux-amd64
env:
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache: 'npm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: npm ci
- name: Build Client
run: pnpm run frontend:ci
run: npm run frontend:ci
- name: Run unit tests
run: pnpm run test:ci --verbose
run: npm run test:ci --verbose
working-directory: client
tests_frontend_windows:
name: Run frontend unit tests on Windows
timeout-minutes: 60
runs-on: windows-latest
env:
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache: 'npm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: npm ci
- name: Build Client
run: pnpm run frontend:ci
run: npm run frontend:ci
- name: Run unit tests
run: pnpm run test:ci --verbose
run: npm run test:ci --verbose
working-directory: client
@@ -0,0 +1,95 @@
name: Generate Release Changelog PR
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
jobs:
generate-release-changelog-pr:
permissions:
contents: write # Needed for pushing commits and creating branches.
pull-requests: write
runs-on: ubuntu-latest
steps:
# 1. Checkout the repository (with full history).
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# 2. Generate the release changelog using our custom configuration.
- name: Generate Release Changelog
id: generate_release
uses: mikepenz/release-changelog-builder-action@v5.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
configuration: ".github/configuration-release.json"
owner: ${{ github.repository_owner }}
repo: ${{ github.event.repository.name }}
outputFile: CHANGELOG-release.md
# 3. Update the main CHANGELOG.md:
# - If it doesn't exist, create it with a basic header.
# - Remove the "Unreleased" section (if present).
# - Prepend the new release changelog above previous releases.
# - Remove all temporary files before committing.
- name: Update CHANGELOG.md
run: |
# Determine the release tag, e.g. "v1.2.3"
TAG=${GITHUB_REF##*/}
echo "Using release tag: $TAG"
# Ensure CHANGELOG.md exists; if not, create a basic header.
if [ ! -f CHANGELOG.md ]; then
echo "# Changelog" > CHANGELOG.md
echo "" >> CHANGELOG.md
echo "All notable changes to this project will be documented in this file." >> CHANGELOG.md
echo "" >> CHANGELOG.md
fi
echo "Updating CHANGELOG.md…"
# Remove the "Unreleased" section (from "## [Unreleased]" until the first occurrence of '---') if it exists.
if grep -q "^## \[Unreleased\]" CHANGELOG.md; then
awk '/^## \[Unreleased\]/{flag=1} flag && /^---/{flag=0; next} !flag' CHANGELOG.md > CHANGELOG.cleaned
else
cp CHANGELOG.md CHANGELOG.cleaned
fi
# Split the cleaned file into:
# - header.md: content before the first release header ("## [v...").
# - tail.md: content from the first release header onward.
awk '/^## \[v/{exit} {print}' CHANGELOG.cleaned > header.md
awk 'f{print} /^## \[v/{f=1; print}' CHANGELOG.cleaned > tail.md
# Combine header, the new release changelog, and the tail.
echo "Combining updated changelog parts..."
cat header.md CHANGELOG-release.md > CHANGELOG.md.new
echo "" >> CHANGELOG.md.new
cat tail.md >> CHANGELOG.md.new
mv CHANGELOG.md.new CHANGELOG.md
# Remove temporary files.
rm -f CHANGELOG.cleaned header.md tail.md CHANGELOG-release.md
echo "Final CHANGELOG.md content:"
cat CHANGELOG.md
# 4. Create (or update) the Pull Request with the updated CHANGELOG.md.
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
sign-commits: true
commit-message: "chore: update CHANGELOG for release ${{ github.ref_name }}"
base: main
branch: "changelog/${{ github.ref_name }}"
reviewers: hanzoai
title: "📜 docs: Changelog for release ${{ github.ref_name }}"
body: |
**Description**:
- This PR updates the CHANGELOG.md by removing the "Unreleased" section and adding new release notes for release ${{ github.ref_name }} above previous releases.
@@ -0,0 +1,107 @@
name: Generate Unreleased Changelog PR
on:
schedule:
- cron: "0 0 * * 1" # Runs every Monday at 00:00 UTC
workflow_dispatch:
jobs:
generate-unreleased-changelog-pr:
permissions:
contents: write # Needed for pushing commits and creating branches.
pull-requests: write
runs-on: ubuntu-latest
steps:
# 1. Checkout the repository on main.
- name: Checkout Repository on Main
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
# 4. Get the latest version tag.
- name: Get Latest Tag
id: get_latest_tag
run: |
LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1) || echo "none")
echo "Latest tag: $LATEST_TAG"
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
# 5. Generate the Unreleased changelog.
- name: Generate Unreleased Changelog
id: generate_unreleased
uses: mikepenz/release-changelog-builder-action@v5.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
configuration: ".github/configuration-unreleased.json"
owner: ${{ github.repository_owner }}
repo: ${{ github.event.repository.name }}
outputFile: CHANGELOG-unreleased.md
fromTag: ${{ steps.get_latest_tag.outputs.tag }}
toTag: main
# 7. Update CHANGELOG.md with the new Unreleased section.
- name: Update CHANGELOG.md
id: update_changelog
run: |
# Create CHANGELOG.md if it doesn't exist.
if [ ! -f CHANGELOG.md ]; then
echo "# Changelog" > CHANGELOG.md
echo "" >> CHANGELOG.md
echo "All notable changes to this project will be documented in this file." >> CHANGELOG.md
echo "" >> CHANGELOG.md
fi
echo "Updating CHANGELOG.md…"
# Extract content before the "## [Unreleased]" (or first version header if missing).
if grep -q "^## \[Unreleased\]" CHANGELOG.md; then
awk '/^## \[Unreleased\]/{exit} {print}' CHANGELOG.md > CHANGELOG_TMP.md
else
awk '/^## \[v/{exit} {print}' CHANGELOG.md > CHANGELOG_TMP.md
fi
# Append the generated Unreleased changelog.
echo "" >> CHANGELOG_TMP.md
cat CHANGELOG-unreleased.md >> CHANGELOG_TMP.md
echo "" >> CHANGELOG_TMP.md
# Append the remainder of the original changelog (starting from the first version header).
awk 'f{print} /^## \[v/{f=1; print}' CHANGELOG.md >> CHANGELOG_TMP.md
# Replace the old file with the updated file.
mv CHANGELOG_TMP.md CHANGELOG.md
# Remove the temporary generated file.
rm -f CHANGELOG-unreleased.md
echo "Final CHANGELOG.md:"
cat CHANGELOG.md
# 8. Check if CHANGELOG.md has any updates.
- name: Check for CHANGELOG.md changes
id: changelog_changes
run: |
if git diff --quiet CHANGELOG.md; then
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "has_changes=true" >> $GITHUB_OUTPUT
fi
# 9. Create (or update) the Pull Request only if there are changes.
- name: Create Pull Request
if: steps.changelog_changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
base: main
branch: "changelog/unreleased-update"
sign-commits: true
commit-message: "action: update Unreleased changelog"
title: "📜 docs: Unreleased Changelog"
body: |
**Description**:
- This PR updates the Unreleased section in CHANGELOG.md.
- It compares the current main branch with the latest version tag (determined as ${{ steps.get_latest_tag.outputs.tag }}),
regenerates the Unreleased changelog, removes any old Unreleased block, and inserts the new content.
+3 -3
View File
@@ -9,10 +9,10 @@ on:
jobs:
generate:
runs-on: hanzo-build-linux-amd64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: supabase/embeddings-generator@v0.0.6
- uses: actions/checkout@v3
- uses: supabase/embeddings-generator@v0.0.5
with:
supabase-url: ${{ secrets.SUPABASE_URL }}
supabase-service-role-key: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
-91
View File
@@ -1,91 +0,0 @@
# Removes a PR's GitNexus index from the droplet when the PR is closed
# (merged or not). The deploy workflow also prunes stale folders as a
# safety net, but this gives us immediate cleanup without waiting for
# the next deploy trigger.
name: GitNexus Cleanup PR
on:
pull_request:
types: [closed]
permissions:
contents: read
actions: read
concurrency:
group: gitnexus-cleanup-pr-${{ github.event.pull_request.number }}
cancel-in-progress: false
jobs:
cleanup:
# Skip fork PRs entirely. GitHub withholds repository secrets from
# pull_request events originating on forks, so an SSH deploy job run
# from a fork close would fail noisily. The deploy workflow's stale-
# folder pruning step catches any fork-contributor indexes that
# actually made it onto the droplet.
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: hanzo-build-linux-amd64
timeout-minutes: 5
steps:
# Skip the SSH round-trip entirely when no index artifact was ever
# built for this PR (docs-only PRs, paths-ignored PRs, PRs closed
# before indexing finished, etc). Eliminates ~95% of no-op SSH
# sessions on a busy repo.
- name: Check for index artifact
id: check
uses: actions/github-script@v7
with:
script: |
const { data } = await github.rest.actions.listArtifactsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
name: `gitnexus-index-pr-${context.payload.pull_request.number}`,
per_page: 1,
});
const hasArtifact = data.total_count > 0;
core.info(`Artifact exists: ${hasArtifact}`);
core.setOutput('has_artifact', hasArtifact ? 'true' : 'false');
- name: Setup SSH
if: steps.check.outputs.has_artifact == 'true'
env:
SSH_KEY: ${{ secrets.GITNEXUS_DO_SSH_KEY }}
KNOWN_HOST: ${{ secrets.GITNEXUS_DO_KNOWN_HOST }}
run: |
set -e
mkdir -p ~/.ssh
chmod 700 ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
if [ -z "$KNOWN_HOST" ]; then
echo "::error::GITNEXUS_DO_KNOWN_HOST secret is empty"
exit 1
fi
printf '%s\n' "$KNOWN_HOST" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
- name: Remove PR index from droplet
if: steps.check.outputs.has_artifact == 'true'
env:
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
PR_NUM: ${{ github.event.pull_request.number }}
run: |
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" PR_NUM="$PR_NUM" bash <<'REMOTE'
set -e
TARGET="/opt/gitnexus/indexes/LibreChat-pr-$PR_NUM"
if [ -d "$TARGET" ]; then
echo "Removing $TARGET"
rm -rf "$TARGET"
cd /opt/gitnexus
docker compose up -d --force-recreate gitnexus
echo "GitNexus restarted without PR #$PR_NUM"
else
echo "No index to clean up for PR #$PR_NUM (artifact existed but droplet folder did not)"
fi
REMOTE
- name: Cleanup SSH key
if: always()
run: rm -f ~/.ssh/deploy_key
-600
View File
@@ -1,600 +0,0 @@
# Deploys GitNexus indexes to a droplet via SSH + rsync.
#
# Architecture:
# GitHub Actions (deploy)
# 1. Resolves latest successful index runs for main, dev, and every
# open PR that already has an index artifact (contributor-gated
# upstream by the index workflow's author_association check)
# 2. Downloads each matching .gitnexus/ artifact
# 3. Rsyncs them into /opt/gitnexus/indexes/<name>/ on the droplet
# 4. Removes any stale folders on the droplet for PRs that closed
# (even though gitnexus-cleanup-pr.yml also handles that path,
# this is a safety net in case the close event was missed)
# 5. Pulls latest image, force-recreates gitnexus, reloads Caddy,
# and polls docker health until the container reports healthy
# The caddy container is untouched — no TLS churn.
#
# First-time droplet bootstrap (run once, manually):
# 1. Create 2GB+ Ubuntu 24.04 droplet, add SSH key
# 2. Point DNS A record for your subdomain at the droplet IP
# 3. SSH in and run:
# curl -fsSL https://get.docker.com | sh
# systemctl enable --now docker
# mkdir -p /opt/gitnexus/indexes
# useradd -m -s /bin/bash deploy
# usermod -aG docker deploy
# mkdir -p /home/deploy/.ssh
# # Add deploy pubkey to /home/deploy/.ssh/authorized_keys
# chown -R deploy:deploy /home/deploy/.ssh /opt/gitnexus
# chmod 700 /home/deploy/.ssh
# ufw allow 22,80,443/tcp
# ufw --force enable
# 4. Copy .do/gitnexus/docker-compose.yml and Caddyfile into /opt/gitnexus/
# 5. Create /opt/gitnexus/.env with: GITNEXUS_DOMAIN=... and API_TOKEN=...
# 6. cd /opt/gitnexus && docker compose up -d
#
# Then capture the droplet's SSH host key from your workstation and
# save it as the GITNEXUS_DO_KNOWN_HOST secret (below) so CI can pin it:
# ssh-keyscan -H gitnexus.yourdomain.com
#
# GHCR image: the workflow runs `docker login ghcr.io` on the droplet
# on every deploy using GITHUB_TOKEN, so the package can stay private.
# If you'd rather not have CI manage droplet auth, make the package
# public under repo Settings -> Packages.
#
# Required GitHub secrets:
# GITNEXUS_DO_HOST — droplet IP or hostname
# GITNEXUS_DO_USER — SSH user (e.g. "deploy")
# GITNEXUS_DO_SSH_KEY — private key matching the authorized pubkey
# GITNEXUS_DO_KNOWN_HOST — output of `ssh-keyscan -H <host>` pinning the
# droplet's host keys (prevents MITM/TOFU risk)
name: GitNexus Deploy
on:
workflow_run:
workflows: ['GitNexus Index']
types: [completed]
workflow_dispatch:
inputs:
pr_number:
description: 'Optional PR number to post completion comment on (set by bot-triggered dispatches from gitnexus-index.yml)'
type: string
default: ''
permissions:
actions: read
contents: read
pull-requests: write # post completion comments on served PR indexes
# Global serialization. Earlier versions used per-ref concurrency with
# cancel-in-progress so rapid pushes to the same ref coalesced but deploys
# targeting different refs ran in parallel. That had a data race: the
# prune-stale-indexes step computes its active_names up front, so if
# deploy A is rsyncing /opt/gitnexus/indexes/LibreChat-pr-12580 while
# deploy B (started slightly later with a different ref) prunes, B can
# rm -rf a folder A is still uploading into.
#
# All deploys now queue behind a single group. cancel-in-progress is
# false so a running rsync/docker-compose restart never gets killed
# mid-operation (which would leave the droplet in a partial state).
# The 20-minute job timeout bounds total queue depth.
concurrency:
group: gitnexus-deploy
cancel-in-progress: false
env:
GITNEXUS_VERSION: '1.5.3'
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/librechat-gitnexus
jobs:
# Rebuilds the long-lived image only when Dockerfile/entrypoint/extensions
# change. Skipped on every other run, so index-only deploys are fast.
build-image:
if: |
github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion == 'success'
runs-on: hanzo-build-linux-amd64
timeout-minutes: 20
permissions:
contents: read
packages: write # push image to GHCR
outputs:
image_tag: ${{ steps.tag.outputs.value }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Detect image changes
id: changes
run: |
# Default to rebuild when we can't cleanly diff (first commit,
# workflow_run from a PR branch where HEAD isn't the trigger, etc).
# Rebuild on miss > skip when we should have rebuilt.
if git rev-parse --verify HEAD~1 >/dev/null 2>&1 && \
git diff --quiet HEAD~1 HEAD -- .do/gitnexus/Dockerfile .do/gitnexus/entrypoint.sh .do/gitnexus/install-extensions.js; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Compute image tag
id: tag
run: echo "value=v${{ env.GITNEXUS_VERSION }}" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
if: steps.changes.outputs.changed == 'true' || github.event_name == 'workflow_dispatch'
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
if: steps.changes.outputs.changed == 'true' || github.event_name == 'workflow_dispatch'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
if: steps.changes.outputs.changed == 'true' || github.event_name == 'workflow_dispatch'
uses: docker/build-push-action@v5
with:
context: .do/gitnexus
file: .do/gitnexus/Dockerfile
push: true
tags: |
${{ env.IMAGE_NAME }}:latest
${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.value }}
build-args: |
GITNEXUS_VERSION=${{ env.GITNEXUS_VERSION }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build-image
runs-on: hanzo-deploy-linux-amd64
timeout-minutes: 20
permissions:
actions: read
contents: read
pull-requests: write # post deploy-complete comments on served PR indexes
steps:
- name: Checkout deploy config
uses: actions/checkout@v4
with:
sparse-checkout: .do/gitnexus
fetch-depth: 1
# Resolve every index to serve. All resolutions go through
# listArtifactsForRepo keyed by the expected artifact name, so a
# run's branch or event type doesn't matter — we always pick the
# freshest artifact that actually exists.
#
# Why this matters: a /gitnexus index command dispatches
# gitnexus-index.yml with ref=main and an input pr_number, which
# produces a run whose head_branch is "main" but whose artifact
# is gitnexus-index-pr-<N>. listWorkflowRuns(branch='main') would
# happily return that run, and we'd then try to download a
# nonexistent gitnexus-index-main artifact from it. Querying by
# artifact name directly avoids the whole mess.
- name: Resolve indexes to serve
id: resolve
uses: actions/github-script@v7
with:
script: |
const serve = []; // [{ name, artifactName, runId }]
// Helper — pick the newest non-expired artifact matching a name.
const latestArtifact = async (artifactName) => {
const { data } = await github.rest.actions.listArtifactsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
name: artifactName,
per_page: 10,
});
return data.artifacts
.filter((a) => !a.expired)
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0];
};
// --- main and dev branches ---
for (const [branch, name] of [
['main', 'LibreChat'],
['dev', 'LibreChat-dev'],
]) {
const artifactName = `gitnexus-index-${branch}`;
const fresh = await latestArtifact(artifactName);
if (!fresh) {
core.warning(`No artifact found for ${branch} (expected ${artifactName})`);
continue;
}
serve.push({
name,
artifactName,
runId: fresh.workflow_run.id,
});
core.info(`${branch}: run ${fresh.workflow_run.id} -> ${name}`);
}
// --- open PRs with at least one successful index run ---
// github.paginate handles the 100-per-page ceiling automatically
// so the resolution works on repos with 200+ concurrent open PRs.
const openPrs = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100,
});
core.info(`Found ${openPrs.length} open PRs`);
// Parallelize artifact lookups in fixed-size batches so the
// resolve step runs in seconds instead of minutes on big repos,
// without burning the GitHub API rate limit all at once.
const BATCH_SIZE = 10;
const prMatches = [];
for (let i = 0; i < openPrs.length; i += BATCH_SIZE) {
const batch = openPrs.slice(i, i + BATCH_SIZE);
const results = await Promise.all(
batch.map(async (pr) => {
const artifactName = `gitnexus-index-pr-${pr.number}`;
const fresh = await latestArtifact(artifactName);
return fresh ? { pr, artifactName, fresh } : null;
}),
);
for (const hit of results) {
if (hit) prMatches.push(hit);
}
}
// Cap to the N most recent PR indexes by artifact creation time.
// On a 10GB droplet each index is ~130MB; 3 PRs + main + dev ≈
// 650MB of index data, leaving headroom for the ~700MB Docker image
// and OS. Older PR indexes are evicted by the prune step.
const MAX_PR_INDEXES = 3;
prMatches.sort(
(a, b) => new Date(b.fresh.created_at) - new Date(a.fresh.created_at),
);
const keptPrs = prMatches.slice(0, MAX_PR_INDEXES);
const evictedPrs = prMatches.slice(MAX_PR_INDEXES);
for (const { pr, artifactName, fresh } of keptPrs) {
serve.push({
name: `LibreChat-pr-${pr.number}`,
artifactName,
runId: fresh.workflow_run.id,
});
core.info(`PR #${pr.number}: run ${fresh.workflow_run.id} -> LibreChat-pr-${pr.number}`);
}
if (evictedPrs.length) {
core.info(
`Evicted ${evictedPrs.length} older PR indexes (cap=${MAX_PR_INDEXES}): ` +
evictedPrs.map((e) => `#${e.pr.number}`).join(', '),
);
}
core.info(`Serving ${keptPrs.length} PR indexes out of ${prMatches.length} with artifacts (${openPrs.length} open PRs total)`);
if (!serve.length) {
core.setFailed('No indexes to serve');
return;
}
core.setOutput('matrix', JSON.stringify(serve));
core.setOutput('active_names', serve.map((s) => s.name).join(','));
- name: Download each index artifact
env:
MATRIX: ${{ steps.resolve.outputs.matrix }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -e
mkdir -p staging
# main/dev artifact download failures are fatal — a missing
# main/dev index is a real deploy failure. PR artifact failures
# are soft — a PR artifact deleted mid-deploy shouldn't abort
# the whole deploy and take main/dev down with it.
echo "$MATRIX" | jq -c '.[]' | while read -r entry; do
name=$(echo "$entry" | jq -r '.name')
artifact=$(echo "$entry" | jq -r '.artifactName')
runId=$(echo "$entry" | jq -r '.runId')
target="staging/${name}/.gitnexus"
echo "Downloading $artifact from run $runId -> $target"
mkdir -p "$target"
if ! gh run download "$runId" \
--repo "${{ github.repository }}" \
--name "$artifact" \
--dir "$target"; then
case "$name" in
LibreChat|LibreChat-dev)
echo "::error::Failed to download critical artifact $artifact"
exit 1
;;
*)
# The name stays in active_names so the prune step
# won't remove the droplet's existing copy. The old
# index keeps being served instead of being wiped to
# nothing — stale beats empty — but observability
# requires an explicit notice since this path is
# invisible in the happy-path deploy log.
echo "::warning::Failed to download PR artifact $artifact — skipping fresh sync; previous index (if any) will continue being served from the droplet"
rm -rf "staging/${name}"
;;
esac
fi
done
echo ""
echo "Staged for rsync:"
du -sh staging/*/.gitnexus/ 2>/dev/null || echo "(none)"
- name: Setup SSH
env:
SSH_KEY: ${{ secrets.GITNEXUS_DO_SSH_KEY }}
KNOWN_HOST: ${{ secrets.GITNEXUS_DO_KNOWN_HOST }}
run: |
set -e
mkdir -p ~/.ssh
chmod 700 ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
# Pin the droplet's SSH host key from a repository secret instead
# of trusting whatever ssh-keyscan returns at deploy time. The
# secret is populated from `ssh-keyscan -H <host>` at bootstrap.
if [ -z "$KNOWN_HOST" ]; then
echo "::error::GITNEXUS_DO_KNOWN_HOST secret is empty. Run ssh-keyscan -H <host> and paste the output as this secret."
exit 1
fi
printf '%s\n' "$KNOWN_HOST" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
- name: Authenticate droplet with GHCR
# GHCR packages pushed by GITHUB_TOKEN start private. The droplet
# pulls the image on every deploy, so we re-authenticate it here
# using the same short-lived token. If the package is public, this
# step is redundant but harmless.
#
# The token MUST travel through SSH stdin (not as a command arg)
# so it's never visible in the droplet's process table via
# /proc/<pid>/cmdline. `printf '%s'` is preferred over `echo`
# so the exact byte sequence sent is explicit — docker login
# tolerates a trailing newline but `printf` makes the intent
# obvious and portable across shells.
env:
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_ACTOR: ${{ github.actor }}
run: |
printf '%s' "$GH_TOKEN" | ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
"docker login ghcr.io -u '$GH_ACTOR' --password-stdin"
- name: Upload config files
env:
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
run: |
rsync -az -e "ssh -i ~/.ssh/deploy_key" \
.do/gitnexus/docker-compose.yml \
.do/gitnexus/Caddyfile \
"$SSH_USER@$SSH_HOST:/opt/gitnexus/"
- name: Prune stale indexes then sync fresh ones
env:
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
ACTIVE_NAMES: ${{ steps.resolve.outputs.active_names }}
run: |
set -e
# ── Step 1: prune FIRST ────────────────────────────────
# Remove any folders on the droplet that aren't in the active set.
# This frees disk BEFORE rsyncing new data, which matters on a
# 10GB disk where each index is ~130MB.
echo "Pruning stale indexes (keeping: $ACTIVE_NAMES)"
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
ACTIVE_NAMES="$ACTIVE_NAMES" bash <<'REMOTE'
set -e
cd /opt/gitnexus/indexes || exit 0
shopt -s nullglob
IFS=',' read -ra ACTIVE <<< "$ACTIVE_NAMES"
for dir in */; do
dir="${dir%/}"
keep=false
for a in "${ACTIVE[@]}"; do
if [ "$dir" = "$a" ]; then keep=true; break; fi
done
if [ "$keep" = false ]; then
echo "Removing stale index: $dir"
rm -rf "$dir"
fi
done
echo "Disk after prune:"
df -h / | tail -1
REMOTE
# ── Step 2: rsync-then-swap ─────────────────────────────
# Upload each index to a temp directory, then atomically swap
# it into place. If rsync fails, the old index survives intact
# and the partial temp dir is cleaned up — no production data
# is lost. The brief period where both old + new exist costs
# ~130MB of extra disk, but the prune step already freed
# space from evicted PR indexes so this fits on a 10GB disk.
for dir in staging/*/; do
[ -d "$dir" ] || continue
name=$(basename "$dir")
echo "Syncing $name (rsync-then-swap)"
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
"mkdir -p /opt/gitnexus/indexes/${name}.new"
if rsync -az -e "ssh -i ~/.ssh/deploy_key" \
"$dir" \
"$SSH_USER@$SSH_HOST:/opt/gitnexus/indexes/${name}.new/"; then
# Swap: remove old, rename new into place
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
"rm -rf /opt/gitnexus/indexes/$name && mv /opt/gitnexus/indexes/${name}.new /opt/gitnexus/indexes/$name"
echo " $name swapped successfully"
else
# Clean up the partial temp dir
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \
"rm -rf /opt/gitnexus/indexes/${name}.new"
# main/dev are critical — abort the deploy so the failure
# is visible and the container isn't restarted with stale
# or missing data. PR indexes are best-effort.
case "$name" in
LibreChat|LibreChat-dev)
echo "::error::rsync failed for critical index $name — aborting deploy"
exit 1
;;
*)
echo "::warning::rsync failed for PR index $name — keeping previous index"
;;
esac
fi
done
- name: Pull image, restart gitnexus, reload Caddy, wait for healthy
env:
SSH_USER: ${{ secrets.GITNEXUS_DO_USER }}
SSH_HOST: ${{ secrets.GITNEXUS_DO_HOST }}
run: |
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" bash <<'REMOTE'
set -e
cd /opt/gitnexus
# ── Disk cleanup ──────────────────────────────────────
# Docker accumulates old image layers, dangling images, and
# build cache across deploys. On a 60GB droplet with a 700MB+
# gitnexus image, this fills the disk after ~40 deploys.
# Prune everything not used by currently-running containers
# BEFORE pulling the new image so the extract has room.
echo "Disk before cleanup:"
df -h / | tail -1
# Omit --volumes: Caddy's caddy-data and caddy-config volumes
# hold TLS certificates and ACME state. If Caddy happens to be
# stopped when this runs (the workflow handles that case later),
# --volumes would wipe them, forcing Let's Encrypt re-issuance
# and risking rate-limit lockout (5 certs/domain/week).
docker system prune -af 2>/dev/null || true
echo "Disk after cleanup:"
df -h / | tail -1
# Fail fast if disk is critically low even after prune
AVAIL_MB=$(df --output=avail -m / | tail -1 | tr -d ' ')
if [ "$AVAIL_MB" -lt 2048 ]; then
echo "::error::Disk critically low (${AVAIL_MB}MB free). Aborting deploy."
exit 1
fi
docker compose pull gitnexus
docker compose up -d --force-recreate gitnexus
# Reload Caddy in-place so a changed Caddyfile takes effect
# without losing TLS certs or restarting connections. If caddy
# isn't running yet (first-time bootstrap), bring it up.
if docker compose ps --status running caddy 2>/dev/null | grep -q caddy; then
echo "Reloading Caddy config"
docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile || {
echo "Caddy reload failed — forcing restart"
docker compose up -d --force-recreate caddy
}
else
echo "Caddy not running — starting"
docker compose up -d caddy
fi
# Poll gitnexus health until ready or timeout. Docker's own
# unhealthy detection takes up to 150s (start_period 60s +
# retries 3 * interval 30s), so the poll ceiling must clear
# that to avoid false negatives when gitnexus legitimately
# takes ~2.5 min to warm up.
# Max wait = 36 sleeps * 5s = 180s (final iteration exits
# before its sleep on failure, so 37 iterations is the
# correct upper bound for a true 180s ceiling).
echo "Waiting for gitnexus to report healthy..."
for i in $(seq 1 37); do
STATUS=$(docker inspect --format='{{.State.Health.Status}}' gitnexus 2>/dev/null || echo unknown)
echo "[$i/37] gitnexus health: $STATUS"
if [ "$STATUS" = "healthy" ]; then
echo "gitnexus is healthy"
break
fi
if [ "$i" -eq 37 ]; then
echo "ERROR: gitnexus failed to become healthy after 180s"
docker compose ps
docker compose logs --tail 80 gitnexus
exit 1
fi
sleep 5
done
docker compose ps
echo "--- Caddy logs (last 20 lines) ---"
docker compose logs --tail 20 caddy || true
echo "--- GitNexus logs (last 30 lines) ---"
docker compose logs --tail 30 gitnexus || true
REMOTE
# When the deploy was triggered by a PR command path, post a
# terminal status comment on that one PR only. Two sub-cases:
#
# 1. workflow_run trigger: the PR's native auto-index run fired
# workflow_run, so github.event.workflow_run.id is the trigger.
# Find the matching PR via the matrix entry whose runId matches.
#
# 2. workflow_dispatch trigger with inputs.pr_number set: the
# index workflow's bot-fallback path dispatched us directly
# because workflow_run is suppressed for GITHUB_TOKEN triggers.
# Use inputs.pr_number as the comment target.
#
# Broadcast-commenting on every active PR would be noise — only the
# PR that asked for a fresh index gets a reply.
- name: Comment on PR — deploy complete
if: always()
uses: actions/github-script@v7
env:
MATRIX: ${{ steps.resolve.outputs.matrix }}
TRIGGER_RUN_ID: ${{ github.event.workflow_run.id }}
DISPATCH_PR_NUMBER: ${{ github.event.inputs.pr_number }}
DEPLOY_STATUS: ${{ job.status }}
with:
script: |
let prNum = null;
// Case 1: dispatched directly with pr_number (bot-fallback path)
if (process.env.DISPATCH_PR_NUMBER && process.env.DISPATCH_PR_NUMBER !== '') {
prNum = parseInt(process.env.DISPATCH_PR_NUMBER, 10);
}
// Case 2: workflow_run trigger from a PR index run
else if (context.eventName === 'workflow_run') {
const matrix = JSON.parse(process.env.MATRIX || '[]');
const triggerRunId = Number(process.env.TRIGGER_RUN_ID);
const match = matrix.find(
(m) => m.runId === triggerRunId && m.name.startsWith('LibreChat-pr-'),
);
if (match) {
prNum = parseInt(match.name.replace('LibreChat-pr-', ''), 10);
}
}
if (!prNum) {
core.info('No PR to comment on (trigger was not a PR-scoped index); skipping.');
return;
}
const deployUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const ok = process.env.DEPLOY_STATUS === 'success';
const body = [
`### GitNexus: ${ok ? '🚀 deployed' : '❌ deploy failed'}`,
'',
ok
? `The \`LibreChat-pr-${prNum}\` index is now live on the MCP server.`
: `The deploy failed — the previous index (if any) continues to be served.`,
`[Deploy run](${deployUrl})`,
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNum,
body,
});
- name: Cleanup SSH key
if: always()
run: rm -f ~/.ssh/deploy_key
-310
View File
@@ -1,310 +0,0 @@
name: GitNexus Index
on:
push:
branches: [main, dev]
paths-ignore: ['**.md', 'docs/**', 'LICENSE', '.github/**']
pull_request:
branches: [main, dev]
paths-ignore: ['**.md', 'docs/**', 'LICENSE', '.github/**']
workflow_dispatch:
inputs:
embeddings:
description: 'Enable embedding generation (slow, increases index size)'
type: boolean
default: false
force:
description: 'Force full re-index'
type: boolean
default: false
# When invoked from the /gitnexus index PR command, the command
# workflow fills these so the index is built from the PR's head
# ref and uploaded under the PR-numbered artifact name.
pr_number:
description: 'PR number to index (set by /gitnexus command)'
type: string
default: ''
pr_ref:
description: 'Optional PR head ref to check out; defaults to refs/pull/<pr_number>/head when pr_number is set'
type: string
default: ''
deploy_after:
description: 'Dispatch GitNexus Deploy after a successful index run'
type: boolean
default: false
permissions:
contents: read
concurrency:
# When triggered by the /gitnexus command, group by PR number so rapid
# re-runs coalesce. Otherwise group by git ref as before.
group: gitnexus-${{ inputs.pr_number != '' && format('pr-{0}', inputs.pr_number) || github.ref }}
cancel-in-progress: true
env:
GITNEXUS_VERSION: '1.5.3'
jobs:
index:
permissions:
contents: read
pull-requests: read # read changed files to decide whether embeddings are needed
# Push + dispatch run unconditionally. Native pull_request events
# are restricted to PRs authored by danny-avila only — this keeps
# automatic CI spend low on a repo with 200+ open PRs.
#
# Other contributors' PRs can still be indexed on demand:
# - /gitnexus index (PR comment command, contributor-gated)
# - workflow_dispatch (manual dispatch from Actions UI)
# Both bypass this filter because they arrive as workflow_dispatch,
# not pull_request.
if: |
github.event_name != 'pull_request' ||
github.event.pull_request.user.login == 'danny-avila'
runs-on: hanzo-build-linux-amd64
timeout-minutes: 25
steps:
- name: Validate dispatch inputs
if: github.event_name == 'workflow_dispatch'
env:
PR_NUMBER: ${{ inputs.pr_number }}
PR_REF: ${{ inputs.pr_ref }}
run: |
set -euo pipefail
if [ -n "$PR_NUMBER" ]; then
if [[ ! "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::pr_number must be numeric"
exit 1
fi
EXPECTED_REF="refs/pull/${PR_NUMBER}/head"
if [ -n "$PR_REF" ] && [ "$PR_REF" != "$EXPECTED_REF" ]; then
echo "::error::pr_ref must match ${EXPECTED_REF}"
exit 1
fi
elif [ -n "$PR_REF" ]; then
echo "::error::pr_ref requires pr_number"
exit 1
fi
- name: Resolve GitNexus flags
id: flags
env:
EVENT_NAME: ${{ github.event_name }}
ENABLE_EMBEDDINGS_INPUT: ${{ inputs.embeddings }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUM: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
# Decide whether to generate embeddings. Rules:
# push (main/dev) -> always embed
# pull_request -> embed ONLY when the PR changes files
# under paths that also trigger backend
# or frontend unit tests (api/, client/,
# packages/). Docs/config-only PRs skip
# embeddings to save ~3-5 min of CI.
# workflow_dispatch -> respect the explicit `embeddings` input
# (default false). This also covers the
# /gitnexus index [embeddings] command.
ENABLE_EMBEDDINGS=false
case "$EVENT_NAME" in
workflow_dispatch)
[ "$ENABLE_EMBEDDINGS_INPUT" = "true" ] && ENABLE_EMBEDDINGS=true
;;
push)
ENABLE_EMBEDDINGS=true
;;
pull_request)
CHANGED=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUM/files" \
--paginate --jq '.[].filename' 2>/dev/null || echo "")
if printf '%s\n' "$CHANGED" | grep -qE '^(api/|client/|packages/)'; then
echo "PR #$PR_NUM touches unit-test paths (api|client|packages) — enabling embeddings"
ENABLE_EMBEDDINGS=true
else
echo "PR #$PR_NUM does not touch unit-test paths — graph-only index"
fi
;;
esac
if [ "$ENABLE_EMBEDDINGS" = "true" ]; then
echo "enable_embeddings=true" >> "$GITHUB_OUTPUT"
else
echo "enable_embeddings=false" >> "$GITHUB_OUTPUT"
fi
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24.16.0'
- name: Install GitNexus CLI
working-directory: ${{ runner.temp }}
env:
NPM_CONFIG_AUDIT: false
NPM_CONFIG_CACHE: ${{ runner.temp }}/gitnexus-npm-cache
NPM_CONFIG_FUND: false
NPM_CONFIG_GLOBALCONFIG: ${{ runner.temp }}/gitnexus-cli/global-npmrc
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/gitnexus-cli/.npmrc
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/gitnexus-cli" "$RUNNER_TEMP/gitnexus-npm-cache"
: > "$RUNNER_TEMP/gitnexus-cli/global-npmrc"
printf '%s\n' \
'registry=https://registry.npmjs.org/' \
'audit=false' \
'fund=false' \
> "$RUNNER_TEMP/gitnexus-cli/.npmrc"
# Keep GitNexus' native DB dependency deterministic in fresh CI installs.
npm install \
--prefix "$RUNNER_TEMP/gitnexus-cli" \
--no-save \
--no-package-lock \
"gitnexus@${{ env.GITNEXUS_VERSION }}" \
"@ladybugdb/core@0.15.2"
test -x "$RUNNER_TEMP/gitnexus-cli/node_modules/.bin/gitnexus"
- name: Checkout repository
uses: actions/checkout@v4
with:
# When the /gitnexus command dispatches us with a pr_ref, it's
# a refs/pull/<N>/head ref that GitHub mirrors into the base
# repo for every PR, so checkout works for fork PRs too. When
# pr_ref is empty (native push/pull_request), fall back to the
# default ref actions/checkout would use.
ref: ${{ inputs.pr_ref || (inputs.pr_number != '' && format('refs/pull/{0}/head', inputs.pr_number) || '') }}
fetch-depth: 1
persist-credentials: false
- name: Run GitNexus Analyze
working-directory: ${{ runner.temp }}
env:
ENABLE_EMBEDDINGS: ${{ steps.flags.outputs.enable_embeddings }}
FORCE: ${{ inputs.force }}
GITNEXUS_BIN: ${{ runner.temp }}/gitnexus-cli/node_modules/.bin/gitnexus
NPM_CONFIG_AUDIT: false
NPM_CONFIG_CACHE: ${{ runner.temp }}/gitnexus-npm-cache
NPM_CONFIG_FUND: false
NPM_CONFIG_GLOBALCONFIG: ${{ runner.temp }}/gitnexus-cli/global-npmrc
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/
NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/gitnexus-cli/.npmrc
run: |
set -euo pipefail
FLAGS=(--skip-agents-md --verbose)
if [ "$ENABLE_EMBEDDINGS" = "true" ]; then
FLAGS+=(--embeddings)
fi
if [ "$FORCE" = "true" ]; then
FLAGS+=(--force)
fi
"$GITNEXUS_BIN" analyze "$GITHUB_WORKSPACE" "${FLAGS[@]}"
- name: Verify index
run: |
if [ ! -d ".gitnexus" ] || [ ! -f ".gitnexus/meta.json" ]; then
echo "::error::GitNexus index was not created"
exit 1
fi
echo "::group::Index metadata"
cat .gitnexus/meta.json
echo ""
echo "::endgroup::"
- name: Upload GitNexus index
uses: actions/upload-artifact@v4
with:
# Artifact naming order of precedence:
# 1. /gitnexus command dispatch: inputs.pr_number -> pr-<N>
# 2. Native pull_request event: github.event.pull_request.number
# 3. Push or manual dispatch without pr_number: github.ref_name
name: >-
gitnexus-index-${{
inputs.pr_number != ''
&& format('pr-{0}', inputs.pr_number)
|| (github.event_name == 'pull_request'
&& format('pr-{0}', github.event.pull_request.number)
|| github.ref_name)
}}
path: .gitnexus/
include-hidden-files: true
retention-days: 30
post-index:
needs: index
if: |
always() &&
(inputs.pr_number != '' ||
inputs.deploy_after)
runs-on: hanzo-build-linux-amd64
timeout-minutes: 5
permissions:
contents: read
actions: write # dispatch gitnexus-deploy.yml when deploy_after is set
pull-requests: write # post completion comments for /gitnexus command runs
steps:
# GitHub suppresses workflow_run events for workflow runs triggered
# by GITHUB_TOKEN (to prevent recursive chaining). Command-triggered
# index runs opt into a deploy by setting deploy_after=true.
- name: Trigger deploy workflow after command-triggered runs
if: inputs.deploy_after && needs.index.result == 'success'
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ inputs.pr_number }}
with:
script: |
core.info('deploy_after=true; dispatching gitnexus-deploy.yml manually.');
// Pass pr_number through so the deploy workflow knows which
// PR to post its completion comment on (for /gitnexus
// command runs this will be set; for other bot dispatches
// it's empty and the deploy step falls back to matrix match).
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'gitnexus-deploy.yml',
ref: 'main',
inputs: {
pr_number: process.env.PR_NUMBER || '',
},
});
# Reply on the PR when the /gitnexus command path runs so the
# requester knows the index step finished. This fires when
# inputs.pr_number is set and reports the index job result. A
# separate comment posts from the deploy workflow when the live
# server has the fresh index.
- name: Comment on PR — index complete
if: inputs.pr_number != ''
uses: actions/github-script@v7
env:
EMBEDDINGS_INPUT: ${{ inputs.embeddings }}
INDEX_RESULT: ${{ needs.index.result }}
PR_NUMBER: ${{ inputs.pr_number }}
with:
script: |
const indexSucceeded = process.env.INDEX_RESULT === 'success';
const outcome = indexSucceeded ? '✅ indexed' : '❌ index failed';
const prNum = parseInt(process.env.PR_NUMBER || '', 10);
if (!Number.isSafeInteger(prNum)) {
core.setFailed(`Invalid PR number: ${process.env.PR_NUMBER}`);
return;
}
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const embeddingsFlag = process.env.EMBEDDINGS_INPUT === 'true' ? 'with embeddings' : 'graph-only';
const body = [
`### GitNexus: ${outcome}`,
``,
`PR #${prNum} was indexed ${embeddingsFlag}.`,
`[Index run](${runUrl})`,
'',
indexSucceeded
? '⏳ Waiting for deploy to serve the fresh index…'
: '_Index run failed — the previous index (if any) continues to be served._',
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNum,
body,
});
-141
View File
@@ -1,141 +0,0 @@
# Responds to `/gitnexus index` comments on pull requests.
#
# Gated to the same author_association roles (OWNER, MEMBER, COLLABORATOR)
# as the automatic PR index trigger, but applied to the COMMENTER, not
# the PR author. This intentionally lets a contributor index a PR from
# a non-contributor / first-time fork author — the contributor takes
# responsibility for the trust boundary by typing the command.
#
# When a matching comment lands on a PR, this workflow dispatches
# `gitnexus-index.yml` with the PR number and the `refs/pull/<N>/head`
# ref so indexing works for fork PRs too (GitHub mirrors every PR's
# head ref into the base repo regardless of which fork it originated
# from, so actions/checkout can always resolve it).
#
# Use cases:
# - Re-index a PR after a rebase without pushing a new commit
# - Index a docs-only PR that was skipped by paths-ignore
# - Index a non-contributor (fork) PR that the auto-trigger skipped
# - Re-run a failed index
#
# Supported commands:
# /gitnexus index — index the PR with embeddings (default)
# /gitnexus index embeddings — explicit form of the above; same effect
# /gitnexus index fast — graph-only index (skip embeddings), for
# a quick re-index without waiting ~5 min
# of embedding generation
name: GitNexus PR Command
on:
issue_comment:
types: [created]
permissions:
contents: read
pull-requests: write
actions: write # needed to dispatch gitnexus-index.yml
concurrency:
group: gitnexus-pr-command-${{ github.event.issue.number }}
cancel-in-progress: false
jobs:
dispatch:
# Only run for PR comments that start with /gitnexus from trusted
# commenters. Intentionally checks the COMMENTER's association so a
# contributor can index a non-contributor's PR on demand.
if: |
github.event.issue.pull_request != null &&
startsWith(github.event.comment.body, '/gitnexus') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR')
runs-on: hanzo-build-linux-amd64
timeout-minutes: 5
steps:
- name: Parse command and resolve PR head ref
id: parse
uses: actions/github-script@v7
with:
script: |
const body = context.payload.comment.body.trim();
const match = body.match(/^\/gitnexus\s+(\w+)(?:\s+(\w+))?/);
if (!match) {
core.setFailed(`Unrecognized command: ${body}. Try: /gitnexus index [fast]`);
return;
}
const [, subcommand, modifier] = match;
if (subcommand !== 'index') {
core.setFailed(`Unknown subcommand: ${subcommand}. Only 'index' is supported.`);
return;
}
// Default to embeddings on — a contributor typing the command
// has already decided they want a full re-index. The `fast`
// modifier is the explicit opt-out for graph-only runs.
// `embeddings` is accepted as a no-op alias for backwards
// compat with the previous command form.
let embeddings = 'true';
if (modifier === 'fast' || modifier === 'graph-only' || modifier === 'no-embeddings') {
embeddings = 'false';
}
// Use refs/pull/<N>/head instead of the raw head SHA. GitHub
// mirrors every PR's head into the base repo as this ref, so
// actions/checkout can always resolve it — even for PRs from
// forks whose raw SHAs don't exist in the base repo.
const prNum = context.payload.issue.number;
core.setOutput('pr_number', String(prNum));
core.setOutput('pr_ref', `refs/pull/${prNum}/head`);
core.setOutput('embeddings', embeddings);
core.info(
`Dispatching index for PR #${prNum} at refs/pull/${prNum}/head (embeddings=${embeddings}, modifier=${modifier || '(none)'})`,
);
- name: Dispatch gitnexus-index workflow
uses: actions/github-script@v7
env:
EMBEDDINGS: ${{ steps.parse.outputs.embeddings }}
PR_NUMBER: ${{ steps.parse.outputs.pr_number }}
PR_REF: ${{ steps.parse.outputs.pr_ref }}
with:
script: |
const prNumber = process.env.PR_NUMBER || '';
const prRef = process.env.PR_REF || '';
const embeddings = process.env.EMBEDDINGS || 'false';
if (!/^[0-9]+$/.test(prNumber)) {
core.setFailed(`Invalid PR number: ${prNumber}`);
return;
}
if (prRef !== `refs/pull/${prNumber}/head`) {
core.setFailed(`Invalid PR ref: ${prRef}`);
return;
}
if (!['true', 'false'].includes(embeddings)) {
core.setFailed(`Invalid embeddings value: ${embeddings}`);
return;
}
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'gitnexus-index.yml',
ref: 'main',
inputs: {
pr_number: prNumber,
pr_ref: prRef,
embeddings,
force: 'false',
deploy_after: 'true',
},
});
- name: React to the comment
uses: actions/github-script@v7
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'rocket',
});
+11 -46
View File
@@ -4,17 +4,16 @@ name: Build Helm Charts on Tag
on:
push:
tags:
- "chart-*"
- "*"
jobs:
release:
permissions:
contents: write
packages: write
runs-on: hanzo-deploy-linux-amd64
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -27,49 +26,15 @@ jobs:
uses: azure/setup-helm@v4
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Build Subchart Deps
run: |
cd helm/librechat
helm dependency build
cd ../librechat-rag-api
helm dependency build
cd helm/hanzochat-rag-api
helm dependency build
- name: Get Chart Version
id: chart-version
run: |
CHART_VERSION=$(echo "${{ github.ref_name }}" | cut -d'-' -f2)
echo "CHART_VERSION=${CHART_VERSION}" >> "$GITHUB_OUTPUT"
# Log in to GitHub Container Registry
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
- name: Run chart-releaser
uses: helm/chart-releaser-action@v1.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Run Helm OCI Charts Releaser
# This is for the librechat chart
- name: Release Helm OCI Charts for librechat
uses: appany/helm-oci-chart-releaser@v0.5.0
with:
name: librechat
repository: ${{ github.actor }}/librechat-chart
tag: ${{ steps.chart-version.outputs.CHART_VERSION }}
path: helm/librechat
registry: ghcr.io
registry_username: ${{ github.actor }}
registry_password: ${{ secrets.GITHUB_TOKEN }}
# this is for the librechat-rag-api chart
- name: Release Helm OCI Charts for librechat-rag-api
uses: appany/helm-oci-chart-releaser@v0.5.0
with:
name: librechat-rag-api
repository: ${{ github.actor }}/librechat-chart
tag: ${{ steps.chart-version.outputs.CHART_VERSION }}
path: helm/librechat-rag-api
registry: ghcr.io
registry_username: ${{ github.actor }}
registry_password: ${{ secrets.GITHUB_TOKEN }}
charts_dir: helm
skip_existing: true
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+3 -35
View File
@@ -1,27 +1,20 @@
name: Detect Unused i18next Strings
# This workflow checks for unused i18n keys in translation files.
# It has special handling for:
# - com_ui_special_var_* keys that are dynamically constructed
# - com_agents_category_* keys that are stored in the database and used dynamically
on:
pull_request:
paths:
- "client/src/**"
- "api/**"
- "packages/data-provider/src/**"
- "packages/client/**"
- "packages/data-schemas/src/**"
jobs:
detect-unused-i18n-keys:
runs-on: hanzo-build-linux-amd64
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v3
- name: Find unused i18next keys
id: find-unused
@@ -30,7 +23,7 @@ jobs:
# Define paths
I18N_FILE="client/src/locales/en/translation.json"
SOURCE_DIRS=("client/src" "api" "packages/data-provider/src" "packages/client" "packages/data-schemas/src")
SOURCE_DIRS=("client/src" "api" "packages/data-provider/src")
# Check if translation file exists
if [[ ! -f "$I18N_FILE" ]]; then
@@ -58,31 +51,6 @@ jobs:
fi
done
# Also check if the key is directly used somewhere
if [[ "$FOUND" == false ]]; then
for DIR in "${SOURCE_DIRS[@]}"; do
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
FOUND=true
break
fi
done
fi
# Special case for agent category keys that are dynamically used from database
elif [[ "$KEY" == com_agents_category_* ]]; then
# Check if agent category localization is being used
for DIR in "${SOURCE_DIRS[@]}"; do
# Check for dynamic category label/description usage
if grep -r --include=\*.{js,jsx,ts,tsx} -E "category\.(label|description).*startsWith.*['\"]com_" "$DIR" > /dev/null 2>&1 || \
# Check for the method that defines these keys
grep -r --include=\*.{js,jsx,ts,tsx} "ensureDefaultCategories" "$DIR" > /dev/null 2>&1 || \
# Check for direct usage in agentCategory.ts
grep -r --include=\*.ts -E "label:.*['\"]$KEY['\"]" "$DIR" > /dev/null 2>&1 || \
grep -r --include=\*.ts -E "description:.*['\"]$KEY['\"]" "$DIR" > /dev/null 2>&1; then
FOUND=true
break
fi
done
# Also check if the key is directly used somewhere
if [[ "$FOUND" == false ]]; then
for DIR in "${SOURCE_DIRS[@]}"; do
-138
View File
@@ -1,138 +0,0 @@
import csv
import os
from github import Github
def interpret_results(csv_file):
with open(csv_file, newline="") as csvfile:
csvreader = csv.DictReader(csvfile)
rows = list(csvreader)
"""
in this csv reader
- Create 1 new column "Status"
- if a row has a median response time < 300 and an average response time < 300, Status = "Passed ✅"
- if a row has a median response time >= 300 or an average response time >= 300, Status = "Failed ❌"
- Order the table in this order Name, Status, Median Response Time, Average Response Time, Requests/s,Failures/s, Min Response Time, Max Response Time, all other columns
"""
# Add a new column "Status"
for row in rows:
median_response_time = float(
row["Median Response Time"].strip().rstrip("ms")
)
average_response_time = float(
row["Average Response Time"].strip().rstrip("s")
)
request_count = int(row["Request Count"])
failure_count = int(row["Failure Count"])
failure_percent = round((failure_count / request_count) * 100, 2)
# Determine status based on conditions
if (
median_response_time < 300
and average_response_time < 300
and failure_percent < 5
):
row["Status"] = "Passed ✅"
else:
row["Status"] = "Failed ❌"
# Construct Markdown table header
markdown_table = "| Name | Status | Median Response Time (ms) | Average Response Time (ms) | Requests/s | Failures/s | Request Count | Failure Count | Min Response Time (ms) | Max Response Time (ms) |"
markdown_table += (
"\n| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |"
)
# Construct Markdown table rows
for row in rows:
markdown_table += f"\n| {row['Name']} | {row['Status']} | {row['Median Response Time']} | {row['Average Response Time']} | {row['Requests/s']} | {row['Failures/s']} | {row['Request Count']} | {row['Failure Count']} | {row['Min Response Time']} | {row['Max Response Time']} |"
print("markdown table: ", markdown_table)
return markdown_table
def _get_docker_run_command_stable_release(release_version):
return f"""
\n\n
## Docker Run LiteLLM Proxy
```
docker run \\
-e STORE_MODEL_IN_DB=True \\
-p 4000:4000 \\
ghcr.io/berriai/litellm:litellm_stable_release_branch-{release_version}
```
"""
def _get_docker_run_command(release_version):
return f"""
\n\n
## Docker Run LiteLLM Proxy
```
docker run \\
-e STORE_MODEL_IN_DB=True \\
-p 4000:4000 \\
ghcr.io/berriai/litellm:main-{release_version}
```
"""
def get_docker_run_command(release_version):
if "stable" in release_version:
return _get_docker_run_command_stable_release(release_version)
else:
return _get_docker_run_command(release_version)
if __name__ == "__main__":
csv_file = "load_test_stats.csv" # Change this to the path of your CSV file
markdown_table = interpret_results(csv_file)
# Update release body with interpreted results
github_token = os.getenv("GITHUB_TOKEN")
g = Github(github_token)
repo = g.get_repo(
"BerriAI/litellm"
) # Replace with your repository's username and name
latest_release = repo.get_latest_release()
print("got latest release: ", latest_release)
print(latest_release.title)
print(latest_release.tag_name)
release_version = latest_release.title
print("latest release body: ", latest_release.body)
print("markdown table: ", markdown_table)
# check if "Load Test LiteLLM Proxy Results" exists
existing_release_body = latest_release.body
if "Load Test LiteLLM Proxy Results" in latest_release.body:
# find the "Load Test LiteLLM Proxy Results" section and delete it
start_index = latest_release.body.find("Load Test LiteLLM Proxy Results")
existing_release_body = latest_release.body[:start_index]
docker_run_command = get_docker_run_command(release_version)
print("docker run command: ", docker_run_command)
new_release_body = (
existing_release_body
+ docker_run_command
+ "\n\n"
+ "### Don't want to maintain your internal proxy? get in touch 🎉"
+ "\nHosted Proxy Alpha: https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat"
+ "\n\n"
+ "## Load Test LiteLLM Proxy Results"
+ "\n\n"
+ markdown_table
)
print("new release body: ", new_release_body)
try:
latest_release.update_release(
name=latest_release.tag_name,
message=new_release_body,
)
except Exception as e:
print(e)
-17
View File
@@ -1,17 +0,0 @@
name: Label ML Ops Team Issues
on:
issues:
types:
- opened
jobs:
add-mlops-label:
runs-on: hanzo-build-linux-amd64
steps:
- name: Check if ML Ops Team is selected
uses: actions-ecosystem/action-add-labels@v1
if: contains(github.event.issue.body, '### Are you a ML Ops Team?') && contains(github.event.issue.body, 'Yes')
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
labels: "mlops user request"
@@ -1,89 +0,0 @@
name: LLM Translation Tests
on:
workflow_dispatch:
inputs:
release_candidate_tag:
description: 'Release candidate tag/version'
required: true
type: string
push:
tags:
- 'v*-rc*' # Triggers on release candidate tags like v1.0.0-rc1
jobs:
run-llm-translation-tests:
runs-on: hanzo-build-linux-amd64
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.release_candidate_tag || github.ref }}
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true
- name: Cache Poetry dependencies
uses: actions/cache@v5
with:
path: |
~/.cache/pypoetry
.venv
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
- name: Install dependencies
run: |
poetry install --with dev
poetry run pip install pytest-xdist pytest-timeout
- name: Create test results directory
run: mkdir -p test-results
- name: Run LLM Translation Tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }}
AZURE_API_BASE: ${{ secrets.AZURE_API_BASE }}
AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }}
# Add other API keys as needed
run: |
python .github/workflows/run_llm_translation_tests.py \
--tag "${{ github.event.inputs.release_candidate_tag || github.ref_name }}" \
--commit "${{ github.sha }}" \
|| true # Continue even if tests fail
- name: Display test summary
if: always()
run: |
if [ -f "test-results/llm_translation_report.md" ]; then
echo "Test report generated successfully!"
echo "Artifact will contain:"
echo "- test-results/junit.xml (JUnit XML results)"
echo "- test-results/llm_translation_report.md (Beautiful markdown report)"
else
echo "Warning: Test report was not generated"
fi
- name: Upload test artifacts
uses: actions/upload-artifact@v6
if: always()
with:
name: LLM-Translation-Artifact-${{ github.event.inputs.release_candidate_tag || github.ref_name }}
path: test-results/
retention-days: 30
+8 -8
View File
@@ -9,13 +9,13 @@ on:
jobs:
sync-translations:
name: Sync Translation Keys with Locize
runs-on: hanzo-build-linux-amd64
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Set Up Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 20
@@ -36,7 +36,7 @@ jobs:
create-pull-request:
name: Create Translation PR on Version Published
runs-on: hanzo-build-linux-amd64
runs-on: ubuntu-latest
needs: sync-translations
permissions:
contents: write
@@ -44,25 +44,25 @@ jobs:
steps:
# 1. Check out the repository.
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
# 2. Download translation files from locize.
- name: Download Translations from locize
uses: locize/download@v3
uses: locize/download@v1
with:
project-id: ${{ secrets.LOCIZE_PROJECT_ID }}
path: "client/src/locales"
# 3. Create a Pull Request using built-in functionality.
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
sign-commits: true
commit-message: "🌍 i18n: Update translation.json with latest translations"
base: main
branch: i18n/locize-translation-update
reviewers: danny-avila
reviewers: hanzoai
title: "🌍 i18n: Update translation.json with latest translations"
body: |
**Description**:
-28
View File
@@ -1,28 +0,0 @@
from locust import HttpUser, task, between
class MyUser(HttpUser):
wait_time = between(1, 5)
@task
def chat_completion(self):
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer sk-8N1tLOOyH8TIxwOLahhIVg",
# Include any additional headers you may need for authentication, etc.
}
# Customize the payload with "model" and "messages" keys
payload = {
"model": "fake-openai-endpoint",
"messages": [
{"role": "system", "content": "You are a chat bot."},
{"role": "user", "content": "Hello, how are you?"},
],
# Add more data as necessary
}
# Make a POST request to the "chat/completions" endpoint
response = self.client.post("chat/completions", json=payload, headers=headers)
# Print or log the response if needed
+6 -29
View File
@@ -3,46 +3,27 @@ name: Docker Compose Build Latest Main Image Tag (Manual Dispatch)
on:
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build:
runs-on: hanzo-build-linux-amd64
runs-on: ubuntu-latest
strategy:
matrix:
include:
- target: api-build
file: Dockerfile.multi
image_name: librechat-api
image_name: hanzochat-api
- target: node
file: Dockerfile
image_name: librechat
image_name: hanzochat
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- name: Fetch tags and set the latest tag
run: |
set -euo pipefail
git fetch --tags --force
LATEST_TAG=$(git tag --list 'v[0-9]*' --sort=-v:refname | grep -E '^v[0-9]+[.][0-9]+[.][0-9]+$' | head -n 1)
if [ -z "$LATEST_TAG" ]; then
echo "::error::No stable v<semver> tag found"
exit 1
fi
printf 'LATEST_TAG=%s\n' "$LATEST_TAG" >> "$GITHUB_ENV"
- name: Compute build metadata
run: |
printf 'BUILD_COMMIT=%s\n' "$(git rev-parse HEAD)" >> "$GITHUB_ENV"
printf 'BUILD_BRANCH=main\n' >> "$GITHUB_ENV"
printf 'BUILD_DATE=%s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_ENV"
git fetch --tags
echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
# Set up QEMU
- name: Set up QEMU
@@ -54,7 +35,7 @@ jobs:
# Log in to GitHub Container Registry
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -86,7 +67,3 @@ jobs:
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
platforms: linux/amd64,linux/arm64
target: ${{ matrix.target }}
build-args: |
BUILD_COMMIT=${{ env.BUILD_COMMIT }}
BUILD_BRANCH=${{ env.BUILD_BRANCH }}
BUILD_DATE=${{ env.BUILD_DATE }}
-34
View File
@@ -1,34 +0,0 @@
name: Publish Dev Release to PyPI
on:
workflow_dispatch:
jobs:
publish-dev-release:
runs-on: hanzo-build-linux-amd64
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.8 # Adjust the Python version as needed
- name: Install dependencies
run: pip install toml twine
- name: Read version from pyproject.toml
id: read-version
run: |
version=$(python -c 'import toml; print(toml.load("pyproject.toml")["tool"]["commitizen"]["version"])')
printf "LITELLM_VERSION=%s" "$version" >> $GITHUB_ENV
- name: Check if version exists on PyPI
id: check-version
run: |
set -e
if twine check --repository-url https://pypi.org/simple/ "litellm==$LITELLM_VERSION" >/dev/null 2>&1; then
echo "Version $LITELLM_VERSION already exists on PyPI. Skipping publish."
+197
View File
@@ -0,0 +1,197 @@
name: PR Checks
on:
pull_request:
types: [opened, synchronize, reopened]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
changes:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
client: ${{ steps.filter.outputs.client }}
api: ${{ steps.filter.outputs.api }}
packages: ${{ steps.filter.outputs.packages }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
client:
- 'client/**'
- 'package.json'
api:
- 'api/**'
- 'package.json'
packages:
- 'packages/**'
test-client:
name: Client Tests
needs: changes
if: needs.changes.outputs.client == 'true' || needs.changes.outputs.packages == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build dependencies
run: |
npm run build:data-provider
npm run build:data-schemas
- name: Run client tests
run: NODE_ENV=test npm run test:client -- --passWithNoTests
- name: Comment PR (Client)
uses: actions/github-script@v7
if: always()
with:
script: |
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' && comment.body.includes('Client Test Results')
);
const body = `### 🧪 Client Test Results
${context.job.status === 'success' ? '✅ All client tests passed!' : '❌ Client tests failed'}
[View full test results](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
test-api:
name: API Tests
needs: changes
if: needs.changes.outputs.api == 'true' || needs.changes.outputs.packages == 'true'
runs-on: ubuntu-latest
services:
mongodb:
image: mongo:7.0
ports:
- 27017:27017
options: >-
--health-cmd "mongosh --eval 'db.adminCommand(\"ping\")'"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build dependencies
run: |
npm run build:data-provider
npm run build:data-schemas
npm run build:api
- name: Setup test environment
run: |
echo "NODE_ENV=test" > .env.test
echo "MONGO_URI=mongodb://localhost:27017/HanzoChat_test" >> .env.test
echo "MEILI_HOST=" >> .env.test
echo "RAG_API_URL=" >> .env.test
echo "ALLOW_REGISTRATION=true" >> .env.test
echo "DEBUG_LOGGING=false" >> .env.test
echo "CREDS_KEY=test-creds-key-32-chars-long-string" >> .env.test
echo "CREDS_IV=test-creds-iv-16" >> .env.test
echo "JWT_SECRET=test-jwt-secret-very-secure" >> .env.test
echo "JWT_REFRESH_SECRET=test-jwt-refresh-secret" >> .env.test
- name: Run API tests
run: NODE_ENV=test npm run test:api -- --maxWorkers=2 --passWithNoTests
size-check:
name: Bundle Size Check
needs: changes
if: needs.changes.outputs.client == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build client
run: npm run build:client
env:
DOMAIN_CLIENT: https://chat.hanzo.ai
DOMAIN_SERVER: https://chat.hanzo.ai
- name: Check bundle size
uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
directory: client/dist
pattern: "**/*.{js,css}"
type-check:
name: TypeScript Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: TypeScript check
run: |
cd client && npx tsc --noEmit || true
cd ../api && npx tsc --noEmit || true
-21
View File
@@ -1,21 +0,0 @@
name: PR Preview
on:
pull_request:
types: [opened, synchronize, reopened, closed]
permissions:
contents: read
packages: write
pull-requests: write
jobs:
preview:
uses: hanzoai/.github/.github/workflows/pr-preview.yml@main
with:
service: chat
image: ghcr.io/hanzoai/chat
port: "3080"
health-path: /api/health
e2e-dir: e2e
secrets: inherit
-20
View File
@@ -1,20 +0,0 @@
"""
redeploy_proxy.py
"""
import os
import requests
import time
# send a get request to this endpoint
deploy_hook1 = os.getenv("LOAD_TEST_REDEPLOY_URL1")
response = requests.get(deploy_hook1, timeout=20)
deploy_hook2 = os.getenv("LOAD_TEST_REDEPLOY_URL2")
response = requests.get(deploy_hook2, timeout=20)
print("SENT GET REQUESTS to re-deploy proxy")
print("sleeeping.... for 60s")
time.sleep(60)
+135
View File
@@ -0,0 +1,135 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
packages: write
jobs:
release:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Build all packages
run: |
npm run build:data-provider
npm run build:data-schemas
npm run build:api
- name: Run tests
run: |
NODE_ENV=test npm run test:client
NODE_ENV=test npm run test:api -- --maxWorkers=2
- name: Build production
run: npm run build:client
env:
DOMAIN_CLIENT: https://chat.hanzo.ai
DOMAIN_SERVER: https://chat.hanzo.ai
NODE_ENV: production
- name: Generate changelog
id: changelog
uses: TriPSs/conventional-changelog-action@v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
skip-version-file: true
output-file: false
- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: ${{ steps.changelog.outputs.clean_changelog }}
draft: false
prerelease: false
- name: Build Docker image
run: |
docker build -t ghcr.io/hanzoai/chat:${{ github.ref_name }} .
docker tag ghcr.io/hanzoai/chat:${{ github.ref_name }} ghcr.io/hanzoai/chat:latest
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push Docker image
run: |
docker push ghcr.io/hanzoai/chat:${{ github.ref_name }}
docker push ghcr.io/hanzoai/chat:latest
publish-npm:
name: Publish to NPM
needs: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Build packages
run: |
npm run build:data-provider
npm run build:data-schemas
npm run build:api
- name: Publish packages
run: |
cd packages/data-provider && npm publish --access public || true
cd ../data-schemas && npm publish --access public || true
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
deploy:
name: Deploy to Production
needs: release
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to Hanzo Cloud
run: |
echo "Deploying version ${{ github.ref_name }} to production"
# Add your deployment script here
# Example: kubectl set image deployment/chat chat=ghcr.io/hanzoai/chat:${{ github.ref_name }}
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'Hanzo Chat ${{ github.ref_name }} deployed to production'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
if: always()
-27
View File
@@ -1,27 +0,0 @@
Date,"Ben
Ashley",Tom Brooks,Jimmy Cooney,"Sue
Daniels",Berlinda Fong,Terry Jones,Angelina Little,Linda Smith
10/1,FALSE,TRUE,TRUE,TRUE,TRUE,TRUE,FALSE,TRUE
10/2,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/3,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/4,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/5,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/6,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/7,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/8,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/9,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/10,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/11,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/12,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/13,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/14,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/15,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/16,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/17,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/18,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/19,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/20,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/21,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/22,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
10/23,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE
Total,0,1,1,1,1,1,0,1
1 Date Ben Ashley Tom Brooks Jimmy Cooney Sue Daniels Berlinda Fong Terry Jones Angelina Little Linda Smith
2 10/1 FALSE TRUE TRUE TRUE TRUE TRUE FALSE TRUE
3 10/2 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
4 10/3 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
5 10/4 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
6 10/5 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
7 10/6 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
8 10/7 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
9 10/8 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
10 10/9 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
11 10/10 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
12 10/11 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
13 10/12 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
14 10/13 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
15 10/14 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
16 10/15 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
17 10/16 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
18 10/17 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
19 10/18 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
20 10/19 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
21 10/20 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
22 10/21 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
23 10/22 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
24 10/23 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
25 Total 0 1 1 1 1 1 0 1
@@ -1,439 +0,0 @@
#!/usr/bin/env python3
"""
Run LLM Translation Tests and Generate Beautiful Markdown Report
This script runs the LLM translation tests and generates a comprehensive
markdown report with provider-specific breakdowns and test statistics.
"""
import os
import sys
import subprocess
import xml.etree.ElementTree as ET
from collections import defaultdict
from datetime import datetime
from pathlib import Path
import json
from typing import Dict, List, Tuple, Optional
# ANSI color codes for terminal output
class Colors:
GREEN = '\033[92m'
RED = '\033[91m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
PURPLE = '\033[95m'
CYAN = '\033[96m'
RESET = '\033[0m'
BOLD = '\033[1m'
def print_colored(message: str, color: str = Colors.RESET):
"""Print colored message to terminal"""
print(f"{color}{message}{Colors.RESET}")
def get_provider_from_test_file(test_file: str) -> str:
"""Map test file names to provider names"""
provider_mapping = {
'test_anthropic': 'Anthropic',
'test_azure': 'Azure',
'test_bedrock': 'AWS Bedrock',
'test_openai': 'OpenAI',
'test_vertex': 'Google Vertex AI',
'test_gemini': 'Google Vertex AI',
'test_cohere': 'Cohere',
'test_databricks': 'Databricks',
'test_groq': 'Groq',
'test_together': 'Together AI',
'test_mistral': 'Mistral',
'test_deepseek': 'DeepSeek',
'test_replicate': 'Replicate',
'test_huggingface': 'HuggingFace',
'test_fireworks': 'Fireworks AI',
'test_perplexity': 'Perplexity',
'test_cloudflare': 'Cloudflare',
'test_voyage': 'Voyage AI',
'test_xai': 'xAI',
'test_nvidia': 'NVIDIA',
'test_watsonx': 'IBM watsonx',
'test_azure_ai': 'Azure AI',
'test_snowflake': 'Snowflake',
'test_infinity': 'Infinity',
'test_jina': 'Jina AI',
'test_deepgram': 'Deepgram',
'test_clarifai': 'Clarifai',
'test_triton': 'Triton',
}
for key, provider in provider_mapping.items():
if key in test_file:
return provider
# For cross-provider test files
if any(name in test_file for name in ['test_optional_params', 'test_prompt_factory',
'test_router', 'test_text_completion']):
return f'Cross-Provider Tests ({test_file})'
return 'Other Tests'
def format_duration(seconds: float) -> str:
"""Format duration in human-readable format"""
if seconds < 60:
return f"{seconds:.2f}s"
elif seconds < 3600:
minutes = int(seconds // 60)
secs = seconds % 60
return f"{minutes}m {secs:.0f}s"
else:
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
return f"{hours}h {minutes}m"
def generate_markdown_report(junit_xml_path: str, output_path: str, tag: str = None, commit: str = None):
"""Generate a beautiful markdown report from JUnit XML"""
try:
tree = ET.parse(junit_xml_path)
root = tree.getroot()
# Handle both testsuite and testsuites root
if root.tag == 'testsuites':
suites = root.findall('testsuite')
else:
suites = [root]
# Overall statistics
total_tests = 0
total_failures = 0
total_errors = 0
total_skipped = 0
total_time = 0.0
# Provider breakdown
provider_stats = defaultdict(lambda: {'passed': 0, 'failed': 0, 'skipped': 0, 'errors': 0, 'time': 0.0})
provider_tests = defaultdict(list)
for suite in suites:
total_tests += int(suite.get('tests', 0))
total_failures += int(suite.get('failures', 0))
total_errors += int(suite.get('errors', 0))
total_skipped += int(suite.get('skipped', 0))
total_time += float(suite.get('time', 0))
for testcase in suite.findall('testcase'):
classname = testcase.get('classname', '')
test_name = testcase.get('name', '')
test_time = float(testcase.get('time', 0))
# Extract test file name from classname
if '.' in classname:
parts = classname.split('.')
test_file = parts[-2] if len(parts) > 1 else 'unknown'
else:
test_file = 'unknown'
provider = get_provider_from_test_file(test_file)
provider_stats[provider]['time'] += test_time
# Check test status
if testcase.find('failure') is not None:
provider_stats[provider]['failed'] += 1
failure = testcase.find('failure')
failure_msg = failure.get('message', '') if failure is not None else ''
provider_tests[provider].append({
'name': test_name,
'status': 'FAILED',
'time': test_time,
'message': failure_msg
})
elif testcase.find('error') is not None:
provider_stats[provider]['errors'] += 1
error = testcase.find('error')
error_msg = error.get('message', '') if error is not None else ''
provider_tests[provider].append({
'name': test_name,
'status': 'ERROR',
'time': test_time,
'message': error_msg
})
elif testcase.find('skipped') is not None:
provider_stats[provider]['skipped'] += 1
skip = testcase.find('skipped')
skip_msg = skip.get('message', '') if skip is not None else ''
provider_tests[provider].append({
'name': test_name,
'status': 'SKIPPED',
'time': test_time,
'message': skip_msg
})
else:
provider_stats[provider]['passed'] += 1
provider_tests[provider].append({
'name': test_name,
'status': 'PASSED',
'time': test_time,
'message': ''
})
passed = total_tests - total_failures - total_errors - total_skipped
# Generate the markdown report
with open(output_path, 'w') as f:
# Header
f.write("# LLM Translation Test Results\n\n")
# Metadata table
f.write("## Test Run Information\n\n")
f.write("| Field | Value |\n")
f.write("|-------|-------|\n")
f.write(f"| **Tag** | `{tag or 'N/A'}` |\n")
f.write(f"| **Date** | {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')} |\n")
f.write(f"| **Commit** | `{commit or 'N/A'}` |\n")
f.write(f"| **Duration** | {format_duration(total_time)} |\n")
f.write("\n")
# Overall statistics with visual elements
f.write("## Overall Statistics\n\n")
# Summary box
f.write("```\n")
f.write(f"Total Tests: {total_tests}\n")
f.write(f"├── Passed: {passed:>4} ({(passed/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n")
f.write(f"├── Failed: {total_failures:>4} ({(total_failures/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n")
f.write(f"├── Errors: {total_errors:>4} ({(total_errors/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n")
f.write(f"└── Skipped: {total_skipped:>4} ({(total_skipped/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n")
f.write("```\n\n")
# Provider summary table
f.write("## Results by Provider\n\n")
f.write("| Provider | Total | Pass | Fail | Error | Skip | Pass Rate | Duration |\n")
f.write("|----------|-------|------|------|-------|------|-----------|----------|")
# Sort providers: specific providers first, then cross-provider tests
sorted_providers = []
cross_provider = []
for p in sorted(provider_stats.keys()):
if 'Cross-Provider' in p or p == 'Other Tests':
cross_provider.append(p)
else:
sorted_providers.append(p)
all_providers = sorted_providers + cross_provider
for provider in all_providers:
stats = provider_stats[provider]
total = stats['passed'] + stats['failed'] + stats['errors'] + stats['skipped']
pass_rate = (stats['passed'] / total * 100) if total > 0 else 0
f.write(f"\n| {provider} | {total} | {stats['passed']} | {stats['failed']} | ")
f.write(f"{stats['errors']} | {stats['skipped']} | {pass_rate:.1f}% | ")
f.write(f"{format_duration(stats['time'])} |")
# Detailed test results by provider
f.write("\n\n## Detailed Test Results\n\n")
for provider in sorted_providers:
if provider_tests[provider]:
stats = provider_stats[provider]
total = stats['passed'] + stats['failed'] + stats['errors'] + stats['skipped']
f.write(f"### {provider}\n\n")
f.write(f"**Summary:** {stats['passed']}/{total} passed ")
f.write(f"({(stats['passed']/total)*100 if total > 0 else 0:.1f}%) ")
f.write(f"in {format_duration(stats['time'])}\n\n")
# Group tests by status
tests_by_status = defaultdict(list)
for test in provider_tests[provider]:
tests_by_status[test['status']].append(test)
# Show failed tests first (if any)
if tests_by_status['FAILED']:
f.write("<details>\n<summary>Failed Tests</summary>\n\n")
for test in tests_by_status['FAILED']:
f.write(f"- `{test['name']}` ({test['time']:.2f}s)\n")
if test['message']:
# Truncate long error messages
msg = test['message'][:200] + '...' if len(test['message']) > 200 else test['message']
f.write(f" > {msg}\n")
f.write("\n</details>\n\n")
# Show errors (if any)
if tests_by_status['ERROR']:
f.write("<details>\n<summary>Error Tests</summary>\n\n")
for test in tests_by_status['ERROR']:
f.write(f"- `{test['name']}` ({test['time']:.2f}s)\n")
f.write("\n</details>\n\n")
# Show passed tests in collapsible section
if tests_by_status['PASSED']:
f.write("<details>\n<summary>Passed Tests</summary>\n\n")
for test in tests_by_status['PASSED']:
f.write(f"- `{test['name']}` ({test['time']:.2f}s)\n")
f.write("\n</details>\n\n")
# Show skipped tests (if any)
if tests_by_status['SKIPPED']:
f.write("<details>\n<summary>Skipped Tests</summary>\n\n")
for test in tests_by_status['SKIPPED']:
f.write(f"- `{test['name']}`\n")
f.write("\n</details>\n\n")
# Cross-provider tests in a separate section
if cross_provider:
f.write("### Cross-Provider Tests\n\n")
for provider in cross_provider:
if provider_tests[provider]:
stats = provider_stats[provider]
total = stats['passed'] + stats['failed'] + stats['errors'] + stats['skipped']
f.write(f"#### {provider}\n\n")
f.write(f"**Summary:** {stats['passed']}/{total} passed ")
f.write(f"({(stats['passed']/total)*100 if total > 0 else 0:.1f}%)\n\n")
# For cross-provider tests, just show counts
f.write(f"- Passed: {stats['passed']}\n")
if stats['failed'] > 0:
f.write(f"- Failed: {stats['failed']}\n")
if stats['errors'] > 0:
f.write(f"- Errors: {stats['errors']}\n")
if stats['skipped'] > 0:
f.write(f"- Skipped: {stats['skipped']}\n")
f.write("\n")
print_colored(f"Report generated: {output_path}", Colors.GREEN)
except Exception as e:
print_colored(f"Error generating report: {e}", Colors.RED)
raise
def run_tests(test_path: str = "tests/llm_translation/",
junit_xml: str = "test-results/junit.xml",
report_path: str = "test-results/llm_translation_report.md",
tag: str = None,
commit: str = None) -> int:
"""Run the LLM translation tests and generate report"""
# Create test results directory
os.makedirs(os.path.dirname(junit_xml), exist_ok=True)
print_colored("Starting LLM Translation Tests", Colors.BOLD + Colors.BLUE)
print_colored(f"Test directory: {test_path}", Colors.CYAN)
print_colored(f"Output: {junit_xml}", Colors.CYAN)
print()
# Run pytest
cmd = [
"poetry", "run", "pytest", test_path,
f"--junitxml={junit_xml}",
"-v",
"--tb=short",
"--maxfail=500",
"-n", "auto"
]
# Add timeout if pytest-timeout is installed
try:
subprocess.run(["poetry", "run", "python", "-c", "import pytest_timeout"],
capture_output=True, check=True)
cmd.extend(["--timeout=300"])
except:
print_colored("Warning: pytest-timeout not installed, skipping timeout option", Colors.YELLOW)
print_colored("Running pytest with command:", Colors.YELLOW)
print(f" {' '.join(cmd)}")
print()
# Run the tests
result = subprocess.run(cmd, capture_output=False)
# Generate the report regardless of test outcome
if os.path.exists(junit_xml):
print()
print_colored("Generating test report...", Colors.BLUE)
generate_markdown_report(junit_xml, report_path, tag, commit)
# Print summary to console
print()
print_colored("Test Summary:", Colors.BOLD + Colors.PURPLE)
# Parse XML for quick summary
tree = ET.parse(junit_xml)
root = tree.getroot()
if root.tag == 'testsuites':
suites = root.findall('testsuite')
else:
suites = [root]
total = sum(int(s.get('tests', 0)) for s in suites)
failures = sum(int(s.get('failures', 0)) for s in suites)
errors = sum(int(s.get('errors', 0)) for s in suites)
skipped = sum(int(s.get('skipped', 0)) for s in suites)
passed = total - failures - errors - skipped
print(f" Total: {total}")
print_colored(f" Passed: {passed}", Colors.GREEN)
if failures > 0:
print_colored(f" Failed: {failures}", Colors.RED)
if errors > 0:
print_colored(f" Errors: {errors}", Colors.RED)
if skipped > 0:
print_colored(f" Skipped: {skipped}", Colors.YELLOW)
if total > 0:
pass_rate = (passed / total) * 100
color = Colors.GREEN if pass_rate >= 80 else Colors.YELLOW if pass_rate >= 60 else Colors.RED
print_colored(f" Pass Rate: {pass_rate:.1f}%", color)
else:
print_colored("No test results found!", Colors.RED)
print()
print_colored("Test run complete!", Colors.BOLD + Colors.GREEN)
return result.returncode
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Run LLM Translation Tests")
parser.add_argument("--test-path", default="tests/llm_translation/",
help="Path to test directory")
parser.add_argument("--junit-xml", default="test-results/junit.xml",
help="Path for JUnit XML output")
parser.add_argument("--report", default="test-results/llm_translation_report.md",
help="Path for markdown report")
parser.add_argument("--tag", help="Git tag or version")
parser.add_argument("--commit", help="Git commit SHA")
args = parser.parse_args()
# Get git info if not provided
if not args.commit:
try:
result = subprocess.run(["git", "rev-parse", "HEAD"],
capture_output=True, text=True)
if result.returncode == 0:
args.commit = result.stdout.strip()
except:
pass
if not args.tag:
try:
result = subprocess.run(["git", "describe", "--tags", "--abbrev=0"],
capture_output=True, text=True)
if result.returncode == 0:
args.tag = result.stdout.strip()
except:
pass
exit_code = run_tests(
test_path=args.test_path,
junit_xml=args.junit_xml,
report_path=args.report,
tag=args.tag,
commit=args.commit
)
sys.exit(exit_code)
-20
View File
@@ -1,20 +0,0 @@
name: "Stale Issue Management"
on:
schedule:
- cron: '0 0 * * *' # Runs daily at midnight UTC
workflow_dispatch:
jobs:
stale:
runs-on: hanzo-build-linux-amd64
steps:
- uses: actions/stale@v10
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
stale-issue-message: "This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs."
stale-pr-message: "This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs."
days-before-stale: 90 # Revert to 60 days
days-before-close: 7 # Revert to 7 days
stale-issue-label: "stale"
operations-per-run: 1000
@@ -1,85 +0,0 @@
name: Sync Helm Chart Tags
on:
push:
branches:
- main
workflow_dispatch:
inputs:
release_existing_tag:
description: "Existing chart-* tag to dispatch if tag creation succeeded but release dispatch failed"
required: false
type: string
permissions:
contents: read
concurrency:
group: sync-helm-chart-tags
cancel-in-progress: false
jobs:
noop:
name: Ignore non-main push
if: github.event_name == 'push' && github.ref != 'refs/heads/main'
runs-on: hanzo-build-linux-amd64
timeout-minutes: 1
steps:
- name: Skip tag sync
run: echo "Helm chart tag sync only runs on main pushes or manual dispatch."
sync:
name: Sync chart tags
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
runs-on: hanzo-build-linux-amd64
timeout-minutes: 10
permissions:
actions: write
contents: write
env:
BASE_REF: refs/remotes/origin/main
BACKFILL_FROM_VERSION: 1.9.0
CHART_PATH: helm/librechat/Chart.yaml
DEFAULT_BRANCH: main
DISPATCH_WORKFLOW: helmcharts.yml
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
RELEASE_EXISTING_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_existing_tag || '' }}
REPO_DIR: /tmp/librechat-sync
TAG_PREFIX: chart-
steps:
- name: Fetch main and tags
shell: bash
run: |
set -euo pipefail
if [[ ! "$GITHUB_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
echo "::error::Unexpected repository name: $GITHUB_REPOSITORY"
exit 1
fi
if [[ "$GITHUB_SERVER_URL" != "https://github.com" ]]; then
echo "::error::Unexpected GitHub server URL: $GITHUB_SERVER_URL"
exit 1
fi
rm -rf "$REPO_DIR"
git init "$REPO_DIR"
cd "$REPO_DIR"
git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
AUTH_HEADER="$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')"
git -c "http.extraheader=AUTHORIZATION: basic ${AUTH_HEADER}" \
fetch --prune --force --tags origin \
"+refs/heads/${DEFAULT_BRANCH}:refs/remotes/origin/${DEFAULT_BRANCH}"
git checkout --detach "$BASE_REF"
- name: Create missing chart tags
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PUSH_TAGS: "true"
run: |
set -euo pipefail
cd "$REPO_DIR"
.github/scripts/sync-helm-chart-tags.sh
+10 -59
View File
@@ -3,49 +3,26 @@ name: Docker Images Build on Tag
on:
push:
tags:
- 'v*'
permissions:
contents: read
packages: write
- '*'
jobs:
build:
runs-on: hanzo-build-linux-amd64
runs-on: ubuntu-latest
strategy:
matrix:
include:
- target: api-build
file: Dockerfile.multi
image_name: librechat-api
image_name: hanzochat-api
- target: node
file: Dockerfile
image_name: librechat
image_name: hanzochat
steps:
# Check out the repository
- name: Checkout
uses: actions/checkout@v4
- name: Validate release tag
id: release-tag
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
TAG_REGEX='^v[0-9]+[.][0-9]+[.][0-9]+(-rc[0-9]+)?$'
STABLE_TAG_REGEX='^v[0-9]+[.][0-9]+[.][0-9]+$'
if [[ ! "$REF_NAME" =~ $TAG_REGEX ]]; then
echo "::error::Docker release tags must use v<semver> or v<semver>-rcN, for example v0.8.5 or v0.8.5-rc1"
exit 1
fi
printf 'image_tag=%s\n' "$REF_NAME" >> "$GITHUB_OUTPUT"
if [[ "$REF_NAME" =~ $STABLE_TAG_REGEX ]]; then
echo "is_stable=true" >> "$GITHUB_OUTPUT"
else
echo "is_stable=false" >> "$GITHUB_OUTPUT"
fi
# Set up QEMU
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -56,7 +33,7 @@ jobs:
# Log in to GitHub Container Registry
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -74,32 +51,6 @@ jobs:
run: |
cp .env.example .env
- name: Compute build metadata
run: |
echo "BUILD_COMMIT=${{ github.sha }}" >> $GITHUB_ENV
echo "BUILD_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
- name: Resolve image tags
id: image-tags
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
IMAGE_NAME: ${{ matrix.image_name }}
IMAGE_TAG: ${{ steps.release-tag.outputs.image_tag }}
IS_STABLE: ${{ steps.release-tag.outputs.is_stable }}
run: |
set -euo pipefail
{
echo 'tags<<EOF'
printf 'ghcr.io/%s/%s:%s\n' "$GITHUB_REPOSITORY_OWNER" "$IMAGE_NAME" "$IMAGE_TAG"
printf '%s/%s:%s\n' "$DOCKERHUB_USERNAME" "$IMAGE_NAME" "$IMAGE_TAG"
if [ "$IS_STABLE" = "true" ]; then
printf 'ghcr.io/%s/%s:latest\n' "$GITHUB_REPOSITORY_OWNER" "$IMAGE_NAME"
printf '%s/%s:latest\n' "$DOCKERHUB_USERNAME" "$IMAGE_NAME"
fi
echo 'EOF'
} >> "$GITHUB_OUTPUT"
# Build and push Docker images for each target
- name: Build and push Docker images
uses: docker/build-push-action@v5
@@ -107,10 +58,10 @@ jobs:
context: .
file: ${{ matrix.file }}
push: true
tags: ${{ steps.image-tags.outputs.tags }}
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 }}
build-args: |
BUILD_COMMIT=${{ env.BUILD_COMMIT }}
BUILD_BRANCH=${{ env.BUILD_BRANCH }}
BUILD_DATE=${{ env.BUILD_DATE }}
-57
View File
@@ -1,57 +0,0 @@
name: LiteLLM Linting
on:
pull_request:
branches: [ main ]
jobs:
lint:
runs-on: hanzo-build-linux-amd64
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install Poetry
uses: snok/install-poetry@v1
- name: Install dependencies
run: |
pip install openai==1.81.0
poetry install --with dev
pip install openai==1.81.0
- name: Run Black formatting
run: |
cd litellm
poetry run black .
cd ..
- name: Run Ruff linting
run: |
cd litellm
poetry run ruff check .
cd ..
- name: Run MyPy type checking
run: |
cd litellm
poetry run mypy . --ignore-missing-imports
cd ..
- name: Check for circular imports
run: |
cd litellm
poetry run python ../tests/documentation_tests/test_circular_imports.py
cd ..
- name: Check import safety
run: |
poetry run python -c "from litellm import *" || (echo '🚨 import failed, this means you introduced unprotected imports! 🚨'; exit 1)
-70
View File
@@ -1,70 +0,0 @@
name: Test Chat Service
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: hanzo-build-linux-amd64
services:
mongodb:
image: mongo:7
ports:
- 27017:27017
meili:
image: getmeili/meilisearch:latest
env:
MEILI_MASTER_KEY: test_master_key
ports:
- 7700:7700
steps:
- uses: actions/checkout@v6
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v5
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build workspace packages
run: |
pnpm run build:data-provider
pnpm run build:data-schemas
pnpm run build:api
pnpm run build:client-package
- name: Run API tests
run: pnpm run test:api
env:
NODE_ENV: test
MONGO_URI: mongodb://localhost:27017/librechat-test
MEILI_HOST: http://localhost:7700
MEILI_MASTER_KEY: test_master_key
- name: Run client tests
run: pnpm run test:client
+16 -144
View File
@@ -7,32 +7,28 @@ on:
- 'package-lock.json'
- 'client/**'
- 'api/**'
- 'packages/client/**'
- 'packages/api/**'
jobs:
detect-unused-packages:
runs-on: hanzo-build-linux-amd64
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache: 'npm'
- name: Install depcheck
run: pnpm add -g depcheck
run: npm install -g depcheck
- name: Validate JSON files
run: |
for FILE in package.json client/package.json api/package.json packages/client/package.json; do
for FILE in package.json client/package.json api/package.json; do
if [[ -f "$FILE" ]]; then
jq empty "$FILE" || (echo "::error title=Invalid JSON::$FILE is invalid" && exit 1)
fi
@@ -66,151 +62,29 @@ jobs:
extract_deps_from_code() {
local folder=$1
local output_file=$2
# Initialize empty output file
> "$output_file"
if [[ -d "$folder" ]]; then
# Extract require() statements (use explicit includes for portability)
grep -rEho "require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)" "$folder" \
--include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
sed -E "s/require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)/\1/" >> "$output_file" || true
grep -rEho "require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)" "$folder" --include=\*.{js,ts,mjs,cjs} | \
sed -E "s/require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)/\1/" > "$output_file"
# Extract ES6 imports - import x from 'module'
grep -rEho "import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
--include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
sed -E "s/import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
# import 'module' (side-effect imports)
grep -rEho "import ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
--include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
sed -E "s/import ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
# export { x } from 'module' or export * from 'module'
grep -rEho "export .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
--include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' --include='*.mjs' --include='*.cjs' 2>/dev/null | \
sed -E "s/export .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
# import type { x } from 'module' (TypeScript)
grep -rEho "import type .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" \
--include='*.ts' --include='*.tsx' 2>/dev/null | \
sed -E "s/import type .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file" || true
grep -rEho "import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" --include=\*.{js,ts,mjs,cjs} | \
sed -E "s/import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file"
# Remove subpath imports but keep the base package
# For scoped packages: '@scope/pkg/subpath' -> '@scope/pkg'
# For regular packages: 'pkg/subpath' -> 'pkg'
# Scoped packages (must keep @scope/package, strip anything after)
sed -i -E 's|^(@[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+)/.*|\1|' "$output_file" 2>/dev/null || true
# Non-scoped packages (keep package name, strip subpath)
sed -i -E 's|^([a-zA-Z0-9_-]+)/.*|\1|' "$output_file" 2>/dev/null || true
sort -u "$output_file" -o "$output_file"
else
touch "$output_file"
fi
}
extract_deps_from_code "." root_used_code.txt
extract_deps_from_code "client" client_used_code.txt
extract_deps_from_code "api" api_used_code.txt
# Extract dependencies used by workspace packages
# These packages are used in the workspace but dependencies are provided by parent package.json
extract_deps_from_code "packages/client" packages_client_used_code.txt
extract_deps_from_code "packages/api" packages_api_used_code.txt
- name: Get @librechat/client dependencies
id: get-librechat-client-deps
run: |
if [[ -f "packages/client/package.json" ]]; then
# Get all dependencies from @librechat/client (dependencies, devDependencies, and peerDependencies)
DEPS=$(jq -r '.dependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
DEV_DEPS=$(jq -r '.devDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
PEER_DEPS=$(jq -r '.peerDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
# Combine all dependencies
echo "$DEPS" > librechat_client_deps.txt
echo "$DEV_DEPS" >> librechat_client_deps.txt
echo "$PEER_DEPS" >> librechat_client_deps.txt
# Also include dependencies that are imported in packages/client
cat packages_client_used_code.txt >> librechat_client_deps.txt
# Remove empty lines and sort
grep -v '^$' librechat_client_deps.txt | sort -u > temp_deps.txt
mv temp_deps.txt librechat_client_deps.txt
else
touch librechat_client_deps.txt
fi
- name: Get @librechat/api dependencies
id: get-librechat-api-deps
run: |
if [[ -f "packages/api/package.json" ]]; then
# Get all dependencies from @librechat/api (dependencies, devDependencies, and peerDependencies)
DEPS=$(jq -r '.dependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
DEV_DEPS=$(jq -r '.devDependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
PEER_DEPS=$(jq -r '.peerDependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
# Combine all dependencies
echo "$DEPS" > librechat_api_deps.txt
echo "$DEV_DEPS" >> librechat_api_deps.txt
echo "$PEER_DEPS" >> librechat_api_deps.txt
# Also include dependencies that are imported in packages/api
cat packages_api_used_code.txt >> librechat_api_deps.txt
# Remove empty lines and sort
grep -v '^$' librechat_api_deps.txt | sort -u > temp_deps.txt
mv temp_deps.txt librechat_api_deps.txt
else
touch librechat_api_deps.txt
fi
- name: Extract Workspace Dependencies
id: extract-workspace-deps
run: |
# Function to get dependencies from a workspace package that are used by another package
get_workspace_package_deps() {
local package_json=$1
local output_file=$2
# Get all workspace dependencies (starting with @librechat/)
if [[ -f "$package_json" ]]; then
local workspace_deps=$(jq -r '.dependencies // {} | to_entries[] | select(.key | startswith("@librechat/")) | .key' "$package_json" 2>/dev/null || echo "")
# For each workspace dependency, get its dependencies
for dep in $workspace_deps; do
# Convert @librechat/api to packages/api
local workspace_path=$(echo "$dep" | sed 's/@librechat\//packages\//')
local workspace_package_json="${workspace_path}/package.json"
if [[ -f "$workspace_package_json" ]]; then
# Extract all dependencies from the workspace package
jq -r '.dependencies // {} | keys[]' "$workspace_package_json" 2>/dev/null >> "$output_file"
# Also extract peerDependencies
jq -r '.peerDependencies // {} | keys[]' "$workspace_package_json" 2>/dev/null >> "$output_file"
fi
done
fi
if [[ -f "$output_file" ]]; then
sort -u "$output_file" -o "$output_file"
else
touch "$output_file"
fi
}
# Get workspace dependencies for each package
get_workspace_package_deps "package.json" root_workspace_deps.txt
get_workspace_package_deps "client/package.json" client_workspace_deps.txt
get_workspace_package_deps "api/package.json" api_workspace_deps.txt
- name: Run depcheck for root package.json
id: check-root
run: |
if [[ -f "package.json" ]]; then
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
# Exclude dependencies used in scripts, code, and workspace packages
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat root_used_deps.txt root_used_code.txt root_workspace_deps.txt | sort) || echo "")
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat root_used_deps.txt root_used_code.txt | sort) || echo "")
echo "ROOT_UNUSED<<EOF" >> $GITHUB_ENV
echo "$UNUSED" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
@@ -223,8 +97,7 @@ jobs:
chmod -R 755 client
cd client
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
# Exclude dependencies used in scripts, code, workspace packages, and @librechat/client imports
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../client_used_deps.txt ../client_used_code.txt ../client_workspace_deps.txt ../packages_client_used_code.txt ../librechat_client_deps.txt 2>/dev/null | sort -u) || echo "")
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../client_used_deps.txt ../client_used_code.txt | sort) || echo "")
# Filter out false positives
UNUSED=$(echo "$UNUSED" | grep -v "^micromark-extension-llm-math$" || echo "")
echo "CLIENT_UNUSED<<EOF" >> $GITHUB_ENV
@@ -240,8 +113,7 @@ jobs:
chmod -R 755 api
cd api
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
# Exclude dependencies used in scripts, code, workspace packages, and @librechat/api imports
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../api_used_deps.txt ../api_used_code.txt ../api_workspace_deps.txt ../packages_api_used_code.txt ../librechat_api_deps.txt 2>/dev/null | sort -u) || echo "")
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../api_used_deps.txt ../api_used_code.txt | sort) || echo "")
echo "API_UNUSED<<EOF" >> $GITHUB_ENV
echo "$UNUSED" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
@@ -280,4 +152,4 @@ jobs:
- name: Fail workflow if unused dependencies found
if: env.ROOT_UNUSED != '' || env.CLIENT_UNUSED != '' || env.API_UNUSED != ''
run: exit 1
run: exit 1
-54
View File
@@ -1,54 +0,0 @@
import os
import requests
from datetime import datetime
# GitHub API endpoints
GITHUB_API_URL = "https://api.github.com"
REPO_OWNER = "BerriAI"
REPO_NAME = "litellm"
# GitHub personal access token (required for uploading release assets)
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN")
# Headers for GitHub API requests
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {GITHUB_ACCESS_TOKEN}",
"X-GitHub-Api-Version": "2022-11-28",
}
# Get the latest release
releases_url = f"{GITHUB_API_URL}/repos/{REPO_OWNER}/{REPO_NAME}/releases/latest"
response = requests.get(releases_url, headers=headers)
latest_release = response.json()
print("Latest release:", latest_release)
# Upload an asset to the latest release
upload_url = latest_release["upload_url"].split("{?")[0]
asset_name = "results_stats.csv"
asset_path = os.path.join(os.getcwd(), asset_name)
print("upload_url:", upload_url)
with open(asset_path, "rb") as asset_file:
asset_data = asset_file.read()
upload_payload = {
"name": asset_name,
"label": "Load test results",
"created_at": datetime.utcnow().isoformat() + "Z",
}
upload_headers = headers.copy()
upload_headers["Content-Type"] = "application/octet-stream"
upload_response = requests.post(
upload_url,
headers=upload_headers,
data=asset_data,
params=upload_payload,
)
if upload_response.status_code == 201:
print(f"Asset '{asset_name}' uploaded successfully to the latest release.")
else:
print(f"Failed to upload asset. Response: {upload_response.text}")
-7
View File
@@ -1,7 +0,0 @@
name: Workflow Sanity
on:
pull_request:
paths: ['.github/workflows/**']
jobs:
sanity:
uses: hanzoai/.github/.github/workflows/workflow-sanity.yml@main
+112 -137
View File
@@ -1,154 +1,129 @@
### node etc ###
__azurite**
__blobstorage__/**/*
!.env.hanzo-cloud
!**/.env.example
!**/.env.test.example
!/client/src/@types/i18next.d.ts
!client/src/components/Nav/SettingsTabs/Data/
!memory/agents/README.md
!memory/sessions/README.md
!vite-env.d.ts
.aider*
.claude-flow/
.claude/settings.local.json
.clineignore
.cursor
.DS_Store
.env.hanzo
.env*
.floo
.floobit
.flooignore
.git
.grunt
.hive-mind/
.idea
.idx
.mcp.json
.npmrc
.swarm/
.turbo
.vscode/settings.json
*.cert
*.code-workspace
*.d.ts
*.db
*.db-journal
*.db-wal
*.iml
*.local.md
# Logs
data-node
meili_data*
data/
logs
*.log
*.pem
# Runtime data
pids
*.pid
*.seed
*.sqlite
*.sqlite-journal
*.sqlite-wal
**/.venv/
**/dist/
**/storageState.json
**/venv/
/.aider/
/.claude/
/.codeium
/.copilot/
/.cursor/
/.openai/
/.tabnine/
/e2e/playwright-report/
/e2e/specs/.test-results/
/images
/packages/ux-shared/
/playwright/.cache/
# AI
# AI Assistants
# Azure Blob Storage Emulator (Azurite)
# CI/CD data
# Claude Flow generated files
# Compiled Dirs (http://nodejs.org/api/addons.html)
# Coverage directory used by tools like istanbul
# Dependency directorys
# Deployed apps should consider commenting these lines out:
.git
# Directory for instrumented libs generated by jscoverage/JSCover
# docker override file
# Environment
# Floobits
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
# Hanzo-specific
# Helm
# Logs
# meilisearch
# owner
# Removed Windows wrapper files per user request
# Runtime data
# SAML Idp cert
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
.grunt
# translation services
# Turborepo
# User uploads
### node etc ###
#config file
api/data/
api/node_modules/
archive
auth.json
bower_components/
config/translations/stores/*
client/src/localization/languages/*_missing_keys.json
# Compiled Dirs (http://nodejs.org/api/addons.html)
build/
cache.json
claude-flow
claude-flow.config.json
CLAUDE.md
client/node_modules/
dist/
public/main.js
public/main.js.map
public/main.js.LICENSE.txt
client/public/images/
client/public/main.js
client/public/main.js.LICENSE.txt
client/public/main.js.map
client/src/localization/languages/*_missing_keys.json
client/public/main.js.LICENSE.txt
# Azure Blob Storage Emulator (Azurite)
__azurite**
__blobstorage__/**/*
# Dependency directorys
# Deployed apps should consider commenting these lines out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
node_modules/
meili_data/
api/node_modules/
client/node_modules/
bower_components/
*.d.ts
!vite-env.d.ts
# AI
.clineignore
.cursor
.aider*
# Floobits
.floo
.floobit
.floo
.flooignore
#config file
chat.yaml
chat.yml
# Environment
.npmrc
.env*
my.secrets
!**/.env.example
!**/.env.test.example
cache.json
api/data/
owner.yml
archive
.vscode/settings.json
src/style - official.css
/e2e/specs/.test-results/
/e2e/playwright-report/
/playwright/.cache/
.DS_Store
*.code-workspace
.idx
monospace.json
.idea
*.iml
*.pem
config.local.ts
config/translations/stores/*
coordination/
coordination/memory_bank/*
coordination/orchestration/*
coordination/subtasks/*
coverage
data-node
data.ms/*
data/
dist/
**/storageState.json
junit.xml
**/.venv/
**/venv/
# docker override file
docker-compose.override.yaml
docker-compose.override.yml
dump.rdb
helm/**/.values.yaml
helm/**/charts/
helm/librechat/Chart.lock
hive-mind-prompt-*.txt
junit.xml
lib-cov
librechat.yaml
librechat.yml
logs
meili_data*
meili_data/
# meilisearch
meilisearch
meilisearch.exe
memory/
memory/agents/*
memory/claude-flow-data.json
memory/sessions/*
monospace.json
my.secrets
node_modules/
owner.yml
pids
public/main.js
public/main.js.LICENSE.txt
public/main.js.map
release/
src/style - official.css
test-image*
tests/**/*.jpg
tests/**/*.mp3
tests/**/*.png
tests/**/*.wav
data.ms/*
auth.json
/packages/ux-shared/
/images
!client/src/components/Nav/SettingsTabs/Data/
# User uploads
uploads/
# owner
release/
# Helm
helm/librechat/Chart.lock
helm/**/charts/
helm/**/.values.yaml
!/client/src/@types/i18next.d.ts
# SAML Idp cert
*.cert
*.orig
.claude
+34 -2
View File
@@ -1,2 +1,34 @@
[ -n "$CI" ] && exit 0
npx lint-staged --config ./.husky/lint-staged.config.js
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Run linting on staged files
npx lint-staged
# Run tests for changed files
CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx)$' || true)
if [ -n "$CHANGED_FILES" ]; then
echo "Running tests for changed files..."
# Check if client files changed
if echo "$CHANGED_FILES" | grep -q "^client/"; then
echo "Client files changed, running client tests..."
cd client && npm run test:ci -- --passWithNoTests --bail
cd ..
fi
# Check if API files changed
if echo "$CHANGED_FILES" | grep -q "^api/"; then
echo "API files changed, running API tests..."
cd api && npm run test:ci -- --passWithNoTests --bail --maxWorkers=2
cd ..
fi
# Check if package files changed
if echo "$CHANGED_FILES" | grep -q "^packages/"; then
echo "Package files changed, building packages..."
npm run build:data-provider
npm run build:data-schemas
npm run build:api
fi
fi

Some files were not shown because too many files have changed in this diff Show More