Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
660fe80e7c | ||
|
|
2fcc04b4c9 | ||
|
|
76066fdb41 | ||
|
|
7f91fa5205 | ||
|
|
03a3842702 | ||
|
|
6fd5f38d93 | ||
|
|
c8653b2cab | ||
|
|
d254ca0f02 | ||
|
|
94abdccfc5 | ||
|
|
a26e7eebb2 | ||
|
|
8c41dfb6a1 |
@@ -1,72 +0,0 @@
|
||||
name: Docker Dev Branch Images Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
- 'packages/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: lc-dev-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: lc-dev
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# 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
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v6
|
||||
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 }}
|
||||
@@ -1,72 +0,0 @@
|
||||
name: Docker Dev Images Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'api/**'
|
||||
- 'client/**'
|
||||
- 'packages/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: chat-dev-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: chat-dev
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# 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
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v6
|
||||
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 }}
|
||||
@@ -1,66 +0,0 @@
|
||||
name: Docker Dev Staging Images Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
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@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Prepare the environment
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ github.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 }}
|
||||
|
||||
@@ -11,12 +11,17 @@ concurrency:
|
||||
group: docker-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
uses: hanzoai/.github/.github/workflows/docker-build.yml@main
|
||||
with:
|
||||
image: ghcr.io/hanzoai/chat
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
secrets: inherit
|
||||
|
||||
dispatch-to-universe:
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
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: ubuntu-latest
|
||||
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
|
||||
@@ -1,433 +0,0 @@
|
||||
# this workflow is triggered by an API call when there is a new PyPI release of LiteLLM
|
||||
name: Build, Publish LiteLLM Docker Image. New Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "The tag version you want to build"
|
||||
release_type:
|
||||
description: "The release type you want to build. Can be 'latest', 'stable', 'dev', 'rc'"
|
||||
type: string
|
||||
default: "latest"
|
||||
commit_hash:
|
||||
description: "Commit hash"
|
||||
required: true
|
||||
|
||||
# Defines two custom environment variables for the workflow. Used for the Container registry domain, and a name for the Docker image that this workflow builds.
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
CHART_NAME: litellm-helm
|
||||
|
||||
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
|
||||
jobs:
|
||||
# print commit hash, tag, and release type
|
||||
print:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "Commit hash: ${{ github.event.inputs.commit_hash }}"
|
||||
echo "Tag: ${{ github.event.inputs.tag }}"
|
||||
echo "Release type: ${{ github.event.inputs.release_type }}"
|
||||
docker-hub-deploy:
|
||||
if: github.repository == 'BerriAI/litellm'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_hash }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: litellm/litellm:${{ github.event.inputs.tag || 'latest' }}
|
||||
-
|
||||
name: Build and push litellm-database image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: ./docker/Dockerfile.database
|
||||
tags: litellm/litellm-database:${{ github.event.inputs.tag || 'latest' }}
|
||||
-
|
||||
name: Build and push litellm-spend-logs image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: ./litellm-js/spend-logs/Dockerfile
|
||||
tags: litellm/litellm-spend_logs:${{ github.event.inputs.tag || 'latest' }}
|
||||
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_hash }}
|
||||
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@916386b00027d425839f8da46d302dab33f5875b
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# Configure multi platform Docker builds
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@e0e4588fad221d38ee467c0bffd91115366dc0c5
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@edfb0fe6204400c56fbfd3feba3fe9ad1adfa345
|
||||
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
|
||||
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
|
||||
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@64c9b141502b80dbbd71e008a0130ad330f480f8
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta.outputs.tags }}-${{ github.event.inputs.tag || 'latest' }},
|
||||
${{ steps.meta.outputs.tags }}-${{ github.event.inputs.release_type }}
|
||||
${{ (github.event.inputs.release_type == 'stable' || github.event.inputs.release_type == 'rc') && format('{0}/berriai/litellm:main-{1}', env.REGISTRY, github.event.inputs.tag) || '' }},
|
||||
${{ github.event.inputs.release_type == 'stable' && format('{0}/berriai/litellm:main-stable', env.REGISTRY) || '' }},
|
||||
${{ (github.event.inputs.release_type == 'stable' || github.event.inputs.release_type == 'rc') && format('{0}/berriai/litellm:{1}', env.REGISTRY, github.event.inputs.tag) || '' }},
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: local,linux/amd64,linux/arm64,linux/arm64/v8
|
||||
|
||||
build-and-push-image-ee:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_hash }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@916386b00027d425839f8da46d302dab33f5875b
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for EE Dockerfile
|
||||
id: meta-ee
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee
|
||||
# Configure multi platform Docker builds
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@e0e4588fad221d38ee467c0bffd91115366dc0c5
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@edfb0fe6204400c56fbfd3feba3fe9ad1adfa345
|
||||
|
||||
- name: Build and push EE Docker image
|
||||
uses: docker/build-push-action@64c9b141502b80dbbd71e008a0130ad330f480f8
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta-ee.outputs.tags }}-${{ github.event.inputs.tag || 'latest' }},
|
||||
${{ steps.meta-ee.outputs.tags }}-${{ github.event.inputs.release_type }}
|
||||
${{ (github.event.inputs.release_type == 'stable' || github.event.inputs.release_type == 'rc') && format('{0}/berriai/litellm-ee:main-{1}', env.REGISTRY, github.event.inputs.tag) || '' }},
|
||||
${{ github.event.inputs.release_type == 'stable' && format('{0}/berriai/litellm-ee:main-stable', env.REGISTRY) || '' }}
|
||||
labels: ${{ steps.meta-ee.outputs.labels }}
|
||||
platforms: local,linux/amd64,linux/arm64,linux/arm64/v8
|
||||
|
||||
build-and-push-image-database:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_hash }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@916386b00027d425839f8da46d302dab33f5875b
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for database Dockerfile
|
||||
id: meta-database
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-database
|
||||
# Configure multi platform Docker builds
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@e0e4588fad221d38ee467c0bffd91115366dc0c5
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@edfb0fe6204400c56fbfd3feba3fe9ad1adfa345
|
||||
|
||||
- name: Build and push Database Docker image
|
||||
uses: docker/build-push-action@64c9b141502b80dbbd71e008a0130ad330f480f8
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile.database
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta-database.outputs.tags }}-${{ github.event.inputs.tag || 'latest' }},
|
||||
${{ steps.meta-database.outputs.tags }}-${{ github.event.inputs.release_type }}
|
||||
${{ (github.event.inputs.release_type == 'stable' || github.event.inputs.release_type == 'rc') && format('{0}/berriai/litellm-database:main-{1}', env.REGISTRY, github.event.inputs.tag) || '' }},
|
||||
${{ github.event.inputs.release_type == 'stable' && format('{0}/berriai/litellm-database:main-stable', env.REGISTRY) || '' }}
|
||||
labels: ${{ steps.meta-database.outputs.labels }}
|
||||
platforms: local,linux/amd64,linux/arm64,linux/arm64/v8
|
||||
|
||||
build-and-push-image-non_root:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_hash }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@916386b00027d425839f8da46d302dab33f5875b
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for non_root Dockerfile
|
||||
id: meta-non_root
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-non_root
|
||||
# Configure multi platform Docker builds
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@e0e4588fad221d38ee467c0bffd91115366dc0c5
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@edfb0fe6204400c56fbfd3feba3fe9ad1adfa345
|
||||
|
||||
- name: Build and push non_root Docker image
|
||||
uses: docker/build-push-action@64c9b141502b80dbbd71e008a0130ad330f480f8
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile.non_root
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta-non_root.outputs.tags }}-${{ github.event.inputs.tag || 'latest' }},
|
||||
${{ steps.meta-non_root.outputs.tags }}-${{ github.event.inputs.release_type }}
|
||||
${{ (github.event.inputs.release_type == 'stable' || github.event.inputs.release_type == 'rc') && format('{0}/berriai/litellm-non_root:main-{1}', env.REGISTRY, github.event.inputs.tag) || '' }},
|
||||
${{ github.event.inputs.release_type == 'stable' && format('{0}/berriai/litellm-non_root:main-stable', env.REGISTRY) || '' }}
|
||||
labels: ${{ steps.meta-non_root.outputs.labels }}
|
||||
platforms: local,linux/amd64,linux/arm64,linux/arm64/v8
|
||||
|
||||
build-and-push-image-spend-logs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.commit_hash }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@916386b00027d425839f8da46d302dab33f5875b
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for spend-logs Dockerfile
|
||||
id: meta-spend-logs
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-spend_logs
|
||||
# Configure multi platform Docker builds
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@e0e4588fad221d38ee467c0bffd91115366dc0c5
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@edfb0fe6204400c56fbfd3feba3fe9ad1adfa345
|
||||
|
||||
- name: Build and push Database Docker image
|
||||
uses: docker/build-push-action@64c9b141502b80dbbd71e008a0130ad330f480f8
|
||||
with:
|
||||
context: .
|
||||
file: ./litellm-js/spend-logs/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta-spend-logs.outputs.tags }}-${{ github.event.inputs.tag || 'latest' }},
|
||||
${{ steps.meta-spend-logs.outputs.tags }}-${{ github.event.inputs.release_type }}
|
||||
${{ (github.event.inputs.release_type == 'stable' || github.event.inputs.release_type == 'rc') && format('{0}/berriai/litellm-spend_logs:main-{1}', env.REGISTRY, github.event.inputs.tag) || '' }},
|
||||
${{ github.event.inputs.release_type == 'stable' && format('{0}/berriai/litellm-spend_logs:main-stable', env.REGISTRY) || '' }}
|
||||
platforms: local,linux/amd64,linux/arm64,linux/arm64/v8
|
||||
|
||||
build-and-push-helm-chart:
|
||||
if: github.event.inputs.release_type != 'dev'
|
||||
needs: [docker-hub-deploy, build-and-push-image, build-and-push-image-database]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@916386b00027d425839f8da46d302dab33f5875b
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: lowercase github.repository_owner
|
||||
run: |
|
||||
echo "REPO_OWNER=`echo ${{github.repository_owner}} | tr '[:upper:]' '[:lower:]'`" >>${GITHUB_ENV}
|
||||
|
||||
- name: Get LiteLLM Latest Tag
|
||||
id: current_app_tag
|
||||
shell: bash
|
||||
run: |
|
||||
LATEST_TAG=$(git describe --tags --exclude "*dev*" --abbrev=0)
|
||||
if [ -z "${LATEST_TAG}" ]; then
|
||||
echo "latest_tag=latest" | tee -a $GITHUB_OUTPUT
|
||||
else
|
||||
echo "latest_tag=${LATEST_TAG}" | tee -a $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Get last published chart version
|
||||
id: current_version
|
||||
shell: bash
|
||||
run: |
|
||||
CHART_LIST=$(helm show chart oci://${{ env.REGISTRY }}/${{ env.REPO_OWNER }}/${{ env.CHART_NAME }} 2>/dev/null || true)
|
||||
if [ -z "${CHART_LIST}" ]; then
|
||||
echo "current-version=0.1.0" | tee -a $GITHUB_OUTPUT
|
||||
else
|
||||
printf '%s' "${CHART_LIST}" | grep '^version:' | awk 'BEGIN{FS=":"}{print "current-version="$2}' | tr -d " " | tee -a $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
HELM_EXPERIMENTAL_OCI: '1'
|
||||
|
||||
# Automatically update the helm chart version one "patch" level
|
||||
- name: Bump release version
|
||||
id: bump_version
|
||||
uses: christian-draeger/increment-semantic-version@1.2.3
|
||||
with:
|
||||
current-version: ${{ steps.current_version.outputs.current-version || '0.1.0' }}
|
||||
version-fragment: 'bug'
|
||||
|
||||
- uses: ./.github/actions/helm-oci-chart-releaser
|
||||
with:
|
||||
name: ${{ env.CHART_NAME }}
|
||||
repository: ${{ env.REPO_OWNER }}
|
||||
tag: ${{ github.event.inputs.chartVersion || steps.bump_version.outputs.next-version || '0.1.0' }}
|
||||
app_version: ${{ steps.current_app_tag.outputs.latest_tag }}
|
||||
path: deploy/charts/${{ env.CHART_NAME }}
|
||||
registry: ${{ env.REGISTRY }}
|
||||
registry_username: ${{ github.actor }}
|
||||
registry_password: ${{ secrets.GITHUB_TOKEN }}
|
||||
update_dependencies: true
|
||||
|
||||
release:
|
||||
name: "New LiteLLM Release"
|
||||
needs: [docker-hub-deploy, build-and-push-image, build-and-push-image-database]
|
||||
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
steps:
|
||||
- name: Display version
|
||||
run: echo "Current version is ${{ github.event.inputs.tag }}"
|
||||
- name: "Set Release Tag"
|
||||
run: echo "RELEASE_TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
||||
- name: Display release tag
|
||||
run: echo "RELEASE_TAG is $RELEASE_TAG"
|
||||
- name: "Create release"
|
||||
uses: "actions/github-script@v8"
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
script: |
|
||||
const commitHash = "${{ github.event.inputs.commit_hash}}";
|
||||
console.log("Commit Hash:", commitHash); // Add this line for debugging
|
||||
try {
|
||||
const response = await github.rest.repos.createRelease({
|
||||
draft: false,
|
||||
generate_release_notes: true,
|
||||
target_commitish: commitHash,
|
||||
name: process.env.RELEASE_TAG,
|
||||
owner: context.repo.owner,
|
||||
prerelease: false,
|
||||
repo: context.repo.repo,
|
||||
tag_name: process.env.RELEASE_TAG,
|
||||
});
|
||||
|
||||
core.exportVariable('RELEASE_ID', response.data.id);
|
||||
core.exportVariable('RELEASE_UPLOAD_URL', response.data.upload_url);
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
- name: Fetch Release Notes
|
||||
id: release-notes
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
script: |
|
||||
try {
|
||||
const response = await github.rest.repos.getRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.RELEASE_ID,
|
||||
});
|
||||
const formattedBody = JSON.stringify(response.data.body).slice(1, -1);
|
||||
return formattedBody;
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
env:
|
||||
RELEASE_ID: ${{ env.RELEASE_ID }}
|
||||
- name: Github Releases To Discord
|
||||
env:
|
||||
WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}
|
||||
REALEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
RELEASE_NOTES: ${{ steps.release-notes.outputs.result }}
|
||||
run: |
|
||||
curl -H "Content-Type: application/json" -X POST -d '{
|
||||
"content": "New LiteLLM release '"${RELEASE_TAG}"'",
|
||||
"username": "Release Changelog",
|
||||
"avatar_url": "https://cdn.discordapp.com/avatars/487431320314576937/bd64361e4ba6313d561d54e78c9e7171.png",
|
||||
"embeds": [
|
||||
{
|
||||
"title": "Changelog for LiteLLM '"${RELEASE_TAG}"'",
|
||||
"description": "'"${RELEASE_NOTES}"'",
|
||||
"color": 2105893
|
||||
}
|
||||
]
|
||||
}' $WEBHOOK_URL
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# this workflow is triggered by an API call when there is a new PyPI release of LiteLLM
|
||||
name: Build, Publish LiteLLM Helm Chart. New Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
chartVersion:
|
||||
description: "Update the helm chart's version to this"
|
||||
|
||||
# Defines two custom environment variables for the workflow. Used for the Container registry domain, and a name for the Docker image that this workflow builds.
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
REPO_OWNER: ${{github.repository_owner}}
|
||||
|
||||
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
|
||||
jobs:
|
||||
build-and-push-helm-chart:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@916386b00027d425839f8da46d302dab33f5875b
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: lowercase github.repository_owner
|
||||
run: |
|
||||
echo "REPO_OWNER=`echo ${{github.repository_owner}} | tr '[:upper:]' '[:lower:]'`" >>${GITHUB_ENV}
|
||||
|
||||
- name: Get LiteLLM Latest Tag
|
||||
id: current_app_tag
|
||||
uses: WyriHaximus/github-action-get-previous-tag@v1.4.0
|
||||
|
||||
- name: Get last published chart version
|
||||
id: current_version
|
||||
shell: bash
|
||||
run: helm show chart oci://${{ env.REGISTRY }}/${{ env.REPO_OWNER }}/litellm-helm | grep '^version:' | awk 'BEGIN{FS=":"}{print "current-version="$2}' | tr -d " " | tee -a $GITHUB_OUTPUT
|
||||
env:
|
||||
HELM_EXPERIMENTAL_OCI: '1'
|
||||
|
||||
# Automatically update the helm chart version one "patch" level
|
||||
- name: Bump release version
|
||||
id: bump_version
|
||||
uses: christian-draeger/increment-semantic-version@1.2.3
|
||||
with:
|
||||
current-version: ${{ steps.current_version.outputs.current-version || '0.1.0' }}
|
||||
version-fragment: 'bug'
|
||||
|
||||
- name: Lint helm chart
|
||||
run: helm lint deploy/charts/litellm-helm
|
||||
|
||||
- uses: ./.github/actions/helm-oci-chart-releaser
|
||||
with:
|
||||
name: litellm-helm
|
||||
repository: ${{ env.REPO_OWNER }}
|
||||
tag: ${{ github.event.inputs.chartVersion || steps.bump_version.outputs.next-version || '0.1.0' }}
|
||||
app_version: ${{ steps.current_app_tag.outputs.tag || 'latest' }}
|
||||
path: deploy/charts/litellm-helm
|
||||
registry: ${{ env.REGISTRY }}
|
||||
registry_username: ${{ github.actor }}
|
||||
registry_password: ${{ secrets.GITHUB_TOKEN }}
|
||||
update_dependencies: true
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
name: Docker Compose Build Latest Main Image Tag (Manual Dispatch)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: chat-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: chat
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Fetch tags and set the latest tag
|
||||
run: |
|
||||
git fetch --tags
|
||||
echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
|
||||
|
||||
# Set up 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
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ env.LATEST_TAG }}
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ env.LATEST_TAG }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: ${{ matrix.target }}
|
||||
@@ -0,0 +1,21 @@
|
||||
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
|
||||
@@ -1,67 +0,0 @@
|
||||
name: Docker Images Build on Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-build
|
||||
file: Dockerfile.multi
|
||||
image_name: chat-api
|
||||
- target: node
|
||||
file: Dockerfile
|
||||
image_name: chat
|
||||
|
||||
steps:
|
||||
# Check out the repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# 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
|
||||
|
||||
# Build and push Docker images for each target
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:${{ github.ref_name }}
|
||||
ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}:latest
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:${{ github.ref_name }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.image_name }}:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
target: ${{ matrix.target }}
|
||||
@@ -0,0 +1,7 @@
|
||||
name: Workflow Sanity
|
||||
on:
|
||||
pull_request:
|
||||
paths: ['.github/workflows/**']
|
||||
jobs:
|
||||
sanity:
|
||||
uses: hanzoai/.github/.github/workflows/workflow-sanity.yml@main
|
||||
@@ -4,6 +4,7 @@ AI chat interface with multi-model support,
|
||||
MCP integration, agents, and RAG. Live at **hanzo.chat**.
|
||||
|
||||
**Repo**: `github.com/hanzoai/chat`
|
||||
**Upstream**: LibreChat (MIT) — internal package names kept (`@librechat/*`)
|
||||
**Package**: `@hanzochat/chat`
|
||||
**Runtime**: Node.js 20 (Alpine)
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ class CommerceClient {
|
||||
async createTrialGrant(userId, amountCents, expiryDays, eligibility = []) {
|
||||
try {
|
||||
const expiresIn = `${expiryDays * 24}h`;
|
||||
const resp = await this._request('POST', '/api/v1/billing/credit-grants', {
|
||||
const resp = await this._request('POST', '/v1/billing/credit-grants', {
|
||||
userId,
|
||||
name: 'Trial Credit',
|
||||
amountCents,
|
||||
@@ -184,7 +184,7 @@ class CommerceClient {
|
||||
try {
|
||||
const resp = await this._request(
|
||||
'GET',
|
||||
`/api/v1/billing/credit-balance/breakdown?userId=${encodeURIComponent(userId)}`,
|
||||
`/v1/billing/credit-balance/breakdown?userId=${encodeURIComponent(userId)}`,
|
||||
);
|
||||
const breakdown = resp.breakdown || {};
|
||||
return {
|
||||
@@ -204,7 +204,7 @@ class CommerceClient {
|
||||
try {
|
||||
const resp = await this._request(
|
||||
'GET',
|
||||
`/api/v1/billing/balance?user=${encodeURIComponent(userId)}¤cy=usd`,
|
||||
`/v1/billing/balance?user=${encodeURIComponent(userId)}¤cy=usd`,
|
||||
);
|
||||
const data = {
|
||||
sufficient: (resp.available || 0) > 0,
|
||||
@@ -225,7 +225,7 @@ class CommerceClient {
|
||||
|
||||
async _fetchTier(userId, tierName) {
|
||||
try {
|
||||
let url = `/api/v1/billing/tier-check?user=${encodeURIComponent(userId)}`;
|
||||
let url = `/v1/billing/tier-check?user=${encodeURIComponent(userId)}`;
|
||||
if (tierName) {
|
||||
url += `&tier=${encodeURIComponent(tierName)}`;
|
||||
}
|
||||
@@ -255,7 +255,7 @@ class CommerceClient {
|
||||
|
||||
for (const usage of batch) {
|
||||
try {
|
||||
await this._request('POST', '/api/v1/billing/usage', usage);
|
||||
await this._request('POST', '/v1/billing/usage', usage);
|
||||
} catch (err) {
|
||||
logger.warn('[CommerceClient] Usage recording failed', {
|
||||
user: usage.user,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="description" content="Hanzo Chat - AI chat platform with support for multiple AI models" />
|
||||
<title>Hanzo Chat</title>
|
||||
<link rel="icon" type="image/svg+xml" href="assets/logo.svg" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" href="assets/apple-touch-icon-180x180.png" />
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@codesandbox/sandpack-react": "^2.19.10",
|
||||
"@dicebear/collection": "^9.2.2",
|
||||
"@dicebear/core": "^9.2.2",
|
||||
"@hanzo/iam": "^0.12.2",
|
||||
"@headlessui/react": "^2.1.2",
|
||||
"@librechat/client": "workspace:*",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -4,7 +4,8 @@ async function postBuild() {
|
||||
try {
|
||||
await fs.copy('public/assets', 'dist/assets');
|
||||
await fs.copy('public/robots.txt', 'dist/robots.txt');
|
||||
console.log('✅ PWA icons and robots.txt copied successfully. Glob pattern warnings resolved.');
|
||||
await fs.copy('public/favicon.ico', 'dist/favicon.ico');
|
||||
console.log('✅ PWA icons, favicon.ico, and robots.txt copied successfully. Glob pattern warnings resolved.');
|
||||
} catch (err) {
|
||||
console.error('❌ Error copying files:', err);
|
||||
process.exit(1);
|
||||
|
||||
+13
-3
@@ -1,10 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { lazy, Suspense, useEffect } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import * as RadixToast from '@radix-ui/react-toast';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { Toast, ThemeProvider, ToastProvider } from '@librechat/client';
|
||||
import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query';
|
||||
import { ScreenshotProvider, useApiErrorBoundary } from './hooks';
|
||||
@@ -14,6 +13,13 @@ import { initializeFontSize } from '~/store/fontSize';
|
||||
import { LiveAnnouncer } from '~/a11y';
|
||||
import { router } from './routes';
|
||||
|
||||
// Dev-only: lazily loaded so the devtools bundle never ships in production builds.
|
||||
const ReactQueryDevtools = import.meta.env.DEV
|
||||
? lazy(() =>
|
||||
import('@tanstack/react-query-devtools').then((m) => ({ default: m.ReactQueryDevtools })),
|
||||
)
|
||||
: null;
|
||||
|
||||
const App = () => {
|
||||
const { setError } = useApiErrorBoundary();
|
||||
|
||||
@@ -63,7 +69,11 @@ const App = () => {
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<RouterProvider router={router} />
|
||||
<WakeLockManager />
|
||||
<ReactQueryDevtools initialIsOpen={false} position="top-right" />
|
||||
{import.meta.env.DEV && ReactQueryDevtools && (
|
||||
<Suspense fallback={null}>
|
||||
<ReactQueryDevtools initialIsOpen={false} position="top-right" />
|
||||
</Suspense>
|
||||
)}
|
||||
<Toast />
|
||||
<RadixToast.Viewport className="pointer-events-none fixed inset-0 z-[1000] mx-auto my-2 flex max-w-[560px] flex-col items-stretch justify-start md:pb-5" />
|
||||
</DndProvider>
|
||||
|
||||
@@ -2,44 +2,16 @@ import { useEffect, useState } from 'react';
|
||||
import { ErrorTypes, registerPage } from 'librechat-data-provider';
|
||||
import { OpenIDIcon, useToastContext } from '@librechat/client';
|
||||
import { useOutletContext, useSearchParams } from 'react-router-dom';
|
||||
import { Login as IamLogin } from '@hanzo/iam/views';
|
||||
import type { TLoginLayoutContext } from '~/common';
|
||||
import { ErrorMessage } from '~/components/Auth/ErrorMessage';
|
||||
import SocialButton from '~/components/Auth/SocialButton';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { getLoginError } from '~/utils';
|
||||
import { getHanzoIamConfig, isStaticIamMode } from '~/utils/iam';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import LoginForm from './LoginForm';
|
||||
|
||||
/**
|
||||
* Build Hanzo IAM OIDC authorize URL from env vars.
|
||||
* Used in static SPA mode when VITE_HANZO_IAM_URL is set.
|
||||
*/
|
||||
function getHanzoIamUrl(): string | null {
|
||||
const iamUrl = import.meta.env.VITE_HANZO_IAM_URL;
|
||||
const appId = import.meta.env.VITE_HANZO_IAM_APP;
|
||||
const org = import.meta.env.VITE_HANZO_IAM_ORG;
|
||||
|
||||
if (!iamUrl || !appId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const redirectUri = `${window.location.origin}/auth/callback`;
|
||||
const state = `hanzo-chat-${Date.now()}`;
|
||||
|
||||
// Store state for CSRF verification
|
||||
sessionStorage.setItem('oauth_state', state);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: appId,
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
state,
|
||||
scope: 'openid profile email',
|
||||
});
|
||||
|
||||
return `${iamUrl}/oauth/authorize?${params.toString()}`;
|
||||
}
|
||||
|
||||
function Login() {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
@@ -53,9 +25,9 @@ function Login() {
|
||||
// Persist the disable flag locally so that once detected, auto-redirect stays disabled.
|
||||
const [isAutoRedirectDisabled, setIsAutoRedirectDisabled] = useState(disableAutoRedirect);
|
||||
|
||||
// Check if we're in static/IAM mode
|
||||
const hanzoIamUrl = getHanzoIamUrl();
|
||||
const isStaticMode = !!import.meta.env.VITE_HANZO_API_URL;
|
||||
// Embedded IAM login (in-app form; no redirect to the IAM-hosted page).
|
||||
const iamConfig = getHanzoIamConfig();
|
||||
const isStaticMode = isStaticIamMode();
|
||||
|
||||
useEffect(() => {
|
||||
const oauthError = searchParams?.get('error');
|
||||
@@ -81,24 +53,20 @@ function Login() {
|
||||
}, [disableAutoRedirect, searchParams, setSearchParams]);
|
||||
|
||||
// Determine whether we should auto-redirect to OpenID or Hanzo IAM
|
||||
// Embedded IAM login does NOT auto-redirect — the form renders in-app.
|
||||
// Only the upstream OpenID provider (non-static mode) auto-redirects.
|
||||
const shouldAutoRedirect =
|
||||
!isAutoRedirectDisabled &&
|
||||
((startupConfig?.openidLoginEnabled &&
|
||||
startupConfig?.openidAutoRedirect &&
|
||||
startupConfig?.serverDomain) ||
|
||||
(isStaticMode && hanzoIamUrl));
|
||||
!isStaticMode &&
|
||||
startupConfig?.openidLoginEnabled &&
|
||||
startupConfig?.openidAutoRedirect &&
|
||||
startupConfig?.serverDomain;
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAutoRedirect) {
|
||||
if (isStaticMode && hanzoIamUrl) {
|
||||
console.log('Auto-redirecting to Hanzo IAM...');
|
||||
window.location.href = hanzoIamUrl;
|
||||
} else if (startupConfig?.serverDomain) {
|
||||
console.log('Auto-redirecting to OpenID provider...');
|
||||
window.location.href = `${startupConfig.serverDomain}/oauth/openid`;
|
||||
}
|
||||
if (shouldAutoRedirect && startupConfig?.serverDomain) {
|
||||
window.location.href = `${startupConfig.serverDomain}/oauth/openid`;
|
||||
}
|
||||
}, [shouldAutoRedirect, startupConfig, isStaticMode, hanzoIamUrl]);
|
||||
}, [shouldAutoRedirect, startupConfig]);
|
||||
|
||||
// Render fallback UI if auto-redirect is active.
|
||||
if (shouldAutoRedirect) {
|
||||
@@ -132,22 +100,53 @@ function Login() {
|
||||
);
|
||||
}
|
||||
|
||||
// Embedded IAM is only available when the static-mode VITE_* config is built in.
|
||||
const hasEmbeddedIam = isStaticMode && iamConfig != null;
|
||||
|
||||
// OpenID is the working fallback whenever the embedded IAM form and the
|
||||
// email/password form are both unavailable — guarantees a usable login path
|
||||
// (server-proxied /oauth/openid -> hanzo.id) regardless of build-time config.
|
||||
const showOpenIDFallback =
|
||||
!hasEmbeddedIam &&
|
||||
startupConfig?.emailLoginEnabled !== true &&
|
||||
startupConfig?.openidLoginEnabled === true &&
|
||||
startupConfig?.serverDomain != null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{error != null && <ErrorMessage>{localize(getLoginError(error))}</ErrorMessage>}
|
||||
|
||||
{/* Hanzo IAM button (static mode) */}
|
||||
{isStaticMode && hanzoIamUrl && (
|
||||
{/* Embedded Hanzo IAM login — in-app form, stays on hanzo.chat. */}
|
||||
{hasEmbeddedIam && iamConfig && (
|
||||
<div className="mt-4">
|
||||
<a
|
||||
href={hanzoIamUrl}
|
||||
className="flex w-full items-center justify-center space-x-3 rounded-2xl border border-border-light bg-surface-primary px-5 py-3 text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
|
||||
>
|
||||
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
||||
</svg>
|
||||
<p>Sign in with Hanzo</p>
|
||||
</a>
|
||||
<IamLogin
|
||||
serverUrl={iamConfig.serverUrl}
|
||||
clientId={iamConfig.clientId}
|
||||
organization={iamConfig.organization}
|
||||
redirectUri={iamConfig.redirectUri}
|
||||
state=""
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OpenID fallback button (e.g. hanzo.id) when no in-app form is available. */}
|
||||
{showOpenIDFallback && startupConfig && (
|
||||
<div className="mt-4">
|
||||
<SocialButton
|
||||
key="openid"
|
||||
enabled={startupConfig.openidLoginEnabled}
|
||||
serverDomain={startupConfig.serverDomain}
|
||||
oauthPath="openid"
|
||||
Icon={() =>
|
||||
startupConfig.openidImageUrl ? (
|
||||
<img src={startupConfig.openidImageUrl} alt="OpenID Logo" className="h-5 w-5" />
|
||||
) : (
|
||||
<OpenIDIcon />
|
||||
)
|
||||
}
|
||||
label={startupConfig.openidLabel}
|
||||
id="openid"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -2,66 +2,42 @@
|
||||
* OAuth Callback Handler for Hanzo IAM (hanzo.id)
|
||||
*
|
||||
* Flow:
|
||||
* 1. User clicks "Sign in" → redirects to hanzo.id OIDC
|
||||
* 1. User clicks "Sign in" -> redirects to hanzo.id OIDC (via BrowserIamSdk)
|
||||
* 2. After auth, hanzo.id redirects back with ?code=xxx&state=yyy
|
||||
* 3. This component exchanges the code via cloud gateway /api/signin
|
||||
* 4. Gateway sets session cookie, returns user claims + access token
|
||||
* 5. We set the auth context and redirect to /c/new
|
||||
* 3. BrowserIamSdk.handleCallback() exchanges the code for tokens (PKCE)
|
||||
* 4. We set the access token header and redirect to /c/new
|
||||
*/
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { setTokenHeader, dataService } from 'librechat-data-provider';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { setTokenHeader } from 'librechat-data-provider';
|
||||
import { Spinner } from '@librechat/client';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { getHanzoIamSdk } from '~/utils/iam';
|
||||
|
||||
export default function OAuthCallback() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuthContext();
|
||||
const exchangedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (exchangedRef.current) {
|
||||
return;
|
||||
}
|
||||
exchangedRef.current = true;
|
||||
|
||||
const code = searchParams.get('code');
|
||||
const state = searchParams.get('state');
|
||||
|
||||
if (!code || !state) {
|
||||
console.error('Missing code or state in OAuth callback');
|
||||
const iamSdk = getHanzoIamSdk();
|
||||
if (!iamSdk) {
|
||||
console.error('IAM SDK not configured in callback');
|
||||
navigate('/login', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
exchangedRef.current = true;
|
||||
|
||||
// Exchange code for session via cloud gateway
|
||||
const exchangeCode = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_HANZO_API_URL || ''}/api/signin?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
);
|
||||
const tokens = await iamSdk.handleCallback();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Signin failed: ${response.status}`);
|
||||
if (tokens.accessToken) {
|
||||
setTokenHeader(tokens.accessToken);
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
const claims = body.status === 'ok' ? body.data : body;
|
||||
|
||||
// Set the access token if present
|
||||
if (claims?.accessToken || claims?.AccessToken) {
|
||||
const token = claims.accessToken || claims.AccessToken;
|
||||
setTokenHeader(token);
|
||||
}
|
||||
|
||||
// Navigate to chat
|
||||
navigate('/c/new', { replace: true });
|
||||
} catch (err) {
|
||||
console.error('OAuth code exchange failed:', err);
|
||||
@@ -70,7 +46,7 @@ export default function OAuthCallback() {
|
||||
};
|
||||
|
||||
exchangeCode();
|
||||
}, [searchParams, navigate, login]);
|
||||
}, [navigate]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
|
||||
@@ -1,326 +1,592 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
import { getHanzoIamSdk } from '~/utils/iam';
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* CSS (injected once) — uses @hanzo/ui CSS variable conventions */
|
||||
/* Light/dark follows prefers-color-scheme + .dark class */
|
||||
/* Design tokens matching dev.hanzo.ai (Geist/fd- design system) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const styleId = 'hanzo-landing-css';
|
||||
function injectStyles() {
|
||||
if (document.getElementById(styleId)) return;
|
||||
const s = document.createElement('style');
|
||||
s.id = styleId;
|
||||
s.textContent = `
|
||||
.hanzo-landing {
|
||||
--hl-bg: #ffffff;
|
||||
--hl-fg: #0a0a0a;
|
||||
--hl-surface: #f5f5f5;
|
||||
--hl-border: rgba(0,0,0,0.08);
|
||||
--hl-border-hover: rgba(0,0,0,0.16);
|
||||
--hl-muted: #737373;
|
||||
--hl-dim: #a3a3a3;
|
||||
--hl-btn-bg: #0a0a0a;
|
||||
--hl-btn-fg: #fafafa;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.hanzo-landing {
|
||||
--hl-bg: #0a0a0a;
|
||||
--hl-fg: #fafafa;
|
||||
--hl-surface: #171717;
|
||||
--hl-border: rgba(255,255,255,0.08);
|
||||
--hl-border-hover: rgba(255,255,255,0.16);
|
||||
--hl-muted: #a3a3a3;
|
||||
--hl-dim: #525252;
|
||||
--hl-btn-bg: #fafafa;
|
||||
--hl-btn-fg: #0a0a0a;
|
||||
}
|
||||
}
|
||||
|
||||
/* Respect .dark class (used by Tailwind dark mode) */
|
||||
.dark .hanzo-landing,
|
||||
.hanzo-landing.dark {
|
||||
--hl-bg: #0a0a0a;
|
||||
--hl-fg: #fafafa;
|
||||
--hl-surface: #171717;
|
||||
--hl-border: rgba(255,255,255,0.08);
|
||||
--hl-border-hover: rgba(255,255,255,0.16);
|
||||
--hl-muted: #a3a3a3;
|
||||
--hl-dim: #525252;
|
||||
--hl-btn-bg: #fafafa;
|
||||
--hl-btn-fg: #0a0a0a;
|
||||
}
|
||||
|
||||
@keyframes hl-logo-spin {
|
||||
0% { opacity: 0; transform: rotateY(180deg) scale(0.6); }
|
||||
60% { opacity: 1; transform: rotateY(-10deg) scale(1.02); }
|
||||
100% { opacity: 1; transform: rotateY(0deg) scale(1); }
|
||||
}
|
||||
@keyframes hl-wordmark-in {
|
||||
from { opacity: 0; max-width: 0; } to { opacity: 1; max-width: 80px; }
|
||||
}
|
||||
@keyframes hl-wordmark-out {
|
||||
from { opacity: 1; max-width: 80px; } to { opacity: 0; max-width: 0; }
|
||||
}
|
||||
@keyframes hl-fade-up {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.hl-input::placeholder { color: var(--hl-dim); }
|
||||
.hl-input:focus { outline: none; }
|
||||
|
||||
.hl-suggestion {
|
||||
border: 1px solid var(--hl-border);
|
||||
background: transparent;
|
||||
color: var(--hl-muted);
|
||||
transition: all 0.15s;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
.hl-suggestion:hover {
|
||||
border-color: var(--hl-border-hover);
|
||||
background: var(--hl-surface);
|
||||
color: var(--hl-fg);
|
||||
}
|
||||
|
||||
.hl-badge {
|
||||
border: 1px solid var(--hl-border);
|
||||
color: var(--hl-dim);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.hl-badge:hover {
|
||||
border-color: var(--hl-border-hover);
|
||||
color: var(--hl-muted);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
const colors = {
|
||||
bg: '#050505', /* hsl(0, 0%, 2%) */
|
||||
card: '#0a0a0a', /* hsl(0, 0%, 4%) */
|
||||
muted: '#1a1a1a', /* hsl(0, 0%, 10%) */
|
||||
mutedFg: 'hsla(0, 0%, 70%, 0.85)',
|
||||
border: 'hsla(0, 0%, 40%, 0.2)',
|
||||
fg: 'hsl(0, 0%, 96%)',
|
||||
brand: '#fd4444',
|
||||
brandDim: 'rgba(253, 68, 68, 0.10)',
|
||||
brandGlow: 'rgba(253, 68, 68, 0.04)',
|
||||
secondary: '#1f1f1f',
|
||||
} as const;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Animated Logo (3D rotation, wordmark show/hide on hover) */
|
||||
/* Inline SVG icons (Lucide-style, no dependencies) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const AnimatedLogo = () => {
|
||||
const [introVisible, setIntroVisible] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const IconTerminal = ({ className = 'size-4' }: { className?: string }) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3M4.5 19.5h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15A2.25 2.25 0 0 0 2.25 6.75v10.5A2.25 2.25 0 0 0 4.5 19.5Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const t1 = setTimeout(() => setIntroVisible(true), 800);
|
||||
const t2 = setTimeout(() => setIntroVisible(false), 2200);
|
||||
return () => { clearTimeout(t1); clearTimeout(t2); };
|
||||
}, []);
|
||||
const IconSparkles = ({ className = 'size-8', style }: { className?: string; style?: React.CSSProperties }) => (
|
||||
<svg className={className} style={style} fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const show = introVisible || hovered;
|
||||
const IconCube = ({ className = 'size-8', style }: { className?: string; style?: React.CSSProperties }) => (
|
||||
<svg className={className} style={style} fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<div style={{ width: 22, height: 22, perspective: 600, animation: 'hl-logo-spin 0.6s cubic-bezier(0.22,1.28,0,1) 0.15s both' }}>
|
||||
<svg viewBox="0 0 67 67" width="22" height="22" xmlns="http://www.w3.org/2000/svg" style={{ color: 'var(--hl-fg)' }}>
|
||||
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="currentColor" />
|
||||
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="currentColor" />
|
||||
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="currentColor" />
|
||||
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="currentColor" />
|
||||
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
letterSpacing: -0.3,
|
||||
color: 'var(--hl-fg)',
|
||||
animation: show ? 'hl-wordmark-in 0.2s ease both' : 'hl-wordmark-out 0.2s ease both',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
Hanzo AI
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const IconPuzzle = ({ className = 'size-8', style }: { className?: string; style?: React.CSSProperties }) => (
|
||||
<svg className={className} style={style} fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 0 1-.657.643 48.39 48.39 0 0 1-4.163-.3c.186 1.613.293 3.25.315 4.907a.656.656 0 0 1-.658.663v0c-.355 0-.676-.186-.959-.401a1.647 1.647 0 0 0-1.003-.349c-1.036 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401v0c.31 0 .555.26.532.57a48.039 48.039 0 0 1-.642 5.056c1.518.19 3.058.309 4.616.354a.64.64 0 0 0 .657-.643v0c0-.355-.186-.676-.401-.959a1.647 1.647 0 0 1-.349-1.003c0-1.035 1.008-1.875 2.25-1.875 1.243 0 2.25.84 2.25 1.875 0 .369-.128.713-.349 1.003-.215.283-.4.604-.4.959v0c0 .333.277.599.61.58a48.1 48.1 0 0 0 5.427-.63 48.05 48.05 0 0 0 .582-4.717.532.532 0 0 0-.533-.57v0c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.035 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.37 0 .713.128 1.003.349.283.215.604.401.96.401v0a.656.656 0 0 0 .657-.663 48.422 48.422 0 0 0-.37-5.36c-1.886.342-3.81.574-5.766.689a.578.578 0 0 1-.61-.58v0Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconCpuChip = ({ className = 'size-8', style }: { className?: string; style?: React.CSSProperties }) => (
|
||||
<svg className={className} style={style} fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5M4.5 15.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 0 0 2.25-2.25V6.75a2.25 2.25 0 0 0-2.25-2.25H6.75A2.25 2.25 0 0 0 4.5 6.75v10.5a2.25 2.25 0 0 0 2.25 2.25Zm.75-12h9v9h-9v-9Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconBolt = ({ className = 'size-8', style }: { className?: string; style?: React.CSSProperties }) => (
|
||||
<svg className={className} style={style} fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconArrowRight = ({ className = 'ml-2 size-4' }: { className?: string }) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
/* Hanzo geometric H logo */
|
||||
const HanzoLogo = ({ className = 'size-5', style }: { className?: string; style?: React.CSSProperties }) => (
|
||||
<svg viewBox="0 0 67 67" className={className} style={style} xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="currentColor" />
|
||||
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="currentColor" />
|
||||
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="currentColor" />
|
||||
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="currentColor" />
|
||||
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
/* GitHub icon */
|
||||
const IconGitHub = ({ className = 'size-5' }: { className?: string }) => (
|
||||
<svg className={className} role="img" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Suggestions */
|
||||
/* Data */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const suggestions = [
|
||||
{ icon: '✦', text: 'Explain how MCP tools work' },
|
||||
{ icon: '→', text: 'Help me deploy a Next.js app' },
|
||||
{ icon: '⚡', text: 'Compare Zen models for my use case' },
|
||||
{ icon: '◆', text: 'Debug my authentication flow' },
|
||||
const features = [
|
||||
{
|
||||
title: 'Zen Models',
|
||||
description:
|
||||
'14 frontier models from 4B to 480B parameters. Code, reason, vision, multimodal -- all trained in-house.',
|
||||
Icon: IconSparkles,
|
||||
},
|
||||
{
|
||||
title: '100+ Models',
|
||||
description:
|
||||
'Claude, GPT-5, DeepSeek, Qwen, and more. Every major provider through one interface.',
|
||||
Icon: IconCube,
|
||||
},
|
||||
{
|
||||
title: 'MCP Tools',
|
||||
description:
|
||||
'260+ Model Context Protocol tools for web search, code execution, file management, and APIs.',
|
||||
Icon: IconPuzzle,
|
||||
},
|
||||
{
|
||||
title: 'Agents',
|
||||
description:
|
||||
'Build and deploy custom AI agents with tool use, memory, and multi-step reasoning.',
|
||||
Icon: IconCpuChip,
|
||||
},
|
||||
];
|
||||
|
||||
const zenModels = [
|
||||
{ name: 'zen4', description: 'General-purpose frontier model', context: '128K', params: '70B' },
|
||||
{ name: 'zen4-coder', description: 'Optimized for code generation', context: '128K', params: '70B' },
|
||||
{ name: 'zen4-ultra', description: 'Maximum capability reasoning', context: '128K', params: '480B' },
|
||||
{ name: 'zen3-omni', description: 'Multimodal vision and text', context: '128K', params: '32B' },
|
||||
];
|
||||
|
||||
const thirdPartyModels = [
|
||||
'GPT-5', 'Claude Opus 4', 'Gemini 2.5', 'DeepSeek R1',
|
||||
'Qwen3', 'Llama 4', 'Mistral Large', 'Command R+',
|
||||
'Grok', 'Phi-4',
|
||||
];
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Main Component */
|
||||
/* Component */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function getLoginHref(serverDomain: string): string {
|
||||
const iamUrl = import.meta.env.VITE_HANZO_IAM_URL;
|
||||
const appId = import.meta.env.VITE_HANZO_IAM_APP;
|
||||
if (iamUrl && appId) {
|
||||
const redirectUri = `${window.location.origin}/auth/callback`;
|
||||
const state = `hanzo-chat-${Date.now()}`;
|
||||
sessionStorage.setItem('oauth_state', state);
|
||||
return `${iamUrl}/oauth/authorize?${new URLSearchParams({ client_id: appId, redirect_uri: redirectUri, response_type: 'code', state, scope: 'openid profile email' })}`;
|
||||
}
|
||||
return `${serverDomain}/oauth/openid`;
|
||||
}
|
||||
|
||||
export default function LandingPage() {
|
||||
const { data: config } = useGetStartupConfig();
|
||||
const serverDomain = config?.serverDomain || '';
|
||||
const loginHref = getLoginHref(serverDomain);
|
||||
const signupHref = `https://hanzo.id/signup?redirect_uri=${encodeURIComponent(window.location.origin + '/oauth/openid')}`;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const typingRef = useRef<number | null>(null);
|
||||
const iamSdk = getHanzoIamSdk();
|
||||
|
||||
useEffect(() => { injectStyles(); }, []);
|
||||
// When IAM SDK is available, login is an async redirect (PKCE).
|
||||
// Otherwise fall back to the backend OAuth endpoint.
|
||||
const loginHref = iamSdk ? '#' : `${serverDomain}/oauth/openid`;
|
||||
|
||||
const handleSubmit = useCallback((e?: React.FormEvent) => {
|
||||
e?.preventDefault();
|
||||
const q = inputRef.current?.value?.trim();
|
||||
if (q) sessionStorage.setItem('hanzo_chat_initial_query', q);
|
||||
window.location.href = loginHref;
|
||||
}, [loginHref]);
|
||||
|
||||
const typeAndSubmit = useCallback((text: string) => {
|
||||
if (isTyping || !inputRef.current) return;
|
||||
setIsTyping(true);
|
||||
inputRef.current.value = '';
|
||||
inputRef.current.focus();
|
||||
let i = 0;
|
||||
const speed = 30; // ms per character
|
||||
typingRef.current = window.setInterval(() => {
|
||||
if (!inputRef.current) return;
|
||||
inputRef.current.value = text.slice(0, ++i);
|
||||
if (i >= text.length) {
|
||||
clearInterval(typingRef.current!);
|
||||
// Brief pause then "submit" animation
|
||||
setTimeout(() => {
|
||||
setIsTyping(false);
|
||||
handleSubmit();
|
||||
}, 400);
|
||||
const handleLoginClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (iamSdk) {
|
||||
e.preventDefault();
|
||||
iamSdk.signinRedirect();
|
||||
}
|
||||
}, speed);
|
||||
}, [isTyping, handleSubmit]);
|
||||
|
||||
useEffect(() => () => { if (typingRef.current) clearInterval(typingRef.current); }, []);
|
||||
},
|
||||
[iamSdk],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hanzo-landing"
|
||||
className="min-h-screen selection:bg-[#fd4444]/30"
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
backgroundColor: 'var(--hl-bg)',
|
||||
color: 'var(--hl-fg)',
|
||||
fontFamily: "'Geist Sans', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: colors.bg,
|
||||
color: colors.fg,
|
||||
fontFamily: "'Inter', 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||
}}
|
||||
>
|
||||
{/* Navbar */}
|
||||
<nav style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 24px', flexShrink: 0 }}>
|
||||
<AnimatedLogo />
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<a
|
||||
href={loginHref}
|
||||
style={{
|
||||
fontSize: 13, color: 'var(--hl-muted)', textDecoration: 'none',
|
||||
padding: '7px 16px', borderRadius: 20,
|
||||
border: '1px solid var(--hl-border)', transition: 'all 0.15s',
|
||||
}}
|
||||
onMouseOver={(e) => { e.currentTarget.style.color = 'var(--hl-fg)'; e.currentTarget.style.borderColor = 'var(--hl-border-hover)'; }}
|
||||
onMouseOut={(e) => { e.currentTarget.style.color = 'var(--hl-muted)'; e.currentTarget.style.borderColor = 'var(--hl-border)'; }}
|
||||
>
|
||||
Log in
|
||||
</a>
|
||||
<a
|
||||
href={signupHref}
|
||||
style={{
|
||||
fontSize: 13, fontWeight: 600, textDecoration: 'none',
|
||||
padding: '7px 18px', borderRadius: 20,
|
||||
backgroundColor: 'var(--hl-btn-bg)', color: 'var(--hl-btn-fg)',
|
||||
transition: 'opacity 0.15s',
|
||||
}}
|
||||
onMouseOver={(e) => (e.currentTarget.style.opacity = '0.85')}
|
||||
onMouseOut={(e) => (e.currentTarget.style.opacity = '1')}
|
||||
>
|
||||
Sign up
|
||||
</a>
|
||||
{/* ---- Navbar (matches fd HomeLayout nav) ---- */}
|
||||
<nav
|
||||
className="sticky top-0 z-50 backdrop-blur-md"
|
||||
style={{
|
||||
backgroundColor: 'rgba(5, 5, 5, 0.8)',
|
||||
borderBottom: `1px solid ${colors.border}`,
|
||||
}}
|
||||
>
|
||||
<div className="mx-auto flex max-w-[1400px] items-center justify-between px-6 py-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<HanzoLogo className="size-5 text-[#fd4444]" />
|
||||
<span className="text-sm font-bold tracking-tight">Hanzo Chat</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<a
|
||||
href="https://hanzo.ai/docs/chat"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden text-sm transition-colors sm:inline"
|
||||
style={{ color: colors.mutedFg }}
|
||||
onMouseOver={(e) => (e.currentTarget.style.color = colors.fg)}
|
||||
onMouseOut={(e) => (e.currentTarget.style.color = colors.mutedFg)}
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/hanzoai/chat"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden transition-colors sm:inline"
|
||||
style={{ color: colors.mutedFg }}
|
||||
onMouseOver={(e) => (e.currentTarget.style.color = colors.fg)}
|
||||
onMouseOut={(e) => (e.currentTarget.style.color = colors.mutedFg)}
|
||||
>
|
||||
<IconGitHub className="size-[18px]" />
|
||||
</a>
|
||||
<a
|
||||
href={loginHref}
|
||||
onClick={handleLoginClick}
|
||||
className="rounded-full px-5 py-2 text-sm font-medium tracking-tight transition-colors"
|
||||
style={{ backgroundColor: colors.brand, color: '#fff' }}
|
||||
onMouseOver={(e) => (e.currentTarget.style.filter = 'brightness(1.15)')}
|
||||
onMouseOut={(e) => (e.currentTarget.style.filter = 'none')}
|
||||
>
|
||||
Log in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Centered search widget */}
|
||||
<main style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '0 24px', maxWidth: 680, width: '100%', margin: '0 auto', marginTop: -48 }}>
|
||||
{/* ---- Hero (bordered card like dev.hanzo.ai) ---- */}
|
||||
<div className="mx-auto w-full max-w-[1400px] px-4 pt-4">
|
||||
<section
|
||||
className="relative flex min-h-[600px] flex-col overflow-hidden rounded-2xl"
|
||||
style={{
|
||||
border: `1px solid ${colors.border}`,
|
||||
background: `linear-gradient(135deg, rgba(253,68,68,0.08) 0%, transparent 50%, rgba(253,68,68,0.04) 100%)`,
|
||||
}}
|
||||
>
|
||||
{/* Grid pattern overlay */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage: 'linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)',
|
||||
backgroundSize: '40px 40px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<h1 style={{ fontSize: 'clamp(28px, 5vw, 42px)', fontWeight: 400, textAlign: 'center', marginBottom: 32, letterSpacing: -0.8, lineHeight: 1.15, animation: 'hl-fade-up 0.5s ease 0.3s both' }}>
|
||||
What can I help with?
|
||||
</h1>
|
||||
|
||||
{/* Search input */}
|
||||
<form onSubmit={handleSubmit} style={{ width: '100%', animation: 'hl-fade-up 0.5s ease 0.45s both' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', width: '100%', padding: '12px 14px 12px 20px', borderRadius: 24, backgroundColor: 'var(--hl-surface)', border: '1px solid var(--hl-border)', transition: 'border-color 0.2s, box-shadow 0.2s' }}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="Ask anything..."
|
||||
className="hl-input"
|
||||
style={{ flex: 1, background: 'transparent', border: 'none', color: 'var(--hl-fg)', fontSize: 15, fontFamily: 'inherit', lineHeight: 1.5 }}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
style={{ width: 34, height: 34, borderRadius: '50%', backgroundColor: 'var(--hl-btn-bg)', color: 'var(--hl-btn-fg)', border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0, transition: 'opacity 0.15s', marginLeft: 8 }}
|
||||
onMouseOver={(e) => (e.currentTarget.style.opacity = '0.8')}
|
||||
onMouseOut={(e) => (e.currentTarget.style.opacity = '1')}
|
||||
<div className="relative z-10 flex flex-1 flex-col px-6 py-12 md:px-12">
|
||||
{/* Badge */}
|
||||
<div
|
||||
className="flex w-fit items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium"
|
||||
style={{
|
||||
border: `1px solid rgba(253,68,68,0.5)`,
|
||||
color: colors.brand,
|
||||
}}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M5 12h14M12 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
<IconTerminal className="size-4" />
|
||||
AI Chat Platform
|
||||
</div>
|
||||
|
||||
{/* Heading */}
|
||||
<h1 className="my-8 text-4xl font-medium leading-tight tracking-tight lg:text-5xl xl:text-6xl">
|
||||
<span style={{ color: colors.brand }}>Hanzo Chat</span> -- Every
|
||||
<br />
|
||||
Model, One Interface
|
||||
</h1>
|
||||
|
||||
<p className="mb-8 max-w-2xl text-lg" style={{ color: colors.mutedFg }}>
|
||||
AI-powered chat with 14 Zen models, 100+ third-party models, MCP tools,
|
||||
and custom agents. Pay only for what you use.
|
||||
</p>
|
||||
|
||||
{/* Buttons (rounded-full like fd) */}
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<a
|
||||
href={loginHref}
|
||||
onClick={handleLoginClick}
|
||||
className="inline-flex items-center justify-center rounded-full px-5 py-3 text-sm font-medium tracking-tight transition-colors"
|
||||
style={{ backgroundColor: colors.brand, color: '#fff' }}
|
||||
onMouseOver={(e) => (e.currentTarget.style.filter = 'brightness(1.15)')}
|
||||
onMouseOut={(e) => (e.currentTarget.style.filter = 'none')}
|
||||
>
|
||||
Get Started Free
|
||||
<IconArrowRight />
|
||||
</a>
|
||||
<a
|
||||
href="https://hanzo.ai/docs/chat"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center rounded-full px-5 py-3 text-sm font-medium tracking-tight transition-colors"
|
||||
style={{
|
||||
border: `1px solid ${colors.border}`,
|
||||
backgroundColor: colors.secondary,
|
||||
color: 'hsl(0, 0%, 92%)',
|
||||
}}
|
||||
onMouseOver={(e) => (e.currentTarget.style.backgroundColor = 'hsla(0, 0%, 40%, 0.3)')}
|
||||
onMouseOut={(e) => (e.currentTarget.style.backgroundColor = colors.secondary)}
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Chat Preview (terminal-style card like dev.hanzo.ai) */}
|
||||
<div className="mt-12 w-full max-w-3xl">
|
||||
<div
|
||||
className="overflow-hidden rounded-xl shadow-2xl"
|
||||
style={{
|
||||
backgroundColor: colors.card,
|
||||
border: `1px solid ${colors.border}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex items-center gap-2 px-4 py-2"
|
||||
style={{
|
||||
borderBottom: `1px solid ${colors.border}`,
|
||||
backgroundColor: 'rgba(26, 26, 26, 0.5)',
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-1.5">
|
||||
<div className="size-3 rounded-full bg-red-500" />
|
||||
<div className="size-3 rounded-full bg-yellow-500" />
|
||||
<div className="size-3 rounded-full bg-green-500" />
|
||||
</div>
|
||||
<span className="ml-2 text-xs" style={{ color: colors.mutedFg }}>
|
||||
chat.hanzo.ai
|
||||
</span>
|
||||
</div>
|
||||
<pre className="overflow-x-auto p-4 text-sm" style={{ fontFamily: "'Roboto Mono', 'Geist Mono', monospace" }}>
|
||||
<code>{`You: Refactor the auth module to use JWT tokens
|
||||
|
||||
zen4-coder: I'll help you refactor the auth module.
|
||||
|
||||
Plan:
|
||||
1. Replace session-based auth with JWT
|
||||
2. Add token refresh endpoint
|
||||
3. Update middleware to verify JWT
|
||||
4. Write migration script
|
||||
|
||||
Let me implement this step by step...
|
||||
|
||||
Created: src/auth/jwt.ts
|
||||
Modified: src/middleware/auth.ts
|
||||
Updated: 8 test files
|
||||
All tests passing.`}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Suggestions */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 8, width: '100%', marginTop: 16, animation: 'hl-fade-up 0.5s ease 0.6s both' }}>
|
||||
{suggestions.map((s, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className="hl-suggestion"
|
||||
onClick={() => typeAndSubmit(s.text)}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '11px 14px', borderRadius: 12, fontSize: 13, lineHeight: 1.35 }}
|
||||
{/* ---- Feature Grid (fd cardVariants pattern) ---- */}
|
||||
<div className="mx-auto mt-12 grid w-full max-w-[1400px] grid-cols-1 gap-10 px-6 md:px-12 lg:grid-cols-2">
|
||||
<p className="col-span-full text-2xl font-light leading-snug tracking-tight md:text-3xl xl:text-4xl">
|
||||
Ship faster with{' '}
|
||||
<span className="font-medium" style={{ color: colors.brand }}>Zen frontier models</span>,{' '}
|
||||
<span className="font-medium" style={{ color: colors.brand }}>100+ providers</span>, and{' '}
|
||||
<span className="font-medium" style={{ color: colors.brand }}>260+ MCP tools</span>.
|
||||
</p>
|
||||
|
||||
{features.map(({ title, description, Icon }) => (
|
||||
<div
|
||||
key={title}
|
||||
className="rounded-2xl p-6 text-sm shadow-lg transition-colors"
|
||||
style={{
|
||||
backgroundColor: colors.card,
|
||||
border: `1px solid ${colors.border}`,
|
||||
}}
|
||||
onMouseOver={(e) => (e.currentTarget.style.borderColor = 'hsla(0, 0%, 40%, 0.35)')}
|
||||
onMouseOut={(e) => (e.currentTarget.style.borderColor = colors.border)}
|
||||
>
|
||||
<Icon className="mb-4 size-8" style={{ color: colors.brand }} />
|
||||
<h3 className="mb-2 text-xl font-medium tracking-tight lg:text-2xl">{title}</h3>
|
||||
<p style={{ color: colors.mutedFg }}>{description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ---- Zen Models (secondary card variant) ---- */}
|
||||
<div className="mx-auto mt-10 w-full max-w-[1400px] px-6 md:px-12">
|
||||
<div
|
||||
className="rounded-2xl p-6 text-sm shadow-lg md:p-8"
|
||||
style={{
|
||||
backgroundColor: 'rgba(253, 68, 68, 0.06)',
|
||||
border: `1px solid rgba(253, 68, 68, 0.15)`,
|
||||
}}
|
||||
>
|
||||
<IconBolt className="mb-4 size-8" style={{ color: colors.brand }} />
|
||||
<h3 className="mb-4 text-xl font-medium tracking-tight lg:text-2xl">
|
||||
Zen Frontier Models
|
||||
</h3>
|
||||
<p className="mb-6" style={{ color: colors.mutedFg }}>
|
||||
Trained in-house. Fast, capable, affordable.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{zenModels.map((m) => (
|
||||
<div
|
||||
key={m.name}
|
||||
className="rounded-lg p-4 transition-colors"
|
||||
style={{
|
||||
border: `1px solid ${colors.border}`,
|
||||
backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
}}
|
||||
onMouseOver={(e) => (e.currentTarget.style.borderColor = 'rgba(253, 68, 68, 0.3)')}
|
||||
onMouseOut={(e) => (e.currentTarget.style.borderColor = colors.border)}
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="inline-block size-2 rounded-full" style={{ backgroundColor: colors.brand }} />
|
||||
<span className="text-sm font-semibold" style={{ fontFamily: "'Roboto Mono', 'Geist Mono', monospace" }}>
|
||||
{m.name}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mb-3 text-xs" style={{ color: colors.mutedFg }}>
|
||||
{m.description}
|
||||
</p>
|
||||
<div className="flex gap-2 text-xs" style={{ color: 'hsl(0, 0%, 45%)' }}>
|
||||
<span className="rounded px-2 py-0.5" style={{ backgroundColor: 'rgba(255,255,255,0.06)' }}>
|
||||
{m.params}
|
||||
</span>
|
||||
<span className="rounded px-2 py-0.5" style={{ backgroundColor: 'rgba(255,255,255,0.06)' }}>
|
||||
{m.context} ctx
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ---- Third-Party Models ---- */}
|
||||
<div className="mx-auto mt-10 w-full max-w-[1400px] px-6 md:px-12">
|
||||
<div
|
||||
className="rounded-2xl p-6 text-sm shadow-lg md:p-8"
|
||||
style={{
|
||||
backgroundColor: colors.card,
|
||||
border: `1px solid ${colors.border}`,
|
||||
}}
|
||||
>
|
||||
<h3 className="mb-4 text-xl font-medium tracking-tight lg:text-2xl">
|
||||
Every Major Model
|
||||
</h3>
|
||||
<p className="mb-6" style={{ color: colors.mutedFg }}>
|
||||
Switch between providers instantly. No vendor lock-in.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{thirdPartyModels.map((model) => (
|
||||
<div
|
||||
key={model}
|
||||
className="rounded-lg px-4 py-2.5 text-sm font-medium transition-colors"
|
||||
style={{
|
||||
border: `1px solid ${colors.border}`,
|
||||
color: 'hsl(0, 0%, 85%)',
|
||||
}}
|
||||
onMouseOver={(e) => {
|
||||
e.currentTarget.style.borderColor = 'hsla(0, 0%, 40%, 0.4)';
|
||||
e.currentTarget.style.color = '#fff';
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
e.currentTarget.style.borderColor = colors.border;
|
||||
e.currentTarget.style.color = 'hsl(0, 0%, 85%)';
|
||||
}}
|
||||
>
|
||||
{model}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ---- Pricing (col-span-full card) ---- */}
|
||||
<div className="mx-auto mt-10 w-full max-w-[1400px] px-6 md:px-12">
|
||||
<div
|
||||
className="rounded-2xl p-6 text-center shadow-lg md:p-12"
|
||||
style={{
|
||||
backgroundColor: colors.card,
|
||||
border: `1px solid ${colors.border}`,
|
||||
}}
|
||||
>
|
||||
<h2 className="mb-4 text-3xl font-medium tracking-tight lg:text-4xl">
|
||||
Simple, transparent pricing
|
||||
</h2>
|
||||
<p className="mb-10 text-lg" style={{ color: colors.mutedFg }}>
|
||||
Pay only for what you use. No subscriptions. No hidden fees.
|
||||
</p>
|
||||
<div className="mx-auto grid max-w-3xl gap-6 sm:grid-cols-3">
|
||||
<div
|
||||
className="rounded-xl p-6"
|
||||
style={{ border: `1px solid ${colors.border}` }}
|
||||
>
|
||||
<span style={{ fontSize: 11, flexShrink: 0, opacity: 0.4 }}>{s.icon}</span>
|
||||
<span style={{ flex: 1 }}>{s.text}</span>
|
||||
</button>
|
||||
))}
|
||||
<p className="mb-2 text-xs font-medium uppercase tracking-wider" style={{ color: 'hsl(0, 0%, 45%)' }}>
|
||||
Starting at
|
||||
</p>
|
||||
<p className="text-3xl font-bold">$0.30</p>
|
||||
<p className="mt-1 text-sm" style={{ color: colors.mutedFg }}>
|
||||
per million tokens
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="rounded-xl p-6"
|
||||
style={{
|
||||
border: `1px solid rgba(253, 68, 68, 0.3)`,
|
||||
backgroundColor: 'rgba(253, 68, 68, 0.04)',
|
||||
}}
|
||||
>
|
||||
<p className="mb-2 text-xs font-medium uppercase tracking-wider" style={{ color: 'rgba(253, 68, 68, 0.7)' }}>
|
||||
Free credit
|
||||
</p>
|
||||
<p className="text-3xl font-bold">$5</p>
|
||||
<p className="mt-1 text-sm" style={{ color: colors.mutedFg }}>
|
||||
no credit card required
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="rounded-xl p-6"
|
||||
style={{ border: `1px solid ${colors.border}` }}
|
||||
>
|
||||
<p className="mb-2 text-xs font-medium uppercase tracking-wider" style={{ color: 'hsl(0, 0%, 45%)' }}>
|
||||
Billing
|
||||
</p>
|
||||
<p className="text-3xl font-bold">Usage</p>
|
||||
<p className="mt-1 text-sm" style={{ color: colors.mutedFg }}>
|
||||
pay as you go
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model badges */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 6, marginTop: 24, animation: 'hl-fade-up 0.5s ease 0.75s both' }}>
|
||||
{['Zen 4', 'Claude', 'GPT-5', 'DeepSeek', 'Gemini', 'Qwen', '100+ models'].map((m) => (
|
||||
<span key={m} className="hl-badge" style={{ fontSize: 11, padding: '3px 10px', borderRadius: 10 }}>{m}</span>
|
||||
))}
|
||||
{/* ---- CTA (secondary variant card) ---- */}
|
||||
<div className="mx-auto mt-10 w-full max-w-[1400px] px-6 pb-6 md:px-12">
|
||||
<div
|
||||
className="rounded-2xl p-6 text-center shadow-lg md:p-12"
|
||||
style={{
|
||||
backgroundColor: colors.card,
|
||||
border: `1px solid ${colors.border}`,
|
||||
}}
|
||||
>
|
||||
<h2 className="mb-4 text-3xl font-medium tracking-tight lg:text-4xl">
|
||||
Ready to Chat?
|
||||
</h2>
|
||||
<p className="mx-auto mb-8 max-w-2xl" style={{ color: colors.mutedFg }}>
|
||||
Sign in with your Hanzo account. $5 free credit, no setup required.
|
||||
</p>
|
||||
<div className="flex flex-row items-center justify-center gap-4">
|
||||
<a
|
||||
href={loginHref}
|
||||
onClick={handleLoginClick}
|
||||
className="inline-flex items-center justify-center rounded-full px-5 py-3 text-sm font-medium tracking-tight transition-colors"
|
||||
style={{ backgroundColor: colors.brand, color: '#fff' }}
|
||||
onMouseOver={(e) => (e.currentTarget.style.filter = 'brightness(1.15)')}
|
||||
onMouseOut={(e) => (e.currentTarget.style.filter = 'none')}
|
||||
>
|
||||
Get Started Free
|
||||
<IconArrowRight />
|
||||
</a>
|
||||
<a
|
||||
href="https://hanzo.ai/docs/chat"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center rounded-full px-5 py-3 text-sm font-medium tracking-tight transition-colors"
|
||||
style={{
|
||||
border: `1px solid ${colors.border}`,
|
||||
backgroundColor: colors.secondary,
|
||||
color: 'hsl(0, 0%, 92%)',
|
||||
}}
|
||||
onMouseOver={(e) => (e.currentTarget.style.backgroundColor = 'hsla(0, 0%, 40%, 0.3)')}
|
||||
onMouseOut={(e) => (e.currentTarget.style.backgroundColor = colors.secondary)}
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer style={{ textAlign: 'center', padding: '16px 24px', fontSize: 11, color: 'var(--hl-dim)', flexShrink: 0 }}>
|
||||
<a href="https://hanzo.ai" target="_blank" rel="noopener noreferrer" style={{ color: 'var(--hl-muted)', textDecoration: 'none' }}>hanzo.ai</a>
|
||||
<span style={{ margin: '0 6px' }}>·</span>
|
||||
<span>$5 free credit, no card required</span>
|
||||
<span style={{ margin: '0 6px' }}>·</span>
|
||||
<a href="https://hanzo.ai/docs/chat" target="_blank" rel="noopener noreferrer" style={{ color: 'var(--hl-muted)', textDecoration: 'none' }}>Docs</a>
|
||||
{/* ---- Footer ---- */}
|
||||
<footer
|
||||
className="mt-0 py-8"
|
||||
style={{ borderTop: `1px solid ${colors.border}` }}
|
||||
>
|
||||
<div className="mx-auto flex max-w-[1400px] flex-col items-center justify-between gap-6 px-6 sm:flex-row md:px-12">
|
||||
<div className="flex items-center gap-2 text-sm" style={{ color: 'hsl(0, 0%, 40%)' }}>
|
||||
<HanzoLogo className="size-4" style={{ color: 'hsl(0, 0%, 40%)' }} />
|
||||
Powered by Hanzo AI
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-center gap-6 text-sm" style={{ color: 'hsl(0, 0%, 40%)' }}>
|
||||
{[
|
||||
{ label: 'hanzo.ai', href: 'https://hanzo.ai' },
|
||||
{ label: 'Documentation', href: 'https://hanzo.ai/docs/chat' },
|
||||
{ label: 'Console', href: 'https://console.hanzo.ai' },
|
||||
{ label: 'Privacy', href: 'https://hanzo.ai/privacy' },
|
||||
{ label: 'Terms', href: 'https://hanzo.ai/terms' },
|
||||
].map(({ label, href }) => (
|
||||
<a
|
||||
key={label}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="transition-colors"
|
||||
onMouseOver={(e) => (e.currentTarget.style.color = colors.fg)}
|
||||
onMouseOut={(e) => (e.currentTarget.style.color = 'hsl(0, 0%, 40%)')}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// build: 1773482930
|
||||
|
||||
@@ -6,14 +6,14 @@ jest.mock('axios');
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
describe('getMemories', () => {
|
||||
it('should fetch memories from /api/memories', async () => {
|
||||
it('should fetch memories from /v1/memories', async () => {
|
||||
const mockData = [{ key: 'foo', value: 'bar', updated_at: '2024-05-01T00:00:00Z' }];
|
||||
|
||||
mockedAxios.get.mockResolvedValueOnce({ data: mockData } as any);
|
||||
|
||||
const result = await (_dataService as any).getMemories();
|
||||
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith('/api/memories', expect.any(Object));
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith('/v1/memories', expect.any(Object));
|
||||
expect(result).toEqual(mockData);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ const MAX_FAVORITES = 50;
|
||||
/**
|
||||
* Hook for managing user favorites (pinned agents and models).
|
||||
*
|
||||
* Favorites are synchronized with the server via `/api/user/settings/favorites`.
|
||||
* Favorites are synchronized with the server via `/v1/user/settings/favorites`.
|
||||
* Each favorite is either:
|
||||
* - An agent: `{ agentId: string }`
|
||||
* - A model: `{ model: string, endpoint: string }`
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Shared @hanzo/iam singleton + config for Hanzo IAM OIDC flows.
|
||||
*
|
||||
* Reads VITE_HANZO_IAM_URL, VITE_HANZO_IAM_APP, VITE_HANZO_IAM_ORG, and
|
||||
* VITE_HANZO_API_URL from the Vite environment. Returns null when IAM is
|
||||
* not configured (backend-proxied mode).
|
||||
*
|
||||
* Login is EMBEDDED (in-app form via `@hanzo/iam/views <Login>`) — no
|
||||
* redirect to the IAM-hosted page. The IAM class mints a PKCE-bound code
|
||||
* via the credential endpoint; this SDK instance completes the standard
|
||||
* PKCE exchange in the /auth/callback route.
|
||||
*/
|
||||
import { IAM } from '@hanzo/iam/browser';
|
||||
|
||||
export interface HanzoIamConfig {
|
||||
serverUrl: string;
|
||||
clientId: string;
|
||||
organization: string;
|
||||
redirectUri: string;
|
||||
proxyBaseUrl?: string;
|
||||
}
|
||||
|
||||
export function getHanzoIamConfig(): HanzoIamConfig | null {
|
||||
const serverUrl = import.meta.env.VITE_HANZO_IAM_URL;
|
||||
const clientId = import.meta.env.VITE_HANZO_IAM_APP;
|
||||
|
||||
if (!serverUrl || !clientId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
serverUrl,
|
||||
clientId,
|
||||
organization: import.meta.env.VITE_HANZO_IAM_ORG || 'hanzo',
|
||||
redirectUri: `${window.location.origin}/auth/callback`,
|
||||
proxyBaseUrl: import.meta.env.VITE_HANZO_API_URL || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
let instance: IAM | null = null;
|
||||
let checked = false;
|
||||
|
||||
export function getHanzoIamSdk(): IAM | null {
|
||||
if (checked) {
|
||||
return instance;
|
||||
}
|
||||
checked = true;
|
||||
|
||||
const cfg = getHanzoIamConfig();
|
||||
if (!cfg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
instance = new IAM({
|
||||
serverUrl: cfg.serverUrl,
|
||||
clientId: cfg.clientId,
|
||||
organization: cfg.organization,
|
||||
redirectUri: cfg.redirectUri,
|
||||
scope: 'openid profile email',
|
||||
proxyBaseUrl: cfg.proxyBaseUrl,
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the app is running in static/IAM mode (VITE_HANZO_API_URL is set).
|
||||
*/
|
||||
export function isStaticIamMode(): boolean {
|
||||
return !!import.meta.env.VITE_HANZO_API_URL;
|
||||
}
|
||||
+1
-1
@@ -32,7 +32,7 @@ services:
|
||||
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_IMAGE_URL: https://hanzo.chat/assets/logo.svg
|
||||
OPENID_USERNAME_CLAIM: name
|
||||
OPENID_NAME_CLAIM: name
|
||||
OPENID_USE_PKCE: "true"
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ services:
|
||||
- pgdata2:/var/lib/postgresql/data
|
||||
rag_api:
|
||||
container_name: rag_api
|
||||
image: ghcr.io/hanzoai/chat-rag-api:latest
|
||||
image: ghcr.io/hanzoai/chat-rag-api:v0.7.8
|
||||
environment:
|
||||
- DB_HOST=vectordb
|
||||
- RAG_PORT=${RAG_PORT:-8000}
|
||||
|
||||
+2
-2
@@ -32,7 +32,7 @@ services:
|
||||
- ./logs:/app/api/logs
|
||||
|
||||
client:
|
||||
image: ghcr.io/hanzoai/static:latest
|
||||
image: ghcr.io/hanzoai/static:0.4.1
|
||||
container_name: HanzoChat-Static
|
||||
ports:
|
||||
- 80:80
|
||||
@@ -74,7 +74,7 @@ services:
|
||||
volumes:
|
||||
- pgdata2:/var/lib/postgresql/data
|
||||
rag_api:
|
||||
image: ghcr.io/hanzoai/chat-rag-api:latest
|
||||
image: ghcr.io/hanzoai/chat-rag-api:v0.7.8
|
||||
environment:
|
||||
- DB_HOST=vectordb
|
||||
- RAG_PORT=${RAG_PORT:-8000}
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ endpoints:
|
||||
# Authenticated users override with their own key via IAM (hanzo.id).
|
||||
apiKey: '${HANZO_API_KEY}'
|
||||
baseURL: 'https://api.hanzo.ai/v1'
|
||||
iconURL: 'https://hanzo.ai/logo/icon.svg'
|
||||
iconURL: 'https://hanzo.chat/assets/logo.svg'
|
||||
models:
|
||||
default:
|
||||
# Zen model family (Hanzo AI)
|
||||
|
||||
@@ -93,9 +93,9 @@ const buildQuery = (params: Record<string, unknown>): string => {
|
||||
// Health & System
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const health = () => `${BASE_URL}/api/health`;
|
||||
export const systemInfo = () => `${BASE_URL}/api/get-system-info`;
|
||||
export const versionInfo = () => `${BASE_URL}/api/get-version-info`;
|
||||
export const health = () => `${BASE_URL}/v1/health`;
|
||||
export const systemInfo = () => `${BASE_URL}/v1/get-system-info`;
|
||||
export const versionInfo = () => `${BASE_URL}/v1/get-version-info`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth (Hanzo IAM via Cloud Gateway)
|
||||
@@ -103,111 +103,111 @@ export const versionInfo = () => `${BASE_URL}/api/get-version-info`;
|
||||
|
||||
/** Sign in via hanzo.id OAuth code exchange */
|
||||
export const signin = (code: string, state: string) =>
|
||||
`${BASE_URL}/api/signin?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
|
||||
`${BASE_URL}/v1/signin?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
|
||||
|
||||
/** Sign out */
|
||||
export const signout = () => `${BASE_URL}/api/signout`;
|
||||
export const signout = () => `${BASE_URL}/v1/signout`;
|
||||
|
||||
/** Get current account (session-based) */
|
||||
export const getAccount = () => `${BASE_URL}/api/get-account`;
|
||||
export const getAccount = () => `${BASE_URL}/v1/get-account`;
|
||||
|
||||
// Legacy aliases for frontend compatibility
|
||||
export const login = () => `${BASE_URL}/api/signin`;
|
||||
export const logout = () => `${BASE_URL}/api/signout`;
|
||||
export const register = () => `${BASE_URL}/api/signin`; // No separate registration — IAM handles it
|
||||
export const refreshToken = (retry?: boolean) => `${BASE_URL}/api/get-account`;
|
||||
export const user = () => `${BASE_URL}/api/get-account`;
|
||||
export const login = () => `${BASE_URL}/v1/signin`;
|
||||
export const logout = () => `${BASE_URL}/v1/signout`;
|
||||
export const register = () => `${BASE_URL}/v1/signin`; // No separate registration — IAM handles it
|
||||
export const refreshToken = (retry?: boolean) => `${BASE_URL}/v1/get-account`;
|
||||
export const user = () => `${BASE_URL}/v1/get-account`;
|
||||
|
||||
// Auth page URLs (client-side navigation)
|
||||
export const loginPage = () => `${BASE_URL}/login`;
|
||||
export const registerPage = () => `${BASE_URL}/register`;
|
||||
|
||||
// Social login (handled by IAM redirect, not gateway)
|
||||
export const loginFacebook = () => `${BASE_URL}/api/signin`;
|
||||
export const loginGoogle = () => `${BASE_URL}/api/signin`;
|
||||
export const loginFacebook = () => `${BASE_URL}/v1/signin`;
|
||||
export const loginGoogle = () => `${BASE_URL}/v1/signin`;
|
||||
|
||||
// Email verification (handled by IAM)
|
||||
export const verifyEmail = () => `${BASE_URL}/api/get-account`;
|
||||
export const resendVerificationEmail = () => `${BASE_URL}/api/get-account`;
|
||||
export const requestPasswordReset = () => `${BASE_URL}/api/get-account`;
|
||||
export const resetPassword = () => `${BASE_URL}/api/get-account`;
|
||||
export const verifyEmail = () => `${BASE_URL}/v1/get-account`;
|
||||
export const resendVerificationEmail = () => `${BASE_URL}/v1/get-account`;
|
||||
export const requestPasswordReset = () => `${BASE_URL}/v1/get-account`;
|
||||
export const resetPassword = () => `${BASE_URL}/v1/get-account`;
|
||||
|
||||
// 2FA (handled by IAM directly)
|
||||
export const enableTwoFactor = () => `${BASE_URL}/api/get-account`;
|
||||
export const verifyTwoFactor = () => `${BASE_URL}/api/get-account`;
|
||||
export const confirmTwoFactor = () => `${BASE_URL}/api/get-account`;
|
||||
export const disableTwoFactor = () => `${BASE_URL}/api/get-account`;
|
||||
export const regenerateBackupCodes = () => `${BASE_URL}/api/get-account`;
|
||||
export const verifyTwoFactorTemp = () => `${BASE_URL}/api/get-account`;
|
||||
export const enableTwoFactor = () => `${BASE_URL}/v1/get-account`;
|
||||
export const verifyTwoFactor = () => `${BASE_URL}/v1/get-account`;
|
||||
export const confirmTwoFactor = () => `${BASE_URL}/v1/get-account`;
|
||||
export const disableTwoFactor = () => `${BASE_URL}/v1/get-account`;
|
||||
export const regenerateBackupCodes = () => `${BASE_URL}/v1/get-account`;
|
||||
export const verifyTwoFactorTemp = () => `${BASE_URL}/v1/get-account`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// User & Balance
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const balance = () => `${BASE_URL}/api/get-usages`;
|
||||
export const userPlugins = () => `${BASE_URL}/api/get-account`;
|
||||
export const deleteUser = () => `${BASE_URL}/api/get-account`;
|
||||
export const balance = () => `${BASE_URL}/v1/get-usages`;
|
||||
export const userPlugins = () => `${BASE_URL}/v1/get-account`;
|
||||
export const deleteUser = () => `${BASE_URL}/v1/get-account`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Conversations (Chats)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const conversationsRoot = `${BASE_URL}/api`;
|
||||
export const conversationsRoot = `${BASE_URL}/v1`;
|
||||
|
||||
export const conversations = (params: q.ConversationListParams) => {
|
||||
return `${BASE_URL}/api/get-chats${buildQuery(params)}`;
|
||||
return `${BASE_URL}/v1/get-chats${buildQuery(params)}`;
|
||||
};
|
||||
|
||||
export const conversationById = (id: string) =>
|
||||
`${BASE_URL}/api/get-chat?id=${encodeURIComponent(id)}`;
|
||||
`${BASE_URL}/v1/get-chat?id=${encodeURIComponent(id)}`;
|
||||
|
||||
export const genTitle = (conversationId: string) =>
|
||||
`${BASE_URL}/api/get-answer?provider=&question=${encodeURIComponent('Generate a short title for this conversation')}`;
|
||||
`${BASE_URL}/v1/get-answer?provider=&question=${encodeURIComponent('Generate a short title for this conversation')}`;
|
||||
|
||||
export const updateConversation = () => `${BASE_URL}/api/update-chat`;
|
||||
export const updateConversation = () => `${BASE_URL}/v1/update-chat`;
|
||||
|
||||
export const archiveConversation = () => `${BASE_URL}/api/update-chat`;
|
||||
export const archiveConversation = () => `${BASE_URL}/v1/update-chat`;
|
||||
|
||||
export const deleteConversation = () => `${BASE_URL}/api/delete-chat`;
|
||||
export const deleteConversation = () => `${BASE_URL}/v1/delete-chat`;
|
||||
|
||||
export const deleteAllConversation = () => `${BASE_URL}/api/delete-chat`;
|
||||
export const deleteAllConversation = () => `${BASE_URL}/v1/delete-chat`;
|
||||
|
||||
export const importConversation = () => `${BASE_URL}/api/add-chat`;
|
||||
export const importConversation = () => `${BASE_URL}/v1/add-chat`;
|
||||
|
||||
export const forkConversation = () => `${BASE_URL}/api/add-chat`;
|
||||
export const forkConversation = () => `${BASE_URL}/v1/add-chat`;
|
||||
|
||||
export const duplicateConversation = () => `${BASE_URL}/api/add-chat`;
|
||||
export const duplicateConversation = () => `${BASE_URL}/v1/add-chat`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Messages
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const messagesRoot = `${BASE_URL}/api`;
|
||||
const messagesRoot = `${BASE_URL}/v1`;
|
||||
|
||||
export const messages = (params: q.MessagesListParams) => {
|
||||
const { conversationId, messageId, ...rest } = params;
|
||||
|
||||
if (conversationId && messageId) {
|
||||
return `${BASE_URL}/api/get-message?id=${encodeURIComponent(messageId)}`;
|
||||
return `${BASE_URL}/v1/get-message?id=${encodeURIComponent(messageId)}`;
|
||||
}
|
||||
|
||||
if (conversationId) {
|
||||
return `${BASE_URL}/api/get-messages?chat=${encodeURIComponent(conversationId)}`;
|
||||
return `${BASE_URL}/v1/get-messages?chat=${encodeURIComponent(conversationId)}`;
|
||||
}
|
||||
|
||||
return `${BASE_URL}/api/get-messages${buildQuery(rest)}`;
|
||||
return `${BASE_URL}/v1/get-messages${buildQuery(rest)}`;
|
||||
};
|
||||
|
||||
export const messagesArtifacts = (messageId: string) =>
|
||||
`${BASE_URL}/api/get-message?id=${encodeURIComponent(messageId)}`;
|
||||
`${BASE_URL}/v1/get-message?id=${encodeURIComponent(messageId)}`;
|
||||
|
||||
export const messagesBranch = () => `${BASE_URL}/api/add-message`;
|
||||
export const messagesBranch = () => `${BASE_URL}/v1/add-message`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shares
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const shareRoot = `${BASE_URL}/api/share`;
|
||||
const shareRoot = `${BASE_URL}/v1/share`;
|
||||
export const shareMessages = (shareId: string) => `${shareRoot}/${shareId}`;
|
||||
export const getSharedLink = (conversationId: string) => `${shareRoot}/link/${conversationId}`;
|
||||
export const getSharedLinks = (
|
||||
@@ -228,13 +228,13 @@ export const updateSharedLink = (shareId: string) => `${shareRoot}/${shareId}`;
|
||||
// API Keys (pk- / sk- via LLM gateway)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const keysEndpoint = `${BASE_URL}/api/keys`;
|
||||
const keysEndpoint = `${BASE_URL}/v1/keys`;
|
||||
export const keys = () => keysEndpoint;
|
||||
export const userKeyQuery = (name: string) => `${keysEndpoint}?name=${name}`;
|
||||
export const revokeUserKey = (name: string) => `${keysEndpoint}/${name}`;
|
||||
export const revokeAllUserKeys = () => `${keysEndpoint}?all=true`;
|
||||
|
||||
const apiKeysEndpoint = `${BASE_URL}/api/api-keys`;
|
||||
const apiKeysEndpoint = `${BASE_URL}/v1/api-keys`;
|
||||
export const apiKeys = () => apiKeysEndpoint;
|
||||
export const apiKeyById = (id: string) => `${apiKeysEndpoint}/${id}`;
|
||||
|
||||
@@ -243,68 +243,68 @@ export const apiKeyById = (id: string) => `${apiKeysEndpoint}/${id}`;
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const search = (q: string, cursor?: string | null) =>
|
||||
`${BASE_URL}/api/get-global-chats?field=messages&value=${encodeURIComponent(q)}${cursor ? `&cursor=${cursor}` : ''}`;
|
||||
`${BASE_URL}/v1/get-global-chats?field=messages&value=${encodeURIComponent(q)}${cursor ? `&cursor=${cursor}` : ''}`;
|
||||
|
||||
export const searchEnabled = () => `${BASE_URL}/api/health`;
|
||||
export const searchEnabled = () => `${BASE_URL}/v1/health`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Presets & Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const presets = () => `${BASE_URL}/api/presets`;
|
||||
export const deletePreset = () => `${BASE_URL}/api/presets/delete`;
|
||||
export const plugins = () => `${BASE_URL}/api/get-providers`;
|
||||
export const config = () => `${BASE_URL}/api/get-account`;
|
||||
export const aiEndpoints = () => `${BASE_URL}/api/get-providers`;
|
||||
export const presets = () => `${BASE_URL}/v1/presets`;
|
||||
export const deletePreset = () => `${BASE_URL}/v1/presets/delete`;
|
||||
export const plugins = () => `${BASE_URL}/v1/get-providers`;
|
||||
export const config = () => `${BASE_URL}/v1/get-account`;
|
||||
export const aiEndpoints = () => `${BASE_URL}/v1/get-providers`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Models (OpenAI-compatible, pk- key accessible)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const models = () => `${BASE_URL}/api/models`;
|
||||
export const models = () => `${BASE_URL}/v1/models`;
|
||||
|
||||
export const tokenizer = () => `${BASE_URL}/api/tokenizer`;
|
||||
export const tokenizer = () => `${BASE_URL}/v1/tokenizer`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LLM Completions (OpenAI + Anthropic compatible)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const chatCompletions = () => `${BASE_URL}/api/chat/completions`;
|
||||
export const anthropicMessages = () => `${BASE_URL}/api/messages`;
|
||||
export const chatCompletions = () => `${BASE_URL}/v1/chat/completions`;
|
||||
export const anthropicMessages = () => `${BASE_URL}/v1/messages`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Providers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const providers = () => `${BASE_URL}/api/get-providers`;
|
||||
export const provider = (id: string) => `${BASE_URL}/api/get-provider?id=${encodeURIComponent(id)}`;
|
||||
export const providers = () => `${BASE_URL}/v1/get-providers`;
|
||||
export const provider = (id: string) => `${BASE_URL}/v1/get-provider?id=${encodeURIComponent(id)}`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Files
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const files = () => `${BASE_URL}/api/get-files`;
|
||||
export const fileUpload = () => `${BASE_URL}/api/upload-file`;
|
||||
export const fileDelete = () => `${BASE_URL}/api/delete-file`;
|
||||
export const files = () => `${BASE_URL}/v1/get-files`;
|
||||
export const fileUpload = () => `${BASE_URL}/v1/upload-file`;
|
||||
export const fileDelete = () => `${BASE_URL}/v1/delete-file`;
|
||||
export const fileDownload = (userId: string, fileId: string) =>
|
||||
`${BASE_URL}/api/get-file?id=${encodeURIComponent(fileId)}`;
|
||||
export const fileConfig = () => `${BASE_URL}/api/get-files`;
|
||||
`${BASE_URL}/v1/get-file?id=${encodeURIComponent(fileId)}`;
|
||||
export const fileConfig = () => `${BASE_URL}/v1/get-files`;
|
||||
export const agentFiles = (agentId: string) =>
|
||||
`${BASE_URL}/api/get-files?agent=${encodeURIComponent(agentId)}`;
|
||||
`${BASE_URL}/v1/get-files?agent=${encodeURIComponent(agentId)}`;
|
||||
|
||||
export const images = () => `${BASE_URL}/api/get-files`;
|
||||
export const avatar = () => `${BASE_URL}/api/upload-file`;
|
||||
export const images = () => `${BASE_URL}/v1/get-files`;
|
||||
export const avatar = () => `${BASE_URL}/v1/upload-file`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Speech (TTS / STT)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const speech = () => `${BASE_URL}/api`;
|
||||
export const speechToText = () => `${BASE_URL}/api/process-speech-to-text`;
|
||||
export const textToSpeech = () => `${BASE_URL}/api/generate-text-to-speech-audio`;
|
||||
export const textToSpeechManual = () => `${BASE_URL}/api/generate-text-to-speech-audio`;
|
||||
export const textToSpeechVoices = () => `${BASE_URL}/api/generate-text-to-speech-audio`;
|
||||
export const getCustomConfigSpeech = () => `${BASE_URL}/api/get-providers`;
|
||||
export const speech = () => `${BASE_URL}/v1`;
|
||||
export const speechToText = () => `${BASE_URL}/v1/process-speech-to-text`;
|
||||
export const textToSpeech = () => `${BASE_URL}/v1/generate-text-to-speech-audio`;
|
||||
export const textToSpeechManual = () => `${BASE_URL}/v1/generate-text-to-speech-audio`;
|
||||
export const textToSpeechVoices = () => `${BASE_URL}/v1/generate-text-to-speech-audio`;
|
||||
export const getCustomConfigSpeech = () => `${BASE_URL}/v1/get-providers`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Assistants & Agents
|
||||
@@ -323,7 +323,7 @@ export const assistants = ({
|
||||
version: number | string;
|
||||
isAvatar?: boolean;
|
||||
}) => {
|
||||
let url = isAvatar === true ? `${images()}/assistants` : `${BASE_URL}/api/assistants/v${version}`;
|
||||
let url = isAvatar === true ? `${images()}/assistants` : `${BASE_URL}/v1/assistants/v${version}`;
|
||||
if (path && path !== '') {
|
||||
url += `/${path}`;
|
||||
}
|
||||
@@ -338,7 +338,7 @@ export const assistants = ({
|
||||
};
|
||||
|
||||
export const agents = ({ path = '', options }: { path?: string; options?: object }) => {
|
||||
let url = `${BASE_URL}/api/agents`;
|
||||
let url = `${BASE_URL}/v1/agents`;
|
||||
if (path && path !== '') {
|
||||
url += `/${path}`;
|
||||
}
|
||||
@@ -349,7 +349,7 @@ export const agents = ({ path = '', options }: { path?: string; options?: object
|
||||
return url;
|
||||
};
|
||||
|
||||
export const activeJobs = () => `${BASE_URL}/api/agents/chat/active`;
|
||||
export const activeJobs = () => `${BASE_URL}/v1/agents/chat/active`;
|
||||
|
||||
export const revertAgentVersion = (agent_id: string) => `${agents({ path: `${agent_id}/revert` })}`;
|
||||
|
||||
@@ -358,34 +358,34 @@ export const revertAgentVersion = (agent_id: string) => `${agents({ path: `${age
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const mcp = {
|
||||
tools: `${BASE_URL}/api/mcp/tools`,
|
||||
servers: `${BASE_URL}/api/mcp/servers`,
|
||||
tools: `${BASE_URL}/v1/mcp/tools`,
|
||||
servers: `${BASE_URL}/v1/mcp/servers`,
|
||||
};
|
||||
|
||||
export const mcpServer = (serverName: string) => `${BASE_URL}/api/mcp/servers/${serverName}`;
|
||||
export const mcpServer = (serverName: string) => `${BASE_URL}/v1/mcp/servers/${serverName}`;
|
||||
export const mcpReinitialize = (serverName: string) =>
|
||||
`${BASE_URL}/api/mcp/${serverName}/reinitialize`;
|
||||
export const mcpConnectionStatus = () => `${BASE_URL}/api/mcp/connection/status`;
|
||||
`${BASE_URL}/v1/mcp/${serverName}/reinitialize`;
|
||||
export const mcpConnectionStatus = () => `${BASE_URL}/v1/mcp/connection/status`;
|
||||
export const mcpServerConnectionStatus = (serverName: string) =>
|
||||
`${BASE_URL}/api/mcp/connection/status/${serverName}`;
|
||||
`${BASE_URL}/v1/mcp/connection/status/${serverName}`;
|
||||
export const mcpAuthValues = (serverName: string) =>
|
||||
`${BASE_URL}/api/mcp/${serverName}/auth-values`;
|
||||
`${BASE_URL}/v1/mcp/${serverName}/auth-values`;
|
||||
export const cancelMCPOAuth = (serverName: string) =>
|
||||
`${BASE_URL}/api/mcp/oauth/cancel/${serverName}`;
|
||||
export const mcpOAuthBind = (serverName: string) => `${BASE_URL}/api/mcp/${serverName}/oauth/bind`;
|
||||
`${BASE_URL}/v1/mcp/oauth/cancel/${serverName}`;
|
||||
export const mcpOAuthBind = (serverName: string) => `${BASE_URL}/v1/mcp/${serverName}/oauth/bind`;
|
||||
export const actionOAuthBind = (actionId: string) =>
|
||||
`${BASE_URL}/api/actions/${actionId}/oauth/bind`;
|
||||
`${BASE_URL}/v1/actions/${actionId}/oauth/bind`;
|
||||
|
||||
// MCP tool refresh (cloud gateway native)
|
||||
export const refreshMcpTools = () => `${BASE_URL}/api/refresh-mcp-tools`;
|
||||
export const refreshMcpTools = () => `${BASE_URL}/v1/refresh-mcp-tools`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Prompts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const prompts = () => `${BASE_URL}/api/prompts`;
|
||||
export const prompts = () => `${BASE_URL}/v1/prompts`;
|
||||
export const addPromptToGroup = (groupId: string) =>
|
||||
`${BASE_URL}/api/prompts/groups/${groupId}/prompts`;
|
||||
`${BASE_URL}/v1/prompts/groups/${groupId}/prompts`;
|
||||
export const getPromptGroup = (_id: string) => `${prompts()}/groups/${_id}`;
|
||||
export const getPromptGroupsWithFilters = (filter: object) => {
|
||||
let url = `${prompts()}/groups`;
|
||||
@@ -422,15 +422,15 @@ export const updatePromptTag = (_id: string) => `${getPrompt(_id)}/tags/producti
|
||||
export const deletePromptGroup = getPromptGroup;
|
||||
export const deletePrompt = ({ _id, groupId }: { _id: string; groupId: string }) =>
|
||||
`${prompts()}/${_id}?groupId=${groupId}`;
|
||||
export const getCategories = () => `${BASE_URL}/api/categories`;
|
||||
export const getCategories = () => `${BASE_URL}/v1/categories`;
|
||||
export const getAllPromptGroups = () => `${prompts()}/all`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Roles & Permissions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const roles = () => `${BASE_URL}/api/get-permissions`;
|
||||
export const getRole = (roleName: string) => `${BASE_URL}/api/get-permission?id=${roleName.toLowerCase()}`;
|
||||
export const roles = () => `${BASE_URL}/v1/get-permissions`;
|
||||
export const getRole = (roleName: string) => `${BASE_URL}/v1/get-permission?id=${roleName.toLowerCase()}`;
|
||||
export const updatePromptPermissions = (roleName: string) => `${getRole(roleName)}/prompts`;
|
||||
export const updateMemoryPermissions = (roleName: string) => `${getRole(roleName)}/memories`;
|
||||
export const updateAgentPermissions = (roleName: string) => `${getRole(roleName)}/agents`;
|
||||
@@ -447,7 +447,7 @@ export const updateMarketplacePermissions = (roleName: string) =>
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const conversationTags = (tag?: string) =>
|
||||
`${BASE_URL}/api/tags${tag != null && tag ? `/${encodeURIComponent(tag)}` : ''}`;
|
||||
`${BASE_URL}/v1/tags${tag != null && tag ? `/${encodeURIComponent(tag)}` : ''}`;
|
||||
export const conversationTagsList = (pageNumber: string, sort?: string, order?: string) =>
|
||||
`${conversationTags()}/list?pageNumber=${pageNumber}${sort ? `&sort=${sort}` : ''}${
|
||||
order ? `&order=${order}` : ''
|
||||
@@ -459,23 +459,23 @@ export const addTagToConversation = (conversationId: string) =>
|
||||
// Misc
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const userTerms = () => `${BASE_URL}/api/user/terms`;
|
||||
export const acceptUserTerms = () => `${BASE_URL}/api/user/terms/accept`;
|
||||
export const banner = () => `${BASE_URL}/api/banner`;
|
||||
export const userTerms = () => `${BASE_URL}/v1/user/terms`;
|
||||
export const acceptUserTerms = () => `${BASE_URL}/v1/user/terms/accept`;
|
||||
export const banner = () => `${BASE_URL}/v1/banner`;
|
||||
|
||||
// Message Feedback
|
||||
export const feedback = (conversationId: string, messageId: string) =>
|
||||
`${BASE_URL}/api/update-message?id=${encodeURIComponent(messageId)}`;
|
||||
`${BASE_URL}/v1/update-message?id=${encodeURIComponent(messageId)}`;
|
||||
|
||||
// Memories
|
||||
export const memories = () => `${BASE_URL}/api/memories`;
|
||||
export const memories = () => `${BASE_URL}/v1/memories`;
|
||||
export const memory = (key: string) => `${memories()}/${encodeURIComponent(key)}`;
|
||||
export const memoryPreferences = () => `${memories()}/preferences`;
|
||||
|
||||
// Permissions search
|
||||
export const searchPrincipals = (params: q.PrincipalSearchParams) => {
|
||||
const { q: query, limit, types } = params;
|
||||
let url = `${BASE_URL}/api/permissions/search-principals?q=${encodeURIComponent(query)}`;
|
||||
let url = `${BASE_URL}/v1/permissions/search-principals?q=${encodeURIComponent(query)}`;
|
||||
if (limit !== undefined) {
|
||||
url += `&limit=${limit}`;
|
||||
}
|
||||
@@ -486,42 +486,42 @@ export const searchPrincipals = (params: q.PrincipalSearchParams) => {
|
||||
};
|
||||
|
||||
export const getAccessRoles = (resourceType: ResourceType) =>
|
||||
`${BASE_URL}/api/permissions/${resourceType}/roles`;
|
||||
`${BASE_URL}/v1/permissions/${resourceType}/roles`;
|
||||
export const getResourcePermissions = (resourceType: ResourceType, resourceId: string) =>
|
||||
`${BASE_URL}/api/permissions/${resourceType}/${resourceId}`;
|
||||
`${BASE_URL}/v1/permissions/${resourceType}/${resourceId}`;
|
||||
export const updateResourcePermissions = (resourceType: ResourceType, resourceId: string) =>
|
||||
`${BASE_URL}/api/permissions/${resourceType}/${resourceId}`;
|
||||
`${BASE_URL}/v1/permissions/${resourceType}/${resourceId}`;
|
||||
export const getEffectivePermissions = (resourceType: ResourceType, resourceId: string) =>
|
||||
`${BASE_URL}/api/permissions/${resourceType}/${resourceId}/effective`;
|
||||
`${BASE_URL}/v1/permissions/${resourceType}/${resourceId}/effective`;
|
||||
export const getAllEffectivePermissions = (resourceType: ResourceType) =>
|
||||
`${BASE_URL}/api/permissions/${resourceType}/effective/all`;
|
||||
`${BASE_URL}/v1/permissions/${resourceType}/effective/all`;
|
||||
|
||||
// SharePoint Graph API Token (IAM-managed)
|
||||
export const graphToken = (scopes: string) =>
|
||||
`${BASE_URL}/api/auth/graph-token?scopes=${encodeURIComponent(scopes)}`;
|
||||
`${BASE_URL}/v1/auth/graph-token?scopes=${encodeURIComponent(scopes)}`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Knowledge Stores (RAG)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const stores = () => `${BASE_URL}/api/get-stores`;
|
||||
export const store = (id: string) => `${BASE_URL}/api/get-store?id=${encodeURIComponent(id)}`;
|
||||
export const addStore = () => `${BASE_URL}/api/add-store`;
|
||||
export const updateStore = () => `${BASE_URL}/api/update-store`;
|
||||
export const deleteStore = () => `${BASE_URL}/api/delete-store`;
|
||||
export const refreshStoreVectors = () => `${BASE_URL}/api/refresh-store-vectors`;
|
||||
export const stores = () => `${BASE_URL}/v1/get-stores`;
|
||||
export const store = (id: string) => `${BASE_URL}/v1/get-store?id=${encodeURIComponent(id)}`;
|
||||
export const addStore = () => `${BASE_URL}/v1/add-store`;
|
||||
export const updateStore = () => `${BASE_URL}/v1/update-store`;
|
||||
export const deleteStore = () => `${BASE_URL}/v1/delete-store`;
|
||||
export const refreshStoreVectors = () => `${BASE_URL}/v1/refresh-store-vectors`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vectors (Embeddings)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const vectors = () => `${BASE_URL}/api/get-vectors`;
|
||||
export const addVector = () => `${BASE_URL}/api/add-vector`;
|
||||
export const deleteVector = () => `${BASE_URL}/api/delete-vector`;
|
||||
export const vectors = () => `${BASE_URL}/v1/get-vectors`;
|
||||
export const addVector = () => `${BASE_URL}/v1/add-vector`;
|
||||
export const deleteVector = () => `${BASE_URL}/v1/delete-vector`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workflows & Tasks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const workflows = () => `${BASE_URL}/api/get-workflows`;
|
||||
export const tasks = () => `${BASE_URL}/api/get-tasks`;
|
||||
export const workflows = () => `${BASE_URL}/v1/get-workflows`;
|
||||
export const tasks = () => `${BASE_URL}/v1/get-tasks`;
|
||||
|
||||
@@ -1244,9 +1244,9 @@ export const initialModelsConfig: TModelsConfig = {
|
||||
};
|
||||
|
||||
export const EndpointURLs = {
|
||||
[EModelEndpoint.assistants]: `${apiBaseUrl()}/api/assistants/v2/chat`,
|
||||
[EModelEndpoint.azureAssistants]: `${apiBaseUrl()}/api/assistants/v1/chat`,
|
||||
[EModelEndpoint.agents]: `${apiBaseUrl()}/api/${EModelEndpoint.agents}/chat`,
|
||||
[EModelEndpoint.assistants]: `${apiBaseUrl()}/v1/assistants/v2/chat`,
|
||||
[EModelEndpoint.azureAssistants]: `${apiBaseUrl()}/v1/assistants/v1/chat`,
|
||||
[EModelEndpoint.agents]: `${apiBaseUrl()}/v1/${EModelEndpoint.agents}/chat`,
|
||||
} as const;
|
||||
|
||||
export const modularEndpoints = new Set<EModelEndpoint | string>([
|
||||
|
||||
@@ -158,13 +158,13 @@ if (typeof window !== 'undefined') {
|
||||
}
|
||||
|
||||
// Don't retry auth endpoints
|
||||
if (originalRequest.url?.includes('/api/signin') === true) {
|
||||
if (originalRequest.url?.includes('/v1/signin') === true) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
if (originalRequest.url?.includes('/api/signout') === true) {
|
||||
if (originalRequest.url?.includes('/v1/signout') === true) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
if (originalRequest.url?.includes('/api/get-account') === true && originalRequest._retry) {
|
||||
if (originalRequest.url?.includes('/v1/get-account') === true && originalRequest._retry) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
|
||||
Generated
+30
@@ -374,6 +374,9 @@ importers:
|
||||
'@dicebear/core':
|
||||
specifier: ^9.2.2
|
||||
version: 9.4.2
|
||||
'@hanzo/iam':
|
||||
specifier: ^0.12.2
|
||||
version: 0.12.3(react@18.3.1)
|
||||
'@hanzo/ui':
|
||||
specifier: ^5.3.41
|
||||
version: 5.5.1(@hookform/resolvers@3.10.0(react-hook-form@7.71.2(react@18.3.1)))(@modelcontextprotocol/sdk@1.22.0(@cfworker/json-schema@4.1.1))(@tanstack/react-query@4.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(date-fns@3.6.0)(embla-carousel@8.6.0)(framer-motion@11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lucide-react@0.394.0(react@18.3.1))(mermaid@11.13.0)(mobx@6.15.0)(next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-hook-form@7.71.2(react@18.3.1))(react-resizable-panels@3.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19(yaml@2.8.3))(validator@13.15.26)
|
||||
@@ -3683,6 +3686,15 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
'@hanzo/iam@0.12.3':
|
||||
resolution: {integrity: sha512-AeeWA6cDDSXgW648UCusqeXetokH8nIMJqyIe7GfZ0P6SZJzxNnMBDWdOdtDgHC0YkFqeJNG09+NZZN0snWLkA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: '>=17'
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
|
||||
'@hanzo/ui@5.5.1':
|
||||
resolution: {integrity: sha512-r52J5OvjcFHqDYbr8Kr3K3j6sAnHb8tWkU261Xrk5pCC4Xhm1YZHdfTAQo9AO+izDKcMvsZKYxJ/p3jUwLS+qA==}
|
||||
hasBin: true
|
||||
@@ -6906,6 +6918,7 @@ packages:
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
|
||||
|
||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
|
||||
@@ -7018,6 +7031,7 @@ packages:
|
||||
'@xmldom/xmldom@0.8.11':
|
||||
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
deprecated: this version has critical issues, please update to the latest version
|
||||
|
||||
abab@2.0.6:
|
||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||
@@ -10088,6 +10102,9 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
libphonenumber-js@1.13.7:
|
||||
resolution: {integrity: sha512-rvr3HIMdOgzhz1RFGjftji+wjoAFlzhqCNqJOU/MKTZQ8d9NZxAR/tI+0weDicyoucqVR0U1GCniqHJ0f8aM2A==}
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -13190,6 +13207,7 @@ packages:
|
||||
|
||||
uuid@10.0.0:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
uuid@11.1.0:
|
||||
@@ -13198,10 +13216,12 @@ packages:
|
||||
|
||||
uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
v8-compile-cache-lib@3.0.1:
|
||||
@@ -17198,6 +17218,14 @@ snapshots:
|
||||
protobufjs: 7.5.4
|
||||
yargs: 17.7.2
|
||||
|
||||
'@hanzo/iam@0.12.3(react@18.3.1)':
|
||||
dependencies:
|
||||
jose: 6.2.2
|
||||
libphonenumber-js: 1.13.7
|
||||
passport-oauth2: 1.8.0
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
|
||||
'@hanzo/ui@5.5.1(@hookform/resolvers@3.10.0(react-hook-form@7.71.2(react@18.3.1)))(@modelcontextprotocol/sdk@1.22.0(@cfworker/json-schema@4.1.1))(@tanstack/react-query@4.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(date-fns@3.6.0)(embla-carousel@8.6.0)(framer-motion@11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(lucide-react@0.394.0(react@18.3.1))(mermaid@11.13.0)(mobx@6.15.0)(next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-hook-form@7.71.2(react@18.3.1))(react-resizable-panels@3.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.19(yaml@2.8.3))(validator@13.15.26)':
|
||||
dependencies:
|
||||
'@hookform/resolvers': 3.10.0(react-hook-form@7.71.2(react@18.3.1))
|
||||
@@ -25253,6 +25281,8 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
libphonenumber-js@1.13.7: {}
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
optional: true
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ services:
|
||||
- "5433:5432"
|
||||
|
||||
rag_api:
|
||||
image: ghcr.io/hanzoai/chat-rag-api:latest
|
||||
image: ghcr.io/hanzoai/chat-rag-api:v0.7.8
|
||||
environment:
|
||||
- DB_HOST=vectordb
|
||||
- DB_PORT=5432
|
||||
|
||||
+3
-3
@@ -14,10 +14,10 @@ const server = http.createServer((req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === '/api/health' || req.url === '/health') {
|
||||
if (req.url === '/v1/health' || req.url === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'ok', service: 'hanzo-runtime' }));
|
||||
} else if (req.url === '/api/sandboxes' && req.method === 'POST') {
|
||||
} else if (req.url === '/v1/sandboxes' && req.method === 'POST') {
|
||||
// Mock sandbox creation
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
@@ -26,7 +26,7 @@ const server = http.createServer((req, res) => {
|
||||
language: 'python',
|
||||
createdAt: new Date().toISOString()
|
||||
}));
|
||||
} else if (req.url.startsWith('/api/sandboxes/') && req.method === 'DELETE') {
|
||||
} else if (req.url.startsWith('/v1/sandboxes/') && req.method === 'DELETE') {
|
||||
// Mock sandbox deletion
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
|
||||
@@ -50,7 +50,7 @@ services:
|
||||
volumes:
|
||||
- pgdata2:/var/lib/postgresql/data
|
||||
rag_api:
|
||||
image: ghcr.io/hanzoai/chat-rag-api:latest
|
||||
image: ghcr.io/hanzoai/chat-rag-api:v0.7.8
|
||||
environment:
|
||||
- DB_HOST=vectordb
|
||||
- RAG_PORT=${RAG_PORT:-8000}
|
||||
|
||||
Reference in New Issue
Block a user