Compare commits

...
Author SHA1 Message Date
hanzo 660fe80e7c fix(auth): OpenID fallback button when embedded-IAM build config absent
The embedded @hanzo/iam <Login> only renders when VITE_HANZO_API_URL +
VITE_HANZO_IAM_URL/APP are baked in at build time. In the GHCR image build
those VITE_* args are not set, so isStaticMode=false and iamConfig=null -> the
login route rendered an EMPTY form (no OIDC button, no email form), since
emailLoginEnabled is also false. Add a self-contained OpenID SocialButton
fallback (server-proxied /oauth/openid -> hanzo.id) so there is always a usable
login path, matching the proven pre-embedded behavior. Reuses the SocialButton
pattern already in this file's auto-redirect branch.
2026-06-23 13:35:58 -07:00
hanzo 2fcc04b4c9 fix(deps): sync pnpm-lock @hanzo/iam 0.4.2 -> 0.12.3 to match client package.json ^0.12.2
frozen-lockfile docker build failed: lockfile pinned ^0.4.0 but the embedded-IAM
login work bumped client dep to ^0.12.2. Regenerated lockfile-only (pnpm 10.27.0).
2026-06-23 13:15:58 -07:00
hanzo 76066fdb41 fix(branding): real @hanzo blocky-H logo+favicon, dev-only devtools, working OIDC button icon
- App.jsx: gate ReactQueryDevtools behind import.meta.env.DEV via lazy dynamic
  import so the devtools floating button (red flower) never ships in prod.
- librechat.yaml + compose.prod.yml: OIDC/model iconURL & OPENID_IMAGE_URL
  hanzo.ai/logo/icon.svg (404) -> hanzo.chat/assets/logo.svg (served, canonical).
- index.html: add scalable SVG favicon (canonical blocky-H) as primary icon.
- post-build.cjs: copy favicon.ico into dist root so /favicon.ico serves.
- add real multi-size favicon.ico generated from @hanzo/brand favicon (blocky-H).

logo.svg/favicons already match @hanzo/brand canonical blocky-H byte-for-byte;
prod was serving a stale dist (title 'Chat', no assets) -> fresh build fixes.
2026-06-23 13:11:39 -07:00
Antje Worring 7f91fa5205 feat(auth): embedded in-app login via @hanzo/iam (no redirect to iam.hanzo.ai)
Replace the full-page signinRedirect() to the IAM-hosted page with the
embedded <Login> from @hanzo/iam/views, rendered in-app on hanzo.chat.
- bump @hanzo/iam ^0.4.0 -> ^0.12.2
- iam.ts: BrowserIamSdk -> IAM (0.12 browser class) + getHanzoIamConfig()
  (serverUrl/clientId/organization/redirectUri); org via VITE_HANZO_IAM_ORG
- Login.tsx: render <IamLogin> (password + email-code, PKCE-bound) instead
  of the redirect button; drop static auto-redirect
- OAuthCallback.tsx: IAMToken.access_token -> accessToken
Users stay on hanzo.chat and know they're entering Chat. PKCE end-to-end
via the same /auth/callback exchange.
2026-06-19 16:37:06 -07:00
zandGitHub 03a3842702 refactor(auth): migrate IAM to @hanzo/iam SDK (#23)
* refactor(auth): migrate IAM auth to @hanzo/iam SDK

Replace manual OIDC implementation with @hanzo/iam/browser SDK:
- Add @hanzo/iam dependency for BrowserIamSdk
- Create utils/iam.ts with SDK singleton and config
- Update Login.tsx to use sdk.signinRedirect()
- Update OAuthCallback.tsx to use sdk.handleCallback()
- Update LandingPage.tsx to use SDK for login redirects

* ci: add E2E tests and PR preview workflows

- e2e.yml: Run Playwright tests on PRs touching client/api/e2e
- pr-preview.yml: Deploy PR preview via reusable DOKS workflow
2026-05-31 14:28:20 -07:00
Hanzo AI 6fd5f38d93 refactor: /v1/ canonical paths (sweep)
Per global rule: /v1/ only, never /api/. Internal API calls + handler
comments + route registrations updated. External vendor APIs (Stripe,
KMS, etc.) left as-is. LibreChat upstream backend routes (api/server/*)
left as-is per "leave upstream-vendored paths" guideline; data-provider
SDK now points at Hanzo Cloud Gateway /v1/* canonical paths since
api.hanzo.ai is our gateway and /api/ on top of api.* is double-prefix.
2026-05-18 21:32:06 -07:00
Hanzo AI c8653b2cab chat: rename VITE_HANZO_IAM_* -> VITE_IAM_* in static SPA login
Collapse the transitional VITE_HANZO_IAM_{URL,APP,ORG} env aliases
to the canonical VITE_IAM_{URL,APP,ORG}. Aligns with the ecosystem
IAM_* convention (no HANZO_ prefix on identity vars).
2026-05-15 12:13:33 -07:00
Hanzo AI d254ca0f02 ci: add id-token: write to caller permissions
Required for hanzoai/.github/.github/workflows/docker-build.yml@main —
without it the workflow_call dies as startup_failure with no jobs
dispatched. Caller permissions are a CEILING.
2026-05-07 09:09:39 -07:00
Hanzo AI 94abdccfc5 deploy: pin :latest → semver across image refs
Replaces all ghcr.io/hanzoai/<svc>:latest pins with the latest published
semver tag for each service. Per CLAUDE.md auto-bump policy: mutable
branch tags (:latest, :main, :dev) are deprecated for cluster pins —
only immutable semver permitted.

Bulk update across services with published v* tags. Services without a
published semver remain on :latest until their release pipeline cuts a
v* tag.
2026-05-07 08:36:35 -07:00
Hanzo DevandGitHub a26e7eebb2 ci: remove upstream LibreChat CI cruft; enable arm64 in canonical (#30)
Deletes seven inherited workflows that published unused images
(chat-api, lc-dev*, lc-dev-staging*, LiteLLM ghcr_deploy/helm chart) —
none are referenced by hanzoai/universe manifests, all used QEMU-emulated
builds instead of ARC runners.

docker-publish.yml now builds linux/amd64 + linux/arm64 via the canonical
reusable. One workflow, one way, multi-arch by default.
2026-04-23 20:21:45 -07:00
Hanzo DevandGitHub 8c41dfb6a1 ci: canonical docker-build (#31)
* ci: remove upstream LibreChat CI cruft; enable arm64 in canonical

Deletes seven inherited workflows that published unused images
(chat-api, lc-dev*, lc-dev-staging*, LiteLLM ghcr_deploy/helm chart) —
none are referenced by hanzoai/universe manifests, all used QEMU-emulated
builds instead of ARC runners.

docker-publish.yml now builds linux/amd64 + linux/arm64 via the canonical
reusable. One workflow, one way, multi-arch by default.

* ci: migrate to canonical hanzoai/.github/docker-build.yml reusable
2026-04-23 19:24:34 -07:00
35 changed files with 977 additions and 1363 deletions
-72
View File
@@ -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 }}
-72
View File
@@ -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 }}
-66
View File
@@ -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 }}
+6 -1
View File
@@ -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:
+71
View File
@@ -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
-433
View File
@@ -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
-67
View File
@@ -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
-69
View File
@@ -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 }}
+21
View File
@@ -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
-67
View File
@@ -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 }}
+7
View File
@@ -0,0 +1,7 @@
name: Workflow Sanity
on:
pull_request:
paths: ['.github/workflows/**']
jobs:
sanity:
uses: hanzoai/.github/.github/workflows/workflow-sanity.yml@main
+1
View File
@@ -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)
+5 -5
View File
@@ -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)}&currency=usd`,
`/v1/billing/balance?user=${encodeURIComponent(userId)}&currency=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,
+1
View File
@@ -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" />
+1
View File
@@ -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

+2 -1
View File
@@ -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
View File
@@ -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>
+56 -57
View File
@@ -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>
)}
+14 -38
View File
@@ -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">
+543 -277
View File
@@ -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);
});
});
+1 -1
View File
@@ -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 }`
+71
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+116 -116
View File
@@ -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`;
+3 -3
View File
@@ -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>([
+3 -3
View File
@@ -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);
}
+30
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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 }));
+1 -1
View File
@@ -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}