Compare commits

...
Author SHA1 Message Date
Hanzo AI 1eea55929e feat(images): Fix action + previous-image picker for generated images
Add a 'Fix' affordance on AI-generated images (hover pill on the message
thumbnail and an action in the fullscreen dialog) that attaches the image
to the composer as a reference file and seeds a 'Fix this image: ' prompt,
so the user only describes the fix.

Three attach paths, all reusing the existing file pipeline:
- Fix: attaches the generated image by reference (file_id, no re-upload) via
  useAttachImage, injecting a completed ExtendedFile (attached: true) into the
  composer file map — the send path forwards it verbatim as multimodal content.
- Drag-and-drop: plain image/file drops with no tool-resource choice now attach
  straight into the current conversation instead of a single-option dead-end modal.
- Attach a previous image: new attach-menu item opens a picker of the current
  conversation's images (collectConversationImages) and attaches by reference.

Pure helper collectConversationImages/resolveImageUrl unit-tested (10 cases).
i18n keys added to the default locale. No backend changes.
2026-07-16 12:58:03 -07:00
b2aab4aff9 refactor: repoint reward-signal client onto the shared @hanzo/ai SDK (#72)
The content-free router-training feedback transport now lives once, in
`@hanzo/ai` `sendFeedback` (v0.2.1). `client/src/utils/rewardSignal.ts` becomes
a THIN adapter: it keeps the exact public `sendRewardSignal(requestId, signal,
rating?)` the message-actions and model-selector surfaces wire to, plus the
local `VITE_HANZO_FEEDBACK` opt-out, and delegates emission to the SDK with the
chat-specific `baseUrl` (VITE_HANZO_API_URL, cross-origin gateway) and the
end-user bearer lifted from axios defaults. The SDK owns the whitelisted
`{request_id, signal, rating?}` body, the dedupe, and the fire-and-forget
transport — so no prompt/response/code can transit, exactly as before.

The existing spec passes untouched (behavior preserved end-to-end through the
SDK; jsdom has no sendBeacon, so the SDK falls back to the same keepalive fetch
with the same headers).

Tests: client/src/utils/rewardSignal.spec.ts — 13/13 passed.

Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-16 11:15:34 -07:00
zandGitHub 3f4addac86 feat(analytics): instrument chat with @hanzo/capture (#70)
Client-side product analytics via @hanzo/capture: pageviews, identify(user.id), and CHAT_STARTED/CHAT_MESSAGE_SENT with endpoint+model identifiers only — no message content or PII. Lockfile + tsconfig subpath mapping included.
2026-07-16 10:14:12 -07:00
f2778c26cb feat(reward-signals): emit content-free router-training feedback to gateway (v0.9.23) (#71)
Emit CONTENT-FREE reward signals to `POST {VITE_HANZO_API_URL}/v1/feedback` so
the router training loop gets production feedback. Payload carries ONLY
{request_id, signal, rating?} — never any prompt/response/tag/text/filename/code.
Fire-and-forget: never blocks UX, silent no-op on any failure.

The routing ledger keys each RoutingEvent on the upstream gateway response id
(chatcmpl-…/msg_…). The chat client previously held only locally-minted UUIDs,
so capture the REAL id server-side and thread it to the client as one field:

- server: ModelEndHandler captures `data.output.response_metadata.id ?? .id`
  (the last CHAT_MODEL_END = the visible answer) into a shared `runMetadata`;
  AgentClient surfaces it via `this.metadata` → `feedbackRequestId` on the saved
  response message, riding the SSE final event + DB save to the client.
- schema: add optional `feedbackRequestId` to tMessageSchema (zod), IMessage
  (mongo type) and the mongoose message schema (persists; SQLite keeps it in its
  JSON doc blob automatically). Absent id ⇒ signal no-ops (never fabricated).

Client signals wired via new `client/src/utils/rewardSignal.ts`:
- thumbs up/down → up/down; regenerate → regenerate; copy → up (weak positive);
  model-switch right after a response → switch (last assistant msg's id).
Dedicated cross-origin credentialed fetch (keepalive) to the Hanzo API — NOT the
same-origin chat backend — forwarding the end-user bearer for per-org attribution.
Honors local opt-out `VITE_HANZO_FEEDBACK=0|false|off` (server-side org/user
training opt-in is the preferred enforcement). 13 unit tests, all green.

Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-16 09:19:20 -07:00
Hanzo AI 12bc51f901 fix: CloudUsage.js imports @hanzochat/data-schemas (not @librechat) — 0.9.22
The controller required '@librechat/data-schemas', but this fork renames all
internal packages to @hanzochat/* (214 other files use @hanzochat/data-schemas;
that upstream name resolves to nothing). Runtime-only failure (require-time), so
the green build hid it and 0.9.21 CrashLooped at CloudUsage.js:1 MODULE_NOT_FOUND
— AFTER clearing the 0.9.21 zod/v4 fix. One-line name fix; all other requires in
the usage backend already resolve.
2026-07-16 01:25:03 -07:00
Hanzo AI dff9f48c17 fix: force zod@3.24.4→3.25.76 so openai@6 resolves zod/v4 (0.9.21)
Any fresh chat image built after 74954c3f8 (@hanzo/iam 0.13 lockfile regen,
which pulled openai@6.46.0) crashes on boot:

  ERR_PACKAGE_PATH_NOT_EXPORTED: subpath './v4' is not defined by
  "exports" in openai/node_modules/zod/package.json

openai@6.46.0 declares zod '^3.25 || ^4.0' and imports zod/v4, but pnpm
mis-resolved its peer to the older zod@3.24.4 (which has no ./v4 subpath).
The deployed 0.9.19 image predates openai@6 entering the tree, so it never
hit this — 0.9.20 was simply the first rebuild to trip the latent landmine.

Fix: a single pnpm override zod@3.24.4 → 3.25.76 (already in-tree, a
backward-compatible superset that ships the zod/v4 compat subpath),
collapsing to ONE zod. Verified: no other package floated; openai@6.46.0
now pairs zod@3.25.76. Unblocks the whole chat build pipeline, not just
the 0.9.20 usage panel.
2026-07-16 01:04:18 -07:00
Hanzo AI 7a24877eaf usage: canonical cloud usage in Settings (proxy /v1/get-cloud-usages, @hanzo/usage)
Adds the shared Hanzo usage read to LibreChat's Usage tab, beside (not
replacing) the Mongo token-credit view. Backend CloudUsage.js proxies
cloud's GET /v1/get-cloud-usages on-behalf-of (hanzo.id bearer resolved
server-side, never to browser); client renders the CloudUsageOverview
with @hanzo/usage's headless normalizeCloudUsage over native Tailwind.
Honest when unavailable: 200 {enabled:false} hides the section.
2026-07-15 18:26:33 -07:00
Hanzo Dev 01597b9bb2 merge: complete @hanzochat fork + org/project/user switcher
Brings the fork completion (de-librechat, @hanzochat scope rename, @hanzo/iam
0.13 API) and the org/project/user switcher (validated active-org -> X-Org-Id
on both cloud seams) onto main for release.
2026-07-14 12:46:42 -07:00
Hanzo Dev 9c4488d66e chat: org/project/user switcher — active org rides X-Org-Id to cloud
The account menu shows user -> org -> project and lets a multi-org member
switch their working org. The switch pins hanzo_active_org (httpOnly),
validated server-side against the caller's own membership set (owner + groups)
via POST /v1/chat/user/active-org. resolveActiveOrg() forwards it as X-Org-Id
on both on-behalf-of seams — the inference path (custom/initialize.ts) and
cloud agents (CloudAgentsClient) — where the gateway re-validates it in the
member's set (HIP-0026), so the header is a validated selection, never a trust
assertion. Adds Account/Console/Billing links. openidStrategy persists
organization/project/groups from the JWT claims; the user schema + TUser carry
them and getUserController serves them.
2026-07-14 12:46:26 -07:00
Hanzo Dev b237551ffa chat: de-librechat — CHAT_ env, chat.yaml, Chat identifiers; README rewrite + zh
- env vars LIBRECHAT_* -> CHAT_* (USER_ID, GRAPH_ACCESS_TOKEN, OPENID_*, LOG_DIR, ...)
- config file librechat.yaml -> chat.yaml (matches prod CONFIG_PATH) + .example
- identifiers LibreChat -> Chat (extractChatParams, ChatKeys, ChatParams, importChatConvo)
- lowercase librechat -> chat: i18n keys, helm charts (helm/chat, helm/chat-rag-api),
  CI workflows, OTel service/span keys, codeapi JWT issuer, x-chat-thread-id header
- README.md rewritten (Node 24, pnpm 10, Zen family, Cloud Agents, hanzo.id OIDC);
  README.zh.md added (Simplified Chinese)

MIT LICENSE copyright + one 'Forked from LibreChat (MIT)' attribution retained.
Build 5/5 green.
2026-07-14 10:41:50 -07:00
Hanzo Dev c7f7ef984e chat: drop all librechat.ai URLs; attribute as Hanzo
- librechat.ai doc/ToS/privacy URLs -> hanzo.ai / hanzo.chat
- OpenRouter attribution HTTP-Referer + X-Title -> https://hanzo.chat / Hanzo Chat
  (source + specs updated in lockstep)
- registry.librechat.ai dev images -> hanzoai/chat + ghcr.io/hanzoai/rag-api
- remove upstream README.zh.md (Hanzo zh translation follows)

MIT LICENSE copyright retained (legal attribution).
2026-07-14 10:31:38 -07:00
Hanzo Dev 74954c3f81 fix: @hanzo/iam 0.13 API (BrowserIamSdk->IAM) + regenerate pnpm-lock
The static/IAM SPA path (client/src/utils/iam.ts, OAuthCallback.tsx) imported
BrowserIamSdk, removed when @hanzo/iam bumped 0.4->0.13 (HIP-0111); the class is
now IAM with an identical constructor config. pnpm-lock.yaml regenerated for the
@hanzochat rename. Fork now builds 5/5 tasks green.
2026-07-14 10:23:39 -07:00
Hanzo Dev e9b4c41d2d fork: complete @hanzochat rename — agents runtime + lockfile canonicalization
- @librechat/agents -> @hanzochat/agents@^3.2.63 (published fork of
  danny-avila/agents at github.com/hanzochat/agents; upstream remote kept
  so agent-runtime fixes stay pullable)
- final sweep: zero @librechat/* package refs (chat internals + runtime)
- remove dead packages/agents residue (stale dist, no package.json)
- drop stale npm/bun lockfiles; pnpm-lock.yaml is the canonical lockfile
2026-07-14 09:31:40 -07:00
Hanzo Dev ba0eb6f2d2 fork: rename internal workspace packages to @hanzochat scope
librechat-data-provider  → @hanzochat/data-provider
@librechat/data-schemas  → @hanzochat/data-schemas
@librechat/client        → @hanzochat/client
(@hanzochat/api already forked; external @librechat/agents fork follows)

All imports, package.json names/deps, tsconfig path aliases, jest
moduleNameMapper, and rollup/vite build aliases updated. The data-provider
react-query subpath resolves via the exports map. Lockfiles regenerated in a
follow-on once the canonical package manager is confirmed.
2026-07-14 05:55:49 -07:00
zeekayandClaude Opus 4.8 3bb78e514c Mobile polish: true-black convergence, overlay cap, 44px composer targets
True-black (client/src/style.css, client/tailwind.config.cjs):
- Converge the off-scale #0a0a0a onto the token scale. --gray-925 (consumed by
  --presentation / --surface-primary-alt / --surface-dialog) and --green-950,
  plus the Tailwind mono ramp's 950, now resolve to #050505, so inner panels /
  dialogs match the elevated-surface value instead of reading as a lighter grey.
  Verified in-browser: --gray-925/#050505, dark --surface-primary-alt/-dialog =
  #050505, --surface-primary/-chat = #000.
- Root.tsx auth-loading fallback and LandingPage.tsx local tokens (bg #000 /
  card #050505 / muted #171717) converged off #0a0a0a onto the same scale.

Overlay cap (client/src/mobile.css):
- Cap .nav-mask and .sidenav-mask at max-width:420px (anchored to the drawer's
  own edge — left for nav, right for sidenav) instead of a full-width scrim, so
  on a tablet the dim covers only the drawer footprint, not the whole viewport.
  Align the nav-mask media query to 768px to match Nav.tsx's
  useMediaQuery('(max-width: 768px)') so the cap also covers the tablet-portrait
  boundary where the drawer is active. Verified: at 768px the scrim renders 420px
  (55% of viewport), not 100%.

Touch targets (client/src/mobile.css, client/src/components/Chat/Input/ChatForm.tsx):
- Add a `composer-actions` marker on the composer action row and bump its round
  action buttons (attach / tools / mic / send-stop, previously size-9 = 36px) to
  a 44px min touch target on touch viewports. Verified 44x44 at 375px, unchanged
  36px at desktop.

Build unblock (pre-existing dependency drift that hard-crashed the client):
- NetworkWallet.tsx: @hanzo/ui 5.x dropped ./network and ./wallet exports, whose
  named imports failed the Vite dep-scan and Rollup build; render null with a
  re-enable recipe (mirrors hanzo/app).
- utils/iam.ts: @hanzo/iam 0.4.x exports the SPA client as BrowserIamSdk, not
  IAM; the wrong name crashed the app at mount and failed the build. Same
  constructor config; swap to BrowserIamSdk.

Verified: `vite build` passes; Playwright at 375/768/1280 confirms true-black
surfaces, the 420px overlay cap, and 44px composer targets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 23:42:54 -07:00
zeekayandClaude Opus 4.8 d4a9b14ba6 chore(release): chat v0.9.20 (QueryBuilder.distinct ACL fix)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:23:51 -07:00
zeekayandClaude Opus 4.8 b49354fd1a fix(store/acl): implement QueryBuilder.distinct so ACL resolution stops crashing
The SQLite DocModel (the Mongo replacement chat migrated onto) exposed
`distinct()` only on the model (static call sites like
`PromptGroup.distinct('category', filter)` worked), but `find()` returns a
`QueryBuilder` that lacked it. Every query-chain call site —
`AclEntry.find(q).distinct('resourceId')` (findAccessibleResources,
findPubliclyAccessibleResources) and `AgentCategory.find(q).distinct('value')`
— threw "find(...).distinct is not a function", crashing permission
resolution.

One-place DRY fix mirroring mongoose's `Query.prototype.distinct(field)`:
QueryBuilder.distinct(field) is chainable (so a trailing `.lean()` still
works) and delegates to the existing DocModel.distinct, keeping filter/tenant
scoping identical to `.find()`. No mongoose reintroduced.

Tests: added ACL `$or`+`$bitsAllSet` distinct + AgentCategory
`.distinct().lean()` cases to DocModel.spec.ts (14/14 pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:21:19 -07:00
zeekayandClaude Opus 4.8 3759e382f6 chat: monochrome the composer badge-row chrome
Neutralize the per-tool accent hues on the composer's active
CheckboxButton chips so the shell chrome recedes and stays true-black
monochrome (matching hanzo.ai). Each tool's "on" state painted itself a
distinct color — Search=blue, Code=purple, FileSearch=green,
Skills=cyan, Artifacts=amber (+ its split-menu button) — reading as a
rainbow in the composer. Unify to one neutral active treatment via the
existing semantic tokens (border-border-heavy + bg-surface-active-alt),
which already track the dark/true-black theme.

Also drops the stray text-cyan-500 on the queued-skills chip icon →
text-text-secondary.

Shell top-left (H mark alone + brand right-click menu) and bottom-left
(consolidated AccountSettings cluster) are already correct via the
shared @hanzo/ui HanzoHeader (v5.5.1) + AccountSettings — no fork.
No org switcher exists in chat (org is server-derived from the hanzo.id
owner claim); none fabricated. Semantic hues (balance status, errors,
online dots) left intact. Message content / syntax highlighting
untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 14:11:12 -07:00
zeekay 963399cedc chat: add login util 2026-07-10 09:40:34 -07:00
6110349085 fix(chat/sse): surface upstream errors instead of a silent empty bubble (#68)
A rejected gateway key or an out-of-credits balance can end a `stream:true`
completion with NO content and WITHOUT throwing — api.hanzo.ai answers as a
200 event-stream that yields zero deltas — so the agent run "succeeds" with an
empty response and the client renders an EMPTY assistant bubble. This is the
exact silent failure behind the dead-guest-key GA blocker (a dead key -> 401 ->
{"msg":"invalid API key"} that the SSE client rendered as an empty reply).

Add a backstop in ResumableAgentController: right after `client.sendMessage`,
if the response carries no user-visible content and the run was not aborted,
throw. The throw lands in the existing catch -> `GenerationJobManager.emitError`
-> the `event: error` SSE frame the client already renders as an error bubble.
No new SSE plumbing — it reuses the one error path, so a dead key or exhausted
balance can never again be invisible.

The emptiness decision is a pure, unit-tested helper (`isEmptyAgentResponse`):
non-empty iff there is non-whitespace `text`, a non-blank text/think part, or
any non-text content part (error, tool_call, image, ...). An ERROR part still
renders, so the gateway-threw path is untouched.

Tests:
- emptyResponse.spec.js: 8 cases pinning the predicate (empty content array,
  blank-text-only, error/tool_call/text parts non-empty).
- emptyResponseController.spec.js: drives the real controller — empty response
  emits `error` (not a silent `done`); a response with content emits `done`.

Claude-Session: https://claude.ai/code/session_016yg7GPhYdWCh9vpp4HEwLZ

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-09 22:14:41 -07:00
zeekayandClaude Opus 4.8 dd8fb7194c feat(picker): Zen family at top, Agents second, providers after
Model selector now surfaces the house family as "Zen": custom endpoints honor
their configured modelDisplayLabel as the group label, so the endpoint VALUE
stays "Hanzo" (existing conversations + pinned guest endpoint unaffected) while
the picker group reads "Zen". Config bumps the third-party providers to
customOrder 2+ so the built-in Agents endpoint (order 1) sits alone between Zen
(0) and the providers — Zen first, Agents second, other providers after. Does
not conflate Hanzo (agents) with Zen (models).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 17:19:14 -07:00
zeekayandClaude Opus 4.8 4448e8d49c feat(chat): project-scoped conversations linked across surfaces
Accept ?project=<slug> (the org-unique key of the ONE cloud /v1/projects store,
org from the IAM JWT). A slim ProjectBanner shows the active project and links
the same slug back to the hanzo.app builder and console.hanzo.ai; a new
conversation opened for a project seeds the composer with a short opener so the
assistant has project context. Slug validated against the store's grammar and
persisted for the session so the scope survives the /c/new -> /c/:id navigation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 16:24:49 -07:00
cb0649e10b feat(chat): shared NetworkSwitcher + WalletMenu — the ONE hanzo.network standard (#67)
* feat(chat): shared NetworkSwitcher + WalletMenu in the HanzoHeader

Puts hanzo.chat on the ONE hanzo.network standard (same model as the
hanzo CLI, console, desktop, app): @hanzo/ui/network + @hanzo/ui/wallet
via the npm alias @hanzo/ui -> @hanzo/ui-shadcn@^5.7.3 (the 5.x line
continued under its post-v8-rename name; existing imports untouched).

- Nav/NetworkWallet: the surface adapter — injected EIP-1193 wallet
  (non-custodial, zero key material) pinned to the selected network env
  (mainnet 36963 / testnet 36964 / devnet 36965 / local / custom).
- Mounted via the HanzoHeader headerRight slot; dark-scoped to match the
  monochrome bar.
- tailwind: scan the shared components in node_modules; add the missing
  popover/destructive tokens (fallback to background/foreground vars).

vite build green; tsc baseline unchanged (1014 pre-existing errors on
clean main, zero added — upstream CI gates vite build only).

* chore(chat): bump @hanzo/ui-shadcn 5.7.3 -> 5.7.4 (genesis-canonical chain IDs)

Picks up the corrected network set (testnet 36962, devnet 36964, local
1337). vite build green.

---------

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-08 09:12:23 -07:00
zeekay 31af61cb8b Merge remote-tracking branch 'origin/main'
# Conflicts:
#	api/server/middleware/__tests__/enforceGuestScope.spec.js
#	api/server/middleware/enforceGuestScope.js
#	api/server/services/guestConfig.js
2026-07-08 06:05:49 -07:00
212ba7d822 feat(mobile): honor server-driven auto-routing defaults from admin (#66)
Mirror the web client's server-driven Smart Routing onto the mobile
surface. On boot (authed), fetch the SAME backend route the web client
uses — GET /v1/chat/routing-defaults — fail-soft to { available: false }
so any error keeps today's local-toggle-only behavior.

- lib/routing.ts: mirror of web resolveSmartRouting (pure) plus org
  derivation helpers and a module cache for the non-React api layer.
- lib/settings.ts: smart-routing pref is now nullable (null === follow
  org default; true/false === explicit user choice that wins).
- lib/api.ts: fetchRoutingDefaults() boot fetch; chat() resolves the
  effective model from pref + cached org defaults.
- App.tsx: authed AppShell fetches defaults once, caches + holds state.
- Usage.tsx: toggle uses resolveSmartRouting; locks off with copy
  'Disabled for your organization' when auto_routing_active is false.

Resolver is mirrored (not imported) across app boundaries, matching the
three web apps; web copy carries the jest coverage. Typecheck + build
green.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-07 23:31:40 -07:00
zeekay 16745e26fa fix: true-black #000 chat body canvas (was #212121 gray) — DESIGN.md §4 2026-07-07 22:24:03 -07:00
e661f4c0b6 feat(routing): honor server-driven auto-routing defaults from admin (#65)
New conversations resolve smart routing from the org's server-driven
defaults (GET /v1/chat/routing-defaults proxying cloud's
/v1/get-routing-defaults on-behalf-of) with the local toggle as a
nullable user override (null = follow org default). Fail-soft: an older
cloud-api (404), network error, or non-ok wrapper behaves exactly as
today (local preference only). When auto_routing_active is false the
toggle is locked with honest copy.

Effective state is one pure function, resolveSmartRouting
(client/src/utils/endpoints.ts), unit-tested in endpoints.spec.ts.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-07 18:40:42 -07:00
Hanzo AI 9e5e158145 feat(routing): user-facing Smart Routing toggle (web + mobile)
Web: 'Smart routing' toggle in the Usage settings tab, persisted via the
smartRouting localStorage recoil atom. When on, new conversations on the
Hanzo endpoint default to model "auto" (gateway routes each prompt to the
best/cheapest capable model; billed as whatever serves it). Scoped to the
Hanzo house endpoint and only applied to defaults — an explicit model pick
or a non-Hanzo provider family is never rewritten. Message header surfaces
the served model (msg.model) when a routed convo echoes it. Docs link +
en localization strings.

Mobile: same toggle on the Usage screen, persisted to localStorage via a
small settings helper; flips chat between VITE_CHAT_MODEL and "auto" in
lib/api.ts.
2026-07-07 17:49:06 -07:00
zeekayandClaude Opus 4.8 c1df3816e3 chat: tier-aware guest model deep-links (GUEST_MODELS allowlist)
Guest deep-links (hanzo.chat/?endpoint=Hanzo&model=zen4-max) honored flexibly:
allowlisted models run free (within GUEST_MESSAGE_MAX); off-list/paid models return
402 MODEL_REQUIRES_LOGIN so the client opens login. Back-compat; fail-closed preserved.
guestConfig GUEST_MODELS allowlist + resolveGuestModel; enforceGuestScope honors it;
buildGuestModelsConfig exposes it. 19 tests pass. Client wiring + GUEST_MODELS env + deploy remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 16:47:41 -07:00
Hanzo AI 77f768f7fa feat(usage): unified per-user AI Usage settings tab
Backend: GET /api/usage (requireJwtAuth) aggregates this user's prompt+
completion Transaction spend over today/7d/30d plus a per-model breakdown,
enriched with the org tier from CommerceClient. Response mirrors the
@hanzo/usage UsageSnapshot shape (providerId 'hanzo', totals, providerCost)
for one wire format across Hanzo products.

Frontend: new Usage settings tab beside Balance, reusing the UsageBar idiom;
useGetUserUsage hook follows useGetUserBalance; en localization strings added.
2026-07-07 15:31:03 -07:00
Hanzo AI 885f6ec33c feat(mobile): scaffold Hanzo AI mobile app (Tauri v2 + React 19 + @hanzo/gui)
Self-contained mobile/ app (not in any workspace):
- Tauri v2 shell (productName "Hanzo AI", id ai.hanzo.chat) with fs + http
  plugins, scoped capabilities (fs: ~/.codex ~/.claude ~/.hanzo; http:
  chatgpt.com, api.anthropic.com, api.hanzo.ai, claude.ai, hanzo.chat).
- React 19 + Vite + @hanzo/gui (createGui + defaultConfig from
  @hanzogui/config/v5). Three state-nav screens: Chat, Sessions, Usage.
- Chat -> POST /api/agents/v1/chat/completions (OpenAI-compatible);
  Sessions -> GET /api/convos; both Bearer-auth. Usage via @hanzo/usage
  (createTauriHost + useUsage), connect empty-state outside Tauri.
- Bearer token via VITE_CHAT_TOKEN or paste screen (localStorage); IAM TODO.
- Rust engine/node feature flags OFF by default; cfg-gated engine_status +
  device_register stubs pointing at hanzoai/engine + hanzoai/node.

vite build green; cargo check green (default + engine,node).
2026-07-07 15:29:38 -07:00
hanzo-dev 83554c557d Merge branch 'feat/unified-iam-tenant-auth' 2026-07-07 10:43:17 -07:00
hanzo-dev 98d0dad34b feat(auth): unified IAM user-token tenancy; delete guest/shared-key cruft 2026-07-07 10:42:29 -07:00
zeekayandClaude Opus 4.8 44956e86b9 fix(test): faithful winston.format mock so data-schemas logger loads
The api jest winston mock returned the raw transform fn from
`winston.format(fn)`, so any `redactFormat()` call ran `fn(undefined)` and
crashed at `info.level`. `@librechat/data-schemas`'s own `config/winston`
calls `redactFormat()` at import time, so every backend suite that pulls in
`createModels`/`logger` failed to run ("Cannot read properties of undefined
(reading 'level')" at data-schemas parsers.ts:63).

Make the mock faithful to winston's contract: `format(fn)` returns a factory
that yields a Format instance (`{ transform: fn }`) — it never invokes the
transform at construction. On the models/strategies/Config slice this turns
20 fail-to-run suites into 20 passing (tests running 174 -> 681); the
level-throw is fully gone. Remaining failures are unrelated pre-existing
issues (DB-backed model tests, azureOpenAI config, role permissions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 09:59:00 -07:00
hanzo-dev 167108dca5 release(0.9.17): P1 session/token/transaction store-aware fix + frontend build-hang fix 2026-07-04 21:10:29 -07:00
hanzo-dev c57194b535 Merge commit '8b4a7a6b4b' into integrate-0917
# Conflicts:
#	packages/data-schemas/dist/index.cjs.map
#	packages/data-schemas/dist/index.es.js.map
2026-07-04 20:11:07 -07:00
hanzo-dev 6cab482687 fix(store/sqlite): decouple store types from better-sqlite3 driver types — fixes frontend build hang
The node:sqlite → better-sqlite3-multiple-ciphers driver swap made the store's
public type surface (SqliteHandle.db, DocModel.db, openDatabase) reference the
driver's shipped `Database.Database` type. That type is large and conditional-
generic-heavy (`prepare<...>` → `Statement<...>`). Because @hanzochat/api imports
@librechat/data-schemas' public types (253 files), the driver types entered api's
TS program and the checker's structural-relation pass exploded (recursiveTypeRelatedTo
/ instantiateType depth-limit storm): api's rollup type-check ran >27 min and CI
canceled it. Pre-swap, node:sqlite's DatabaseSync was absent from @types/node@20 so
it resolved to `any` — trivial surface, build completed in ~3 min.

Fix: declare the minimal better-sqlite3 surface the store actually uses
(exec/prepare/pragma/close, run/get/all) as a local `SqliteDatabase`/`SqliteStatement`
interface and use it in place of `Database.Database`; cast the lazy require to the
minimal ctor. Runtime is unchanged — the native handle satisfies the contract
structurally. Rebuilt data-schemas dist.

Local proof (Node 20):
- api tsc --noEmit: >27min/canceled -> 151s
- api rollup build (--max-old-space-size=6144, as Dockerfile): 98s, emits dist
- pnpm run frontend: completes end-to-end, exit 0
2026-07-04 20:02:26 -07:00
hanzo-dev 8b4a7a6b4b fix(api/models): route Transaction + Role creation through the store-aware .create()
The prod hand-written model layer (api/models/*, separate from the data-schemas
methods) still constructed mongoose documents, which throw under the CHAT_STORE_SQLITE
flip where these models resolve to the SQLite DocModel/DualWriteModel:

- api/models/Transaction.js (createTransaction / createStructuredTransaction /
  createAutoRefillTransaction): 'new Transaction(txData); ...; await transaction.save()'
  -> a persistTransaction(txData, calculate) helper that runs the calculator FIRST
  (it mutates rate/tokenValue/rateDetail in place, so ordering matters), strips the
  non-schema calculator inputs (endpointTokenConfig/inputTokenCount/rateDetail) to
  match mongoose's on-save strip, then Transaction.create(). Balance math, the
  returned {rate,user,balance,[tokenType]} and result.transaction are unchanged.
- api/models/Role.js getRoleByName: 'new Role(defaults).save()' -> Role.create();
  '.toObject()' is present on the created doc across all backends. The name-required
  guard is untouched. Prevents 'Role is not a constructor' breaking RBAC self-heal.

Same bug class as the session P1; these are the actual prod paths (a DRY smell vs
the data-schemas methods — consolidation deferred). api-only; no data-schemas dist
change. The existing api Transaction/Role specs cannot run in this env due to a
PRE-EXISTING logger-mock harness break (reproduced on clean origin/main); the
identical persistTransaction pattern is proven green in data-schemas on both the
mongoose and SQLite/DualWrite stores.
2026-07-04 19:59:21 -07:00
hanzo-dev ca72857533 build(data-schemas): rebuild dist; annotate return types to unblock declaration emit
Typing the store-resolved model as Model<IUser>/Model<ITransaction> (matching the
28 migrated domains) ballooned the inferred return of the methods that had NO
explicit return type — searchUsers, and getTransactions/deleteTransactions/
deleteBalances/createAutoRefillTransaction — past TS's serialization limit
(TS7056), so their factory .d.ts stopped emitting. Add explicit return types to
exactly those methods (pure type annotations + two no-op .lean() casts; zero
runtime change). Declaration emit is restored.

Rebuild the shipped dist (npm run build, exit 0): bundles + session/token/user
type declarations now carry the store-aware DataHandle signatures. Transaction
methods are internal (not in AllMethods), so they are tree-shaken from the bundle
and their .d.ts is not part of the shipped surface.
2026-07-04 19:48:40 -07:00
hanzo-dev 3595b2af22 test(data-schemas/auth): prove Session/Token/User/Transaction against the SQLite + DualWrite store
Adds authStore.sqlite.spec.ts (isolated native-sqlite spec): the REAL production
factories run against both served shapes the flip produces —
  1) DualWriteModel (SQLite primary + SQLite mirror), and
  2) the real createModels() wiring under CHAT_STORE_SQLITE (DocModel served).

Covers the exact regression (createSession no longer throws 'Session is not a
constructor'): createSession → findSession(refreshToken|sessionId) →
updateExpiration → generateRefreshToken(rotate) → deleteSession →
deleteAllUserSessions → countActiveSessions; createToken → findToken →
updateToken → deleteTokens (verify/reset + email normalization); createUser →
findUser → getUserById → updateUser → deleteUserById + Balance seeding;
createTransaction (debit + non-schema-field stripping) / createStructuredTransaction
/ createAutoRefillTransaction → getTransactions / findBalanceByUser /
deleteTransactions. Asserts refresh tokens stay hashed and the served model is
never a mongoose Model. Registered in test/ci.mjs ISOLATED (native driver).

14/14 green; the 101 pre-existing mongoose specs for the touched files still pass.
2026-07-04 19:32:51 -07:00
hanzo-dev 0359f4c8fb fix(data-schemas/auth): route User+Transaction onto DataHandle; drop storeHandle cast
Same store-bypass class as Session: transaction.ts constructed documents with
new Transaction() + .save() (latent P1 — breaks identically once Transaction is
SQLite-served); user.ts + transaction.ts resolved their model via mongoose.models.
Refactor both factories to take a DataHandle and resolve via handle.models.<Name>.

Transaction create/auto-refill/structured now persist via the bounded .create()
(persistTransaction), stripping the non-schema calculator inputs
(endpointTokenConfig/inputTokenCount/rateDetail) so the persisted document is
byte-identical to the mongoose path (which strips non-schema paths on save);
balance math + returned TransactionResult unchanged. User already used .create()/
.findByIdAndUpdate(); only its model resolution moves.

createMethods now hands the real DataHandle to all four (User/Session/Token) —
the storeHandle mongoose-cast hack is removed; every domain resolves the store
uniformly.
2026-07-04 19:25:21 -07:00
hanzo-dev 04d041c2f4 fix(data-schemas/auth): route Session+Token methods through the store-aware DataHandle
Cold logins have been failing since Session was flipped to the SQLite store
(CHAT_STORE_SQLITE): createSession threw 'Session is not a constructor' because
session.ts used the mongoose-document constructor (new Session()) + doc.save(),
which the DocModel/DualWriteModel served under the flip do not implement.

Refactor createSessionMethods + createTokenMethods to take a DataHandle (the same
seam the 28 migrated domains use) and resolve their model via handle.models.<Name>.
Session create/rotate now use the bounded Model API: createSession generates the
_id up front (handle.Types.ObjectId), signs the refresh token bound to it, and
persists via .create(); generateRefreshToken (rotation) persists via .updateOne();
updateExpiration via .findByIdAndUpdate(). Refresh tokens stay hashed (hashToken),
JWT claims + expiry math unchanged, no session fixation. Token methods already used
the bounded API; only their model resolution moves off mongoose.models.

storeHandle (mongoose-cast) in createMethods still feeds these factories unchanged
(it is structurally a DataHandle); the caller cleanup follows in the next commit.
2026-07-04 19:24:49 -07:00
hanzo-devandz 3fe63b68d6 ci(data-schemas): isolate native-sqlite specs one-file-per-process; v0.9.16
The 15 specs that instantiate the native better-sqlite3 driver corrupt each
other under a shared jest worker: jest gives each spec file its own JS module
realm while the native addon is process-cached per worker, so cross-file state
breaks sibling in-memory databases (driver-agnostic — reproduces with mainstream
better-sqlite3; a compound json_extract UNIQUE stops firing). Prod is unaffected
(one realm, one shared handle for the process lifetime).

test:ci now runs test/ci.mjs: the normal coverage pass for everything except the
native-driver set (via testPathIgnorePatterns), then each of those 15 specs in
its own jest process, exiting non-zero on any failure. NOT --runInBand /
maxWorkers=1 (a single shared worker still corrupts). No test weakening, no skips.

Bump chat 0.9.15 -> 0.9.16 (patch).
2026-07-04 15:55:19 -07:00
hanzo-devandz b6e5d1082b fix(store/sqlite): close shared handle on rekey + test teardown (real leak)
sharedSqliteHandle() reassigned sharedHandle on a collection-key change without
closing the prior connection — a latent native-handle leak on any rekey. Close
it before replacing, and export closeSharedSqliteHandle() so storeRegistry.spec
(rekeys twice) and tenantIsolation.coverage.spec tear the handle down instead of
leaking it past the file.

NOTE: this fixes the real leak the review identified, but does NOT resolve the
cross-file store-spec flake. That flake is a separate, deeper issue (see report):
driver-agnostic (reproduces with mainstream better-sqlite3 too), isolated to the
store's create path, timing/heap-sensitive (any probe masks it), and NOT fixed by
closing handles/GC/statement-cache/wrapper-pinning. Every store suite passes in
its own process; production (single long-lived handle, one module realm) is
unaffected.
2026-07-04 15:55:19 -07:00
hanzo-devandz 1990fa4072 fix(store/sqlite): swap node:sqlite→better-sqlite3-multiple-ciphers (Node 20) + honor tenant sentinel in debug log
node:sqlite (DatabaseSync) is a Node 22+ builtin; prod runs Node 20 (Alpine),
so require('node:sqlite') threw ERR_UNKNOWN_BUILTIN_MODULE, cascading through
createModels → applySqliteOverrides → openDatabase and breaking the built dist
("createModels is not a function"). Swap to better-sqlite3-multiple-ciphers
12.11.1 (engines include 20.x, synchronous drop-in, SQLCipher AES-256 at rest —
the Node embodiment of the hanzoai/sqlite contract).

- data-schemas dep + root pnpm.onlyBuiltDependencies allowlist (native addon);
  rollup externalizes the driver.
- openDatabase(): lazy require of the driver, identical WAL/synchronous/
  busy_timeout/foreign_keys pragmas. Wire the CHAT_SQLITE_KEY encryption seam
  (SQLCipher cipher/legacy/key pragmas applied before any page-touching
  statement; 64-hex validated, fail-closed; key/path never logged). KMS/CEK
  derivation is Milestone 2.
- DocModel: retype db to the driver's Database instance; binding/return parity
  already correct (booleans→1/0, dates→ISO, TEXT reads only). engine untouched
  (27/27 held).
- parsers: restore the structured-logging-context impl dropped in an upstream
  merge (spec #13110 landed without its parsers.ts) — appendRequestContext for
  non-debug lines and drop the __SYSTEM__ tenant sentinel from debug traversal.
- models: register the SystemGrant model in createModels (schema/model/methods/
  SQLite-spec + coverage guard all existed; only the registration was dangling).

Scrub all node:sqlite/DatabaseSync references from src (comments + spec names).
2026-07-04 15:55:19 -07:00
hanzo-devandz 31bd2144cf fix(store/sqlite): guard engine setPath/deletePath against prototype pollution
The SQLite store's update path feeds attacker-influenced keys ($set/$unset/
$inc/$push/$addToSet — conversation import, saveConvo, agent metadata) straight
into setPath, which walked cur['__proto__'] / cur['constructor'] and could mutate
a shared prototype (global prototype pollution). Reject any dotted path whose
segments include __proto__/prototype/constructor (and the single-segment own-key
case a JSON.parse'd body produces). Legitimate nested writes are unaffected.

engine.spec: +5 guard cases (dotted $set, constructor.prototype, JSON-sourced
own __proto__, $setOnInsert/$inc, legit nested still writes). 27/27 green on
Node 20 (pure-JS engine, no node:sqlite dependency).
2026-07-04 14:14:56 -07:00
zeekayandClaude Fable 5 8360c518b6 fix(backfill): resolve @librechat/data-schemas via workspace path fallback
The pnpm workspace does not hoist a bare @librechat/data-schemas symlink to
the app root where the backfill runs (node config/backfill-sqlite.js), so a
bare require throws MODULE_NOT_FOUND in the pod. Fall back to the workspace
path (packages/data-schemas). Verified: require('/app/packages/data-schemas')
resolves in the chat pod.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 13:15:39 -07:00
zeekayandClaude Fable 5 bd938fc6c6 feat(store): dual-write mirror + auth/billing specs to drop chat-docdb (0.9.15)
The Mongo→SQLite cutover, completed so chat-docdb can be DELETED with zero
data loss and an instant revert at every step.

- DualWriteModel: wraps a primary (served) + mirror model; reads/props fall
  through to primary, the 10 write methods run on primary then replicate the
  affected docs to the mirror KEYED BY THE PRIMARY'S _id (upsert if present,
  delete if gone). Symmetric — same code mirrors mongoose->sqlite (pre-flip)
  and sqlite->mongoose (post-flip escape hatch). Mirror failures are logged,
  never thrown, so the served path can't break; gaps are caught by the
  pre-flip count reconcile + the idempotent backfill.
- DocModel.upsertRaw: exact by-_id upsert (verbatim, no timestamp stamping) —
  the shared primitive for the mirror AND the backfill, so live mirroring and
  the one-shot copy converge on one keyspace without duplicating.
- Seam: applySqliteOverrides now honors TWO flags — CHAT_STORE_SQLITE (served)
  + CHAT_STORE_DUALWRITE (mirrored) — yielding the four cutover states. One
  shared node:sqlite connection per process (was per-createModels-call).
- Route User/Session/Token through the handle (methods/index.ts) and add the
  auth+billing CollectionSpecs (User/Session/Token/Balance/Transaction). These
  are the collections actually populated in chat-docdb outside the 24 already
  wired; User is the hot-path record every request loads and every conversation
  references by _id, so it MUST move for a lossless Mongo delete.
- connectDb() is now Mongo-optional: unset MONGO_URI => SQLite-only mode, skip
  the connection (final state, chat-docdb gone) with bufferCommands off so a
  stray mongoose query fails fast instead of hanging.
- Dockerfile{,.multi,.static}: node:20/22-alpine -> ghcr.io/hanzoai/nodejs
  :v24.18.0 (Node 24; node:sqlite built in, better-sqlite3 compiles).
- config/backfill-sqlite.js: one-shot Mongo->SQLite copy + count reconcile.

Tests: DualWriteModel.spec (10) green — mirror-by-primary-_id both directions,
read passthrough, bulkWrite, idempotent backfill, all 29 specs build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 13:04:11 -07:00
b6a82bcf8f fix(agents): raise cloud-agent run timeout 30s→180s (long-run 502) (#58)
* fix(agents): raise cloud-agent run timeout 30s→180s (env CLOUD_AGENT_TIMEOUT)

A cloud agent run is a real chat completion — a zen5-mini answer routinely
takes ~25-30s (measured 28s), larger models/prompts longer. The hardcoded 30s
client timeout aborted long runs mid-flight, surfacing in the UI as a 502 even
though the cloud run finished and was recorded. Bump the default to 180s and
make it env-configurable (CLOUD_AGENT_TIMEOUT), matching the existing
CLOUD_AGENT_MAX_CONCURRENT knob. List/get are fast; this headroom only affects
/run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(agents): lock cloud-agent run timeout contract (180s default + env override)

Adds coverage the timeout bump lacked: asserts the 180s default (the fix for
the 30s -> in-UI 502), that an explicit constructor timeout wins, and that
CLOUD_AGENT_TIMEOUT overrides the module-load default (isolated re-require).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 05:14:04 -07:00
zeekayandClaude Opus 4.8 650ad06b15 ci: ignore all three transitive-pinned otel packages in depcheck
The unpinned depcheck (workflow installs latest each run) shifts which
@opentelemetry/* packages it reports: it flagged only @opentelemetry/core
before, now also exporter-trace-otlp-http and sdk-trace-base. All three are
transitive deps of @opentelemetry/sdk-node (imported/driven via NodeSDK in
packages/api/src/telemetry/sdk.ts), pinned top-level for otel version
alignment, none imported by name. Ignore the complete set so ROOT_UNUSED is
empty regardless of depcheck's version-dependent otel detection. Only
@opentelemetry/api is imported directly and stays checked.

Verified locally: with the three ignored, depcheck's root output contains only
@opentelemetry/api, which the workflow's used-in-code list subtracts -> empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 04:47:57 -07:00
zeekayandClaude Opus 4.8 52bcd7de06 ci: silence otel-core depcheck false-positive + post PR comments via github-script
detect-unused-packages and detect-unused-i18next-strings each hard-fail on the
comment step because the ARC runners have no `gh` CLI (`gh api` -> exit 127), and
detect-unused-packages additionally flags @opentelemetry/core.

- Add .depcheckrc.yml ignoring @opentelemetry/core: it is a transitive
  requirement of the otel stack in use (sdk-node / sdk-trace-base /
  exporter-trace-otlp-http), intentionally version-pinned at the top level and
  never imported by name, so depcheck reports a false positive. Verified locally:
  with the ignore, depcheck no longer lists it (it was the sole ROOT_UNUSED item).
- Move both workflows' "post comment" step from `gh api` to
  actions/github-script@v7 (Octokit + built-in token; jobs already grant
  pull-requests: write). No dependency on a gh binary that isn't on the runner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 04:33:19 -07:00
zeekayandClaude Opus 4.8 495340a3b7 i18n: remove orphaned en key com_agents_cloud_section
Genuinely unused: defined only in en/translation.json, referenced nowhere in
code (the sibling com_agents_cloud_* keys are all used; this section-header
string never shipped a consumer). Its presence was the sole finding of the
detect-unused-i18next-strings workflow, which hard-fails on any unused key.
Removing it makes that check pass without weakening it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 04:26:23 -07:00
zeekayandClaude Opus 4.8 864c8a7bc7 ci: fix workspace-build OOM heap + drop dead LiteLLM lint workflow
Two pre-existing, repo-wide CI failures that reddened main and every PR:

1. test.yml / e2e.yml OOM'd building packages/api (rollup peaks ~5 GiB RSS;
   Node's default ~2 GiB old-space heap => "heap out of memory", exit 134).
   Add the same NODE_OPTIONS heap knob backend-review.yml/frontend-review.yml
   already use (6144, well under the 8 GiB runner limit). Verified: build:api
   completes in ~54s at 6144 (peak RSS 4.98 GiB); it OOMs at 3072.

2. test-linting.yml ("LiteLLM Linting") lints a litellm/ Python dir that does
   not exist in this repo and runs `poetry install` with no pyproject.toml, so
   it always failed at "Install dependencies". Pure upstream LibreChat/LiteLLM
   residue — the real JS lint is eslint-ci.yml ("Run ESLint Linting"), which
   passes. Remove the dead workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 04:18:13 -07:00
zeekayandClaude Fable 5 1bcdcfc1e1 chore(chat): runtime image node:20 -> node:22-alpine (0.9.13)
Node >= 22.5 ships `node:sqlite` (DatabaseSync), which the SQLite document
store requires once CHAT_STORE_SQLITE / CHAT_STORE_DUALWRITE is enabled.
Dockerfile.static was already node:22-alpine; this brings the runtime image
to parity. The store lazy-requires node:sqlite (stores/sqlite/index.ts), so
with the flag unset the runtime boots unchanged — this is a pure Node bump,
no SQLite enabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 01:10:29 -07:00
72700de2f0 fix(mcp): full connection/transport port (proxy + response-size caps + WS SSRF) + wire suite into CI (#61)
* fix(mcp): restore drifted ~/auth SSRF/allowedAddresses helpers

The tests + callers (hardenedFetch, MCPOAuthHandler, MCP connection) already
expected the port-scoped allowedAddresses SSRF surface (LibreChat #12933/#13022),
but domain.ts/agent.ts had been reduced to the pre-#12933 versions while their
specs were trimmed to match — a tests-without-impl drift babel hid at test time.

Restore the upstream pair (matched impl + spec):
- domain.ts: isAddressAllowed; 3-arg resolveHostnameSSRF/isSSRFTarget (allowed
  host:port exemption); isOAuthUrlAllowed; validateEndpointURL; port-scoped
  isMCPDomainAllowed fail-closed on unparseable allowlisted URLs.
- agent.ts: createSSRFSafeUndiciConnect(allowedAddresses, port) + allowedAddresses
  connect-time exemption (fixes the dead 2-arg call in hardenedFetch.ts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): restore circuit-breaker config + OAuth reconnection cooldown

mcpConfig gains the CB_* connect/disconnect circuit-breaker knobs and
OAuthReconnectionTracker regains its progressive cooldown (5m/10m/20m/30m capped)
that reconnection-storm.test.ts asserts. Both were trimmed while their tests
stayed at the upstream version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): finish connection.ts transport port — proxy, response caps, WS SSRF

Completes the half-ported MCP connection/transport layer (LibreChat
#13076/#13219/#13224/#13274). The test suite (MCPConnectionSSRF,
MCPConnectionAgentLifecycle, reconnection-storm) was byte-identical to upstream
but the implementation stopped at the redirect-SSRF guard, leaving 22
tests-without-impl in MCPConnectionSSRF alone.

- Proxy support: serverConfig.proxy + PROXY/HTTP(S)_PROXY env with full NO_PROXY
  semantics (wildcard, CIDR, IP-range, IPv6, host-suffix, port scoping); per-URL
  ProxyAgent/Agent dispatcher selection tracked in this.agents; recomputed across
  redirects; proxied-target SSRF preflight (IP literal -> resolveHostnameSSRF,
  hostname -> allowedAddresses exemption or reject).
- Response-size caps: guardStreamableHTTPResponses opt-in wraps the body stream
  with MCP_STREAMABLE_HTTP_MAX_RESPONSE_BYTES + MCP_STREAMABLE_HTTP_MAX_LINE_BYTES,
  emitting a JSON-RPC error SSE frame instead of unbounded buffering.
- WS SSRF: resolveHostnameSSRF(host, allowedAddresses, port) now runs regardless
  of useSSRFProtection (allowlist deployments), closing DNS-rebind to private IPs.
- Connect-lifecycle circuit breaker + agent cleanup (closeAgents) on disconnect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(mcp): accept proxy + sseReadTimeout in MCP server config schema

Adds ProxyUrlSchema (http/https/socks, env-var resolved) to SSE/streamable-http
options and sseReadTimeout to the base schema so connection.ts's proxy/idle-read
support is reachable from real config. proxy is admin-only: z.never() in the
UI/API user-input schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(chat): gate MCP connection/transport suite in the primary test workflow

Root cause of the drift: test.yml (the Hanzo-active CI) only ran 'test:api'
(the api/ Express dir) and 'test:client' — packages/api tests never ran here, so
the connection.ts port could drift from its byte-identical tests unnoticed.

Add packages/api 'test:transport' (MCPConnection*, MCPRedirectSSRFGuard,
reconnection-storm, MCPManager, auth domain/agent SSRF specs, hardenedFetch,
OAuthReconnection*) and run it in test.yml. A future half-port of the transport
layer now fails CI loudly. No .skip's exist in these suites; nothing un-skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 00:44:05 -07:00
zeekayandClaude Opus 4.8 32acfb52e3 fix(chat): stream #titleConvo so the Hanzo gateway response parses (real auto-title fix)
Root cause of new chats never titling: the title path invokes the model
NON-streaming (stream:false). The Hanzo Cloud gateway returns a valid
{choices:[{message}]} body, but the pinned langchain ChatOpenAI parses that
non-streaming body to ZERO generations, so invoke() throws
'Cannot read properties of undefined (reading \'message\')' and no title is
saved. Every agent RUN already streams (SSE), which parses the same gateway
correctly — that is why generation works but titles did not.

Force clientOptions.streaming = true in #titleConvo (after the omitTitleOptions
filter, which strips 'streaming'). Verified against the live gateway with Dave's
per-user key: streaming=true -> clean title; streaming=false -> the crash.

Pairs with the zen5-flash repoint (fast, non-reasoning): streaming fixes the
parse for all models; zen5-flash keeps the call under the 45s title timeout
(zen3-nano/qwen3-8b took ~60s).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:38:02 -07:00
zeekayandClaude Opus 4.8 3cb0488797 fix(chat): title/summary model zen3-nano -> zen5-flash (fast, non-reasoning)
New chats didn't reliably auto-title: zen3-nano maps to the reasoning model
qwen3-8b (live upstream), which took 16-31s per title — routinely near the
45s #titleConvo timeout. The other 6 families pointed titleModel at
third-party names (llama-3.1-8b, qwen3-coder-flash, ...) that are NOT in the
live gateway catalog, so they returned a 200 error-envelope.

Repoint every family's titleModel (and the Hanzo summaryModel) to zen5-flash:
a fast, NON-reasoning, in-catalog model (live upstream deepseek-4-flash) that
returns a clean single-line title in ~1-2s. Picked by direct api.hanzo.ai/v1
probes: zen5-flash ~1.5s clean; zen3-nano 16-31s; zen5-mini 10-20s; zen5 /
zen3-vl >40s; zen5-nano not servable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:43:57 -07:00
zeekayandClaude Opus 4.8 43cc8a73bb fix(chat): make #titleConvo envelope-safe (parity with agent runs)
The Hanzo Cloud gateway answers some failures with HTTP 200 + a JSON
error-envelope ({status:"error", msg}) that has no `choices`. On the
title path the OpenAI client parsed that 200 to `undefined` and
#titleConvo threw `Cannot read properties of undefined (reading 'message')`,
so new conversations never got a title.

Apply the same wrapHanzoGatewayFetch rewrite agent runs use, now explicitly
on the title client's fetch, so the envelope becomes a clean 402 the outer
try/catch skips gracefully. Export the wrapper from @hanzochat/api; the
re-wrap is idempotent (a second pass sees a 402, not a 200 envelope) and
response-only, so per-user hk- billing and normal completions are untouched.

Adds an idempotency unit test (7/7 pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:43:49 -07:00
zeekayandClaude Opus 4.8 e4c75ffdd1 docs(chat): title-route limitation note (zen3-nano) — config source of truth
The #titleConvo path no longer 404s (zen5-nano was off-catalog), but full
auto-title still needs a follow-up: zen3-nano reasons past the 45s title timeout,
and off-title-path models (e.g. llama-3.1-8b) return a 200 error-envelope the
title client mis-parses. Needs the gateway-envelope rewrite on #titleConvo + a
fast non-reasoning title model. librechat.yaml is the source the chat-config
ConfigMap is generated from (not used at runtime; CONFIG_PATH=/app/chat.yaml).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:12:29 -07:00
zeekayandClaude Opus 4.8 8486223c89 chore(chat): 0.9.10 — ensō icon fix folded into the design-maven pass
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:25:02 -07:00
zeekayandClaude Opus 4.8 fa01f11732 fix(chat): ensō for the Zen family in UnknownIcon (the real icon path)
Custom endpoints resolve their icon via getIconKey -> 'unknown' -> UnknownIcon
(by endpoint NAME), not icons[custom] — so the earlier icons[custom] remap was
inert (reverted). The Hanzo house endpoint had no asset/iconURL match and fell
through to the generic lucide "bot". Now:
- 'hanzo'/'zen' -> ZenLogoIcon (ensō), matching the assistant message avatar,
  on the welcome screen + model pill + picker menu.
- qwen/google-gemma/openai-gpt-oss -> real provider marks by name (the
  KnownEndpoints enum lacks those keys). DeepSeek/Mistral logos unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:24:54 -07:00
zeekayandClaude Opus 4.8 6e56f51429 chore(chat): 0.9.9 — design-maven polish pass (true-black, ensō, monochrome)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:10:46 -07:00
zeekayandClaude Opus 4.8 e27fb77e5b feat(chat): polish welcome heading + code blocks
- Welcome heading: tracking-tight for crisp large-display type (Linear/Vercel).
- Code blocks: hairline-bordered true-black surface (rounded-lg, border-medium,
  #0a0a0a) with a subtle bottom-bordered header instead of the heavier grey bar;
  reads calm + monochrome now that code renders in Geist Mono.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:10:00 -07:00
zeekayandClaude Opus 4.8 d2ded972bc feat(chat): monochrome brand — ensō everywhere, neutral avatars, kill leaks
- Zen ensō (円相) replaces the generic lucide "bot" for the custom Zen endpoint:
  welcome screen, model pill and menu icon now match the assistant avatar.
- Monochrome avatars: collapse the colorful DiceBear palette to a neutral grey
  ramp (the green "GU" guest chip is gone), and neutralize the periwinkle
  no-seed fallback in Icon/Avatar.
- Kill the stray "(" glyph: the right control-panel NavToggle handle was floating
  mid-edge at 0.25 opacity; now invisible until hover (header controls remain).
- Model-picker menu separator used cool border-slate-*; now neutral border-light.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:03:20 -07:00
zeekayandClaude Opus 4.8 5e9a043837 feat(chat): true-black hard-default theme + crisp type + auto-title fix
- Hard-default to dark (true-black) unless the user explicitly picks light:
  ThemeProvider getInitialTheme + index.html no-flash script now default dark,
  and the loading canvas is true #000 (was blue-tinted #070b13). theme-color #000.
- Crisp type: font-synthesis:none + antialiased. Basel ships 400/500 only, so
  faux-bold was blurring headings; hierarchy now comes from size + the real
  Medium face. Code now renders Geist Mono (was forced to Consolas via !important).
- Auto-title: repoint titleModel/summaryModel zen5-nano -> zen3-nano (zen5-nano
  isn't in the live /v1/models catalog, so #titleConvo 404'd and new chats never
  auto-titled). zen3-nano is the known-good guest/test model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:03:10 -07:00
zeekayandClaude Opus 4.8 d31f7546bd fix(chat): seed GUEST role default so guest generation stops crashing (0.9.8)
Root cause: commit 46b636c8c0 (server-side anonymous guest chat) added
GUEST to the SystemRoles enum and stamped guest JWTs with role=GUEST, but
never added a roleDefaults[GUEST] entry nor seeded the role. Every guest
generation ran checkAccess -> getRoleByName('GUEST'), which missed the DB,
fell into the self-heal branch `if (!role && SystemRoles['GUEST'])`
(truthy) and called `new Role(roleDefaults['GUEST']).save()` ===
`new Role(undefined)` -> "Role validation failed: name: Path `name` is
required" -> the whole generation threw. Since the logged-out landing IS
the guest composer (0.9.3), nearly every visitor hit this.

Fix (three orthogonal parts):
- data-provider/roles.ts: add the missing roleDefaults[GUEST] (named,
  mirrors USER's minimal grant; guest scope is enforced by
  enforceGuestScope middleware, not by these permissions).
- data-schemas initializeRoles: seed GUEST at boot alongside ADMIN/USER so
  it is deterministic, not lazily created.
- models/Role.js: gate the self-heal create on roleDefaults[roleName]
  (possessing the canonical defaults) instead of SystemRoles[roleName]
  (mere enum membership). roleDefaults entries always carry a name, so a
  nameless create is now structurally impossible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:59:51 -07:00
zeekayandClaude Opus 4.8 7f0bdb3145 feat(build): "Build an app" affordance — hanzo.app handoff + inline build shell (0.9.7)
Uniform "build an app" entry across chat -> app -> console. One pure module
(utils/buildApp.ts) is the single source of the hanzo.app builder wire
(hanzo.app/dev?prompt=) and the `/build [prompt]` command grammar; every
surface funnels through it (DRY).

Phase 1 (ships): the hanzo.app handoff, reachable three ways —
- `/build [prompt]` slash command, intercepted in ChatForm.onSubmit
- "Build this as an app" action on assistant messages (HoverButtons)
- the inline preview pane's "Open in App" CTA
All open https://hanzo.app/dev?prompt=<encoded> in a new tab (noopener).

Phase 2 (scaffold): inline build mode. A `buildMode` recoil flag toggled by the
composer "Build an app" button (BuildAppButton) makes ChatView render a
stripped-down split — chat thread on the left + a side preview pane
(BuildApp/BuildPreviewPane) on the right. The pane is a placeholder whose CTA is
the Phase 1 handoff, seeded live from the composer text. A `/build` route
deep-links into build mode (seeds the composer from ?prompt=/?q=). When buildMode
is off, ChatView renders byte-identical to before (zero regression to normal chat
or the guest landing). Phase 3 (real inline codegen/preview) is documented inline.

9 unit tests for buildApp (url + command grammar). Guest flow untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:18:15 -07:00
zeekayandClaude Opus 4.8 523c38b371 fix(store): lazy-require node:sqlite so the server boots on Node 20 (0.9.6)
The SQLite DocModel store statically `import`s `DatabaseSync` from
`node:sqlite` at the top of `stores/sqlite/{index,DocModel}.ts`. That module
is re-exported by the data-schemas package index, which the API server loads
at boot — so the eager `require('node:sqlite')` in the bundled dist runs
unconditionally. `node:sqlite` only exists on Node >= 22.5; the runtime image
is node:20-alpine, so boot dies with
`ERR_UNKNOWN_BUILTIN_MODULE: No such built-in module: node:sqlite`
(crashloop) — independent of the store's inert-by-default flag.

The store is only ever exercised when `CHAT_STORE_SQLITE` is set (a CSV of
collections), via `createSqliteHandle` -> `openDatabase`. Make the import
lazy: `import type` for the erased type references, and a single
function-scoped `require('node:sqlite')` inside `openDatabase`. Module load
no longer touches the builtin, so the server boots on Node 20 with the store
inert; when the flag is set (on a Node >= 22.5 runtime) it works verbatim.

Proven: rebuilt dist has no module-scope require; loading dist/index.cjs with
`node:sqlite` blocked (simulated Node 20) succeeds, and openDatabase() still
defers to the builtin only when actually opening a database.

Unblocks deploying any main-based image (incl. the 0.9.5 guest-chat fix).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:13:28 -07:00
zeekayandClaude Opus 4.8 de8a33d8a3 fix(chat): guest can send + never bounce to /login (0.9.5)
Two anonymous-guest client bugs, both proven with live Playwright.

BUG A — guest can't SEND ("Unknown endpoint: Hanzo"):
`new QueryClient()` was built in App's render body, so every re-render
minted a fresh EMPTY client. A guest's expected 401s (mcp/servers,
files/config, keys?name=Hanzo, …) fire the queryCache `onError` →
`setError` → App re-render → the provider swaps in an empty client. The
lazy chat-form's `useChatFunctions` then reads `getQueryData([endpoints])`
off an endpoints-less client, so the custom `Hanzo` endpoint resolves with
`endpointType === undefined` and `parseCompactConvo` throws
"Unknown endpoint: Hanzo" — the completion POST never happens. Backend is
correct: guest token → /v1/chat/endpoints={Hanzo:{type:custom}},
/v1/chat/models={Hanzo:[zen3-nano]}. Fix: stabilize the client with
`useState(() => new QueryClient(...))` — one client, one cache, every
consumer (incl. the lazy chunk) shares the populated store.

BUG B — intermittent first-load /login redirect:
The axios 401 interceptor hard-`window.location.href`'d to /login whenever
a one-shot refresh yielded no token. On a cold visit the guest bearer is
still in flight (`isGuestSession()` false because there's no bearer yet),
so an early expected 401 bounced the visitor to /login, racing the
guest-acquire. Fix: only hard-redirect when a real *session* bearer is
actually present (`currentBearer() != null && !isGuestSession()`); an
anonymous cold-start with no bearer is left to routing/the login gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 16:47:29 -07:00
zeekayandClaude Opus 4.8 2f2378ab21 fix(chat): keep guests on chat surface — don't hard-redirect 401s to /login
The axios 401 interceptor hard-redirected to /login whenever a one-shot
refresh yielded no token. A guest (anonymous preview) session carries a
{guest:true} JWT that is valid ONLY on the chat-completion route; every
other endpoint (/api/mcp/servers, /api/files/config, ...) answers 401 by
design. Those expected 401s bounced the guest to /login, wiping the guest
session in an infinite loop, so the landing never rendered the composer.

Derive guest-ness from the active bearer (isGuestSession) and skip the
hard redirect for guests — real users with a truly-expired session still
redirect. Fixes anonymous-guest landing on hanzo.chat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:33:31 -07:00
120c39a197 feat(image): text-to-image via the Hanzo gateway, metered per-user
Enables 'generate an image of X' to render a real image inline through the
DALLE3 agent tool pointed at the Hanzo gateway (/v1/images/generations):
- DALLE3.js: model is configurable (DALLE3_MODEL, e.g. zen3-image); DALL-E-3-only
  knobs (quality/style) are sent ONLY for a dall-e model so the Hanzo image
  backend never sees a param it would reject. Agent path already fetches the
  result server-side and returns it inline (upstream host never reaches client).
- handleTools.js: dalle is now a custom constructor that injects the signed-in
  user's PER-USER hk- key (resolveHanzoCloudKey) so image generation is metered
  to them — mirroring the chat per-user key path. Guests keep the shared key;
  an authed user whose key can't be resolved FAILS CLOSED (no shared-org spend).

Deploy also sets env DALLE_REVERSE_PROXY=https://api.hanzo.ai/v1/images/generations
and DALLE3_MODEL=zen3-image. Zen-brand model id only; upstream never surfaced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 14:51:13 -07:00
8d2549322a feat(store): SQLite DocModel migration (29 domains) + read-only commerce + identity-a — inert by default (CHAT_STORE_SQLITE unset = pure mongoose) (#60)
* feat(data-schemas): pure Mongo-shaped query/update engine for SQLite store

Correctness core of the mongoose->SQLite migration seam: pure, I/O-free
matchesFilter / applyUpdate / projectDoc / sortDocs implementing the exact
Mongo operator subset the chat data methods use ($eq $ne $in $nin $gt/$gte
$lt/$lte $exists $regex $not $and $or $nor; $set $unset $setOnInsert $inc
$push $pull $addToSet). Date-aware comparison, mongo null/absent semantics.
21 unit tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(data-schemas): SQLite DocModel + handle backing the Model API on node:sqlite

DocModel presents the Mongoose Model-API subset the chat data methods use
(findOne/find/findOneAndUpdate/updateOne/updateMany/deleteMany/deleteOne/
countDocuments/distinct/create/insertMany/bulkWrite + chainable QueryBuilder
with select/sort/limit/skip/lean/deleteMany). Docs stored as JSON via node:sqlite
(stdlib, zero new dep); JSON1 expression indexes on unique/anchor fields; exact
mongo semantics delegated to the pure engine; date rehydration on read; no
mongoose, no tenant middleware (Conversation/Message are not tenant-plugged
upstream). createSqliteHandle() returns a mongoose-shaped handle the unchanged
method factories run against. 7 adapter tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(data-schemas): prove conversations+messages fully off mongoose on SQLite

- Decouple message.ts + conversation.ts from the mongoose package: factories now
  take a structural DataHandle ({models}) satisfied by BOTH mongoose and the
  SQLite handle. Only type-only imports from 'mongoose' remain; zero runtime
  mongoose in the data path for this domain.
- engine: type-aware operand coercion so cursor pagination (String(Date) operand
  vs Date field) compares chronologically, mirroring mongoose schema casting.
- convoMessage.sqlite.spec: 14 tests running the REAL createMessageMethods /
  createConversationMethods against createSqliteHandle — save/upsert, get,
  update, delete, deleteMessagesSince, cursor pagination, bulk, archived +
  retention-visibility filtering, getConvosQueried, cross-collection deleteConvos,
  deleteNullOrEmptyConversations, searchConversation. All green.

Isolates one unrelated pre-existing fork gap via jest mock: this tree's
librechat-data-provider never synced the RetentionMode enum (upstream #13049),
so message.ts/conversation.ts reference an undefined export on the mongoose path
too. Documented for separate fix.

42/42 store tests green (engine 21 + DocModel 7 + contract 14).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(data-schemas): registry-aware createModels + store exports (the seam)

createModels now applies per-domain backend selection via CHAT_STORE_SQLITE
(CSV of collection names -> SQLite DocModel). Unset default = pure mongoose,
live path unchanged; only collections with a CollectionSpec are overridable
(fails closed otherwise). Exports createSqliteHandle/DocModel/CollectionSpec/
DataHandle from the package index. node:sqlite + node:crypto added to rollup
externals. Package builds clean (rollup, EXIT=0); 45/45 store+registry tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(data-schemas): drop local build artifacts + shared-deps symlink from branch

Revert dist/* to base (CI rebuilds bundles deterministically — no local builds)
and untrack the packages/data-schemas/node_modules dev symlink. Source-only branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(data-schemas): Batch 2 — Preset, ConversationTag, SharedLink on SQLite

Migrate three self-contained, non-tenant-plugged chat-document domains onto the
DocModel store behind the same seam. Decouple their factories to DataHandle;
createMethods now passes a registry-aware handle to createShareMethods so the
CHAT_STORE_SQLITE flag flips Share (pattern-2) in the live path too.

DocModel extensions (all reusable for later batches):
- compound unique indexes (ConversationTag {tag,user})
- ObjectId-ref casting on write + cross-collection .populate() (SharedLink.messages -> Message)
- findByIdAndUpdate / findOneAndDelete; findOneAndUpdate is now a chainable
  QueryBuilder (mutate mode) so .lean()/.select()/.populate() chain after a write
- schema defaults on insert (SharedLink.isPublic:true)
engine: mixed-update semantics (top-level fields fold into $set) + $pullAll.

Contract specs run the REAL createPreset/ConversationTag/Share methods against
createSqliteHandle. 60/60 store+contract+registry tests green; rollup build EXIT=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(data-schemas): Batch 3 — Project on SQLite + realtime-vs-storage finding

Project migrated (pattern-1, registry-aware): add DocModel.findById; CollectionSpec
with array defaults; contract spec mirrors api/models/Project.js exact ops
(getProjectByName upsert, $addToSet $each, $pull $in, updateMany $pull).

Prompt/PromptGroup deferred with rationale: they construct mongoose.Types.ObjectId,
use an aggregate $lookup/$unwind pipeline + populate + manual tenant/ACL
(accessibleIds: ObjectId[]) — need an aggregate primitive, not the mechanical
recipe. Categories read-path is already mongoose-free (getCategories is static).

REALTIME finding (verified codebase-wide): chat has ZERO Mongo change-streams /
tailable cursors / .watch(). Its realtime is SSE token streaming (sendEvent /
agent GenerationJobManager) tied to the generation request — application layer,
DB-independent. So NO migrated domain needs a DB-subscription replacement; plain
node:sqlite is correct for all. Hanzo Base realtime is reserved for future
DB-driven push (multi-device live sync, presence, collab sessions) — none today.
Documented at the storage-decision source (collections.ts).

63/63 store+contract+registry green; rollup build EXIT=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(data-schemas): Batch 4 — File, Key, PluginAuth, Banner on SQLite

Migrate four non-tenant storage domains onto the DocModel store. Decouple their
factories to DataHandle; createMethods passes the registry-aware handle to
File/Key/PluginAuth (pattern-2); Banner is pattern-1 (~/db/models).

- Key: encrypted roundtrip proven (updateUserKey encrypt -> getUserKey decrypt)
  against SQLite; CREDS_KEY/IV set via jest setupFiles (runs before module load).
- PluginAuth: replaced a redundant `new Model().save()` else-branch with
  Model.create (equivalent on mongoose, part of the shared Model API — DocModel
  supports it). No behavior change.
- Realtime directive recorded: Conversation+Message are the Base-realtime cutover
  targets ("Base for realtime, SQLite for storage"); all other migrated domains
  are pure storage. Empirically chat has zero Mongo change-streams (realtime=SSE),
  so the SQLite store is correct in the interim; Base swap is backend-only.

MCPServer deferred (constructs mongoose.Types.ObjectId + _id ObjectId cursor).
70/70 store+contract+registry tests green; rollup build EXIT=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(data-schemas): Batch 5 — tenant-aware DocModel variant + Config on SQLite

Add tenant isolation to the store, mirroring the mongoose applyTenantIsolation
plugin, gated by CollectionSpec.tenantIsolated (unlocks Config/Skill/SkillFile/
SystemGrant):
- scopeFilter: every read/write filter scoped to getTenantId() (SYSTEM bypasses;
  no-tenant + TENANT_ISOLATION_STRICT=true fails closed) — wired at the single
  candidates() chokepoint.
- stampTenant: inserts stamped with tenantId (create/insertMany/upsert).
- sanitizeTenantUpdate: update payloads cannot mutate tenantId (throws cross-tenant;
  strips from $set/$setOnInsert/$unset/top-level) — wired into
  mutateOne/updateOne/updateMany/bulkWrite.

Config migrated (compound unique {principalType,principalId,tenantId}); decouple
createConfigMethods to DataHandle; QueryBuilder.session() no-op (single connection).
Fix: anchorWhere binds booleans as 1/0 (node:sqlite rejects JS booleans; json_extract
returns 1/0) — hardens every boolean-filtered collection.

batch5 contract spec proves cross-tenant isolation with REAL createConfigMethods:
per-tenant stamping, identical principals isolated across tenants, tenant-scoped
list/find, no cross-tenant delete. 74/74 green; rollup build EXIT=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(data-schemas): Batch 6 — SystemGrant on SQLite + ObjectId read-coercion

SystemGrant migrated (tenant-plugged, same recipe as Config): CollectionSpec with
compound unique {principalType,principalId,capability,tenantId} + tenantIsolated;
add DocModel.exists(); decouple createSystemGrantMethods to DataHandle.

Add ObjectId read-coercion to the engine (coerceId): filters carrying real
mongoose ObjectId operands (SystemGrant.normalizePrincipalId casts USER ids to
Types.ObjectId) now compare against the hex strings the store persists (docs
stringify ObjectIds to hex; _ids are ObjectId-hex). Applied in comparable /
valueEquals / anchorWhere. This is the shared primitive that unblocks the
ObjectId-coupled domains (Skill/SkillFile, MCPServer, Prompt/PromptGroup).

Contract spec runs REAL createSystemGrantMethods with USER principals
(ObjectId path exercised end-to-end): grant/has(exists)/revoke, idempotent upsert,
platform-vs-tenant isolation. 77/77 green; rollup build EXIT=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(data-schemas): Batch 7a — ObjectId shim + MCPServer, Skill, SkillFile

Add handle.Types.ObjectId shim (thin: store _ids are already ObjectId-hex,
coerceId resolves comparison/storage): a 24-hex ObjectId class with toHexString/
toString/toJSON/equals/isValid, exposed as SqliteHandle.Types and typed on
DataHandle.Types. createMethods' dbHandle now carries mongoose.Types (real
ObjectIds coerce too).

- MCPServer (pattern-2): full real-method spec — create / findByServerName /
  findByObjectId (findById + ObjectId operand) / byAuthor / update / delete +
  unique serverName. Decouple models + Types to handle; wired via dbHandle.
- Skill/SkillFile (tenant-plugged): decouple models + Types to handle; storage
  contract proven — compound unique, ACL _id-in-accessibleIds ObjectId filtering
  via coercion, SkillFile upsert. Their 800-line ACL/validation method layer
  rides on these ops (full harness = follow-up).

81/81 green across 10 suites; rollup build EXIT=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(data-schemas): Batch 7b — aggregate primitive + Prompt, PromptGroup

Add DocModel.aggregate supporting the bounded stage set the chat methods use
($match / $lookup / $unwind / $sort / $limit / $project), run in JS over
candidate docs; $lookup resolves the mongo collection name to a sibling model
(prompts -> Prompt) and joins via the shared engine. Fix deepCoerceIds: coerce
ObjectId-like values to hex BEFORE structuredClone in create/insertOne (clone
was stripping the shim's methods, serializing productionId as an object husk).

Prompt / PromptGroup migrated (pattern-1): decouple models + Types to handle;
CollectionSpecs with productionId/prompts refs. Contract spec proves the
aggregate primitive directly AND the REAL getPromptGroup end-to-end (casts _id
-> ObjectId, $lookup productionId -> Prompt, $unwind preserveNullAndEmptyArrays).

All storage-tier domains are now on the DocModel. 85/85 green across 11 suites;
rollup build EXIT=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(data-schemas): Skill/SkillFile full-method harness + findOneAndUpdate(new:false) fix

Close the Skill/SkillFile proof gap: real createSkillMethods with stub ACL deps
(PermissionService injected) — createSkill (validation + uniqueness), getSkillById,
getSkillByName(accessibleIds ObjectId ACL), updateSkill optimistic version bump,
deleteSkill (+ removeAllPermissions), and SkillFile upsert (new-vs-replace) /
getByPath / list. Skill + SkillFile now fully proven, not just storage-proven.

Fix mutateOne: findOneAndUpdate(new:false, upsert:true) now returns null on an
insert (no pre-image) and the old doc on update — mongoose semantics that
upsertSkillFile's atomic new-vs-replace fileCount detection depends on. Locked
with a direct DocModel.spec test. All prior findOneAndUpdate upserts use new:true
(unaffected).

89/89 green across 12 suites; rollup build EXIT=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(data-schemas): Batch 8a — 7 chat-native domains on SQLite

Migrate MemoryEntry, ToolCall, Assistant, Action, AccessRole, Role, AgentApiKey
onto the DocModel store (mechanical: no external subsystem owns them — cloud
/v1/agents is additive, there is no /v1/memory, and app-domain authz has no IAM
equivalent). Decouple all 7 factories to DataHandle; createMethods passes the
registry-aware handle to Role/Memory/AgentApiKey/AccessRole (pattern-2).

Adapt role.initializeRoles off the `new Role().save()` mongoose-document pattern
to the shared Model API (findOne/create/updateOne) — equivalent on both backends.

Contract spec runs the REAL methods: memory set/create(dup-throws)/list/delete,
toolcall create/get/byConvo/delete, assistant/action upsert+get+delete, accessRole
create/find/list/delete, role initializeRoles seeds + listRoles, agentApiKey
create/validate(hash)/list. 96/96 SQLite-spec tests green; rollup build EXIT=0.
(Pre-existing mongoose specs fail identically on base — fork gaps, out of scope.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(data-schemas): Batch 8b — Agent, AgentCategory, AclEntry, Group on SQLite

Migrate the ObjectId/aggregate/ACL chat-native domains. Decouple factories to
DataHandle (models + Types); wire AgentCategory/AclEntry/UserGroup through the
registry handle. Agent uses the ObjectId shim (version tracking, _id cursor);
AgentCategory uses the aggregate primitive; AclEntry uses bitwise permission
queries; Group links members by id.

Store primitives added/fixed (all reusable):
- aggregate $group (with $sum/$first/$last/$max/$min/$push/$addToSet) — powers
  AgentCategory.getCategoriesWithCounts.
- bitwise operators $bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClear — powers
  AclEntry.hasPermission.
- applyUpdate deepCoerceIds the upsert seed (ObjectId shims survived to storage
  as hex, fixing findOneAndUpdate-upsert with ObjectId filter fields).
- array-safe index anchor: json_each(...) EXISTS replaces json_extract= so
  {field: value} over an ARRAY field (Mongo array-contains, e.g. Group.memberIds,
  tags, projectIds) hits the index instead of being excluded by the prefilter.

Contract spec runs REAL methods: agent create/get/update/delete, category $group
counts, acl grant/has(bitwise)/revoke, group create/find/addMember/getUserGroups.
100/100 SQLite-spec tests green across 14 suites; rollup build EXIT=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(billing): chat→commerce debit wiring behind COMMERCE_WRITES (default OFF)

Step 2 of the money migration. Default OFF = current local-Mongo billing path
runs byte-for-byte; the Commerce-first fail-closed READ gate is unchanged.

CommerceClient (the previously-dead write methods, now correct):
- recordUsage now debits the billing SUBJECT (billingSubject(owner,email)) — not
  the Mongo user id — with amountMicros (lossless micro-USD; commerce rounds to
  nearest cent + records exact micros) + requestId (stable per-spend idempotency
  key → no double-debit) + totalTokens/provider.
- _flushUsageQueue now passes X-Hanzo-Org per entry (was omitted → debits hit the
  wrong tenant and never netted the balance the gate reads). Fixed.
- add deposit() for credits (POST /v1/billing/deposit).

Wiring (flag-gated, additive, fail-open):
- commerceWrites.js: COMMERCE_WRITES gate + recordCommerceDebit (never throws
  into the spend path; local Mongo authoritative until cutover).
- createTransaction: after the local debit, ALSO record to commerce when the
  flag is ON and the request threaded `subject` (tokenValue is micro-USD;
  transaction _id is the idempotency key). Inert until subject is threaded + the
  flag flipped.

Tests (executed, PASS): CommerceClient.spec — recordUsage builds the right body
(subject/amountMicros/requestId/totalTokens) + X-Hanzo-Org header, no _namespace
leak; the flag gate is OFF by default.

REMAINING (gated): thread `subject` into txMetadata at the spendTokens call
sites; wire credits (deposit/grantStarter); Step-3 live-verify; then retire local
writes behind the flag. Local Balance/Transaction writes are NOT retired.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(billing): revert chat→commerce debit — the gateway is the single debit authority

The COMMERCE_WRITES chat-debit hook (bf6302a72c, today) is a double-debit
footgun and is reverted. It contradicts the established, LIVE architecture:

  - hanzoCloudKey.ts (module doc): every IAM user has one hk- Cloud key; "the
    cloud gateway (api.hanzo.ai) debits that key's org commerce balance and
    returns 402 when the org runs out. Forwarding the right per-user key ===
    correct per-user billing automatically." initialize.ts forwards that key on
    every authed request (baseURL api.hanzo.ai/v1).
  - packages/api usage.ts (NOTE, 2026-06-27): a prior chat→commerce write in
    recordCollectedUsage was a SECOND debit to a mis-keyed account; it was
    REMOVED. "Chat must NOT also record usage to Commerce."
  - prod CR universe/.../crs/chat.yaml: HANZO_PER_USER_KEY=true, balance gate
    off, and in its own words "chat records NO usage to commerce (that write was
    removed); the single debit is still cloud's, per the user's hk- key."

Two writers to one ledger for one spend = double charge. The gateway is the ONE
debit authority for AI spend across every product (chat/code/agents/API); a
per-client debit path is the wrong DRY seam. So chat stays a READER of Commerce.

Removed (the whole unused chat→commerce WRITE surface):
  - api/models/commerceWrites.js (the COMMERCE_WRITES gate + recordCommerceDebit)
  - the recordCommerceDebit call + import in Transaction.js (now a comment
    stating the single-debit invariant)
  - CommerceClient.recordUsage (the debit) + its now-dead _usageQueue /
    _flushUsageQueue machinery + interval; deposit/grantStarter (unused credit
    helpers — the real first-chat grant is resolveHanzoCloudKey's direct POST
    /v1/billing/grant-starter in packages/api, the correct layer)
  - CommerceClient.spec.js (only tested the removed surface)

Kept (CommerceClient is now purely read-only, one responsibility):
  checkBalance (fail-closed money gate), getTierConfig, isModelAllowed,
  getCreditBreakdown — all live (balanceMethods.js, Balance.js).

Verify: node --check clean on both edited files; zero dangling references to the
removed symbols repo-wide; balanceMethods.spec mocks only the kept READ surface.

Money domain off-Mongo status after this: Commerce (via the gateway) is the
balance/debit authority — no Mongo Balance/Transaction WRITE is authoritative in
prod (gate off). The local Transaction doc remains an in-app usage log only;
retiring/relocating it to the SQLite store is tracked with the cutover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(identity): User is a thin IAM projection — drop the local password credential (step a)

Identity migration step (a): IAM (hanzo.id) owns identity; chat's User doc is a
thin OIDC-keyed projection (provider='openid', openidId=userinfo.sub, org from
userinfo), never a second credential store.

Confirmed no OIDC path writes a password: openidStrategy.js (508-523) and
process.js createSocialUser build the User with NO password; createUser
(packages/data-schemas) writes no default; the schema field was select:false.
The ONLY writers were two local-email flows, both gated OFF in prod
(ALLOW_REGISTRATION=false, ALLOW_EMAIL_LOGIN=false).

Changes (source only — Docker rebuilds packages/data-schemas dist via
`pnpm run frontend`):
  - schema/user.ts: remove the `password` field. The User projection carries no
    local secret. (IUser keeps `password?: string` — always undefined — so the
    disabled local strategy / comparePassword still compile; the full
    local-strategy teardown lands with the cutover.)
  - AuthService.registerUser: stop writing password (local signup stores no
    credential).
  - AuthService.resetPassword: reject — "Password reset is managed by Hanzo IAM
    (hanzo.id)." Nothing to reset locally.

Login-safety: the authed OIDC flow (openidStrategy) references `password` ZERO
times — provably untouched by this change. comparePassword/localStrategy run
only when local login is enabled (off in prod), so removing the field is inert
for the live path.

Verify: data-schemas builds clean; store-registry + convo/message + user-schema
specs green (17/17 targeted); full suite identical to clean tree
(1210 pass / 250 pre-existing mongo-env fails / 100 skip — my change adds zero
failures). LIVE Dave-login verification is the deploy-time gate in the cutover
runbook — this branch is not deployed, so prod is untouched (default path stays
pure-mongoose + password field until the cutover deploy).

STOP before step (b): OPENID_REUSE_TOKENS is the documented login-breaker
(chat.yaml: =true → /api/auth/refresh 403 at hanzo.id → login dead). Do NOT flip
until the IAM refresh-token fix lands and is live-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 14:33:53 -07:00
zandGitHub c3257d2238 Merge pull request #59 from hanzoai/feat/kill-api-prefix
refactor(chat): kill /api/ prefix → /v1/chat/* (#42 flagship)
2026-07-03 13:58:15 -07:00
hanzo-dev 87b581388d fix(chat): migrate regex-form /api survivors (telemetry matchers + OAuth actionId parse)
Two regex-form OUR-route references the string-based sweep couldn't reach:

- api metrics.ts PATH_NORMALIZATIONS: matcher regexes still /^\\/api\\/...
  while replacements were /v1/chat/... — a half-migration. Real request
  paths (/v1/chat/*) never matched -> high-cardinality routes (stream/status/
  files/messages/convos/agents/tags/tools/sessions with IDs) unnormalized ->
  Prometheus label cardinality blowup. Matchers -> /^\\/v1\\/chat\\/.
  (Aligns with metrics.spec's already-migrated /v1/chat/X/#id expectations.)
- client ToolCall.tsx: regex extracting actionId from the Action OAuth
  redirect_uri matched /api/actions/:id/oauth/callback; callback is now
  /v1/chat/actions/... -> actionId parse failed -> Action OAuth UI broken.

OUR-route /api now ZERO in all forms (string, template, regex, cookie-path).
2026-07-03 13:55:43 -07:00
hanzo-dev 2abc855b9c fix(chat): migrate no-trailing-slash /api survivors the sweep missed
Blue's transform anchored on '/api/' (trailing slash) and missed 4 places
where the path ends exactly at '/api':

- client MarkdownComponents.tsx: chat-message file links built as
  ${origin}/api/files/... -> BROKEN (files now at /v1/chat/files).
  Fixed base -> /v1/chat. [user-facing regression: file downloads]
- data-provider getDomainServerBaseUrl(): same ${origin}/api -> /v1/chat.
- api oauth/csrf.ts OAUTH_SESSION_COOKIE_PATH '/api' -> '/v1/chat': the
  OAuth session cookie (24h CSRF fallback) was never sent to the relocated
  /v1/chat/{actions,mcp} callbacks. [vector-6 miss]
- api metrics.ts path normalization '=== /api' -> '=== /v1/chat' (matches
  sibling /images,/avatars pattern).

OUR-route /api/ now ZERO across mounts, fetches, base-URLs, cookie paths,
redirect_uris.
2026-07-03 13:55:43 -07:00
hanzo-dev fdbb17da6c fix(chat): revert over-swept third-party paths + fix resources API URL
Red-team review of the /api/ -> /v1/chat/* migration found the sweep
over-reached into paths that are NOT the chat mount prefix:

- client/src/utils/resources.ts: REMOTE_AGENT 'copy API endpoint' URL was
  mangled /api/v1/responses -> /v1/chat/v1/responses (double-prefix, no
  route serves it). Canonicalize to /v1/responses (OpenAI-compat Responses
  API). User-facing.
- data-provider actions.spec + openapiSpecs (scholar-ai.net, swapi.dev) +
  api mcp.spec wss template: third-party API path fixtures wrongly swept to
  /v1/chat/*. Reverted to /api/* (they represent EXTERNAL APIs, not chat
  routes). Fixes 5 failing createURL/executor tests.
- Stale doc comments: useFavorites (/v1/user -> /v1/chat/user), tokens.ts
  typedef ref.

OUR-route /api/ remains ZERO. Login/OAuth/SSE/CSRF paths unaffected.
2026-07-03 13:55:43 -07:00
hanzo-dev ec32864a00 refactor(chat): eliminate /api/ prefix -> /v1/chat/* (namespaced, no-collision)
App REST surface moves from /api/* to /v1/chat/* in lockstep across server
mounts, client URL builders, SSE stream paths, OAuth redirect_uris, telemetry
route-grouping, RUM proxy, and the nginx bridge.

Namespace /v1/chat/* (not flat /v1/) to avoid colliding with the OpenAI-compat
inference surface (/v1/chat/completions, /v1/models -> router_backend) and to
match the canonical chat/openapi.yaml. Social OIDC login (/oauth/*) and /health
are unchanged (login-safe).

Levers:
- server: api/server/index.js 26 app.use('/v1/chat/*') mounts (+ experimental.js)
- client: packages/data-provider/src/api-endpoints.ts (75 builders) + config.ts
Lockstep: checkBan matcher, Files/Code download path, actions/mcp CSRF cookie
paths, ActionService + mcp/oauth handler redirect_uris, admin OpenID callback,
telemetry middleware/sdk/stream, rum proxy path, vite dev proxy + PWA denylist,
robots.txt, .env.example, pr-preview health-path (/api/health -> /health).

Third-party APIs chat CALLS are untouched (Ollama, Wolfram, Discord, GitHub
Enterprise, Mistral, OpenRouter, DataDog). Live-surface /api/: 683 -> 0.
Also untracks runtime log artifacts (api/logs, client/junit.xml; already gitignored).
2026-07-03 13:55:43 -07:00
zeekayandClaude Opus 4.8 3e83bc6779 feat(guest): logged-out landing IS the chat composer + airtight per-IP quota (0.9.3)
ChatGPT-style anonymous preview: when ALLOW_GUEST_CHAT is on, a logged-out
visitor renders the real chat view (composer + starter cards + model picker)
and can send a message as a guest on the free Zen model, WITHOUT logging in.
Sign-in is only prompted after the free per-IP quota (402 GUEST_LIMIT).

Client (the missing wiring — server guest path was already complete):
- ChatRoute now renders ChatView for canChat = isAuthenticated || isGuest;
  /api/models + /api/endpoints queries run for guests (guest-scoped config),
  and the roles gate treats a guest as loaded (no agent access). Previously
  ChatRoute hard-returned null for !isAuthenticated, so a guest got the shell
  but no composer.
- useAuthRedirect now surfaces isGuest.

Abuse control (airtight, server-enforced):
- guestMessageLimiter + guestTokenLimiter now key on the REAL client IP via
  utils/guestClientIp (Cloudflare CF-Connecting-IP, falls back to req.ip), NOT
  the guest token — clearing cookies / incognito / minting a fresh token can't
  reset the count. Shared Redis limiterCache holds it across replicas.
- guestLimiters env parsed as positive ints.

Prod runs GUEST_MESSAGE_MAX=2. Guests always use the shared capped HANZO_API_KEY
(per-user hk- billing is skipped for guest principals).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:59:48 -07:00
zeekayandClaude Opus 4.8 39e5a71c7e chore(chat): release 0.9.2 — design unification (Basel Grotesk + Geist Mono, PanelLeft sidebar, dark tokens)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:55:06 -07:00
1eb2caecec fix(mcp): close redirect-based SSRF on MCP request path (backport LibreChat #12931) (#57)
* fix(mcp): block private IPs when the connector resolves all addresses

undici's connect-time DNS lookup calls the SSRF-safe wrapper with
{ all: true }, so dns.lookup returns a LookupAddress[] rather than a
single string. The previous guard only inspected `typeof address ===
'string'`, silently failing open on the array shape — a hostname
resolving to a private/reserved IP would pass unchecked.

Normalize both shapes to a flat address list and reject if ANY resolved
address is private, mirroring upstream LibreChat's getBlockedLookupAddress.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(mcp): re-check SSRF on every redirect hop, strip cross-origin creds

An MCP server URL is server-controlled. Our customFetch issued the
request with undici's default redirect:'follow', so a public MCP server
(which passes the add-time isMCPDomainAllowed check) could 301/302/307/
308-redirect the connection to an internal IP literal (169.254.169.254
cloud metadata, 127.0.0.1, in-cluster 10.x/172.16-31/192.168 services).
undici skips its connect-time DNS lookup for IP literals, so the
redirect hop reached the internal target unguarded — a live SSRF on the
multi-tenant chat surface (proven: a 301 to 127.0.0.1/latest/meta-data/
was followed).

Harden createFetchFunction to follow redirects manually (redirect:
'manual'):
- Only 307/308 are followed, up to MAX_REDIRECTS=5; 301/302/303 are
  returned unfollowed (the MCP SDK rejects a bare 3xx).
- Every hop's target is re-validated with isSSRFTarget (catches IP
  literals the connect lookup skips) + resolveHostnameSSRF (catches
  hostnames resolving to private IPs); a blocked hop is not followed.
- Cross-origin hops strip credential headers (Authorization, cookie,
  mcp-session-id, plus runtime/config secret header keys) so a bearer
  token / session id never leaks to a redirect target's origin.
- Cross-origin hops pin an SSRF-safe connect dispatcher for the rest of
  the chain, closing the allowlist-mode DNS-rebinding gap.

Mirrors upstream LibreChat MCP redirect SSRF hardening (PR #12931).
Redirect targets get no allowlist exemption by design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(mcp): prove redirect SSRF guard rejects internal-IP targets

End-to-end test using the REAL isSSRFTarget/resolveHostnameSSRF
classifiers (the sibling MCPConnectionSSRF suite mocks ~/auth). Stands
up loopback HTTP servers and asserts a 302/307/308 redirect to an
internal IP literal is never followed — the redirect is issued
(entryHit) but the internal metadata endpoint is never reached
(internalHit stays false). Also pins the classifier: 169.254.169.254,
127.0.0.1, RFC1918, localhost, ::1 are SSRF targets; a public hostname
is not.

Reverting the connection.ts guard makes all three redirect cases fail
(internal endpoint reached), confirming the test exercises the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:54:19 -07:00
zandGitHub d44146f1fc Merge pull request #56 from hanzoai/design/unify-basel-geist-mono
design: unify Basel Grotesk + Geist Mono, sidebar icon, panels, dark tokens
2026-07-03 08:46:21 -07:00
zeekayandClaude Opus 4.8 dca8e39e69 design: unify sidebar toggle to lucide PanelLeft + Basel/Geist Mono type
Converge hanzo.chat onto the shared @hanzo/ui design language:
- Sidebar toggle glyph: replace the custom filled panel SVG with the
  canonical lucide PanelLeft geometry (stroke-2, 24x24). Kept as the shared
  `Sidebar` export so OpenSidebar/NewChat/ExpandedPanel all get the one
  canonical icon with zero call-site churn.
- Typography: Basel Grotesk (UI/body/display/heading, self-hosted woff2/woff,
  Book 400 + Medium 500) as tailwind `sans`; Geist Mono (code/data) as
  tailwind `mono` via CDN. Replaces Inter / Roboto Mono.

Dark aesthetic already matches the target (true-black OLED #000 / #0a0a0a).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 01:44:36 -07:00
zeekayandClaude Opus 4.8 72b061877a config(chat): full multi-provider picker — Zen default + Qwen/Llama/DeepSeek/Mistral/Gemma/GPT-OSS
Mirror the live chat-config CM: house-brand Zen (default zen5-mini) plus the
independent third-party families the gateway serves, current-gen models only
(no sunset zen4). Same api.hanzo.ai/v1 gateway + per-user key → identical
Commerce metering across all families.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 01:12:32 -07:00
zandGitHub 5a3aede148 Merge pull request #55 from hanzoai/polish/chat-empty-state
polish(chat): premium starter cards — Railway × OpenAI (0.9.1)
2026-07-03 01:08:18 -07:00
zandClaude Opus 4.8 4f25c3131a polish(chat): premium starter cards (Railway x OpenAI); 0.9.1
Elevate the empty-state starters from flat chips to a refined 2-col card grid:
lucide icon in a rounded well, hover lift (-translate-y) + soft shadow + border
lightening, smooth 200ms ease-out, focus-visible ring, reduced-motion safe.
Uses existing theme tokens (surface-primary-alt / border-light / text-secondary)
so it tracks light+dark + the monochrome brand. Agent starters render in the
same grid (default icon).

Proper semver: 0.9.0 -> 0.9.1 (patch, visual refinement of the 0.9.0 feature).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 01:07:58 -07:00
zeekayandClaude Opus 4.8 512289012c merge(chat): unify → main — true-black theme, Zen ensō logo, /v1 unify, provider families
Brings the true-black OLED dark theme (style.css), the Zen ensō avatar
(ZenLogoIcon replacing the generic robot on the Zen/custom endpoint), the
chat→api.hanzo.ai/v1 unification, and the multi-provider picker (Zen house
brand + open families) onto main for a published build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 01:06:05 -07:00
zandGitHub 3af4415366 Merge pull request #54 from hanzoai/feat/chat-default-starters
feat(chat): default starter prompts on empty state (0.9.0)
2026-07-03 01:01:07 -07:00
zandClaude Opus 4.8 749784a50c feat(chat): default starter prompts on empty state; release 0.9.0
Plain-model chats (e.g. zen5-mini) showed a bare empty state — LibreChat only
rendered conversation starters for agents/assistants. Add 4 curated default
starters as the fallback so every empty chat gets ChatGPT/Claude-style
suggestion chips, wired through the existing useSubmitMessage path (landing-only;
agents that intentionally omit starters are untouched).

Proper semver: v0.8.3-rc1 (rc, non-standard v-prefix) -> 0.9.0 (minor, feature).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 01:00:44 -07:00
zeekayandClaude Opus 4.8 26e83d7645 fix(chat): drop Qwen + DeepSeek families (brand policy — Zen upstreams)
Qwen3 and DeepSeek are the upstreams behind Zen's flagship tiers
(zen5-max→qwen3.5-397b, zen5-pro/flash→deepseek-v4-pro/deepseek-4-flash).
Listing a Qwen or DeepSeek family next to Hanzo/Zen reveals the Zen mapping —
the single most forbidden thing in the brand policy. Keep only genuinely
independent open-weight families Zen's flagships don't build on: Meta Llama
and Mistral. Final picker: Hanzo (Zen, default zen5-mini) + Meta Llama +
Mistral. Every model is served by api.hanzo.ai/v1/models and its DO upstream
is verified invocable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 00:41:53 -07:00
zeekayandClaude Opus 4.8 662f054658 feat(chat): unified model picker — Zen + open families (Meta/Mistral/DeepSeek/Qwen)
Mirror librechat.yaml to the LIVE chat-config CM (v1.3.4) and add per-family
custom endpoints alongside Hanzo/Zen. Every endpoint is the same
api.hanzo.ai/v1 gateway carrying the per-user HANZO_API_KEY, so metering is
identical across families (the cloud billing gate). Hanzo stays customOrder:0
with the zen5-mini default.

Families listed are the ones the gateway can genuinely invoke today via
DigitalOcean GenAI (funded): Meta Llama, Mistral, DeepSeek, Qwen. OpenAI and
Anthropic branded resale is intentionally omitted — no funded key path exists
yet (direct OPENAI/ANTHROPIC keys unfunded; DO account lacks proprietary
access), so listing them would surface non-invocable models. Documented inline
for a one-block-each add once a funded key/enablement lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 00:27:53 -07:00
zeekayandClaude Opus 4.8 150c5fff4d fix(chat): live zen5+zen3 catalog in librechat.yaml mirror (drop sunset zen4)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:47:32 -07:00
zeekayandClaude Opus 4.8 abe59a625c feat(theme): true-black OLED dark theme + Zen ensō avatar
Dark theme pushed toward black per brand: main canvas #000, panels/sidebar
#050505/#0a0a0a, elevated controls (input, cards) #171717 — remapped the `.dark`
--surface-* + shadcn HSL block onto new near-black gray steps (--gray-925/950/975)
in one place (DRY; the scale, not per-component overrides).

Zen avatar: the custom endpoint (hanzo.chat is Zen-only — Hanzo AI zen* models)
rendered the generic LibreChat "custom" robot glyph because the custom branch
hardcodes CustomMinimalIcon and ignores the endpoint iconURL. Replaced with a
proper ZenLogoIcon (ensō — the single-brushstroke Zen circle, monochrome
currentColor so it reads on true-black), named 'Zen'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:36:18 -07:00
zeekayandClaude Opus 4.8 14bb1bf35a fix(landing): drop raw upstream base-model names (brand policy)
The logged-out landing page listed Zen's upstream families (DeepSeek R1,
Qwen3, Llama 4, Phi-4) in the "third-party models" grid and named DeepSeek/
Qwen in the 100+ Models blurb — a brand-policy leak (Zen must present as our
own family; no raw Qwen/DeepSeek/Kimi/Llama/HuggingFace names). Keep only the
genuine independent providers offered via the gateway (GPT-5, Claude Opus 4,
Gemini 2.5, Grok, Mistral Large, Command R+) and lead the blurb with the Zen
family. Ships on next chat image build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:28:32 -07:00
zeekayandClaude Opus 4.8 9f6c243327 docs(unify): map chat→api.hanzo.ai/v1 architecture + enforce zen-only picker
Investigate-before-ripping map of hanzo.chat as the cloud "chat view":
- Documents the ONE inference path (POST /api/agents/chat/Hanzo →
  api.hanzo.ai/v1/chat/completions, per-user hk- key, SSE) plus code-exec →
  /v1/exec, websearch → /v1/websearch, cloud agents → /v1/agents. No shadow
  LLM backend; config.yaml/litellm is dead upstream residue.
- Flags the ONE real parallel store: LibreChat Mongo (convos/messages/presets/
  prompts/users/balances/files/sessions). Go backend has casibase-named
  persistence (/v1/get-chats,/v1/add-message) + a speced-but-unimplemented
  chat/openapi.yaml (/v1/chat/convos|messages|presets). Kill path documented;
  do NOT rip Mongo (data loss + dead chat) — coordinate with openapi.
- IAM-native status: prod passport OIDC → hanzo.id (client hanzo-chat), LIVE;
  static SPA mode uses client app-chat (align) on dormant @hanzo/iam ^0.4.0.
- @hanzo/ui (Tailwind/shadcn, chat's stack) vs @hanzo/gui (Tamagui/Next15,
  console's stack): unify via @hanzo/ui, not a framework-swap rewrite.

librechat.yaml: drop gpt-4o/claude from the Hanzo endpoint default so the repo
reference mirrors the authoritative prod ConfigMap — zen-only picker, no raw
upstream names (brand policy). One way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:18:09 -07:00
hanzo-dev 34096ffe12 fix(agents): mandatory principal-binding + no OIDC refresh in decoupled session
Red-team hardening of the on-behalf-of decouple:

- cloud.js getUserCloudBearer: bind EVERY forwarded token (id_token AND the
  access_token fallback, session or cookie) via isForwardableToken. Principal
  binding is now MANDATORY (sub === req.user.openidId) with no fail-open when
  openidId/sub is absent, and the access_token fallback is bound too — closes
  the unbound-access_token and null-binding confused-deputy paths.
- AuthService.persistOpenIDTokensToSession: take the OIDC refresh credential
  EXPLICITLY (not from the tokenset). The decoupled default persists only the
  on-behalf-of bearer (id_token/access_token), NOT the OIDC refresh_token, so
  logout keeps using the local cookie -> findSession matches -> the server-side
  session is invalidated (was silently skipped once openidTokens landed in the
  decoupled session). REUSE path unchanged (passes the resolved credential).

Tests: cloud.spec + AuthService.spec green (bound-access, opaque-401,
foreign-401, null-openidId-401, cookie-injection-401, decoupled-omits-refresh).
Pre-existing oauth admin-exchange arity failures untouched (out of scope).
2026-07-02 18:12:25 -07:00
hanzo-dev a12198a01f fix(agents): decouple hanzo.id id_token persistence from the OIDC refresh gate
/api/agents/cloud returned 401 "cloud agents require hanzo.id sign-in" for
EVERY signed-in user: getUserCloudBearer forwarded the id_token read from
req.session.openidTokens, which is only populated by setOpenIDAuthTokens —
called at login solely when OPENID_REUSE_TOKENS=true. The live deploy runs
OPENID_REUSE_TOKENS=false on purpose (true makes /api/auth/refresh use the OIDC
refresh-grant, which 403s against hanzo.id and breaks login), so the id_token
was never persisted and the cloud-agent run was dead on arrival. One flag
braided two orthogonal concerns: (a) persist the id_token for downstream
on-behalf-of calls, (b) use the OIDC refresh-grant on token refresh.

Decouple:
- AuthService: new persistOpenIDTokensToSession(req, tokenset) — the one writer
  of req.session.openidTokens (server-side only, never a cookie). setOpenIDAuth-
  Tokens now delegates its session write to it (DRY, behavior-preserving).
- oauth login: an OpenID login ALWAYS persists the id_token to the session,
  regardless of OPENID_REUSE_TOKENS. With REUSE disabled the login still runs
  setAuthTokens (local-JWT refresh) so login/refresh cookies are byte-identical;
  only server-side session state is added. Persist is wrapped so it can never
  break the login redirect. OPENID_REUSE_TOKENS now SOLELY gates the refresh-grant.
- cloud proxy: getUserCloudBearer keys off the VALIDATED principal
  (req.user.provider === 'openid'), not the mutable token_provider cookie, so the
  on-behalf-of decision is tied to identity. The forwarded id_token must also name
  req.user (sub === openidId) and be unexpired — expired/absent yields an honest
  401, never a fabricated run. Confused deputy denied at the identity layer.

Durable refresh of the ~1h id_token (hanzo.id/Casdoor OIDC refresh, or an
RFC-8693 token-exchange from the chat session) is a tracked FOLLOW-UP; the
login-breaking refresh-grant is NOT enabled.

Tests (jest): cloud proxy re-keyed to the principal + honest expiry/binding/
confused-deputy (20); oauth persists id_token when REUSE=false, uses the OIDC
writer when true, local user never persists, login survives a persist failure
(4); persistOpenIDTokensToSession session/expiry/no-session/no-tokenset (4).
Refresh path unchanged.

Note: oauth.spec's 2 admin-exchange tests are pre-existing red on origin/main
(spec expects a 7-arg generateAdminExchangeCode; source passes 4) — orthogonal
to this change, proven by a stashed baseline.
2026-07-02 17:43:58 -07:00
hanzo-dev 4abec9f46a harden(agents): rate-limit + load-shed + principal-guard the cloud-agent run proxy
Red review of the cloud /v1/agents run proxy: core security (SSRF, CSRF,
token-exfil, wrong-principal, mount-order, injection) confirmed closed; three
gaps fixed:

- MEDIUM: the run proxy (a real billable cloud completion) escaped the throttle
  guarding the sibling chat-completion path. Add per-user cloudAgentLimiter on
  the whole /cloud router (CLOUD_AGENT_USER_MAX/_WINDOW), a process-wide in-flight
  ceiling with fail-fast 503 (CLOUD_AGENT_MAX_CONCURRENT), and a 4 MiB buffered-
  response cap (Content-Length fast path + streamed abort) so no single call can
  pin the shared backend's memory.
- LOW: only forward OpenID tokens when the request itself is an OpenID login
  (token_provider==='openid'); a local-JWT user with a stale OpenID session can
  no longer run as that prior identity (confused deputy).
- LOW: cap input by UTF-8 BYTE length, not UTF-16 units, matching cloud's maxInput.

Tests: api 34 passed (was 25; +9: principal-guard, cookie-read, byte-cap,
response-cap x3, concurrency-cap x2). eslint clean.
2026-07-02 13:56:56 -07:00
hanzo-dev 7c4c1b676c harden(agents): validate cloud agent :name at the HTTP boundary
Move the cloud handle-grammar guard to the route boundary via router.param,
not only inside CloudAgentsClient. A malformed/decoded :name (traversal,
null byte, CRLF, backslash, space) is now rejected with 400 before any client
call is constructed — defense at the boundary. Production was already safe
(the client rejects the same names), but the guard belongs at the entry point.

Adds boundary-validation tests proving decoded smuggles (../etc, ../admin,
..\evil, %00, %0d, %20) 400 without reaching the client. cloud route 14/14,
CloudAgentsClient 11/11 green.
2026-07-02 00:22:19 -07:00
hanzo-dev 0138889e81 feat(agents): run canonical cloud /v1/agents from chat + mobile builder fixes
Add cloud-agent RUN alongside the existing LibreChat-legacy agent builder
(which is untouched). The canonical registry is cloud /v1/agents; chat now
lets a signed-in user run their OWN cloud agents from the thread via a
/agent <name> [prompt] slash command and via the @mention picker.

Backend (server-side proxy, token never reaches the browser):
- CloudAgentsClient forwards the user's hanzo.id id_token as a Bearer to
  cloud; cloud's SanitizeIdentity (HIP-0026) validates it and pins X-Org-Id
  from the owner claim, so a user only reaches their own org. Not an open
  proxy: fixed host from HANZO_CLOUD_URL (falls back to OPENAI_BASE_URL host),
  three fixed endpoints, agent name validated against cloud's handle grammar
  (traversal/SSRF guard), 128KB input cap, 30s timeout, fail-secure 401 on
  missing token (no service-token fallback).
- /api/agents/cloud/{,:name,:name/run} router, requireJwtAuth (guests rejected),
  honest error passthrough. Mounted before the legacy /:id route.

Frontend (DRY, one run path, reuses Mention/MentionItem):
- useRunCloudAgent renders the run in-thread; parseAgentCommand is the single
  command grammar; /agent and @mention both funnel through it.
- Cloud agents surface in @mention (cloudAgent type) and a /agent popover.

Mobile: AgentConfig responsive padding + full-width wrapping error text +
stacked tool/action buttons; AgentPanel form min-w-0 to stop overflow <768px.

Convergence of chat's /api/agents CRUD onto /v1/agents is a later step.

Tests: CloudAgentsClient (11) + cloud route (7) + parseAgentCommand (8) green.
2026-07-02 00:18:02 -07:00
devandDarkhorse7stars 9ef8cf46a9 fix(analytics): bake VITE_ANALYTICS_SITE_ID into the Vite build
The index.html tracker tag uses %VITE_ANALYTICS_SITE_ID%, substituted by
Vite at build time. The var was never set at build, so the placeholder
shipped literally and the browser POSTed website id '%VITE_ANALYTICS_SITE_ID%'
to analytics.hanzo.ai/api/send -> 400. The CR only set NEXT_PUBLIC_ANALYTICS_SITE_ID
(runtime, wrong prefix + phase for a Vite build). Default the real registered
Hanzo Chat site id as a Docker ARG/ENV so the frontend build bakes it in.
2026-07-01 10:30:00 -05:00
dev 4d96ee9e4d fix(pwa): precache index.html so the SW nav fallback resolves
vite-plugin-pwa's default navigateFallback is 'index.html', so the SW
binds createHandlerBoundToURL('index.html'). index.html was in globIgnores
(not precached) -> 'non-precached-url: index.html' -> the service worker
breaks and strands users on a stale shell after each deploy (they see
/api/* 401s until a manual SW clear). Precache it; registerType:autoUpdate
keeps it fresh on release.
2026-07-01 08:53:07 -05:00
c4f48f6af5 feat(chat): wire Run Code + Web Search to Hanzo unified backend (no external SaaS) (#52)
Both Agent Builder capabilities were 🔑-gated because nothing was wired.

- librechat.yaml: add webSearch{} pinned to Hanzo's own /v1/websearch surface
  — searchProvider=searxng (Hanzo metasearch), scraperProvider=firecrawl
  (Hanzo Crawl), no reranker. NO external Serper/Tavily/Jina/Cohere. Add
  endpoints.agents.capabilities so execute_code + web_search show active.
- .env.hanzo-cloud: the old CODE_EXECUTION_ENDPOINT/RUNTIME_API_KEY were DEAD
  (LibreChat never reads them, and the path was /execute not /exec). Replace
  with the real vars: LIBRECHAT_CODE_BASEURL=https://api.hanzo.ai/v1 +
  LIBRECHAT_CODE_API_KEY (KMS). Add SEARXNG_INSTANCE_URL / FIRECRAWL_API_URL /
  WEBSEARCH_API_KEY -> api.hanzo.ai/v1/websearch.
- .env.example: document the Hanzo-backend vars as the one way; note external
  providers are intentionally disabled.

Validated against the LibreChat Zod configSchema: CONFIG VALID (searxng +
firecrawl + execute_code/web_search capabilities).

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-06-30 18:39:33 -07:00
f466b6c2ca fix(chat): treat missing agent_id as ephemeral so 'hi' just replies (#51)
In the agents-centric chat UI ('My Agents'), sending a message with no agent
created/selected POSTed to the agents completion endpoint without an agent_id,
which canAccessAgentFromBody rejected with 400 'agent_id is required in request
body'. A missing agent_id on the agents endpoint is an ad-hoc (ephemeral) chat,
not an error — isEphemeralAgentId(undefined) is already true.

- canAccessAgentFromBody: drop the 400 branch; a missing id falls through the
  existing ephemeral path (no per-agent ACL) to plain chat.
- agents/build.buildOptions: resolve a missing agent_id to EPHEMERAL_AGENT_ID so
  loadAgent builds an ephemeral plain-model agent instead of returning null.
- Hermetic regression specs for both fix points (no Mongo/winston); update the
  stale middleware test that codified the 400.

Logged-in user types 'hi' -> ephemeral agent on the configured default model ->
reply, with no agent build required first.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-06-29 22:07:27 -07:00
z 26591fb2ba docs(brand): add hero banner 2026-06-28 20:06:46 -07:00
z f54d01289b chore(brand): dynamic hero banner 2026-06-28 20:06:45 -07:00
Hanzo AI 1a8456bbb9 fix(chat): surface Hanzo gateway 200 error-envelope instead of crashing the agent run
The Hanzo Cloud gateway (api.hanzo.ai) answers some failures -- most importantly a request for a premium model (e.g. zen5-mini) when the caller's balance is only the $5 starter credit -- with HTTP 200 and a JSON error envelope ({status:"error", msg}) that carries no `choices`. The OpenAI client treated the 200 as success and parsed the choices-less body to `undefined`, so the agent run threw the opaque "Cannot read properties of undefined (reading 'role')" (and the title call died on reading 'message') -- no AI reply rendered, the generation job expired, and the resume stream 404'd.

Wrap the custom-endpoint OpenAI client fetch (scoped to the Hanzo gateway) so that envelope is rewritten into a clean 402 carrying the gateway's actionable message. Successful SSE streams and normal completions pass through untouched; the request is never inspected, so per-user hk- billing is unaffected.

- packages/api: hanzoGatewayFetch response-only wrapper + wire into initializeCustom + unit tests
2026-06-27 23:48:12 -07:00
Hanzo AI 0dfa7df1da fix(chat): default new conversations to the Hanzo endpoint, not bare agents
A fresh /c/new defaulted to the built-in `agents` endpoint with no agent
selected. All chat routes through POST /api/agents/chat/:endpoint; with
endpoint=agents and no agent_id the access middleware (canAccessAgentFromBody)
returns 400 "agent_id is required in request body" and the SSE stream never
starts — every first message failed with no AI response.

Root cause: orderEndpointsConfig gives built-in `agents` order 1 but defaults
every custom endpoint to order 9999, so the client's getDefaultEndpoint picked
`agents` first. The intended override — a custom endpoint's `customOrder` — was
silently dropped by loadCustomEndpointsConfig (it never emitted `order`), even
though orderEndpointsConfig already types the custom spread as `& { order?: number }`.

Fix: loadCustomEndpointsConfig now honors `customOrder` -> `order`, and the Hanzo
endpoint sets `customOrder: 0` so it outranks `agents`. A new conversation now
defaults to Hanzo, which routes as an ephemeral agent (provider=Hanzo) through
the proven per-user hk- key path (resolveHanzoCloudKey) — real AI response,
metered, no agent_id 400. Agents endpoint stays available for explicit selection.

- packages/api: honor customOrder, widen TCustomEndpointsConfig, + unit test
- librechat.yaml: customOrder: 0 on Hanzo (mirrored in universe chat-config)
2026-06-27 22:22:06 -07:00
Hanzo AI 44d41f6235 fix(billing): auto-$5 starter credit on first chat + per-user billing subject
New-account chat now works end to end, fail-closed, no shared-org bleed:

- resolveHanzoCloudKey computes the canonical per-user billing subject
  (object.BillingSubject parity: 'owner/name' for the shared hanzo catch-all),
  stamps it on req.user, and ensures the one-time $5 Commerce starter credit on
  THAT subject (POST /v1/billing/grant-starter) BEFORE forwarding the user's
  hk- key — so the gateway's first balance check sees $5 instead of 402-ing.
  Idempotent (in-process + commerce tag-dedupe), best-effort (gateway enforces).
- CommerceClient: checkBalance/tier/breakdown key on the subject and derive the
  X-Hanzo-Org namespace from it; replace the wrong-ledger createTrialGrant
  (credit-grants, invisible to /billing/balance) with grantStarter (Deposit).
- balanceMethods gate keys on the per-user subject (req.user.billingSubject).
- Drop chat-side Commerce usage write (single debit = the gateway).
- Remove the dead createTrialGrant call in user.ts (never fired for SSO users,
  wrong ledger).
2026-06-27 17:57:43 -07:00
Hanzo AI 0becb68c80 fix(billing): converge money path on per-user hk- (single debit) + exempt guests
The prior HEAD (4233c8760) was reverted in the preceding commit: it rewrote the
gate to forward the user's IAM JWT to commerce AND added a chat-side
spendTokens -> /v1/billing/usage decrement. With per-user hk- forwarding ON,
that double-debits (cloud gateway debits the org via the user's hk- key, AND
chat debits the same org via spendTokens). One money authority only: the cloud
gateway debits via the per-user hk- key; chat's balanceMethods is a READ-ONLY,
fail-closed pre-flight gate (service token + X-Hanzo-Org, the proven pattern).

This commit adds the one missing piece for the anon free tier: guests are
exempt from the balance gate. Guests hit checkBalance via BaseClient
(supportsBalanceCheck[custom]=true) with no billing org; under startBalance:0
the legacy local gate would block the free tier entirely. Their spend is
bounded instead by (1) the per-IP guest message limiter and (2) the separate,
small-capped, NON-exempt guest key (HANZO_API_KEY) the gateway 402s when empty.

Tests: fail-closed decision matrix (funded/insufficient/unavailable/
model_not_allowed/no-org) + guest exemption. 8/8 green.
2026-06-27 12:26:37 -07:00
Hanzo AI 04210dea16 Revert "fix(billing): IAM-native, fail-closed AI metering (stop unmetered leak)"
This reverts commit 4233c8760f.
2026-06-27 12:21:31 -07:00
Hanzo AI 4233c8760f fix(billing): IAM-native, fail-closed AI metering (stop unmetered leak)
Commerce is already IAM-native + multi-tenant; chat was sending a static
service token (not a JWT) so commerce 401'd and the balance check fell open =
unmetered AI. Rewire to forward the logged-in user's hanzo.id IAM JWT per
request and fail CLOSED.

- CommerceClient: drop the static COMMERCE_TOKEN singleton; every call carries
  the user's IAM JWT (Authorization: Bearer). getMyBalance (fail-closed read),
  grantWelcome ($5 idempotent), recordUsage (decrement). getIamToken/
  getBillingOrg/computeUsageCents/recordCommerceSpend helpers.
- balanceMethods.checkBalance: IAM-native money gate, FAIL CLOSED — no token /
  commerce-unreachable / below-min => refuse. Funded user passes. Self-heals a
  new account by granting the idempotent $5 once and re-reading (a spent wallet
  stays blocked — grant is tag-deduped).
- spendTokens / spendStructuredTokens: decrement the user's per-org commerce
  balance, mirroring the local ledger cost; threaded from the agent spend sites.
- openidStrategy: grant the $5 welcome credit on new-account signup under the
  user's IAM identity (synchronous, best-effort).
- Balance controller: surface the authoritative commerce balance (display).
- Tests: fail-closed/fail-open decision matrix + cost mirror (12 cases).

Requires OPENID_REUSE_TOKENS=true (chat CR) so the IAM JWT is on the request,
and COMMERCE_EDGE_AUTH=true + hanzo-chat audience (commerce CR).
2026-06-27 11:21:52 -07:00
Hanzo AI d20ba76651 fix(billing): per-user hk- key forwarding + commerce-first fail-closed balance gate (stop the bleed)
hanzo.chat money path. Previously every authenticated chat ran on z's shared
hk- key (hanzo/z) against the shared 'hanzo' commerce org, plus a $5 local
startBalance per user and a fail-OPEN balance gate -> unbounded free spend.

- Per-user billing: resolve (mint on first chat) each authenticated user's OWN
  hk- key from IAM by org+email and forward it to the gateway, which debits
  THEIR org. New packages/api/src/endpoints/custom/hanzoCloudKey.ts; injected at
  the single apiKey chokepoint in custom/initialize.ts. FAIL CLOSED: an authed
  user whose key cannot be resolved is blocked, never falls back to the shared
  key. The resolver also stamps the authoritative billing org back onto req.user
  (OIDC 'owner' can be the Casdoor super-org 'admin', not the real billing org).
- Balance gate commerce-first + FAIL CLOSED + decisive (balanceMethods.js):
  when commerce is configured and the user's org is known, the org's commerce
  balance is authoritative (>= HANZO_MIN_BALANCE); insufficient/unreachable ->
  block. No fall-through to local tokenCredits, so startBalance:0 cannot
  false-block a funded user. CommerceClient balance read is now fail-closed
  (throws on cold error) and per-org scoped (X-Hanzo-Org).
- startBalance: 0 (librechat.yaml): no free local credits; new users claim $5
  at billing.hanzo.ai. Client shows a claim-credit link on empty balance.

Gated by HANZO_PER_USER_KEY=true (reuses OPENID_* client creds for IAM).
2026-06-27 10:59:29 -07:00
Hanzo AI 3e36ede65d fix(theme): monochrome UI — neutralize blue-hued tokens (222/215/220 → 0 0%) + #2563eb ring/border → gray
The dark theme tinted all panels/sidebar blue via --background/--card/--border/
--input/--muted/--accent at hue 222 (and ring #2563eb). Neutralized to 0%
saturation (grayscale) to match the Hanzo monochrome brand.
2026-06-26 23:42:25 -07:00
Hanzo AI 18f3d9ce80 fix(brand): convert residual landing-page red accents to monochrome white
The monochrome rebrand changed the LandingPage `colors` object + login CTAs
but left 8 inline rgba(253,68,68,…) (#fd4444) accents — hero gradient,
"AI Chat Platform" badge border, Zen-models card border/bg + hover, and the
"Free credit" pricing tier border/bg/label. Convert all to rgba(255,255,255,…)
(preserving alpha), matching the existing white monochrome accents. The
terminal traffic-light dots (bg-red/yellow/green-500) already render grey via
the tailwind color-scale remap. Landing is now fully black/white/grey.
2026-06-26 19:45:46 -07:00
Hanzo AI 5578ef924d feat(brand): monochrome Hanzo rebrand — feather→H-mark, white CTAs, canonical favicon
Replace the upstream LibreChat feather/quill brand fallback with the
official Hanzo ▼/H mark (currentColor, monochrome) across every agent and
endpoint icon path (Landing welcome via ConvoIcon→AgentAvatar, agent
avatars, message + minimal icons).

Collapse all Tailwind color scales (green/red/blue/sky/…/rose) to a single
neutral grey ramp so UI chrome is black/white/grey only; destructive
semantics keep one muted red via --text-destructive. Convert the green
Create/Save CTAs (Add-MCP-Server dialog, Agent + Assistant builders,
preset and API-key dialogs) and .btn-primary to the adaptive
white-on-dark primary button.

Set favicon/PWA assets to the canonical hanzo.app ▼/H set (favicon.ico
byte-identical to hanzo.app) and the manifest theme_color to monochrome.

Tests: 34 passed — Hanzo-mark fallback assertions updated.
2026-06-26 19:19:23 -07:00
Hanzo AI 5e0b6038af chore(iam): unify @hanzo/iam to ^0.13.1; BrowserIamSdk->IAM (localStorage PKCE login fix)
client/package.json: ^0.4.0 -> ^0.13.1 (npm latest). 0.13.x moves the PKCE tx (state+verifier) from sessionStorage to localStorage so it survives the OAuth redirect, fixing login.

iam.ts: BrowserIamSdk class renamed to IAM. Import bare '@hanzo/iam' (not the '/browser' subpath): the client tsconfig uses moduleResolution=node, which cannot resolve the exports subpath for types; the bare entry (dist/index.d.ts) re-exports IAM and Vite still bundles dist/browser.js via the browser export condition. Constructor config (IAMConfig) is unchanged.

OAuthCallback.tsx: IAMToken fields camelCased in 0.13 (access_token -> accessToken); without this the callback never set the auth header.

Lockfile: pnpm-lock.yaml regenerated (authoritative per packageManager=pnpm@10.27.0 + Dockerfile 'pnpm install --frozen-lockfile'). package-lock.json/bun.lock left untouched (not consumed by any build or CI).
2026-06-26 13:11:15 -07:00
a5a2498997 fix(oauth): scope loginLimiter to initiation routes, not OIDC callbacks (fixes 429 on code exchange) (#42)
Co-authored-by: Antje Worring <worringantje@gmail.com>
2026-06-25 15:22:56 -07:00
Antje Worring dbed3bf2fb fix(csp): allow analytics + Cloudflare beacon scripts, fonts, media
The CSP script-src was 'self' only, blocking the app's own
analytics.hanzo.ai/script.js and static.cloudflareinsights.com beacon, plus a
data: font and assets/silence.mp3 (console errors + broken assets on every
page load). Allow-list the two external script hosts in script-src/connect-src
and add font-src/media-src for data:/blob:.
2026-06-23 18:05:34 -07:00
Antje Worring 462dbfdc84 fix(branding): canonical @hanzo blocky-H assets/logo.svg (replace placeholder 24x24 H)
The OIDC/model iconURL and OPENID_IMAGE_URL point at hanzo.chat/assets/logo.svg;
that asset was still a made-up 24x24 single-path H. Replace with the canonical
64x64 blocky-H (5 paths) byte-matching @hanzo/brand — matches what the live
0.7.12 image serves.
2026-06-23 14:29:53 -07:00
hanzoandAntje Worring c2aff88ca6 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 14:29:53 -07:00
Hanzo DevandGitHub 05396959ae Merge pull request #50 from hanzoai/fix/guest-bootstrap-endpoints
fix(guest): UI-bootstrap endpoints serve guest-scoped data (fix HTTP 500 → working composer)
2026-06-21 14:12:49 -07:00
zeekay da475d299b feat(guest): serve guest-scoped data on UI-bootstrap endpoints
The composer hard-requires a handful of read-only bootstrap calls. Switch
exactly those to requireGuestOrJwtAuth and add a guest branch that returns
safe, scoped data — never a DB lookup, never another user's data:

- GET /api/endpoints  → ONLY the guest endpoint (Hanzo)
- GET /api/models     → ONLY the single guest model
- GET /api/user       → ephemeral guest principal (no email)
- GET /api/convos     → empty conversation page
- GET /api/user/settings/favorites → []
- GET /api/agents/chat/active      → { activeJobIds: [] }

Every mutation and read-by-id route stays JWT-only and rejects guests with
a clean 401 (post jwt-strategy fix). Banner (optionalJwtAuth) now resolves
with no user for guests. Adds controller + full-chain integration tests.
2026-06-21 14:10:21 -07:00
zeekay 2e794d3228 feat(guest): central guest principal + scoped-config builders
Single source of truth in guestConfig.js for the ephemeral guest shape:
- buildGuestPrincipal(id): plain GUEST req.user (no DB id/email)
- buildGuestUser: safe /api/user payload (name 'Guest', no email)
- buildGuestEndpointsConfig: ONLY the configured guest endpoint
- buildGuestModelsConfig: ONLY the single configured guest model

requireGuestOrJwtAuth now builds its principal from buildGuestPrincipal
(adds name 'Guest'), keeping the guest shape DRY across auth and bootstrap.
2026-06-21 14:10:12 -07:00
zeekay 9cd97355fe fix(guest): jwt strategy rejects guest tokens cleanly (no CastError → 500)
The guest JWT carries a synthetic `guest_<uuid>` id that is not a Mongo
ObjectId. On any requireJwtAuth route the 'jwt' passport strategy called
getUserById(guest_id) → Mongoose CastError → done(err) → HTTP 500.

Detect the `guest: true` claim at the top of the verify callback and
return done(null, false) before any DB lookup. Guest tokens now fail the
strict strategy with a clean 401, and reach their scoped routes only via
requireGuestOrJwtAuth. Fail closed by construction.
2026-06-21 14:10:05 -07:00
Hanzo DevandGitHub b70c5400a2 Merge pull request #49 from hanzoai/fix/guest-route-guard
fix(guest): route guard honors guest mode (composer for logged-out)
2026-06-21 13:45:38 -07:00
zeekay 8a89cfed07 fix(guest): useAuthRedirect must honor guest mode (stop bouncing guests to /login)
The chat route guard redirected any !isAuthenticated user to /login on a
300ms timer, ignoring isGuest/allowGuestChat and beating startup-config
load — so logged-out guests never reached the composer. Wait for config,
and skip the redirect when guest chat is enabled or the user is a guest.
2026-06-21 13:45:32 -07:00
Hanzo DevandGitHub c3114a1258 Merge pull request #48 from hanzoai/fix/guest-acquire-race
fix(guest): acquire guest session when allowGuestChat resolves (fixes landing-page race)
2026-06-21 13:26:05 -07:00
zeekay 821b3b59a2 fix(guest): acquire guest session when allowGuestChat config resolves
silentRefresh's guest fallback could run before startupConfig loaded
(allowGuestChat undefined) and was never retried, leaving logged-out
visitors on the landing page instead of the guest composer. Add a
dedicated effect that acquires the guest token once the flag is true.
2026-06-21 13:25:59 -07:00
Hanzo DevandGitHub 8a6c63a869 Merge pull request #47 from hanzoai/feat/guest-chat
feat(guest): anonymous guest preview chat (opt-in, fail-closed)
2026-06-21 12:52:28 -07:00
zeekay 86ecab135e docs(guest): document guest-chat env vars and security model 2026-06-21 12:46:56 -07:00
zeekay 196e3c3d61 feat(guest): client guest-chat mode + login gate
When unauthenticated and startupConfig.allowGuestChat is set, acquire a
guest token on load and render the chat composer instead of the landing
gate. On a 402 GUEST_LIMIT response, surface GuestLimitDialog which
triggers the existing OpenID/hanzo.id login flow.

- useGuestAuth: acquires an ephemeral guest session via dataService.
- AuthContext: isGuest state; guest fallback in silentRefresh; userQuery
  and silentRefresh disabled for guests (capability-scoped).
- Root: render chat for guests (auth-gated hooks stay disabled); mount
  GuestLimitDialog.
- useResumableSSE: dispatch guestLimitReached on 402 GUEST_LIMIT.
2026-06-21 12:46:56 -07:00
zeekay 46b636c8c0 feat(guest): server-side anonymous guest chat (fail-closed, scoped)
Add an opt-in anonymous preview mode gated behind ALLOW_GUEST_CHAT
(default off). Guests get a per-IP free quota (GUEST_MESSAGE_MAX,
default 3) on the free Zen model via the Hanzo gateway endpoint.

- POST /api/auth/guest issues a short-lived guest JWT ({guest:true},
  per-token random id) signed with JWT_SECRET; rate-limited per IP.
- requireGuestOrJwtAuth accepts guest tokens ONLY on the chat-completion
  router; the standard jwt strategy rejects them everywhere else.
- enforceGuestScope pins endpoint+model to the free Zen endpoint and
  strips agents/tools/files/spec/preset server-side.
- guestMessageLimiter enforces the per-IP quota via the shared Redis
  limiterCache; returns 402 {type:'GUEST_LIMIT'} on exhaustion.
- Reserved chat subpaths (stream/active/status/abort) stay JWT-only.
- Expose allowGuestChat/guestMessageMax in /api/config; add GUEST role.

Tests: 27 unit/integration tests covering config, guest auth wrapper,
scope enforcement, quota, and the router chain.
2026-06-21 12:46:27 -07:00
0cc792d02d fix(agents): stop post-reply error banner from nulled config.configurable (#46)
run.processStream (@librechat/agents >= 3.1.52) treats the passed config as
owned and, in its post-stream cleanup, sets config.configurable = undefined to
break the AsyncLocalStorage -> LangGraph reference chain that keeps heavy graph
state (base64 images/PDFs) alive (agents dist/esm/run.mjs:239). Reading
config.configurable.hide_sequential_outputs AFTER runAgents() therefore threw
"Cannot read properties of undefined (reading 'hide_sequential_outputs')",
caught by chatCompletion and pushed as an ERROR content part -- the red
post-reply banner. The same throw aborted the post-stream finalize, which is
why auto-titling also stopped.

Hoist the read to before runAgents() (the value is a static agent property only
needed afterward by the deprecated Agent Chain output filter), so it no longer
depends on post-run config state. Matches upstream LibreChat's fix
(PR #11942, a0f9782e6).

Adds a regression test that reproduces the mutation (processStream nulls
config.configurable) and asserts no ERROR content part is produced, plus
coverage for the hide_sequential_outputs true/false filter behavior.

Co-authored-by: Antje Worring <worringantje@gmail.com>
2026-06-21 10:28:01 -07:00
2122 changed files with 37466 additions and 80945 deletions
+16
View File
@@ -0,0 +1,16 @@
# depcheck configuration.
#
# ignores: dependencies that are present on purpose but never imported directly,
# so depcheck's static scan reports them as a false positive.
#
# The three @opentelemetry/* entries below are all transitive dependencies of
# @opentelemetry/sdk-node, which we DO import and drive (NodeSDK in
# packages/api/src/telemetry/sdk.ts). They are pinned at the top level to keep
# every otel package on one aligned version (mismatched otel versions break at
# runtime). None is imported by name, so depcheck flags them — expected, not
# dead. Do NOT remove them (that would unpin the otel version set). Only
# @opentelemetry/api is imported directly and stays checked.
ignores:
- "@opentelemetry/core"
- "@opentelemetry/exporter-trace-otlp-http"
- "@opentelemetry/sdk-trace-base"
+1 -1
View File
@@ -19,7 +19,7 @@ services:
# - /var/run/docker.sock:/var/run/docker.sock
environment:
- HOST=0.0.0.0
- MONGO_URI=mongodb://mongodb:27017/LibreChat
- MONGO_URI=mongodb://mongodb:27017/Chat
# - OPENAI_REVERSE_PROXY=http://host.docker.internal:8070/v1
- MEILI_HOST=http://meilisearch:7700
+1 -1
View File
@@ -29,7 +29,7 @@ services:
gitnexus:
# Override via GITNEXUS_IMAGE in /opt/gitnexus/.env to use a fork or
# a pinned version tag like :v1.5.3 for reproducible rollbacks.
image: ${GITNEXUS_IMAGE:-ghcr.io/danny-avila/librechat-gitnexus:latest}
image: ${GITNEXUS_IMAGE:-ghcr.io/danny-avila/chat-gitnexus:latest}
container_name: gitnexus
restart: unless-stopped
networks:
+3 -3
View File
@@ -14,10 +14,10 @@ export NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=1280}"
# metadata pointer without touching the index data.
#
# Registration failure handling:
# - main (LibreChat) and dev (LibreChat-dev) are critical. If either
# - main (Chat) and dev (Chat-dev) are critical. If either
# fails to register, exit 1 so docker marks the container unhealthy
# and the deploy workflow's readiness check surfaces the error.
# - PR indexes (LibreChat-pr-*) are best-effort. A corrupt PR index
# - PR indexes (Chat-pr-*) are best-effort. A corrupt PR index
# shouldn't take the whole server down.
if [ -d /indexes ]; then
for dir in /indexes/*/; do
@@ -27,7 +27,7 @@ if [ -d /indexes ]; then
echo "Registering index: $name"
if ! gitnexus index "$dir" --allow-non-git; then
case "$name" in
LibreChat|LibreChat-dev)
Chat|Chat-dev)
echo "ERROR: failed to register critical index $name" >&2
exit 1
;;
+41 -20
View File
@@ -117,6 +117,17 @@ PROXY=
# Endpoint: https://api.hanzo.ai/v1
HANZO_API_KEY=
# Canonical Hanzo Cloud agents (/v1/agents). Lets signed-in users run their own
# cloud agents from chat via `/agent <name>` or the @mention picker. The chat
# backend proxies /v1/chat/agents/cloud/* to this host, forwarding the user's
# hanzo.id token server-side (never to the browser); cloud scopes to their org.
# Optional: if unset, derived from OPENAI_BASE_URL (its host, minus /v1).
# HANZO_CLOUD_URL=https://api.hanzo.ai
# A cloud-agent run is a real billable completion; these bound abuse of the proxy.
# CLOUD_AGENT_USER_MAX=30 # max cloud-agent requests per user per window
# CLOUD_AGENT_WINDOW=1 # rate-limit window, minutes
# CLOUD_AGENT_MAX_CONCURRENT=50 # process-wide in-flight ceiling to cloud (503 past it)
#===================================#
# Known Endpoints - chat.yaml #
#===================================#
@@ -463,6 +474,21 @@ ALLOW_REGISTRATION=true
ALLOW_SOCIAL_LOGIN=false
ALLOW_SOCIAL_REGISTRATION=false
ALLOW_PASSWORD_RESET=false
#=====================================================#
# Guest Chat (anonymous preview) #
#=====================================================#
# Off by default. When enabled, unauthenticated visitors get a small free
# quota on the free Zen model routed through api.hanzo.ai. After the quota is
# spent the UI surfaces the existing OpenID/hanzo.id login. Guests are scoped
# server-side to the free model only and rejected from every other route.
ALLOW_GUEST_CHAT=false
# GUEST_MESSAGE_MAX=3 # free messages per IP before login is required
# GUEST_ENDPOINT=Hanzo # custom endpoint name from chat.yaml (api.hanzo.ai)
# GUEST_MODEL=zen3-nano # free Zen model id guests are pinned to
# GUEST_TOKEN_EXPIRY=3600000 # guest JWT lifetime in ms (default 1h)
# GUEST_TOKEN_MAX=20 # max guest sessions issued per IP per window
# GUEST_TOKEN_WINDOW=60 # guest-session issuance window in minutes
# ALLOW_ACCOUNT_DELETION=true # note: enabled by default if omitted/commented out
ALLOW_UNVERIFIED_EMAIL_LOGIN=true
@@ -749,7 +775,7 @@ HELP_AND_FAQ_URL=https://hanzo.ai/chat
# Use environment variable name for dynamic prefix (recommended for cloud deployments)
# REDIS_KEY_PREFIX_VAR=K_REVISION
# Or use static prefix directly
# REDIS_KEY_PREFIX=librechat
# REDIS_KEY_PREFIX=chat
# Redis connection limits
# REDIS_MAX_LISTENERS=40
@@ -810,31 +836,26 @@ OPENWEATHER_API_KEY=
# Hanzo Chat Code Interpreter API #
#====================================#
# https://hanzo.ai/docs/chat/code-interpreter
# LIBRECHAT_CODE_API_KEY=your-key
# "Run Code" routes through the Hanzo unified backend (api.hanzo.ai/v1/exec),
# which executes in a Hanzo sandbox. execute_code POSTs {BASEURL}/exec with
# X-API-Key. https://hanzo.ai/docs/chat/code-interpreter
# CHAT_CODE_BASEURL=https://api.hanzo.ai/v1
# CHAT_CODE_API_KEY=your-key # prod: KMS chat-secrets/CHAT_CODE_API_KEY
#======================#
# Web Search #
#======================#
# Note: All of the following variable names can be customized.
# Omit values to allow user to provide them.
# For more information on configuration values, see:
# Web Search routes through the Hanzo unified backend ONLY — no external SaaS.
# The searxng provider + firecrawl scraper both point at api.hanzo.ai/v1/websearch
# (Hanzo metasearch + Hanzo Crawl). See chat.yaml `webSearch`.
# https://hanzo.ai/docs/chat/features/web_search
# Search Provider (Required)
# SERPER_API_KEY=your_serper_api_key
# Scraper (Required)
# FIRECRAWL_API_KEY=your_firecrawl_api_key
# Optional: Custom Firecrawl API URL
# FIRECRAWL_API_URL=your_firecrawl_api_url
# Reranker (Required)
# JINA_API_KEY=your_jina_api_key
# or
# COHERE_API_KEY=your_cohere_api_key
# SEARXNG_INSTANCE_URL=https://api.hanzo.ai/v1/websearch
# FIRECRAWL_API_URL=https://api.hanzo.ai/v1/websearch
# WEBSEARCH_API_KEY=your-key # prod: KMS chat-secrets/WEBSEARCH_API_KEY
#
# Do NOT set SERPER_API_KEY / TAVILY_API_KEY / JINA_API_KEY / COHERE_API_KEY:
# external providers are intentionally disabled (one way, unified backend).
#======================#
# MCP Configuration #
+20 -20
View File
@@ -1,10 +1,10 @@
#=====================================================================#
# LibreChat Configuration #
# Chat Configuration #
#=====================================================================#
# Please refer to the reference documentation for assistance #
# with configuring your LibreChat environment. #
# with configuring your Chat environment. #
# #
# https://www.librechat.ai/docs/configuration/dotenv #
# https://hanzo.ai/docs/chat/configuration/dotenv #
#=====================================================================#
#==================================================#
@@ -14,7 +14,7 @@
HOST=localhost
PORT=3080
MONGO_URI=mongodb://127.0.0.1:27017/LibreChat
MONGO_URI=mongodb://127.0.0.1:27017/Chat
DOMAIN_CLIENT=http://localhost:3080
DOMAIN_SERVER=http://localhost:3080
@@ -52,7 +52,7 @@ DEBUG_CONSOLE=false
#===============#
# Use an absolute path, a relative path, or a URL
# CONFIG_PATH="/alternative/path/to/librechat.yaml"
# CONFIG_PATH="/alternative/path/to/chat.yaml"
#===================================================#
# Endpoints #
@@ -63,9 +63,9 @@ DEBUG_CONSOLE=false
PROXY=
#===================================#
# Known Endpoints - librechat.yaml #
# Known Endpoints - chat.yaml #
#===================================#
# https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints
# https://hanzo.ai/docs/chat/configuration/chat_yaml/ai_endpoints
# ANYSCALE_API_KEY=
# APIPIE_API_KEY=
@@ -96,8 +96,8 @@ ANTHROPIC_API_KEY=user_provided
#============#
# Note: these variables are DEPRECATED
# Use the `librechat.yaml` configuration for `azureOpenAI` instead
# You may also continue to use them if you opt out of using the `librechat.yaml` configuration
# Use the `chat.yaml` configuration for `azureOpenAI` instead
# You may also continue to use them if you opt out of using the `chat.yaml` configuration
# AZURE_OPENAI_DEFAULT_MODEL=gpt-3.5-turbo # Deprecated
# AZURE_OPENAI_MODELS=gpt-3.5-turbo,gpt-4 # Deprecated
@@ -207,7 +207,7 @@ ASSISTANTS_API_KEY=user_provided
# The models for Azure Assistants are also determined by your Azure OpenAI configuration.
# More info, including how to enable use of Assistants with Azure here:
# https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints/azure#using-assistants-with-azure
# https://hanzo.ai/docs/chat/configuration/chat_yaml/ai_endpoints/azure#using-assistants-with-azure
#============#
# Plugins #
@@ -320,7 +320,7 @@ TTS_API_KEY=
#==================================================#
# RAG #
#==================================================#
# More info: https://www.librechat.ai/docs/configuration/rag_api
# More info: https://hanzo.ai/docs/chat/configuration/rag_api
# RAG_OPENAI_BASEURL=
# RAG_OPENAI_API_KEY=
@@ -513,7 +513,7 @@ EMAIL_ALLOW_SELFSIGNED=
EMAIL_USERNAME=
EMAIL_PASSWORD=
EMAIL_FROM_NAME=
EMAIL_FROM=noreply@librechat.ai
EMAIL_FROM=noreply@hanzo.ai
#========================#
# Mailgun API #
@@ -522,7 +522,7 @@ EMAIL_FROM=noreply@librechat.ai
# MAILGUN_API_KEY=your-mailgun-api-key
# MAILGUN_DOMAIN=mg.yourdomain.com
# EMAIL_FROM=noreply@yourdomain.com
# EMAIL_FROM_NAME="LibreChat"
# EMAIL_FROM_NAME="Chat"
# # Optional: For EU region
# MAILGUN_HOST=https://api.eu.mailgun.net
@@ -572,16 +572,16 @@ ALLOW_SHARED_LINKS_PUBLIC=true
# STATIC_CACHE_MAX_AGE=172800
# STATIC_CACHE_S_MAX_AGE=86400
# If you have another service in front of your LibreChat doing compression, disable express based compression here
# If you have another service in front of your Chat doing compression, disable express based compression here
# DISABLE_COMPRESSION=true
#===================================================#
# UI #
#===================================================#
APP_TITLE=LibreChat
APP_TITLE=Chat
# CUSTOM_FOOTER="My custom footer"
HELP_AND_FAQ_URL=https://librechat.ai
HELP_AND_FAQ_URL=https://hanzo.chat
# SHOW_BIRTHDAY_ICON=true
@@ -630,11 +630,11 @@ HELP_AND_FAQ_URL=https://librechat.ai
OPENWEATHER_API_KEY=
#====================================#
# LibreChat Code Interpreter API #
# Chat Code Interpreter API #
#====================================#
# https://code.librechat.ai
# LIBRECHAT_CODE_API_KEY=your-key
# https://code.hanzo.ai
# CHAT_CODE_API_KEY=your-key
#======================#
# Web Search #
@@ -644,7 +644,7 @@ OPENWEATHER_API_KEY=
# Omit values to allow user to provide them.
# For more information on configuration values, see:
# https://librechat.ai/docs/features/web_search
# https://hanzo.ai/docs/chat/features/web_search
# Search Provider (Required)
# SERPER_API_KEY=your_serper_api_key
+14 -4
View File
@@ -5,9 +5,19 @@
OPENAI_API_KEY=sk-hanzo-your-api-key-here
OPENAI_BASE_URL=https://api.hanzo.ai/v1
# Runtime Execution (Code Interpreter)
CODE_EXECUTION_ENDPOINT=https://api.hanzo.ai/v1/execute
RUNTIME_API_KEY=${OPENAI_API_KEY}
# Code Interpreter ("Run Code") — Hanzo unified backend.
# Chat's execute_code tool POSTs {CHAT_CODE_BASEURL}/exec with header
# X-API-Key. cloud-api serves /v1/exec (-> sandboxed executor). The key is
# KMS-sourced (chat-secrets/CHAT_CODE_API_KEY). These are the ONLY vars
# Chat reads — the old CODE_EXECUTION_ENDPOINT/RUNTIME_API_KEY were dead.
CHAT_CODE_BASEURL=https://api.hanzo.ai/v1
CHAT_CODE_API_KEY=set-in-KMS-chat-secrets
# Web Search — Hanzo unified backend (searxng + firecrawl compat over Hanzo
# search + crawl). See chat.yaml `webSearch`. One shared service key.
SEARXNG_INSTANCE_URL=https://api.hanzo.ai/v1/websearch
FIRECRAWL_API_URL=https://api.hanzo.ai/v1/websearch
WEBSEARCH_API_KEY=set-in-KMS-chat-secrets
# ====== BRANDING ======
APP_TITLE=Hanzo Chat
@@ -75,5 +85,5 @@ DEBUG_LOGGING=false
# ====== NOTES ======
# 1. Get your Hanzo API key from https://hanzo.ai/dashboard
# 2. All AI providers are accessed through api.hanzo.ai -- no individual keys needed
# 3. Model list is configured in librechat.yaml under endpoints.custom (Hanzo endpoint)
# 3. Model list is configured in chat.yaml under endpoints.custom (Hanzo endpoint)
# 4. zen4 is the default model for new conversations
+1 -1
View File
@@ -38,7 +38,7 @@ Project maintainers have the right and responsibility to remove, edit, or reject
- Install [MongoDB Community Edition](https://www.mongodb.com/docs/manual/administration/install-community/), ensure that `mongosh` connects to your local instance.
- Run: `npx install playwright`, then `npx playwright install`.
- Copy `config.local`: `cp e2e/config.local.example.ts e2e/config.local.ts`.
- Copy `librechat.yaml`: `cp librechat.example.yaml librechat.yaml`.
- Copy `chat.yaml`: `cp chat.example.yaml chat.yaml`.
- Run: `npm run e2e`.
## 2. Development Notes
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-label="chat">
<rect width="1280" height="640" fill="#0A0A0A"/>
<svg x="96" y="215" width="210" height="210" viewBox="0 0 67 67"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="#fff"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#fff"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="#fff"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#fff"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#fff"/></svg>
<text x="378" y="276" font-family="Inter,system-ui,-apple-system,sans-serif" font-size="78" font-weight="800" letter-spacing="-2" fill="#ffffff">chat</text>
<text x="378" y="322" font-family="Inter,system-ui,sans-serif" font-size="30" fill="#ffffff" opacity=".66">AI chat with MCP integration and multi-provider support</text>
<rect x="378" y="338" width="806" height="3" rx="1.5" fill="#ffffff" opacity=".9"/>
<text x="378" y="390" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">github.com/hanzoai</text>
<text x="1184" y="390" text-anchor="end" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">hanzo.ai</text>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
CHART_PATH="${CHART_PATH:-helm/librechat/Chart.yaml}"
CHART_PATH="${CHART_PATH:-helm/chat/Chart.yaml}"
DEFAULT_BRANCH="${DEFAULT_BRANCH:-main}"
BASE_REF="${BASE_REF:-refs/remotes/origin/${DEFAULT_BRANCH}}"
BACKFILL_FROM_VERSION="${BACKFILL_FROM_VERSION:-1.9.0}"
+3 -3
View File
@@ -67,11 +67,11 @@ jobs:
- name: Run unit tests
run: cd api && pnpm run test:ci
- name: Run librechat-data-provider unit tests
- name: Run @hanzochat/data-provider unit tests
run: cd packages/data-provider && pnpm run test:ci
- name: Run @librechat/data-schemas unit tests
- name: Run @hanzochat/data-schemas unit tests
run: cd packages/data-schemas && pnpm run test:ci
- name: Run @librechat/api unit tests
- name: Run @hanzochat/api unit tests
run: cd packages/api && pnpm run test:ci
+2 -2
View File
@@ -1,4 +1,4 @@
name: Publish `@librechat/client` to NPM
name: Publish `@hanzochat/client` to NPM
on:
push:
@@ -47,7 +47,7 @@ jobs:
working-directory: packages/client
run: |
PACKAGE_VERSION=$(node -p "require('./package.json').version")
PUBLISHED_VERSION=$(npm view @librechat/client version 2>/dev/null || echo "0.0.0")
PUBLISHED_VERSION=$(npm view @hanzochat/client version 2>/dev/null || echo "0.0.0")
if [ "$PACKAGE_VERSION" = "$PUBLISHED_VERSION" ]; then
echo "No version change, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
+1 -1
View File
@@ -1,4 +1,4 @@
name: Publish `librechat-data-provider` to NPM
name: Publish `@hanzochat/data-provider` to NPM
on:
push:
+2 -2
View File
@@ -1,4 +1,4 @@
name: Publish `@librechat/data-schemas` to NPM
name: Publish `@hanzochat/data-schemas` to NPM
on:
push:
@@ -47,7 +47,7 @@ jobs:
working-directory: packages/data-schemas
run: |
PACKAGE_VERSION=$(node -p "require('./package.json').version")
PUBLISHED_VERSION=$(npm view @librechat/data-schemas version 2>/dev/null || echo "0.0.0")
PUBLISHED_VERSION=$(npm view @hanzochat/data-schemas version 2>/dev/null || echo "0.0.0")
if [ "$PACKAGE_VERSION" = "$PUBLISHED_VERSION" ]; then
echo "No version change, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
+3 -3
View File
@@ -14,7 +14,7 @@ jobs:
deploy:
runs-on: hanzo-deploy-linux-amd64
if: |
github.repository == 'danny-avila/LibreChat' &&
github.repository == 'danny-avila/Chat' &&
(github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'dev'))
steps:
@@ -34,10 +34,10 @@ jobs:
run: |
ssh ${DO_USER}@${DO_HOST} << EOF
sudo -i -u danny bash << 'EEOF'
cd ~/LibreChat && \
cd ~/Chat && \
git fetch origin main && \
sudo npm run stop:deployed && \
sudo docker images --format "{{.Repository}}:{{.ID}}" | grep -E "lc-dev|librechat" | cut -d: -f2 | xargs -r sudo docker rmi -f || true && \
sudo docker images --format "{{.Repository}}:{{.ID}}" | grep -E "lc-dev|chat" | cut -d: -f2 | xargs -r sudo docker rmi -f || true && \
sudo npm run update:deployed && \
git checkout dev && \
git pull origin dev && \
+2 -2
View File
@@ -27,10 +27,10 @@ jobs:
include:
- target: api-build
file: Dockerfile.multi
image_name: librechat-dev-api
image_name: chat-dev-api
- target: node
file: Dockerfile
image_name: librechat-dev
image_name: chat-dev
steps:
# Check out the repository
+4 -1
View File
@@ -21,9 +21,12 @@ jobs:
runs-on: hanzo-build-linux-amd64
env:
CI: true
# pnpm run frontend builds packages/api (rollup, ~5 GiB peak) + the client;
# the default ~2 GiB heap OOMs. Match the review workflows' heap knob.
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'
NODE_ENV: CI
SEARCH: false
MONGO_URI: mongodb://localhost:27017/librechat-test
MONGO_URI: mongodb://localhost:27017/chat-test
JWT_SECRET: e2e-test-jwt-secret
JWT_REFRESH_SECRET: e2e-test-jwt-refresh-secret
CREDS_KEY: e2e-test-creds-key-32chars00000000
+1 -1
View File
@@ -74,7 +74,7 @@ jobs:
run: |
ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" PR_NUM="$PR_NUM" bash <<'REMOTE'
set -e
TARGET="/opt/gitnexus/indexes/LibreChat-pr-$PR_NUM"
TARGET="/opt/gitnexus/indexes/Chat-pr-$PR_NUM"
if [ -d "$TARGET" ]; then
echo "Removing $TARGET"
rm -rf "$TARGET"
+11 -11
View File
@@ -71,7 +71,7 @@ permissions:
# cancel-in-progress so rapid pushes to the same ref coalesced but deploys
# targeting different refs ran in parallel. That had a data race: the
# prune-stale-indexes step computes its active_names up front, so if
# deploy A is rsyncing /opt/gitnexus/indexes/LibreChat-pr-12580 while
# deploy A is rsyncing /opt/gitnexus/indexes/Chat-pr-12580 while
# deploy B (started slightly later with a different ref) prunes, B can
# rm -rf a folder A is still uploading into.
#
@@ -85,7 +85,7 @@ concurrency:
env:
GITNEXUS_VERSION: '1.5.3'
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/librechat-gitnexus
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/chat-gitnexus
jobs:
# Rebuilds the long-lived image only when Dockerfile/entrypoint/extensions
@@ -200,8 +200,8 @@ jobs:
// --- main and dev branches ---
for (const [branch, name] of [
['main', 'LibreChat'],
['dev', 'LibreChat-dev'],
['main', 'Chat'],
['dev', 'Chat-dev'],
]) {
const artifactName = `gitnexus-index-${branch}`;
const fresh = await latestArtifact(artifactName);
@@ -260,11 +260,11 @@ jobs:
for (const { pr, artifactName, fresh } of keptPrs) {
serve.push({
name: `LibreChat-pr-${pr.number}`,
name: `Chat-pr-${pr.number}`,
artifactName,
runId: fresh.workflow_run.id,
});
core.info(`PR #${pr.number}: run ${fresh.workflow_run.id} -> LibreChat-pr-${pr.number}`);
core.info(`PR #${pr.number}: run ${fresh.workflow_run.id} -> Chat-pr-${pr.number}`);
}
if (evictedPrs.length) {
core.info(
@@ -305,7 +305,7 @@ jobs:
--name "$artifact" \
--dir "$target"; then
case "$name" in
LibreChat|LibreChat-dev)
Chat|Chat-dev)
echo "::error::Failed to download critical artifact $artifact"
exit 1
;;
@@ -438,7 +438,7 @@ jobs:
# is visible and the container isn't restarted with stale
# or missing data. PR indexes are best-effort.
case "$name" in
LibreChat|LibreChat-dev)
Chat|Chat-dev)
echo "::error::rsync failed for critical index $name — aborting deploy"
exit 1
;;
@@ -566,10 +566,10 @@ jobs:
const matrix = JSON.parse(process.env.MATRIX || '[]');
const triggerRunId = Number(process.env.TRIGGER_RUN_ID);
const match = matrix.find(
(m) => m.runId === triggerRunId && m.name.startsWith('LibreChat-pr-'),
(m) => m.runId === triggerRunId && m.name.startsWith('Chat-pr-'),
);
if (match) {
prNum = parseInt(match.name.replace('LibreChat-pr-', ''), 10);
prNum = parseInt(match.name.replace('Chat-pr-', ''), 10);
}
}
@@ -584,7 +584,7 @@ jobs:
`### GitNexus: ${ok ? '🚀 deployed' : '❌ deploy failed'}`,
'',
ok
? `The \`LibreChat-pr-${prNum}\` index is now live on the MCP server.`
? `The \`Chat-pr-${prNum}\` index is now live on the MCP server.`
: `The deploy failed — the previous index (if any) continues to be served.`,
`[Deploy run](${deployUrl})`,
].join('\n');
+12 -12
View File
@@ -30,9 +30,9 @@ jobs:
- name: Build Subchart Deps
run: |
cd helm/librechat
cd helm/chat
helm dependency build
cd ../librechat-rag-api
cd ../chat-rag-api
helm dependency build
- name: Get Chart Version
@@ -50,26 +50,26 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
# Run Helm OCI Charts Releaser
# This is for the librechat chart
- name: Release Helm OCI Charts for librechat
# This is for the chat chart
- name: Release Helm OCI Charts for chat
uses: appany/helm-oci-chart-releaser@v0.5.0
with:
name: librechat
repository: ${{ github.actor }}/librechat-chart
name: chat
repository: ${{ github.actor }}/chat-chart
tag: ${{ steps.chart-version.outputs.CHART_VERSION }}
path: helm/librechat
path: helm/chat
registry: ghcr.io
registry_username: ${{ github.actor }}
registry_password: ${{ secrets.GITHUB_TOKEN }}
# this is for the librechat-rag-api chart
- name: Release Helm OCI Charts for librechat-rag-api
# this is for the chat-rag-api chart
- name: Release Helm OCI Charts for chat-rag-api
uses: appany/helm-oci-chart-releaser@v0.5.0
with:
name: librechat-rag-api
repository: ${{ github.actor }}/librechat-chart
name: chat-rag-api
repository: ${{ github.actor }}/chat-chart
tag: ${{ steps.chart-version.outputs.CHART_VERSION }}
path: helm/librechat-rag-api
path: helm/chat-rag-api
registry: ghcr.io
registry_username: ${{ github.actor }}
registry_password: ${{ secrets.GITHUB_TOKEN }}
+23 -22
View File
@@ -119,30 +119,31 @@ jobs:
echo "unused_keys=[]" >> $GITHUB_ENV
fi
# Post via github-script (Octokit + the built-in token) rather than the gh
# CLI: gh is not installed on the self-hosted ARC runners, so `gh api` here
# exits 127. The job already grants `pull-requests: write`.
- name: Post verified comment on PR
if: env.unused_keys != '[]'
run: |
PR_NUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
# Format the unused keys list as checkboxes for easy manual checking.
FILTERED_KEYS=$(echo "$unused_keys" | jq -r '.[]' | grep -v '^\s*$' | sed 's/^/- [ ] `/;s/$/`/' )
COMMENT_BODY=$(cat <<EOF
### 🚨 Unused i18next Keys Detected
The following translation keys are defined in \`translation.json\` but are **not used** in the codebase:
$FILTERED_KEYS
⚠️ **Please remove these unused keys to keep the translation files clean.**
EOF
)
gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
-f body="$COMMENT_BODY" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/github-script@v7
with:
script: |
const keys = JSON.parse(process.env.unused_keys || '[]').filter(Boolean);
const list = keys.map((k) => `- [ ] \`${k}\``).join('\n');
const body = [
'### 🚨 Unused i18next Keys Detected',
'',
'The following translation keys are defined in `translation.json` but are **not used** in the codebase:',
'',
list,
'',
'⚠️ **Please remove these unused keys to keep the translation files clean.**',
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
- name: Fail workflow if unused keys found
if: env.unused_keys != '[]'
+2 -2
View File
@@ -15,10 +15,10 @@ jobs:
include:
- target: api-build
file: Dockerfile.multi
image_name: librechat-api
image_name: chat-api
- target: node
file: Dockerfile
image_name: librechat
image_name: chat
steps:
- name: Checkout
+1 -1
View File
@@ -16,6 +16,6 @@ jobs:
service: chat
image: ghcr.io/hanzoai/chat
port: "3080"
health-path: /api/health
health-path: /health
e2e-dir: e2e
secrets: inherit
+2 -2
View File
@@ -39,14 +39,14 @@ jobs:
env:
BASE_REF: refs/remotes/origin/main
BACKFILL_FROM_VERSION: 1.9.0
CHART_PATH: helm/librechat/Chart.yaml
CHART_PATH: helm/chat/Chart.yaml
DEFAULT_BRANCH: main
DISPATCH_WORKFLOW: helmcharts.yml
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
RELEASE_EXISTING_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_existing_tag || '' }}
REPO_DIR: /tmp/librechat-sync
REPO_DIR: /tmp/chat-sync
TAG_PREFIX: chart-
steps:
- name: Fetch main and tags
+2 -2
View File
@@ -17,10 +17,10 @@ jobs:
include:
- target: api-build
file: Dockerfile.multi
image_name: librechat-api
image_name: chat-api
- target: node
file: Dockerfile
image_name: librechat
image_name: chat
steps:
# Check out the repository
-57
View File
@@ -1,57 +0,0 @@
name: LiteLLM Linting
on:
pull_request:
branches: [ main ]
jobs:
lint:
runs-on: hanzo-build-linux-amd64
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install Poetry
uses: snok/install-poetry@v1
- name: Install dependencies
run: |
pip install openai==1.81.0
poetry install --with dev
pip install openai==1.81.0
- name: Run Black formatting
run: |
cd litellm
poetry run black .
cd ..
- name: Run Ruff linting
run: |
cd litellm
poetry run ruff check .
cd ..
- name: Run MyPy type checking
run: |
cd litellm
poetry run mypy . --ignore-missing-imports
cd ..
- name: Check for circular imports
run: |
cd litellm
poetry run python ../tests/documentation_tests/test_circular_imports.py
cd ..
- name: Check import safety
run: |
poetry run python -c "from litellm import *" || (echo '🚨 import failed, this means you introduced unprotected imports! 🚨'; exit 1)
+17 -1
View File
@@ -10,6 +10,12 @@ jobs:
test:
runs-on: hanzo-build-linux-amd64
# The packages/api rollup build peaks ~5 GiB RSS and OOMs at Node's default
# ~2 GiB old-space heap. Match the heap the review workflows already use
# (well under the 8 GiB runner limit). One knob, same expression everywhere.
env:
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'
services:
mongodb:
image: mongo:7
@@ -62,9 +68,19 @@ jobs:
run: pnpm run test:api
env:
NODE_ENV: test
MONGO_URI: mongodb://localhost:27017/librechat-test
MONGO_URI: mongodb://localhost:27017/chat-test
MEILI_HOST: http://localhost:7700
MEILI_MASTER_KEY: test_master_key
# Gate against tests-without-implementation drift in the MCP connection/
# transport layer (proxy, response-size caps, WS SSRF, allowedAddresses).
# These suites live in packages/api and were never run by this workflow —
# which is how the connection.ts port drifted from its byte-identical tests.
# A future half-port now fails here loudly.
- name: Run MCP connection/transport tests
run: cd packages/api && pnpm run test:transport
env:
NODE_ENV: test
- name: Run client tests
run: pnpm run test:client
+55 -55
View File
@@ -117,52 +117,52 @@ jobs:
extract_deps_from_code "packages/client" packages_client_used_code.txt
extract_deps_from_code "packages/api" packages_api_used_code.txt
- name: Get @librechat/client dependencies
id: get-librechat-client-deps
- name: Get @hanzochat/client dependencies
id: get-chat-client-deps
run: |
if [[ -f "packages/client/package.json" ]]; then
# Get all dependencies from @librechat/client (dependencies, devDependencies, and peerDependencies)
# Get all dependencies from @hanzochat/client (dependencies, devDependencies, and peerDependencies)
DEPS=$(jq -r '.dependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
DEV_DEPS=$(jq -r '.devDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
PEER_DEPS=$(jq -r '.peerDependencies // {} | keys[]' packages/client/package.json 2>/dev/null || echo "")
# Combine all dependencies
echo "$DEPS" > librechat_client_deps.txt
echo "$DEV_DEPS" >> librechat_client_deps.txt
echo "$PEER_DEPS" >> librechat_client_deps.txt
echo "$DEPS" > chat_client_deps.txt
echo "$DEV_DEPS" >> chat_client_deps.txt
echo "$PEER_DEPS" >> chat_client_deps.txt
# Also include dependencies that are imported in packages/client
cat packages_client_used_code.txt >> librechat_client_deps.txt
cat packages_client_used_code.txt >> chat_client_deps.txt
# Remove empty lines and sort
grep -v '^$' librechat_client_deps.txt | sort -u > temp_deps.txt
mv temp_deps.txt librechat_client_deps.txt
grep -v '^$' chat_client_deps.txt | sort -u > temp_deps.txt
mv temp_deps.txt chat_client_deps.txt
else
touch librechat_client_deps.txt
touch chat_client_deps.txt
fi
- name: Get @librechat/api dependencies
id: get-librechat-api-deps
- name: Get @hanzochat/api dependencies
id: get-chat-api-deps
run: |
if [[ -f "packages/api/package.json" ]]; then
# Get all dependencies from @librechat/api (dependencies, devDependencies, and peerDependencies)
# Get all dependencies from @hanzochat/api (dependencies, devDependencies, and peerDependencies)
DEPS=$(jq -r '.dependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
DEV_DEPS=$(jq -r '.devDependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
PEER_DEPS=$(jq -r '.peerDependencies // {} | keys[]' packages/api/package.json 2>/dev/null || echo "")
# Combine all dependencies
echo "$DEPS" > librechat_api_deps.txt
echo "$DEV_DEPS" >> librechat_api_deps.txt
echo "$PEER_DEPS" >> librechat_api_deps.txt
echo "$DEPS" > chat_api_deps.txt
echo "$DEV_DEPS" >> chat_api_deps.txt
echo "$PEER_DEPS" >> chat_api_deps.txt
# Also include dependencies that are imported in packages/api
cat packages_api_used_code.txt >> librechat_api_deps.txt
cat packages_api_used_code.txt >> chat_api_deps.txt
# Remove empty lines and sort
grep -v '^$' librechat_api_deps.txt | sort -u > temp_deps.txt
mv temp_deps.txt librechat_api_deps.txt
grep -v '^$' chat_api_deps.txt | sort -u > temp_deps.txt
mv temp_deps.txt chat_api_deps.txt
else
touch librechat_api_deps.txt
touch chat_api_deps.txt
fi
- name: Extract Workspace Dependencies
@@ -173,14 +173,14 @@ jobs:
local package_json=$1
local output_file=$2
# Get all workspace dependencies (starting with @librechat/)
# Get all workspace dependencies (starting with @hanzochat/)
if [[ -f "$package_json" ]]; then
local workspace_deps=$(jq -r '.dependencies // {} | to_entries[] | select(.key | startswith("@librechat/")) | .key' "$package_json" 2>/dev/null || echo "")
local workspace_deps=$(jq -r '.dependencies // {} | to_entries[] | select(.key | startswith("@hanzochat/")) | .key' "$package_json" 2>/dev/null || echo "")
# For each workspace dependency, get its dependencies
for dep in $workspace_deps; do
# Convert @librechat/api to packages/api
local workspace_path=$(echo "$dep" | sed 's/@librechat\//packages\//')
# Convert @hanzochat/api to packages/api
local workspace_path=$(echo "$dep" | sed 's/@chat\//packages\//')
local workspace_package_json="${workspace_path}/package.json"
if [[ -f "$workspace_package_json" ]]; then
@@ -223,8 +223,8 @@ jobs:
chmod -R 755 client
cd client
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
# Exclude dependencies used in scripts, code, workspace packages, and @librechat/client imports
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../client_used_deps.txt ../client_used_code.txt ../client_workspace_deps.txt ../packages_client_used_code.txt ../librechat_client_deps.txt 2>/dev/null | sort -u) || echo "")
# Exclude dependencies used in scripts, code, workspace packages, and @hanzochat/client imports
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../client_used_deps.txt ../client_used_code.txt ../client_workspace_deps.txt ../packages_client_used_code.txt ../chat_client_deps.txt 2>/dev/null | sort -u) || echo "")
# Filter out false positives
UNUSED=$(echo "$UNUSED" | grep -v "^micromark-extension-llm-math$" || echo "")
echo "CLIENT_UNUSED<<EOF" >> $GITHUB_ENV
@@ -240,43 +240,43 @@ jobs:
chmod -R 755 api
cd api
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
# Exclude dependencies used in scripts, code, workspace packages, and @librechat/api imports
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../api_used_deps.txt ../api_used_code.txt ../api_workspace_deps.txt ../packages_api_used_code.txt ../librechat_api_deps.txt 2>/dev/null | sort -u) || echo "")
# Exclude dependencies used in scripts, code, workspace packages, and @hanzochat/api imports
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../api_used_deps.txt ../api_used_code.txt ../api_workspace_deps.txt ../packages_api_used_code.txt ../chat_api_deps.txt 2>/dev/null | sort -u) || echo "")
echo "API_UNUSED<<EOF" >> $GITHUB_ENV
echo "$UNUSED" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
cd ..
fi
# Post via github-script (Octokit + the built-in token) rather than the gh
# CLI: gh is not installed on the self-hosted ARC runners, so `gh api` here
# exits 127. The job already grants `pull-requests: write`.
- name: Post comment on PR if unused dependencies are found
if: env.ROOT_UNUSED != '' || env.CLIENT_UNUSED != '' || env.API_UNUSED != ''
run: |
PR_NUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
ROOT_LIST=$(echo "$ROOT_UNUSED" | awk '{print "- `" $0 "`"}')
CLIENT_LIST=$(echo "$CLIENT_UNUSED" | awk '{print "- `" $0 "`"}')
API_LIST=$(echo "$API_UNUSED" | awk '{print "- `" $0 "`"}')
COMMENT_BODY=$(cat <<EOF
### 🚨 Unused NPM Packages Detected
The following **unused dependencies** were found:
$(if [[ ! -z "$ROOT_UNUSED" ]]; then echo "#### 📂 Root \`package.json\`"; echo ""; echo "$ROOT_LIST"; echo ""; fi)
$(if [[ ! -z "$CLIENT_UNUSED" ]]; then echo "#### 📂 Client \`client/package.json\`"; echo ""; echo "$CLIENT_LIST"; echo ""; fi)
$(if [[ ! -z "$API_UNUSED" ]]; then echo "#### 📂 API \`api/package.json\`"; echo ""; echo "$API_LIST"; echo ""; fi)
⚠️ **Please remove these unused dependencies to keep your project clean.**
EOF
)
gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
-f body="$COMMENT_BODY" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/github-script@v7
with:
script: |
const section = (title, val) => {
const items = (val || '').split('\n').map((s) => s.trim()).filter(Boolean);
if (!items.length) return '';
return `#### 📂 ${title}\n\n${items.map((i) => `- \`${i}\``).join('\n')}\n`;
};
const body = [
'### 🚨 Unused NPM Packages Detected',
'',
'The following **unused dependencies** were found:',
'',
section('Root `package.json`', process.env.ROOT_UNUSED),
section('Client `client/package.json`', process.env.CLIENT_UNUSED),
section('API `api/package.json`', process.env.API_UNUSED),
'⚠️ **Please remove these unused dependencies to keep your project clean.**',
].filter(Boolean).join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
- name: Fail workflow if unused dependencies found
if: env.ROOT_UNUSED != '' || env.CLIENT_UNUSED != '' || env.API_UNUSED != ''
+3 -3
View File
@@ -121,12 +121,12 @@ docker-compose.override.yml
dump.rdb
helm/**/.values.yaml
helm/**/charts/
helm/librechat/Chart.lock
helm/chat/Chart.lock
hive-mind-prompt-*.txt
junit.xml
lib-cov
librechat.yaml
librechat.yml
chat.yaml
chat.yml
logs
meili_data*
meili_data/
+166 -166
View File
@@ -11,41 +11,41 @@ All notable changes to this project will be documented in this file.
### ✨ New Features
- ✨ feat: implement search parameter updates by **@mawburn** in [#7151](https://github.com/danny-avila/LibreChat/pull/7151)
- 🎏 feat: Add MCP support for Streamable HTTP Transport by **@benverhees** in [#7353](https://github.com/danny-avila/LibreChat/pull/7353)
- 🔒 feat: Add Content Security Policy using Helmet middleware by **@rubentalstra** in [#7377](https://github.com/danny-avila/LibreChat/pull/7377)
- ✨ feat: Add Normalization for MCP Server Names by **@danny-avila** in [#7421](https://github.com/danny-avila/LibreChat/pull/7421)
- 📊 feat: Improve Helm Chart by **@hofq** in [#3638](https://github.com/danny-avila/LibreChat/pull/3638)
- 🦾 feat: Claude-4 Support by **@danny-avila** in [#7509](https://github.com/danny-avila/LibreChat/pull/7509)
- 🪨 feat: Bedrock Support for Claude-4 Reasoning by **@danny-avila** in [#7517](https://github.com/danny-avila/LibreChat/pull/7517)
- ✨ feat: implement search parameter updates by **@mawburn** in [#7151](https://github.com/danny-avila/Chat/pull/7151)
- 🎏 feat: Add MCP support for Streamable HTTP Transport by **@benverhees** in [#7353](https://github.com/danny-avila/Chat/pull/7353)
- 🔒 feat: Add Content Security Policy using Helmet middleware by **@rubentalstra** in [#7377](https://github.com/danny-avila/Chat/pull/7377)
- ✨ feat: Add Normalization for MCP Server Names by **@danny-avila** in [#7421](https://github.com/danny-avila/Chat/pull/7421)
- 📊 feat: Improve Helm Chart by **@hofq** in [#3638](https://github.com/danny-avila/Chat/pull/3638)
- 🦾 feat: Claude-4 Support by **@danny-avila** in [#7509](https://github.com/danny-avila/Chat/pull/7509)
- 🪨 feat: Bedrock Support for Claude-4 Reasoning by **@danny-avila** in [#7517](https://github.com/danny-avila/Chat/pull/7517)
### 🌍 Internationalization
- 🌍 i18n: Add `Danish` and `Czech` and `Catalan` localization support by **@rubentalstra** in [#7373](https://github.com/danny-avila/LibreChat/pull/7373)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7375](https://github.com/danny-avila/LibreChat/pull/7375)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7468](https://github.com/danny-avila/LibreChat/pull/7468)
- 🌍 i18n: Add `Danish` and `Czech` and `Catalan` localization support by **@rubentalstra** in [#7373](https://github.com/danny-avila/Chat/pull/7373)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7375](https://github.com/danny-avila/Chat/pull/7375)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7468](https://github.com/danny-avila/Chat/pull/7468)
### 🔧 Fixes
- 💬 fix: update aria-label for accessibility in ConvoLink component by **@berry-13** in [#7320](https://github.com/danny-avila/LibreChat/pull/7320)
- 🔑 fix: use `apiKey` instead of `openAIApiKey` in OpenAI-like Config by **@danny-avila** in [#7337](https://github.com/danny-avila/LibreChat/pull/7337)
- 🔄 fix: update navigation logic in `useFocusChatEffect` to ensure correct search parameters are used by **@mawburn** in [#7340](https://github.com/danny-avila/LibreChat/pull/7340)
- 🔄 fix: Improve MCP Connection Cleanup by **@danny-avila** in [#7400](https://github.com/danny-avila/LibreChat/pull/7400)
- 🛡️ fix: Preset and Validation Logic for URL Query Params by **@danny-avila** in [#7407](https://github.com/danny-avila/LibreChat/pull/7407)
- 🌘 fix: artifact of preview text is illegible in dark mode by **@nhtruong** in [#7405](https://github.com/danny-avila/LibreChat/pull/7405)
- 🛡️ fix: Temporarily Remove CSP until Configurable by **@danny-avila** in [#7419](https://github.com/danny-avila/LibreChat/pull/7419)
- 💽 fix: Exclude index page `/` from static cache settings by **@sbruel** in [#7382](https://github.com/danny-avila/LibreChat/pull/7382)
- 💬 fix: update aria-label for accessibility in ConvoLink component by **@berry-13** in [#7320](https://github.com/danny-avila/Chat/pull/7320)
- 🔑 fix: use `apiKey` instead of `openAIApiKey` in OpenAI-like Config by **@danny-avila** in [#7337](https://github.com/danny-avila/Chat/pull/7337)
- 🔄 fix: update navigation logic in `useFocusChatEffect` to ensure correct search parameters are used by **@mawburn** in [#7340](https://github.com/danny-avila/Chat/pull/7340)
- 🔄 fix: Improve MCP Connection Cleanup by **@danny-avila** in [#7400](https://github.com/danny-avila/Chat/pull/7400)
- 🛡️ fix: Preset and Validation Logic for URL Query Params by **@danny-avila** in [#7407](https://github.com/danny-avila/Chat/pull/7407)
- 🌘 fix: artifact of preview text is illegible in dark mode by **@nhtruong** in [#7405](https://github.com/danny-avila/Chat/pull/7405)
- 🛡️ fix: Temporarily Remove CSP until Configurable by **@danny-avila** in [#7419](https://github.com/danny-avila/Chat/pull/7419)
- 💽 fix: Exclude index page `/` from static cache settings by **@sbruel** in [#7382](https://github.com/danny-avila/Chat/pull/7382)
### ⚙️ Other Changes
- 📜 docs: CHANGELOG for release v0.7.8 by **@github-actions[bot]** in [#7290](https://github.com/danny-avila/LibreChat/pull/7290)
- 📦 chore: Update API Package Dependencies by **@danny-avila** in [#7359](https://github.com/danny-avila/LibreChat/pull/7359)
- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7321](https://github.com/danny-avila/LibreChat/pull/7321)
- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7434](https://github.com/danny-avila/LibreChat/pull/7434)
- 🛡️ chore: `multer` v2.0.0 for CVE-2025-47935 and CVE-2025-47944 by **@danny-avila** in [#7454](https://github.com/danny-avila/LibreChat/pull/7454)
- 📂 refactor: Improve `FileAttachment` & File Form Deletion by **@danny-avila** in [#7471](https://github.com/danny-avila/LibreChat/pull/7471)
- 📊 chore: Remove Old Helm Chart by **@hofq** in [#7512](https://github.com/danny-avila/LibreChat/pull/7512)
- 🪖 chore: bump helm app version to v0.7.8 by **@austin-barrington** in [#7524](https://github.com/danny-avila/LibreChat/pull/7524)
- 📜 docs: CHANGELOG for release v0.7.8 by **@github-actions[bot]** in [#7290](https://github.com/danny-avila/Chat/pull/7290)
- 📦 chore: Update API Package Dependencies by **@danny-avila** in [#7359](https://github.com/danny-avila/Chat/pull/7359)
- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7321](https://github.com/danny-avila/Chat/pull/7321)
- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7434](https://github.com/danny-avila/Chat/pull/7434)
- 🛡️ chore: `multer` v2.0.0 for CVE-2025-47935 and CVE-2025-47944 by **@danny-avila** in [#7454](https://github.com/danny-avila/Chat/pull/7454)
- 📂 refactor: Improve `FileAttachment` & File Form Deletion by **@danny-avila** in [#7471](https://github.com/danny-avila/Chat/pull/7471)
- 📊 chore: Remove Old Helm Chart by **@hofq** in [#7512](https://github.com/danny-avila/Chat/pull/7512)
- 🪖 chore: bump helm app version to v0.7.8 by **@austin-barrington** in [#7524](https://github.com/danny-avila/Chat/pull/7524)
@@ -56,38 +56,38 @@ Changes from v0.7.8-rc1 to v0.7.8.
### ✨ New Features
- ✨ feat: Enhance form submission for touch screens by **@berry-13** in [#7198](https://github.com/danny-avila/LibreChat/pull/7198)
- 🔍 feat: Additional Tavily API Tool Parameters by **@glowforge-opensource** in [#7232](https://github.com/danny-avila/LibreChat/pull/7232)
- 🐋 feat: Add python to Dockerfile for increased MCP compatibility by **@technicalpickles** in [#7270](https://github.com/danny-avila/LibreChat/pull/7270)
- ✨ feat: Enhance form submission for touch screens by **@berry-13** in [#7198](https://github.com/danny-avila/Chat/pull/7198)
- 🔍 feat: Additional Tavily API Tool Parameters by **@glowforge-opensource** in [#7232](https://github.com/danny-avila/Chat/pull/7232)
- 🐋 feat: Add python to Dockerfile for increased MCP compatibility by **@technicalpickles** in [#7270](https://github.com/danny-avila/Chat/pull/7270)
### 🔧 Fixes
- 🔧 fix: Google Gemma Support & OpenAI Reasoning Instructions by **@danny-avila** in [#7196](https://github.com/danny-avila/LibreChat/pull/7196)
- 🛠️ fix: Conversation Navigation State by **@danny-avila** in [#7210](https://github.com/danny-avila/LibreChat/pull/7210)
- 🔄 fix: o-Series Model Regex for System Messages by **@danny-avila** in [#7245](https://github.com/danny-avila/LibreChat/pull/7245)
- 🔖 fix: Custom Headers for Initial MCP SSE Connection by **@danny-avila** in [#7246](https://github.com/danny-avila/LibreChat/pull/7246)
- 🛡️ fix: Deep Clone `MCPOptions` for User MCP Connections by **@danny-avila** in [#7247](https://github.com/danny-avila/LibreChat/pull/7247)
- 🔄 fix: URL Param Race Condition and File Draft Persistence by **@danny-avila** in [#7257](https://github.com/danny-avila/LibreChat/pull/7257)
- 🔄 fix: Assistants Endpoint & Minor Issues by **@danny-avila** in [#7274](https://github.com/danny-avila/LibreChat/pull/7274)
- 🔄 fix: Ollama Think Tag Edge Case with Tools by **@danny-avila** in [#7275](https://github.com/danny-avila/LibreChat/pull/7275)
- 🔧 fix: Google Gemma Support & OpenAI Reasoning Instructions by **@danny-avila** in [#7196](https://github.com/danny-avila/Chat/pull/7196)
- 🛠️ fix: Conversation Navigation State by **@danny-avila** in [#7210](https://github.com/danny-avila/Chat/pull/7210)
- 🔄 fix: o-Series Model Regex for System Messages by **@danny-avila** in [#7245](https://github.com/danny-avila/Chat/pull/7245)
- 🔖 fix: Custom Headers for Initial MCP SSE Connection by **@danny-avila** in [#7246](https://github.com/danny-avila/Chat/pull/7246)
- 🛡️ fix: Deep Clone `MCPOptions` for User MCP Connections by **@danny-avila** in [#7247](https://github.com/danny-avila/Chat/pull/7247)
- 🔄 fix: URL Param Race Condition and File Draft Persistence by **@danny-avila** in [#7257](https://github.com/danny-avila/Chat/pull/7257)
- 🔄 fix: Assistants Endpoint & Minor Issues by **@danny-avila** in [#7274](https://github.com/danny-avila/Chat/pull/7274)
- 🔄 fix: Ollama Think Tag Edge Case with Tools by **@danny-avila** in [#7275](https://github.com/danny-avila/Chat/pull/7275)
### ⚙️ Other Changes
- 📜 docs: CHANGELOG for release v0.7.8-rc1 by **@github-actions[bot]** in [#7153](https://github.com/danny-avila/LibreChat/pull/7153)
- 🔄 refactor: Artifact Visibility Management by **@danny-avila** in [#7181](https://github.com/danny-avila/LibreChat/pull/7181)
- 📦 chore: Bump Package Security by **@danny-avila** in [#7183](https://github.com/danny-avila/LibreChat/pull/7183)
- 🌿 refactor: Unmount Fork Popover on Hide for Better Performance by **@danny-avila** in [#7189](https://github.com/danny-avila/LibreChat/pull/7189)
- 🧰 chore: ESLint configuration to enforce Prettier formatting rules by **@mawburn** in [#7186](https://github.com/danny-avila/LibreChat/pull/7186)
- 🎨 style: Improve KaTeX Rendering for LaTeX Equations by **@andresgit** in [#7223](https://github.com/danny-avila/LibreChat/pull/7223)
- 📝 docs: Update `.env.example` Google models by **@marlonka** in [#7254](https://github.com/danny-avila/LibreChat/pull/7254)
- 💬 refactor: MCP Chat Visibility Option, Google Rates, Remove OpenAPI Plugins by **@danny-avila** in [#7286](https://github.com/danny-avila/LibreChat/pull/7286)
- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7214](https://github.com/danny-avila/LibreChat/pull/7214)
- 📜 docs: CHANGELOG for release v0.7.8-rc1 by **@github-actions[bot]** in [#7153](https://github.com/danny-avila/Chat/pull/7153)
- 🔄 refactor: Artifact Visibility Management by **@danny-avila** in [#7181](https://github.com/danny-avila/Chat/pull/7181)
- 📦 chore: Bump Package Security by **@danny-avila** in [#7183](https://github.com/danny-avila/Chat/pull/7183)
- 🌿 refactor: Unmount Fork Popover on Hide for Better Performance by **@danny-avila** in [#7189](https://github.com/danny-avila/Chat/pull/7189)
- 🧰 chore: ESLint configuration to enforce Prettier formatting rules by **@mawburn** in [#7186](https://github.com/danny-avila/Chat/pull/7186)
- 🎨 style: Improve KaTeX Rendering for LaTeX Equations by **@andresgit** in [#7223](https://github.com/danny-avila/Chat/pull/7223)
- 📝 docs: Update `.env.example` Google models by **@marlonka** in [#7254](https://github.com/danny-avila/Chat/pull/7254)
- 💬 refactor: MCP Chat Visibility Option, Google Rates, Remove OpenAPI Plugins by **@danny-avila** in [#7286](https://github.com/danny-avila/Chat/pull/7286)
- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7214](https://github.com/danny-avila/Chat/pull/7214)
[See full release details][release-v0.7.8]
[release-v0.7.8]: https://github.com/danny-avila/LibreChat/releases/tag/v0.7.8
[release-v0.7.8]: https://github.com/danny-avila/Chat/releases/tag/v0.7.8
---
## [v0.7.8-rc1] -
@@ -96,141 +96,141 @@ Changes from v0.7.7 to v0.7.8-rc1.
### ✨ New Features
- 🔍 feat: Mistral OCR API / Upload Files as Text by **@danny-avila** in [#6274](https://github.com/danny-avila/LibreChat/pull/6274)
- 🤖 feat: Support OpenAI Web Search models by **@danny-avila** in [#6313](https://github.com/danny-avila/LibreChat/pull/6313)
- 🔗 feat: Agent Chain (Mixture-of-Agents) by **@danny-avila** in [#6374](https://github.com/danny-avila/LibreChat/pull/6374)
- ⌛ feat: `initTimeout` for Slow Starting MCP Servers by **@perweij** in [#6383](https://github.com/danny-avila/LibreChat/pull/6383)
- 🚀 feat: `S3` Integration for File handling and Image uploads by **@rubentalstra** in [#6142](https://github.com/danny-avila/LibreChat/pull/6142)
- 🔒feat: Enable OpenID Auto-Redirect by **@leondape** in [#6066](https://github.com/danny-avila/LibreChat/pull/6066)
- 🚀 feat: Integrate `Azure Blob Storage` for file handling and image uploads by **@rubentalstra** in [#6153](https://github.com/danny-avila/LibreChat/pull/6153)
- 🚀 feat: Add support for custom `AWS` endpoint in `S3` by **@rubentalstra** in [#6431](https://github.com/danny-avila/LibreChat/pull/6431)
- 🚀 feat: Add support for LDAP STARTTLS in LDAP authentication by **@rubentalstra** in [#6438](https://github.com/danny-avila/LibreChat/pull/6438)
- 🚀 feat: Refactor schema exports and update package version to 0.0.4 by **@rubentalstra** in [#6455](https://github.com/danny-avila/LibreChat/pull/6455)
- 🔼 feat: Add Auto Submit For URL Query Params by **@mjaverto** in [#6440](https://github.com/danny-avila/LibreChat/pull/6440)
- 🛠 feat: Enhance Redis Integration, Rate Limiters & Log Headers by **@danny-avila** in [#6462](https://github.com/danny-avila/LibreChat/pull/6462)
- 💵 feat: Add Automatic Balance Refill by **@rubentalstra** in [#6452](https://github.com/danny-avila/LibreChat/pull/6452)
- 🗣️ feat: add support for gpt-4o-transcribe models by **@berry-13** in [#6483](https://github.com/danny-avila/LibreChat/pull/6483)
- 🎨 feat: UI Refresh for Enhanced UX by **@berry-13** in [#6346](https://github.com/danny-avila/LibreChat/pull/6346)
- 🌍 feat: Add support for Hungarian language localization by **@rubentalstra** in [#6508](https://github.com/danny-avila/LibreChat/pull/6508)
- 🚀 feat: Add Gemini 2.5 Token/Context Values, Increase Max Possible Output to 64k by **@danny-avila** in [#6563](https://github.com/danny-avila/LibreChat/pull/6563)
- 🚀 feat: Enhance MCP Connections For Multi-User Support by **@danny-avila** in [#6610](https://github.com/danny-avila/LibreChat/pull/6610)
- 🚀 feat: Enhance S3 URL Expiry with Refresh; fix: S3 File Deletion by **@danny-avila** in [#6647](https://github.com/danny-avila/LibreChat/pull/6647)
- 🚀 feat: enhance UI components and refactor settings by **@berry-13** in [#6625](https://github.com/danny-avila/LibreChat/pull/6625)
- 💬 feat: move TemporaryChat to the Header by **@berry-13** in [#6646](https://github.com/danny-avila/LibreChat/pull/6646)
- 🚀 feat: Use Model Specs + Specific Endpoints, Limit Providers for Agents by **@danny-avila** in [#6650](https://github.com/danny-avila/LibreChat/pull/6650)
- 🪙 feat: Sync Balance Config on Login by **@danny-avila** in [#6671](https://github.com/danny-avila/LibreChat/pull/6671)
- 🔦 feat: MCP Support for Non-Agent Endpoints by **@danny-avila** in [#6775](https://github.com/danny-avila/LibreChat/pull/6775)
- 🗃️ feat: Code Interpreter File Persistence between Sessions by **@danny-avila** in [#6790](https://github.com/danny-avila/LibreChat/pull/6790)
- 🖥️ feat: Code Interpreter API for Non-Agent Endpoints by **@danny-avila** in [#6803](https://github.com/danny-avila/LibreChat/pull/6803)
- ⚡ feat: Self-hosted Artifacts Static Bundler URL by **@danny-avila** in [#6827](https://github.com/danny-avila/LibreChat/pull/6827)
- 🐳 feat: Add Jemalloc and UV to Docker Builds by **@danny-avila** in [#6836](https://github.com/danny-avila/LibreChat/pull/6836)
- 🤖 feat: GPT-4.1 by **@danny-avila** in [#6880](https://github.com/danny-avila/LibreChat/pull/6880)
- 👋 feat: remove Edge TTS by **@berry-13** in [#6885](https://github.com/danny-avila/LibreChat/pull/6885)
- feat: nav optimization by **@berry-13** in [#5785](https://github.com/danny-avila/LibreChat/pull/5785)
- 🗺️ feat: Add Parameter Location Mapping for OpenAPI actions by **@peeeteeer** in [#6858](https://github.com/danny-avila/LibreChat/pull/6858)
- 🤖 feat: Support `o4-mini` and `o3` Models by **@danny-avila** in [#6928](https://github.com/danny-avila/LibreChat/pull/6928)
- 🎨 feat: OpenAI Image Tools (GPT-Image-1) by **@danny-avila** in [#7079](https://github.com/danny-avila/LibreChat/pull/7079)
- 🗓️ feat: Add Special Variables for Prompts & Agents, Prompt UI Improvements by **@danny-avila** in [#7123](https://github.com/danny-avila/LibreChat/pull/7123)
- 🔍 feat: Mistral OCR API / Upload Files as Text by **@danny-avila** in [#6274](https://github.com/danny-avila/Chat/pull/6274)
- 🤖 feat: Support OpenAI Web Search models by **@danny-avila** in [#6313](https://github.com/danny-avila/Chat/pull/6313)
- 🔗 feat: Agent Chain (Mixture-of-Agents) by **@danny-avila** in [#6374](https://github.com/danny-avila/Chat/pull/6374)
- ⌛ feat: `initTimeout` for Slow Starting MCP Servers by **@perweij** in [#6383](https://github.com/danny-avila/Chat/pull/6383)
- 🚀 feat: `S3` Integration for File handling and Image uploads by **@rubentalstra** in [#6142](https://github.com/danny-avila/Chat/pull/6142)
- 🔒feat: Enable OpenID Auto-Redirect by **@leondape** in [#6066](https://github.com/danny-avila/Chat/pull/6066)
- 🚀 feat: Integrate `Azure Blob Storage` for file handling and image uploads by **@rubentalstra** in [#6153](https://github.com/danny-avila/Chat/pull/6153)
- 🚀 feat: Add support for custom `AWS` endpoint in `S3` by **@rubentalstra** in [#6431](https://github.com/danny-avila/Chat/pull/6431)
- 🚀 feat: Add support for LDAP STARTTLS in LDAP authentication by **@rubentalstra** in [#6438](https://github.com/danny-avila/Chat/pull/6438)
- 🚀 feat: Refactor schema exports and update package version to 0.0.4 by **@rubentalstra** in [#6455](https://github.com/danny-avila/Chat/pull/6455)
- 🔼 feat: Add Auto Submit For URL Query Params by **@mjaverto** in [#6440](https://github.com/danny-avila/Chat/pull/6440)
- 🛠 feat: Enhance Redis Integration, Rate Limiters & Log Headers by **@danny-avila** in [#6462](https://github.com/danny-avila/Chat/pull/6462)
- 💵 feat: Add Automatic Balance Refill by **@rubentalstra** in [#6452](https://github.com/danny-avila/Chat/pull/6452)
- 🗣️ feat: add support for gpt-4o-transcribe models by **@berry-13** in [#6483](https://github.com/danny-avila/Chat/pull/6483)
- 🎨 feat: UI Refresh for Enhanced UX by **@berry-13** in [#6346](https://github.com/danny-avila/Chat/pull/6346)
- 🌍 feat: Add support for Hungarian language localization by **@rubentalstra** in [#6508](https://github.com/danny-avila/Chat/pull/6508)
- 🚀 feat: Add Gemini 2.5 Token/Context Values, Increase Max Possible Output to 64k by **@danny-avila** in [#6563](https://github.com/danny-avila/Chat/pull/6563)
- 🚀 feat: Enhance MCP Connections For Multi-User Support by **@danny-avila** in [#6610](https://github.com/danny-avila/Chat/pull/6610)
- 🚀 feat: Enhance S3 URL Expiry with Refresh; fix: S3 File Deletion by **@danny-avila** in [#6647](https://github.com/danny-avila/Chat/pull/6647)
- 🚀 feat: enhance UI components and refactor settings by **@berry-13** in [#6625](https://github.com/danny-avila/Chat/pull/6625)
- 💬 feat: move TemporaryChat to the Header by **@berry-13** in [#6646](https://github.com/danny-avila/Chat/pull/6646)
- 🚀 feat: Use Model Specs + Specific Endpoints, Limit Providers for Agents by **@danny-avila** in [#6650](https://github.com/danny-avila/Chat/pull/6650)
- 🪙 feat: Sync Balance Config on Login by **@danny-avila** in [#6671](https://github.com/danny-avila/Chat/pull/6671)
- 🔦 feat: MCP Support for Non-Agent Endpoints by **@danny-avila** in [#6775](https://github.com/danny-avila/Chat/pull/6775)
- 🗃️ feat: Code Interpreter File Persistence between Sessions by **@danny-avila** in [#6790](https://github.com/danny-avila/Chat/pull/6790)
- 🖥️ feat: Code Interpreter API for Non-Agent Endpoints by **@danny-avila** in [#6803](https://github.com/danny-avila/Chat/pull/6803)
- ⚡ feat: Self-hosted Artifacts Static Bundler URL by **@danny-avila** in [#6827](https://github.com/danny-avila/Chat/pull/6827)
- 🐳 feat: Add Jemalloc and UV to Docker Builds by **@danny-avila** in [#6836](https://github.com/danny-avila/Chat/pull/6836)
- 🤖 feat: GPT-4.1 by **@danny-avila** in [#6880](https://github.com/danny-avila/Chat/pull/6880)
- 👋 feat: remove Edge TTS by **@berry-13** in [#6885](https://github.com/danny-avila/Chat/pull/6885)
- feat: nav optimization by **@berry-13** in [#5785](https://github.com/danny-avila/Chat/pull/5785)
- 🗺️ feat: Add Parameter Location Mapping for OpenAPI actions by **@peeeteeer** in [#6858](https://github.com/danny-avila/Chat/pull/6858)
- 🤖 feat: Support `o4-mini` and `o3` Models by **@danny-avila** in [#6928](https://github.com/danny-avila/Chat/pull/6928)
- 🎨 feat: OpenAI Image Tools (GPT-Image-1) by **@danny-avila** in [#7079](https://github.com/danny-avila/Chat/pull/7079)
- 🗓️ feat: Add Special Variables for Prompts & Agents, Prompt UI Improvements by **@danny-avila** in [#7123](https://github.com/danny-avila/Chat/pull/7123)
### 🌍 Internationalization
- 🌍 i18n: Add Thai Language Support and Update Translations by **@rubentalstra** in [#6219](https://github.com/danny-avila/LibreChat/pull/6219)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6220](https://github.com/danny-avila/LibreChat/pull/6220)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6240](https://github.com/danny-avila/LibreChat/pull/6240)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6241](https://github.com/danny-avila/LibreChat/pull/6241)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6277](https://github.com/danny-avila/LibreChat/pull/6277)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6414](https://github.com/danny-avila/LibreChat/pull/6414)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6505](https://github.com/danny-avila/LibreChat/pull/6505)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6530](https://github.com/danny-avila/LibreChat/pull/6530)
- 🌍 i18n: Add Persian Localization Support by **@rubentalstra** in [#6669](https://github.com/danny-avila/LibreChat/pull/6669)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6667](https://github.com/danny-avila/LibreChat/pull/6667)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7126](https://github.com/danny-avila/LibreChat/pull/7126)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7148](https://github.com/danny-avila/LibreChat/pull/7148)
- 🌍 i18n: Add Thai Language Support and Update Translations by **@rubentalstra** in [#6219](https://github.com/danny-avila/Chat/pull/6219)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6220](https://github.com/danny-avila/Chat/pull/6220)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6240](https://github.com/danny-avila/Chat/pull/6240)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6241](https://github.com/danny-avila/Chat/pull/6241)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6277](https://github.com/danny-avila/Chat/pull/6277)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6414](https://github.com/danny-avila/Chat/pull/6414)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6505](https://github.com/danny-avila/Chat/pull/6505)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6530](https://github.com/danny-avila/Chat/pull/6530)
- 🌍 i18n: Add Persian Localization Support by **@rubentalstra** in [#6669](https://github.com/danny-avila/Chat/pull/6669)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6667](https://github.com/danny-avila/Chat/pull/6667)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7126](https://github.com/danny-avila/Chat/pull/7126)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7148](https://github.com/danny-avila/Chat/pull/7148)
### 👐 Accessibility
- 🎨 a11y: Update Model Spec Description Text by **@berry-13** in [#6294](https://github.com/danny-avila/LibreChat/pull/6294)
- 🗑️ a11y: Add Accessible Name to Button for File Attachment Removal by **@kangabell** in [#6709](https://github.com/danny-avila/LibreChat/pull/6709)
- ⌨️ a11y: enhance accessibility & visual consistency by **@berry-13** in [#6866](https://github.com/danny-avila/LibreChat/pull/6866)
- 🙌 a11y: Searchbar/Conversations List Focus by **@danny-avila** in [#7096](https://github.com/danny-avila/LibreChat/pull/7096)
- 👐 a11y: Improve Fork and SplitText Accessibility by **@danny-avila** in [#7147](https://github.com/danny-avila/LibreChat/pull/7147)
- 🎨 a11y: Update Model Spec Description Text by **@berry-13** in [#6294](https://github.com/danny-avila/Chat/pull/6294)
- 🗑️ a11y: Add Accessible Name to Button for File Attachment Removal by **@kangabell** in [#6709](https://github.com/danny-avila/Chat/pull/6709)
- ⌨️ a11y: enhance accessibility & visual consistency by **@berry-13** in [#6866](https://github.com/danny-avila/Chat/pull/6866)
- 🙌 a11y: Searchbar/Conversations List Focus by **@danny-avila** in [#7096](https://github.com/danny-avila/Chat/pull/7096)
- 👐 a11y: Improve Fork and SplitText Accessibility by **@danny-avila** in [#7147](https://github.com/danny-avila/Chat/pull/7147)
### 🔧 Fixes
- 🐛 fix: Avatar Type Definitions in Agent/Assistant Schemas by **@danny-avila** in [#6235](https://github.com/danny-avila/LibreChat/pull/6235)
- 🔧 fix: MeiliSearch Field Error and Patch Incorrect Import by #6210 by **@rubentalstra** in [#6245](https://github.com/danny-avila/LibreChat/pull/6245)
- 🔏 fix: Enhance Two-Factor Authentication by **@rubentalstra** in [#6247](https://github.com/danny-avila/LibreChat/pull/6247)
- 🐛 fix: Await saveMessage in abortMiddleware to ensure proper execution by **@sh4shii** in [#6248](https://github.com/danny-avila/LibreChat/pull/6248)
- 🔧 fix: Axios Proxy Usage And Bump `mongoose` by **@danny-avila** in [#6298](https://github.com/danny-avila/LibreChat/pull/6298)
- 🔧 fix: comment out MCP servers to resolve service run issues by **@KunalScriptz** in [#6316](https://github.com/danny-avila/LibreChat/pull/6316)
- 🔧 fix: Update Token Calculations and Mapping, MCP `env` Initialization by **@danny-avila** in [#6406](https://github.com/danny-avila/LibreChat/pull/6406)
- 🐞 fix: Agent "Resend" Message Attachments + Source Icon Styling by **@danny-avila** in [#6408](https://github.com/danny-avila/LibreChat/pull/6408)
- 🐛 fix: Prevent Crash on Duplicate Message ID by **@Odrec** in [#6392](https://github.com/danny-avila/LibreChat/pull/6392)
- 🔐 fix: Invalid Key Length in 2FA Encryption by **@rubentalstra** in [#6432](https://github.com/danny-avila/LibreChat/pull/6432)
- 🏗️ fix: Fix Agents Token Spend Race Conditions, Expand Test Coverage by **@danny-avila** in [#6480](https://github.com/danny-avila/LibreChat/pull/6480)
- 🔃 fix: Draft Clearing, Claude Titles, Remove Default Vision Max Tokens by **@danny-avila** in [#6501](https://github.com/danny-avila/LibreChat/pull/6501)
- 🔧 fix: Update username reference to use user.name in greeting display by **@rubentalstra** in [#6534](https://github.com/danny-avila/LibreChat/pull/6534)
- 🔧 fix: S3 Download Stream with Key Extraction and Blob Storage Encoding for Vision by **@danny-avila** in [#6557](https://github.com/danny-avila/LibreChat/pull/6557)
- 🔧 fix: Mistral type strictness for `usage` & update token values/windows by **@danny-avila** in [#6562](https://github.com/danny-avila/LibreChat/pull/6562)
- 🔧 fix: Consolidate Text Parsing and TTS Edge Initialization by **@danny-avila** in [#6582](https://github.com/danny-avila/LibreChat/pull/6582)
- 🔧 fix: Ensure continuation in image processing on base64 encoding from Blob Storage by **@danny-avila** in [#6619](https://github.com/danny-avila/LibreChat/pull/6619)
- ✉️ fix: Fallback For User Name In Email Templates by **@danny-avila** in [#6620](https://github.com/danny-avila/LibreChat/pull/6620)
- 🔧 fix: Azure Blob Integration and File Source References by **@rubentalstra** in [#6575](https://github.com/danny-avila/LibreChat/pull/6575)
- 🐛 fix: Safeguard against undefined addedEndpoints by **@wipash** in [#6654](https://github.com/danny-avila/LibreChat/pull/6654)
- 🤖 fix: Gemini 2.5 Vision Support by **@danny-avila** in [#6663](https://github.com/danny-avila/LibreChat/pull/6663)
- 🔄 fix: Avatar & Error Handling Enhancements by **@danny-avila** in [#6687](https://github.com/danny-avila/LibreChat/pull/6687)
- 🔧 fix: Chat Middleware, Zod Conversion, Auto-Save and S3 URL Refresh by **@danny-avila** in [#6720](https://github.com/danny-avila/LibreChat/pull/6720)
- 🔧 fix: Agent Capability Checks & DocumentDB Compatibility for Agent Resource Removal by **@danny-avila** in [#6726](https://github.com/danny-avila/LibreChat/pull/6726)
- 🔄 fix: Improve audio MIME type detection and handling by **@berry-13** in [#6707](https://github.com/danny-avila/LibreChat/pull/6707)
- 🪺 fix: Update Role Handling due to New Schema Shape by **@danny-avila** in [#6774](https://github.com/danny-avila/LibreChat/pull/6774)
- 🗨️ fix: Show ModelSpec Greeting by **@berry-13** in [#6770](https://github.com/danny-avila/LibreChat/pull/6770)
- 🔧 fix: Keyv and Proxy Issues, and More Memory Optimizations by **@danny-avila** in [#6867](https://github.com/danny-avila/LibreChat/pull/6867)
- ✨ fix: Implement dynamic text sizing for greeting and name display by **@berry-13** in [#6833](https://github.com/danny-avila/LibreChat/pull/6833)
- 📝 fix: Mistral OCR Image Support and Azure Agent Titles by **@danny-avila** in [#6901](https://github.com/danny-avila/LibreChat/pull/6901)
- 📢 fix: Invalid `engineTTS` and Conversation State on Navigation by **@berry-13** in [#6904](https://github.com/danny-avila/LibreChat/pull/6904)
- 🛠️ fix: Improve Accessibility and Display of Conversation Menu by **@danny-avila** in [#6913](https://github.com/danny-avila/LibreChat/pull/6913)
- 🔧 fix: Agent Resource Form, Convo Menu Style, Ensure Draft Clears on Submission by **@danny-avila** in [#6925](https://github.com/danny-avila/LibreChat/pull/6925)
- 🔀 fix: MCP Improvements, Auto-Save Drafts, Artifact Markup by **@danny-avila** in [#7040](https://github.com/danny-avila/LibreChat/pull/7040)
- 🐋 fix: Improve Deepseek Compatbility by **@danny-avila** in [#7132](https://github.com/danny-avila/LibreChat/pull/7132)
- 🐙 fix: Add Redis Ping Interval to Prevent Connection Drops by **@peeeteeer** in [#7127](https://github.com/danny-avila/LibreChat/pull/7127)
- 🐛 fix: Avatar Type Definitions in Agent/Assistant Schemas by **@danny-avila** in [#6235](https://github.com/danny-avila/Chat/pull/6235)
- 🔧 fix: MeiliSearch Field Error and Patch Incorrect Import by #6210 by **@rubentalstra** in [#6245](https://github.com/danny-avila/Chat/pull/6245)
- 🔏 fix: Enhance Two-Factor Authentication by **@rubentalstra** in [#6247](https://github.com/danny-avila/Chat/pull/6247)
- 🐛 fix: Await saveMessage in abortMiddleware to ensure proper execution by **@sh4shii** in [#6248](https://github.com/danny-avila/Chat/pull/6248)
- 🔧 fix: Axios Proxy Usage And Bump `mongoose` by **@danny-avila** in [#6298](https://github.com/danny-avila/Chat/pull/6298)
- 🔧 fix: comment out MCP servers to resolve service run issues by **@KunalScriptz** in [#6316](https://github.com/danny-avila/Chat/pull/6316)
- 🔧 fix: Update Token Calculations and Mapping, MCP `env` Initialization by **@danny-avila** in [#6406](https://github.com/danny-avila/Chat/pull/6406)
- 🐞 fix: Agent "Resend" Message Attachments + Source Icon Styling by **@danny-avila** in [#6408](https://github.com/danny-avila/Chat/pull/6408)
- 🐛 fix: Prevent Crash on Duplicate Message ID by **@Odrec** in [#6392](https://github.com/danny-avila/Chat/pull/6392)
- 🔐 fix: Invalid Key Length in 2FA Encryption by **@rubentalstra** in [#6432](https://github.com/danny-avila/Chat/pull/6432)
- 🏗️ fix: Fix Agents Token Spend Race Conditions, Expand Test Coverage by **@danny-avila** in [#6480](https://github.com/danny-avila/Chat/pull/6480)
- 🔃 fix: Draft Clearing, Claude Titles, Remove Default Vision Max Tokens by **@danny-avila** in [#6501](https://github.com/danny-avila/Chat/pull/6501)
- 🔧 fix: Update username reference to use user.name in greeting display by **@rubentalstra** in [#6534](https://github.com/danny-avila/Chat/pull/6534)
- 🔧 fix: S3 Download Stream with Key Extraction and Blob Storage Encoding for Vision by **@danny-avila** in [#6557](https://github.com/danny-avila/Chat/pull/6557)
- 🔧 fix: Mistral type strictness for `usage` & update token values/windows by **@danny-avila** in [#6562](https://github.com/danny-avila/Chat/pull/6562)
- 🔧 fix: Consolidate Text Parsing and TTS Edge Initialization by **@danny-avila** in [#6582](https://github.com/danny-avila/Chat/pull/6582)
- 🔧 fix: Ensure continuation in image processing on base64 encoding from Blob Storage by **@danny-avila** in [#6619](https://github.com/danny-avila/Chat/pull/6619)
- ✉️ fix: Fallback For User Name In Email Templates by **@danny-avila** in [#6620](https://github.com/danny-avila/Chat/pull/6620)
- 🔧 fix: Azure Blob Integration and File Source References by **@rubentalstra** in [#6575](https://github.com/danny-avila/Chat/pull/6575)
- 🐛 fix: Safeguard against undefined addedEndpoints by **@wipash** in [#6654](https://github.com/danny-avila/Chat/pull/6654)
- 🤖 fix: Gemini 2.5 Vision Support by **@danny-avila** in [#6663](https://github.com/danny-avila/Chat/pull/6663)
- 🔄 fix: Avatar & Error Handling Enhancements by **@danny-avila** in [#6687](https://github.com/danny-avila/Chat/pull/6687)
- 🔧 fix: Chat Middleware, Zod Conversion, Auto-Save and S3 URL Refresh by **@danny-avila** in [#6720](https://github.com/danny-avila/Chat/pull/6720)
- 🔧 fix: Agent Capability Checks & DocumentDB Compatibility for Agent Resource Removal by **@danny-avila** in [#6726](https://github.com/danny-avila/Chat/pull/6726)
- 🔄 fix: Improve audio MIME type detection and handling by **@berry-13** in [#6707](https://github.com/danny-avila/Chat/pull/6707)
- 🪺 fix: Update Role Handling due to New Schema Shape by **@danny-avila** in [#6774](https://github.com/danny-avila/Chat/pull/6774)
- 🗨️ fix: Show ModelSpec Greeting by **@berry-13** in [#6770](https://github.com/danny-avila/Chat/pull/6770)
- 🔧 fix: Keyv and Proxy Issues, and More Memory Optimizations by **@danny-avila** in [#6867](https://github.com/danny-avila/Chat/pull/6867)
- ✨ fix: Implement dynamic text sizing for greeting and name display by **@berry-13** in [#6833](https://github.com/danny-avila/Chat/pull/6833)
- 📝 fix: Mistral OCR Image Support and Azure Agent Titles by **@danny-avila** in [#6901](https://github.com/danny-avila/Chat/pull/6901)
- 📢 fix: Invalid `engineTTS` and Conversation State on Navigation by **@berry-13** in [#6904](https://github.com/danny-avila/Chat/pull/6904)
- 🛠️ fix: Improve Accessibility and Display of Conversation Menu by **@danny-avila** in [#6913](https://github.com/danny-avila/Chat/pull/6913)
- 🔧 fix: Agent Resource Form, Convo Menu Style, Ensure Draft Clears on Submission by **@danny-avila** in [#6925](https://github.com/danny-avila/Chat/pull/6925)
- 🔀 fix: MCP Improvements, Auto-Save Drafts, Artifact Markup by **@danny-avila** in [#7040](https://github.com/danny-avila/Chat/pull/7040)
- 🐋 fix: Improve Deepseek Compatbility by **@danny-avila** in [#7132](https://github.com/danny-avila/Chat/pull/7132)
- 🐙 fix: Add Redis Ping Interval to Prevent Connection Drops by **@peeeteeer** in [#7127](https://github.com/danny-avila/Chat/pull/7127)
### ⚙️ Other Changes
- 📦 refactor: Move DB Models to `@librechat/data-schemas` by **@rubentalstra** in [#6210](https://github.com/danny-avila/LibreChat/pull/6210)
- 📦 chore: Patch `axios` to address CVE-2025-27152 by **@danny-avila** in [#6222](https://github.com/danny-avila/LibreChat/pull/6222)
- ⚠️ refactor: Use Error Content Part Instead Of Throwing Error for Agents by **@danny-avila** in [#6262](https://github.com/danny-avila/LibreChat/pull/6262)
- 🏃‍♂️ refactor: Improve Agent Run Context & Misc. Changes by **@danny-avila** in [#6448](https://github.com/danny-avila/LibreChat/pull/6448)
- 📝 docs: librechat.example.yaml by **@ineiti** in [#6442](https://github.com/danny-avila/LibreChat/pull/6442)
- 🏃‍♂️ refactor: More Agent Context Improvements during Run by **@danny-avila** in [#6477](https://github.com/danny-avila/LibreChat/pull/6477)
- 🔃 refactor: Allow streaming for `o1` models by **@danny-avila** in [#6509](https://github.com/danny-avila/LibreChat/pull/6509)
- 🔧 chore: `Vite` Plugin Upgrades & Config Optimizations by **@rubentalstra** in [#6547](https://github.com/danny-avila/LibreChat/pull/6547)
- 🔧 refactor: Consolidate Logging, Model Selection & Actions Optimizations, Minor Fixes by **@danny-avila** in [#6553](https://github.com/danny-avila/LibreChat/pull/6553)
- 🎨 style: Address Minor UI Refresh Issues by **@berry-13** in [#6552](https://github.com/danny-avila/LibreChat/pull/6552)
- 🔧 refactor: Enhance Model & Endpoint Configurations with Global Indicators 🌍 by **@berry-13** in [#6578](https://github.com/danny-avila/LibreChat/pull/6578)
- 💬 style: Chat UI, Greeting, and Message adjustments by **@berry-13** in [#6612](https://github.com/danny-avila/LibreChat/pull/6612)
- ⚡ refactor: DocumentDB Compatibility for Balance Updates by **@danny-avila** in [#6673](https://github.com/danny-avila/LibreChat/pull/6673)
- 🧹 chore: Update ESLint rules for React hooks by **@rubentalstra** in [#6685](https://github.com/danny-avila/LibreChat/pull/6685)
- 🪙 chore: Update Gemini Pricing by **@RedwindA** in [#6731](https://github.com/danny-avila/LibreChat/pull/6731)
- 🪺 refactor: Nest Permission fields for Roles by **@rubentalstra** in [#6487](https://github.com/danny-avila/LibreChat/pull/6487)
- 📦 chore: Update `caniuse-lite` dependency to version 1.0.30001706 by **@rubentalstra** in [#6482](https://github.com/danny-avila/LibreChat/pull/6482)
- ⚙️ refactor: OAuth Flow Signal, Type Safety, Tool Progress & Updated Packages by **@danny-avila** in [#6752](https://github.com/danny-avila/LibreChat/pull/6752)
- 📦 chore: bump vite from 6.2.3 to 6.2.5 by **@dependabot[bot]** in [#6745](https://github.com/danny-avila/LibreChat/pull/6745)
- 💾 chore: Enhance Local Storage Handling and Update MCP SDK by **@danny-avila** in [#6809](https://github.com/danny-avila/LibreChat/pull/6809)
- 🤖 refactor: Improve Agents Memory Usage, Bump Keyv, Grok 3 by **@danny-avila** in [#6850](https://github.com/danny-avila/LibreChat/pull/6850)
- 💾 refactor: Enhance Memory In Image Encodings & Client Disposal by **@danny-avila** in [#6852](https://github.com/danny-avila/LibreChat/pull/6852)
- 🔁 refactor: Token Event Handler and Standardize `maxTokens` Key by **@danny-avila** in [#6886](https://github.com/danny-avila/LibreChat/pull/6886)
- 🔍 refactor: Search & Message Retrieval by **@berry-13** in [#6903](https://github.com/danny-avila/LibreChat/pull/6903)
- 🎨 style: standardize dropdown styling & fix z-Index layering by **@berry-13** in [#6939](https://github.com/danny-avila/LibreChat/pull/6939)
- 📙 docs: CONTRIBUTING.md by **@dblock** in [#6831](https://github.com/danny-avila/LibreChat/pull/6831)
- 🧭 refactor: Modernize Nav/Header by **@danny-avila** in [#7094](https://github.com/danny-avila/LibreChat/pull/7094)
- 🪶 refactor: Chat Input Focus for Conversation Navigations & ChatForm Optimizations by **@danny-avila** in [#7100](https://github.com/danny-avila/LibreChat/pull/7100)
- 🔃 refactor: Streamline Navigation, Message Loading UX by **@danny-avila** in [#7118](https://github.com/danny-avila/LibreChat/pull/7118)
- 📜 docs: Unreleased changelog by **@github-actions[bot]** in [#6265](https://github.com/danny-avila/LibreChat/pull/6265)
- 📦 refactor: Move DB Models to `@hanzochat/data-schemas` by **@rubentalstra** in [#6210](https://github.com/danny-avila/Chat/pull/6210)
- 📦 chore: Patch `axios` to address CVE-2025-27152 by **@danny-avila** in [#6222](https://github.com/danny-avila/Chat/pull/6222)
- ⚠️ refactor: Use Error Content Part Instead Of Throwing Error for Agents by **@danny-avila** in [#6262](https://github.com/danny-avila/Chat/pull/6262)
- 🏃‍♂️ refactor: Improve Agent Run Context & Misc. Changes by **@danny-avila** in [#6448](https://github.com/danny-avila/Chat/pull/6448)
- 📝 docs: chat.example.yaml by **@ineiti** in [#6442](https://github.com/danny-avila/Chat/pull/6442)
- 🏃‍♂️ refactor: More Agent Context Improvements during Run by **@danny-avila** in [#6477](https://github.com/danny-avila/Chat/pull/6477)
- 🔃 refactor: Allow streaming for `o1` models by **@danny-avila** in [#6509](https://github.com/danny-avila/Chat/pull/6509)
- 🔧 chore: `Vite` Plugin Upgrades & Config Optimizations by **@rubentalstra** in [#6547](https://github.com/danny-avila/Chat/pull/6547)
- 🔧 refactor: Consolidate Logging, Model Selection & Actions Optimizations, Minor Fixes by **@danny-avila** in [#6553](https://github.com/danny-avila/Chat/pull/6553)
- 🎨 style: Address Minor UI Refresh Issues by **@berry-13** in [#6552](https://github.com/danny-avila/Chat/pull/6552)
- 🔧 refactor: Enhance Model & Endpoint Configurations with Global Indicators 🌍 by **@berry-13** in [#6578](https://github.com/danny-avila/Chat/pull/6578)
- 💬 style: Chat UI, Greeting, and Message adjustments by **@berry-13** in [#6612](https://github.com/danny-avila/Chat/pull/6612)
- ⚡ refactor: DocumentDB Compatibility for Balance Updates by **@danny-avila** in [#6673](https://github.com/danny-avila/Chat/pull/6673)
- 🧹 chore: Update ESLint rules for React hooks by **@rubentalstra** in [#6685](https://github.com/danny-avila/Chat/pull/6685)
- 🪙 chore: Update Gemini Pricing by **@RedwindA** in [#6731](https://github.com/danny-avila/Chat/pull/6731)
- 🪺 refactor: Nest Permission fields for Roles by **@rubentalstra** in [#6487](https://github.com/danny-avila/Chat/pull/6487)
- 📦 chore: Update `caniuse-lite` dependency to version 1.0.30001706 by **@rubentalstra** in [#6482](https://github.com/danny-avila/Chat/pull/6482)
- ⚙️ refactor: OAuth Flow Signal, Type Safety, Tool Progress & Updated Packages by **@danny-avila** in [#6752](https://github.com/danny-avila/Chat/pull/6752)
- 📦 chore: bump vite from 6.2.3 to 6.2.5 by **@dependabot[bot]** in [#6745](https://github.com/danny-avila/Chat/pull/6745)
- 💾 chore: Enhance Local Storage Handling and Update MCP SDK by **@danny-avila** in [#6809](https://github.com/danny-avila/Chat/pull/6809)
- 🤖 refactor: Improve Agents Memory Usage, Bump Keyv, Grok 3 by **@danny-avila** in [#6850](https://github.com/danny-avila/Chat/pull/6850)
- 💾 refactor: Enhance Memory In Image Encodings & Client Disposal by **@danny-avila** in [#6852](https://github.com/danny-avila/Chat/pull/6852)
- 🔁 refactor: Token Event Handler and Standardize `maxTokens` Key by **@danny-avila** in [#6886](https://github.com/danny-avila/Chat/pull/6886)
- 🔍 refactor: Search & Message Retrieval by **@berry-13** in [#6903](https://github.com/danny-avila/Chat/pull/6903)
- 🎨 style: standardize dropdown styling & fix z-Index layering by **@berry-13** in [#6939](https://github.com/danny-avila/Chat/pull/6939)
- 📙 docs: CONTRIBUTING.md by **@dblock** in [#6831](https://github.com/danny-avila/Chat/pull/6831)
- 🧭 refactor: Modernize Nav/Header by **@danny-avila** in [#7094](https://github.com/danny-avila/Chat/pull/7094)
- 🪶 refactor: Chat Input Focus for Conversation Navigations & ChatForm Optimizations by **@danny-avila** in [#7100](https://github.com/danny-avila/Chat/pull/7100)
- 🔃 refactor: Streamline Navigation, Message Loading UX by **@danny-avila** in [#7118](https://github.com/danny-avila/Chat/pull/7118)
- 📜 docs: Unreleased changelog by **@github-actions[bot]** in [#6265](https://github.com/danny-avila/Chat/pull/6265)
[See full release details][release-v0.7.8-rc1]
[release-v0.7.8-rc1]: https://github.com/danny-avila/LibreChat/releases/tag/v0.7.8-rc1
[release-v0.7.8-rc1]: https://github.com/danny-avila/Chat/releases/tag/v0.7.8-rc1
---
+13 -2
View File
@@ -1,7 +1,9 @@
# v0.8.3-rc1
# Base node image
FROM node:20-alpine AS node
# Base node image — Hanzo Node 24 (Alpine) base: node:sqlite (DatabaseSync) is
# built in and native better-sqlite3 compiles (build-base + python3 + g++ baked
# in), so the CHAT_STORE_SQLITE document store runs. Node 20 lacked node:sqlite.
FROM ghcr.io/hanzoai/nodejs:v24.18.0 AS node
# Install jemalloc
RUN apk add --no-cache jemalloc
@@ -42,6 +44,15 @@ RUN \
COPY --chown=node:node . .
# Bake the Umami analytics website id into the Vite build so the index.html
# %VITE_ANALYTICS_SITE_ID% placeholder is substituted at build time. Vite's HTML
# env replacement (loadEnv, envPrefix includes VITE_) picks this up from process.env.
# Without it the tracker POSTs a literal "%VITE_ANALYTICS_SITE_ID%" as the website
# id and analytics.hanzo.ai/api/send answers 400. Public site identifier (it appears
# in the served HTML), safe to default here; override with --build-arg if needed.
ARG VITE_ANALYTICS_SITE_ID=2f72b944-f1f8-4d2d-8f6c-26063bde0d1a
ENV VITE_ANALYTICS_SITE_ID=$VITE_ANALYTICS_SITE_ID
RUN \
# React client build with configurable memory
NODE_OPTIONS="--max-old-space-size=${NODE_MAX_OLD_SPACE_SIZE}" pnpm run frontend; \
+1 -1
View File
@@ -5,7 +5,7 @@
ARG NODE_MAX_OLD_SPACE_SIZE=6144
# Base for all builds
FROM node:20-alpine AS base-min
FROM ghcr.io/hanzoai/nodejs:v24.18.0 AS base-min
# Install jemalloc
RUN apk add --no-cache jemalloc
# Set environment variable to use jemalloc
+1 -1
View File
@@ -6,7 +6,7 @@
# Build: docker build -f Dockerfile.static -t ghcr.io/hanzoai/chat:latest .
# Run: docker run -p 3080:3080 ghcr.io/hanzoai/chat:latest
FROM node:22-alpine
FROM ghcr.io/hanzoai/nodejs:v24.18.0
RUN npm install -g serve@14
+219 -13
View File
@@ -4,7 +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/*`)
**Upstream**: Chat (MIT) — internal package names kept (`@hanzochat/*`)
**Package**: `@hanzochat/chat`
**Runtime**: Node.js 20 (Alpine)
@@ -52,7 +52,7 @@ client/ # React frontend (Vite)
src/routes/ # Client-side routing
src/store/ # State management
packages/
data-provider/ # Shared data layer (librechat-data-provider)
data-provider/ # Shared data layer (@hanzochat/data-provider)
data-schemas/ # Validation schemas
api/ # API client package (@hanzochat/api)
client/ # Shared client components
@@ -62,7 +62,7 @@ packages/
## Configuration
- `librechat.yaml` (or ConfigMap `chat-config` -> `/app/librechat.yaml`)
- `chat.yaml` (or ConfigMap `chat-config` -> `/app/chat.yaml`)
- `hanzo-chat.example.yaml` - Hanzo-specific example config
- `.env` for secrets
@@ -90,20 +90,226 @@ CREDS_KEY= CREDS_IV= # Credential encryption
- `uv` bundled for MCP server support
- `dompurify` must be in `client/package.json` (externalized by bundler)
## Guest Chat (anonymous preview)
Off by default (`ALLOW_GUEST_CHAT=false`). When enabled, the landing IS the chat
composer (ChatGPT-style): an unauthenticated visitor renders the real chat view —
composer, starter cards, model picker — WITHOUT logging in, scoped to the free
Zen model (`GUEST_MODEL`, default `zen3-nano`) via the `Hanzo` custom endpoint
(`api.hanzo.ai`). Prod uses `GUEST_MESSAGE_MAX=2`. Exhausting the quota returns
`402 {type:'GUEST_LIMIT'}` and the client opens the existing OpenID/hanzo.id login.
Client render path (guest === chat, not a marketing gate):
- `AuthContext` auto-acquires a guest token when `startupConfig.allowGuestChat`
is true (`silentRefresh` fallback + a dedicated effect closing the config race);
sets `isGuest=true`, `isAuthenticated=false`. `useAuthRedirect` keeps guests on
the chat surface (only truly anonymous, non-guest, non-guest-enabled users go to
`/login`). `Root` shows the chat shell for `isAuthenticated || isGuest`.
- `ChatRoute` renders `ChatView` for `canChat = isAuthenticated || isGuest`; the
`/v1/chat/models` + `/v1/chat/endpoints` queries run for guests (both routes use
`requireGuestOrJwtAuth` and return the guest-scoped single-model config), and the
roles gate treats a guest as loaded (guests have no agent access). Files:
`client/src/routes/{ChatRoute,useAuthRedirect}.tsx`, `hooks/useGuestAuth.ts`,
`hooks/AuthContext.tsx`, `components/Auth/GuestLimitDialog.tsx`.
Security model (fail-closed, server-enforced):
- `POST /v1/chat/auth/guest` issues a short-lived guest JWT (`{guest:true}`,
per-token random id) signed with `JWT_SECRET`. Rate-limited per IP
(`guestTokenLimiter`, `GUEST_TOKEN_MAX`/`GUEST_TOKEN_WINDOW`) so tokens can't be
spam-minted.
- `requireGuestOrJwtAuth` (chat-completion route ONLY) accepts guest tokens;
the standard `jwt` strategy rejects them everywhere else (no DB user), so
every other route stays closed. `enforceGuestScope` pins endpoint+model and
strips agents/tools/files/spec/preset. Guests always use the shared, capped
`HANZO_API_KEY` (per-user `hk-` billing is skipped for `guest` principals).
- `guestMessageLimiter` enforces the quota against the REAL client IP
(`utils/guestClientIp` → Cloudflare `CF-Connecting-IP`, falls back to `req.ip`),
NOT the token — clearing cookies / incognito / minting a fresh token does NOT
reset it. Backed by the shared Redis `limiterCache` so it holds across replicas.
`USE_REDIS=true` is MANDATORY (a memory store would let a visitor round-robin
pods to multiply their quota).
- Key files: `api/server/services/guestConfig.js`,
`api/server/controllers/auth/GuestController.js`,
`api/server/middleware/{requireGuestOrJwtAuth,enforceGuestScope}.js`,
`api/server/middleware/limiters/{guestLimiters,guestMessageLimiter}.js`,
`api/server/utils/guestClientIp.js`,
router wiring in `api/server/routes/agents/index.js`.
- Env: `ALLOW_GUEST_CHAT`, `GUEST_MESSAGE_MAX`, `GUEST_ENDPOINT`, `GUEST_MODEL`,
`GUEST_TOKEN_EXPIRY`, `GUEST_TOKEN_MAX`, `GUEST_TOKEN_WINDOW`. Requires
`HANZO_API_KEY` (the free publishable gateway key) and `USE_REDIS` for the
shared per-IP quota across replicas.
## Cloud Agents (canonical /v1/agents)
Chat can RUN a user's canonical Hanzo Cloud agents (cloud `/v1/agents`, the ONE
production agent registry) from the thread — alongside the Chat-legacy local
agent builder, which is untouched.
- Two surfaces, ONE run path: the `/agent <name> [prompt]` slash command and the
@mention picker (cloud agents appear as a `cloudAgent` type). Both funnel
through `useRunCloudAgent``POST /v1/chat/agents/cloud/:name/run`. The @mention /
`/agent` picker arms `/agent <name> ` in the composer; submit is intercepted in
`ChatForm` (`parseAgentCommand`) and dispatched to the run path.
- Server proxy + auth (token never reaches the browser): the chat backend reads
the user's hanzo.id token from the server-side session
(`req.session.openidTokens.idToken`, then `accessToken`, then the httpOnly
cookies) and forwards it as `Authorization: Bearer` to cloud. Cloud's
`SanitizeIdentity` (HIP-0026) validates it and pins `X-Org-Id` from the `owner`
claim, so a user only ever reaches their OWN org's agents — chat never asserts
an org. `requireJwtAuth` gates the proxy (guests rejected); missing token →
honest 401, never a service-token fallback (fail-secure). Agent name is
validated against cloud's handle grammar (traversal/SSRF guard); it is NOT an
open proxy (three fixed endpoints). Principal guard: the on-behalf-of decision
keys off the VALIDATED principal (`req.user.provider==='openid'`), not the
mutable `token_provider` refresh-strategy cookie, so a local user (who never
carries a hanzo.id token) can't run under a stale OpenID session. EVERY
forwarded token — id_token preferred, access_token fallback, from session or
the httpOnly cookie — must pass `isForwardableToken`: a decodable JWT whose
`sub` EQUALS `req.user.openidId` (binding MANDATORY — absent `openidId`/`sub`
or a mismatch ⇒ no forward, no fail-open) and is unexpired. Decode-only is
sound because cloud does the authoritative JWKS validation over the SAME claims,
so it runs as exactly that `sub`; the gate only ever removes a token. An
unbindable/expired/foreign token yields an honest 401, never a fabricated or
wrong-principal run.
- id_token persistence is DECOUPLED from the refresh strategy: OIDC login ALWAYS
persists the on-behalf-of BEARER (id_token + access_token) to
`req.session.openidTokens` (server-side only), regardless of
`OPENID_REUSE_TOKENS`. It does NOT persist the OIDC refresh credential in the
decoupled default — the session refreshes via the local JWT cookie, so
`session.openidTokens.refreshToken` is written ONLY in REUSE mode (where
`refreshController`/`logoutController` read it). That keeps login, refresh AND
logout on the local-JWT path byte-identical to a non-OpenID login; that flag
SOLELY gates whether `/v1/chat/auth/refresh` performs the OIDC refresh-grant.
The ~1h id_token is used while valid; durable refresh (hanzo.id/Casdoor OIDC
refresh or an RFC-8693 token-exchange from the chat session) is a tracked
FOLLOW-UP — the login-breaking refresh-grant is NOT enabled here.
- Abuse limits (a run is a real billable completion): a per-user rate limiter
(`cloudAgentLimiter`, `CLOUD_AGENT_USER_MAX`/`CLOUD_AGENT_WINDOW`) guards the
whole `/cloud` router; the client caps input by UTF-8 **bytes** (128 KiB), caps
the buffered response (4 MiB → 502), and sheds load past a process-wide in-flight
ceiling (`CLOUD_AGENT_MAX_CONCURRENT`, 503).
- Key files: backend `api/server/services/CloudAgentsClient.js`,
`api/server/routes/agents/cloud.js` (mounted `/cloud` in
`api/server/routes/agents/index.js`); data layer
`packages/data-provider/src/{types/cloudAgents.ts,api-endpoints.ts,data-service.ts}`;
client `client/src/hooks/Agents/useRunCloudAgent.ts`,
`client/src/utils/agentCommand.ts`,
`client/src/components/Chat/Input/AgentsCommand.tsx`, and the @mention wiring in
`client/src/hooks/Input/useMentions.ts` + `Mention.tsx`.
- Env: `HANZO_CLOUD_URL` (optional; falls back to the `OPENAI_BASE_URL` host).
- Convergence path (later): chat's Chat-legacy `/v1/chat/agents` CRUD should
converge onto cloud `/v1/agents`; this step only ADDS cloud-agent RUN.
## Unified cloud architecture (2026-07) — investigate-before-ripping map
hanzo.chat is the **chat view** of the Hanzo AI cloud (sibling to hanzo.app =
builder, console = admin). This section is the honest map of what is ALREADY
unified onto the Go backend (`api.hanzo.ai/v1`) vs the one real seam that is not.
Verified by full call-graph + route-table trace; do NOT rip blind.
### What already routes through the Go backend `api.hanzo.ai/v1` (no shadow LLM)
- **Chat completions**: client `useSSE``POST /v1/chat/agents/chat/Hanzo` (all
chat, incl. plain-model, goes through the agents framework) → custom-endpoint
resolver (`packages/api/src/endpoints/custom/initialize.ts`) reads
`HANZO_API_KEY` + literal `baseURL https://api.hanzo.ai/v1` from the loaded
config → LangChain OpenAI client → **`POST https://api.hanzo.ai/v1/chat/completions`**
(SSE stream, resumable via `GenerationJobManager`). Per-user `hk-` key +
per-org Commerce debit; fail-closed 402. THIS is the one inference path.
- **Code interpreter** → `CHAT_CODE_BASEURL` = cloud `/v1/exec`.
- **Web search** → `webSearch` block (searxng+firecrawl contracts) = cloud
`/v1/websearch`.
- **Cloud agents** → `POST /v1/chat/agents/cloud/:name/run` server-proxies to cloud
`/v1/agents` with the user's hanzo.id bearer (see "Cloud Agents" section).
- **Model list**: curated **zen-only** (`fetch:false`) in the loaded config —
NO raw upstream names (brand policy). Authoritative prod list lives in the
`chat-config` ConfigMap (`universe infra/k8s/chat/configmap.yaml`); repo
`chat.yaml` mirrors it (one way).
### DEAD residue — do NOT treat as a live backend
- `config.yaml` (LiteLLM `model_list`/`litellm_params`), `docker/Dockerfile.{simple,dev,custom_ui}`
(`CMD litellm`), `deploy/migrations/*` (`LiteLLM_*` Prisma tables),
`CONTRIBUTING.md` (upstream LiteLLM's), `scripts/cleanup-{litellm,hanzo-chat}.sh`:
all **unreferenced** by any compose/k8s/helm/Dockerfile.multi. Prod runs
`node server/index.js` and hits `api.hanzo.ai/v1` directly — NO local litellm
sidecar. This is upstream merge residue; safe to delete in a dedicated sweep.
### The ONE real parallel store (FLAG — needs a Go-backend home)
Chat's Express backend owns, in **MongoDB** (`HanzoChat` DB), all of:
`convos`, `messages`, `presets`, `prompts`/`promptGroups`, `users`, `balances`/
`transactions`, `files`, `sessions` (refresh-token hashes), plus agents/assistants/
memory/RBAC. Schemas: `packages/data-schemas/src/schema/*`. This is the shadow
store that is NOT on the Go backend.
- The Go backend (`hanzoai/ai`, mounted at bare `/v1/*` in cloud) DOES have a
persistence home, but under **casibase names** (`/v1/get-chats`, `/v1/get-chat`,
`/v1/add-chat`, `/v1/get-messages`, `/v1/add-message`, `/v1/get-usages`) — a
different schema/shape than Chat's Mongo.
- The canonical OpenAPI repo has `chat/openapi.yaml` describing the INTENDED
Chat-shaped REST surface (`/v1/chat/convos`, `/v1/chat/messages`,
`/v1/chat/presets`, `/v1/chat/balance`, `/v1/chat/auth/*`) — but the Go binary
**does not implement it yet**, and `ai/openapi.yaml` under-documents the real
casibase routes.
- To truly kill the parallel store WITHOUT breaking live chat: the Go backend
(or Base) must implement `chat/openapi.yaml` (conversations/messages/presets),
then repoint chat's data layer at it behind a flag and dual-write during
cutover. Until then Mongo stays (ripping it = data loss + dead chat).
**Coordinate with the openapi agent** (canonical spec + SDK regen).
### IAM-native auth (HIP-0111) — federated to hanzo.id, LIVE
- **Prod (backend-proxied)**: Chat passport `openid-client` strategy,
OIDC **discovery** from `${OPENID_ISSUER}` = `https://hanzo.id`
(`/.well-known/openid-configuration`; discovery fetched via in-cluster
`iam.hanzo.svc` to dodge the CF hairpin), client_id **`hanzo-chat`**, callback
`/oauth/openid/callback`. Local email/password is OFF in prod
(`ALLOW_EMAIL_LOGIN=false`, `ALLOW_REGISTRATION=false`); social OIDC only.
Files: `api/strategies/openidStrategy.js`, `api/server/socialLogins.js`,
`api/server/routes/oauth.js`. This IS IAM-native (federated), just not the
console `@hanzo/iam-js-sdk` shape.
- **Static/IAM SPA mode** (`Dockerfile.static`, not the live prod deploy): browser
`@hanzo/iam` `BrowserIamSdk` PKCE straight to hanzo.id
(`client/src/utils/iam.ts`, `OAuthCallback.tsx`). ⚠️ INCONSISTENCY: uses
client_id **`app-chat`** while prod uses `hanzo-chat` — align to `hanzo-chat`.
`@hanzo/iam` is pinned `^0.4.0` (HIP-0111 wants ≥0.11.0); this path is dormant.
### One brand system, but pick the RIGHT one for a Tailwind app
`@hanzo/ui` and `@hanzo/gui` are **two different, non-overlapping** design systems:
- **`@hanzo/ui`** = shadcn/ui + Tailwind + Radix (multi-framework). chat is
Vite + React 18 + Tailwind, so THIS is the correct shared lib. Already used:
`client/src/components/Nav/HanzoHeader.tsx` mounts `@hanzo/ui/navigation`
`HanzoHeader` for cross-app chrome. Monochrome rebrand already done (grey ramp,
H mark, favicon = hanzo.app set).
- **`@hanzo/gui`** = a **Tamagui** fork (Next.js 15 / React 19, RN-web) — console's
stack. Forcing it into the Vite/React18 Chat client = a ground-up rewrite
of a live product; NOT done. Unify by widening `@hanzo/ui` adoption + matching
console's monochrome tokens, NOT by swapping component frameworks.
### Config filename caveat
`loadCustomConfig.js` defaults to **`chat.yaml`** (`CONFIG_PATH || <root>/chat.yaml`).
Prod sets `CONFIG_PATH=/app/chat.yaml` (ConfigMap mount). Repo ships
`chat.yaml` (reference); a deploy that doesn't set `CONFIG_PATH` to it (or
provide `chat.yaml`) falls back to the built-in `openAI` endpoint. `OPENAI_BASE_URL`
in `compose.prod.yml` is inert here (built-in openAI reads `OPENAI_REVERSE_PROXY`).
## Internal Package Names
These are kept as-is from upstream (npm deps, not worth renaming):
- `@hanzochat/api`, `@librechat/client`, `@librechat/data-schemas`, `librechat-data-provider`, `@librechat/agents`
- Functions: `extractLibreChatParams`, `importLibreChatConvo`
- Type names: `LibreChatKeys`, `LibreChatParams`
- Config filename: `librechat.yaml` (upstream convention)
- Env var: `LIBRECHAT_LOG_DIR`
- `@hanzochat/api`, `@hanzochat/client`, `@hanzochat/data-schemas`, `@hanzochat/data-provider`, `@hanzochat/agents`
- Functions: `extractChatParams`, `importChatConvo`
- Type names: `ChatKeys`, `ChatParams`
- Config filename: `chat.yaml` (upstream convention)
- Env var: `CHAT_LOG_DIR`
## Branding Cleanup Log
All user-visible `LibreChat` / `librechat.ai` references replaced with Hanzo equivalents:
- All `librechat.ai` URLs -> `hanzo.ai/docs/chat/...`
- `code.librechat.ai` -> `hanzo.ai/docs/chat/code-interpreter/...`
All user-visible `Chat` / `chat.ai` references replaced with Hanzo equivalents:
- All `chat.ai` URLs -> `hanzo.ai/docs/chat/...`
- `code.chat.ai` -> `hanzo.ai/docs/chat/code-interpreter/...`
- package.json repo URLs -> `github.com/hanzoai/chat`
- package.json homepages -> `hanzo.ai/chat`
- package.json descriptions -> "Hanzo Chat"
@@ -111,6 +317,6 @@ All user-visible `LibreChat` / `librechat.ai` references replaced with Hanzo equ
- Docker Compose MongoDB DB name -> `HanzoChat`
- GitHub workflow repo refs -> `hanzoai/chat`
- MCP User-Agent -> `HanzoChat-MCP-Client`
- JSDoc comments: LibreChat -> Hanzo Chat
- Log messages: LibreChat -> Hanzo Chat
- JSDoc comments: Chat -> Hanzo Chat
- Log messages: Chat -> Hanzo Chat
- Helm chart URLs -> hanzo.ai/docs/chat/...
+1 -1
View File
@@ -134,7 +134,7 @@ build-static-local:
@echo "$(GREEN)Building static SPA locally...$(NC)"
@cp .env.static .env
@pnpm run build:data-provider
@pnpm run --filter @librechat/client build
@pnpm run --filter @hanzochat/client build
@cd client && pnpm run build
@cp .env.backup .env 2>/dev/null || true
@echo "$(GREEN)Local build complete: client/dist/$(NC)"
+2 -2
View File
@@ -1,6 +1,6 @@
chat
Copyright (c) 2023 Hanzo AI Inc.
This product includes software from LibreChat (https://github.com/danny-avila/LibreChat), licensed under MIT:
This product includes software from Chat (https://github.com/danny-avila/Chat), licensed under MIT:
Copyright (c) 2023 LibreChat (Danny Avila)
Copyright (c) 2023 Chat (Danny Avila)
+55 -136
View File
@@ -1,166 +1,85 @@
# Hanzo AI Chat
<p align="center"><img src=".github/hero.svg" alt="Hanzo Chat" width="880"></p>
AI-powered chat platform with enterprise features, using Hanzo's cloud API or local deployment.
# Hanzo Chat
## Quick Start
The chat surface of the Hanzo AI cloud: multi-model chat with agents, tools, and retrieval, running on Hanzo's backend. Live at [hanzo.chat](https://hanzo.chat).
Hanzo Chat is a sibling to [hanzo.app](https://hanzo.app) (the app builder) and the Hanzo console (admin). All inference, code execution, and web search route through the unified Hanzo API at `api.hanzo.ai/v1`, and sign-in is federated to Hanzo IAM ([hanzo.id](https://hanzo.id)).
## Features
- **Multi-model chat** — the Zen model family and other frontier models, served through `api.hanzo.ai`.
- **Agents** — build agents in the thread, or run your Hanzo Cloud agents (`/v1/agents`) with an `/agent` command or `@mention`.
- **MCP tools** — connect Model Context Protocol servers for tool use.
- **RAG** — chat over your own files and documents.
- **Web search** — grounded answers via Hanzo web search.
- **Code interpreter** — run code in a sandboxed runtime.
- **Image generation** — generate images inline.
- **Guest chat** — try a free Zen model with no account (optional, off by default).
## Requirements
- Node.js 24 (see `.nvmrc`)
- pnpm 10
- A Hanzo API key — get one at [hanzo.ai/dashboard](https://hanzo.ai/dashboard)
## Quick start (Docker)
```bash
# Clone and setup
git clone https://github.com/hanzoai/chat.git
cd chat
# Copy environment template
cp .env.example .env
# Edit .env and add your Hanzo API key
# Get your key at: https://hanzo.ai/dashboard
nano .env
# Start the platform
cp .env.example .env # set HANZO_API_KEY
make up
```
Access the chat at http://localhost:3081
Open http://localhost:3080. `make up` starts the full stack (app, MongoDB, Meilisearch) from `compose.yml`; `make down` stops it.
## Development
### Basic Development (with hot reload)
```bash
make dev
pnpm install # install workspace dependencies
pnpm build:packages # build the shared workspace packages
pnpm backend:dev # API server on :3080 (nodemon)
pnpm frontend:dev # Vite client dev server (second terminal)
```
### Full Development (with local router)
Tests and checks:
```bash
# Set your LLM API keys
export ANTHROPIC_API_KEY=your-key
export OPENAI_API_KEY=your-key
# Start full dev stack
make dev-full
```
## Architecture
```
┌─────────────────────────┐ ┌─────────────────────────┐
│ Hanzo Chat UI │────▶│ api.hanzo.ai │
│ (Hanzo Chat) │ │ (or local router) │
│ localhost:3081 │ │ │
└─────────────────────────┘ │ • 100+ AI Models │
│ │ • MCP Tools │
│ │ • Code Execution │
▼ └─────────────────────────┘
┌─────────────────────────┐
│ Local Data Storage │
│ • MongoDB (chat history)│
│ • Meilisearch (search) │
└─────────────────────────┘
pnpm test:all # all workspace tests
pnpm e2e # Playwright end-to-end tests
pnpm lint # ESLint
pnpm format # Prettier
```
## Configuration
### Required Environment Variables
Secrets live in `.env`; the model catalog and endpoints live in `chat.yaml` (copy `chat.example.yaml`). Key variables:
```env
# Your Hanzo API key (required)
OPENAI_API_KEY=sk-hanzo-your-key-here
# API endpoint (default: Hanzo cloud)
OPENAI_BASE_URL=https://api.hanzo.ai/v1
# Features
MCP_ENABLED=true
ALLOW_REGISTRATION=true
HANZO_API_KEY= # Hanzo API key — inference, tools, search
MONGO_URI= # MongoDB connection — chat history, users
JWT_SECRET= # session token signing
CREDS_KEY= # credential encryption
CREDS_IV=
```
### Optional Customization
Sign-in is federated to Hanzo IAM over OpenID Connect (`OPENID_ISSUER=https://hanzo.id`, client `hanzo-chat`).
```env
# Branding
APP_TITLE=My AI Assistant
CUSTOM_FOOTER=Powered by Hanzo AI
## Workspace
# Security
JWT_SECRET=your-secret-key
```
api/ Express backend (:3080) — routes, controllers, Mongoose models
client/ React frontend (Vite)
packages/ data-provider · data-schemas · api · client · agents · mcp
```
## Commands
## Documentation
### Basic Usage
```bash
make up # Start services
make down # Stop services
make logs # View logs
make status # Check health
make clean # Remove all data
```
### Development
```bash
make dev # Dev mode with hot reload
make build # Build containers
make test # Run tests
make lint # Check code quality
make format # Format code
```
### Production
```bash
make prod # Deploy with Traefik
make backup # Backup database
```
## Docker Compose Structure
- `compose.yml` - Base configuration for local development
- `compose.dev.yml` - Development overrides (hot reload, local router)
- `compose.prod.yml` - Production overrides (Traefik, security)
## Features
- 🤖 **100+ AI Models** via Hanzo Router
- 💬 **Clean Chat UI** with modern design
- 🔍 **Full-Text Search** with Meilisearch
- 📝 **Persistent Chat History**
- 🛠️ **MCP Tools** for enhanced capabilities
- 🚀 **Code Execution** via secure runtime
- 🔐 **Enterprise Security** with JWT auth
## Troubleshooting
### Chat not loading
```bash
# Check service status
make status
# View logs
make logs-chat
# Verify API key
echo $OPENAI_API_KEY
```
### Database issues
```bash
# Reset database
make db-reset
# Export data
make db-export
# Import data
make db-import FILE=backup.json
```
## Additional Documentation
- [Production Deployment](./docs/production-domains.md)
- [IAM Integration](./docs/iam-integration.md)
- [Platform Overview](./docs/platform-overview.md)
- [Demo User Guide](./docs/demo-user.md)
## Support
- Documentation: https://docs.hanzo.ai
- Docs: https://hanzo.ai/docs/chat
- Deployment, IAM, and platform notes: [`docs/`](./docs)
- Issues: https://github.com/hanzoai/chat/issues
- Discord: https://discord.gg/hanzoai# CI Test
## License
MIT. Forked from LibreChat (MIT). See [LICENSE](./LICENSE).
+61 -203
View File
@@ -1,227 +1,85 @@
<!-- Last synced with README.md: 2026-03-28 (cae3888) -->
<p align="center"><img src=".github/hero.svg" alt="Hanzo Chat" width="880"></p>
<p align="center">
<a href="https://librechat.ai">
<img src="client/public/assets/logo.svg" height="256">
</a>
<h1 align="center">
<a href="https://librechat.ai">LibreChat</a>
</h1>
</p>
# Hanzo Chat
<p align="center">
<a href="README.md">English</a> ·
<strong>中文</strong>
</p>
Hanzo AI 云的对话入口:多模型对话,内置智能体、工具与检索增强,全部运行在 Hanzo 的后端之上。已上线:[hanzo.chat](https://hanzo.chat)。
<p align="center">
<a href="https://discord.librechat.ai">
<img
src="https://img.shields.io/discord/1086345563026489514?label=&logo=discord&style=for-the-badge&logoWidth=20&logoColor=white&labelColor=000000&color=blueviolet">
</a>
<a href="https://www.youtube.com/@LibreChat">
<img
src="https://img.shields.io/badge/YOUTUBE-red.svg?style=for-the-badge&logo=youtube&logoColor=white&labelColor=000000&logoWidth=20">
</a>
<a href="https://docs.librechat.ai">
<img
src="https://img.shields.io/badge/DOCS-blue.svg?style=for-the-badge&logo=read-the-docs&logoColor=white&labelColor=000000&logoWidth=20">
</a>
<a aria-label="Sponsors" href="https://github.com/sponsors/danny-avila">
<img
src="https://img.shields.io/badge/SPONSORS-brightgreen.svg?style=for-the-badge&logo=github-sponsors&logoColor=white&labelColor=000000&logoWidth=20">
</a>
</p>
Hanzo Chat 与 [hanzo.app](https://hanzo.app)(应用构建器)和 Hanzo 控制台(管理端)互为姊妹产品。所有推理、代码执行与网页搜索都经由统一的 Hanzo API(`api.hanzo.ai/v1`)完成,登录则联合到 Hanzo IAM([hanzo.id](https://hanzo.id))。
<p align="center">
<a href="https://railway.com/deploy/librechat-official?referralCode=HI9hWz&utm_medium=integration&utm_source=readme&utm_campaign=librechat">
<img src="https://railway.com/button.svg" alt="Deploy on Railway" height="30">
</a>
<a href="https://zeabur.com/templates/0X2ZY8">
<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30"/>
</a>
<a href="https://template.cloud.sealos.io/deploy?templateName=librechat">
<img src="https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg" alt="Deploy on Sealos" height="30">
</a>
</p>
## 功能
<p align="center">
<a href="https://www.librechat.ai/docs/translation">
<img
src="https://img.shields.io/badge/dynamic/json.svg?style=for-the-badge&color=2096F3&label=locize&query=%24.translatedPercentage&url=https://api.locize.app/badgedata/4cb2598b-ed4d-469c-9b04-2ed531a8cb45&suffix=%+translated"
alt="翻译进度">
</a>
</p>
- **多模型对话** — Zen 系列模型及其他前沿模型,通过 `api.hanzo.ai` 提供服务。
- **智能体** — 在对话中直接构建智能体,或用 `/agent` 命令或 `@提及` 运行你的 Hanzo Cloud 智能体(`/v1/agents`)。
- **MCP 工具** — 接入 Model Context Protocol(模型上下文协议)服务器以调用工具。
- **检索增强(RAG)** — 基于你自己的文件和文档进行对话。
- **网页搜索** — 通过 Hanzo 网页搜索获得有据可循的回答。
- **代码解释器** — 在沙箱运行时中执行代码。
- **图像生成** — 在对话中直接生成图像。
- **访客对话** — 无需注册即可试用免费的 Zen 模型(可选,默认关闭)。
## 环境要求
# ✨ 功能
- Node.js 24(见 `.nvmrc`
- pnpm 10
- Hanzo API 密钥 — 在 [hanzo.ai/dashboard](https://hanzo.ai/dashboard) 获取
- 🖥️ **UI 与体验**:受 ChatGPT 启发,并具备更强的设计与功能。
## 快速开始(Docker
- 🤖 **AI 模型选择**
- Anthropic (Claude), AWS Bedrock, OpenAI, Azure OpenAI, Google, Vertex AI, OpenAI Responses API (包含 Azure)
- [自定义端点 (Custom Endpoints)](https://www.librechat.ai/docs/quick_start/custom_endpoints)LibreChat 支持任何兼容 OpenAI 规范的 API,无需代理。
- 兼容[本地与远程 AI 服务商](https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints)
- Ollama, groq, Cohere, Mistral AI, Apple MLX, koboldcpp, together.ai,
- OpenRouter, Helicone, Perplexity, ShuttleAI, Deepseek, Qwen 等。
```bash
git clone https://github.com/hanzoai/chat.git
cd chat
cp .env.example .env # 填入 HANZO_API_KEY
make up
```
- 🔧 **[代码解释器 (Code Interpreter) API](https://www.librechat.ai/docs/features/code_interpreter)**
- 安全的沙箱执行环境,支持 Python, Node.js (JS/TS), Go, C/C++, Java, PHP, Rust 和 Fortran。
- 无缝文件处理:直接上传、处理并下载文件。
- 隐私无忧:完全隔离且安全的执行环境。
打开 http://localhost:3080。`make up` 会从 `compose.yml` 启动完整技术栈(应用、MongoDB、Meilisearch);`make down` 停止。
- 🔦 **智能体与工具集成**
- **[LibreChat 智能体 (Agents)](https://www.librechat.ai/docs/features/agents)**
- 无代码定制助手:无需编程即可构建专业化的 AI 驱动助手。
- 智能体市场:发现并部署社区构建的智能体。
- 协作共享:与特定用户和群组共享智能体。
- 灵活且可扩展:支持 MCP 服务器、工具、文件搜索、代码执行等。
- 兼容自定义端点、OpenAI, Azure, Anthropic, AWS Bedrock, Google, Vertex AI, Responses API 等。
- [支持模型上下文协议 (MCP)](https://modelcontextprotocol.io/clients#librechat) 用于工具调用。
## 开发
- 🔍 **网页搜索**
- 搜索互联网并检索相关信息以增强 AI 上下文。
- 结合搜索提供商、内容爬虫和结果重排序,确保最佳检索效果。
- **可定制 Jina 重排序**:配置自定义 Jina API URL 用于重排序服务。
- **[了解更多 →](https://www.librechat.ai/docs/features/web_search)**
```bash
pnpm install # 安装工作区依赖
pnpm build:packages # 构建共享的工作区包
pnpm backend:dev # API 服务,端口 :3080nodemon
pnpm frontend:dev # Vite 客户端开发服务器(另开一个终端)
```
- 🪄 **支持代码 Artifacts 的生成式 UI**
- [代码 Artifacts](https://youtu.be/GfTj7O4gmd0?si=WJbdnemZpJzBrJo3) 允许在对话中直接创建 React 组件、HTML 页面和 Mermaid 图表。
测试与检查:
- 🎨 **图像生成与编辑**
- 使用 [GPT-Image-1](https://www.librechat.ai/docs/features/image_gen#1--openai-image-tools-recommended) 进行文生图与图生图。
- 支持 [DALL-E (3/2)](https://www.librechat.ai/docs/features/image_gen#2--dalle-legacy), [Stable Diffusion](https://www.librechat.ai/docs/features/image_gen#3--stable-diffusion-local), [Flux](https://www.librechat.ai/docs/features/image_gen#4--flux) 或任何 [MCP 服务器](https://www.librechat.ai/docs/features/image_gen#5--model-context-protocol-mcp)。
- 根据提示词生成惊艳的视觉效果,或通过指令精修现有图像。
```bash
pnpm test:all # 全部工作区测试
pnpm e2e # Playwright 端到端测试
pnpm lint # ESLint
pnpm format # Prettier
```
- 💾 **预设与上下文管理**
- 创建、保存并分享自定义预设。
- 在对话中随时切换 AI 端点和预设。
- 编辑、重新提交并通过对话分支继续消息。
- 创建并与特定用户和群组共享提示词。
- [消息与对话分叉 (Fork)](https://www.librechat.ai/docs/features/fork) 以实现高级上下文控制。
## 配置
- 💬 **多模态与文件交互**
- 使用 Claude 3, GPT-4.5, GPT-4o, o1, Llama-Vision 和 Gemini 上传并分析图像 📸。
- 支持通过自定义端点、OpenAI, Azure, Anthropic, AWS Bedrock 和 Google 进行文件对话 🗃️。
密钥存放在 `.env` 中;模型目录与端点配置位于 `chat.yaml`(复制 `chat.example.yaml`)。主要变量:
- 🌎 **多语言 UI**
- English, 中文 (简体), 中文 (繁體), العربية, Deutsch, Español, Français, Italiano
- Polski, Português (PT), Português (BR), Русский, 日本語, Svenska, 한국어, Tiếng Việt
- Türkçe, Nederlands, עברית, Català, Čeština, Dansk, Eesti, فارسی
- Suomi, Magyar, Հայերեն, Bahasa Indonesia, ქართული, Latviešu, ไทย, ئۇيغۇرچە
```env
HANZO_API_KEY= # Hanzo API 密钥 — 推理、工具、搜索
MONGO_URI= # MongoDB 连接 — 对话历史、用户
JWT_SECRET= # 会话令牌签名
CREDS_KEY= # 凭据加密
CREDS_IV=
```
- 🧠 **推理 UI**
- 针对 DeepSeek-R1 等思维链/推理 AI 模型的动态推理 UI。
登录通过 OpenID Connect 联合到 Hanzo IAM`OPENID_ISSUER=https://hanzo.id`,客户端 `hanzo-chat`)。
- 🎨 **可定制界面**
- 可定制的下拉菜单和界面,同时适配高级用户和初学者。
## 工作区结构
- 🌊 **[可恢复流 (Resumable Streams)](https://www.librechat.ai/docs/features/resumable_streams)**
- 永不丢失响应:AI 响应在连接中断后自动重连并继续。
- 多标签页与多设备同步:在多个标签页打开同一对话,或在另一设备上继续。
- 生产级可靠性:支持从单机部署到基于 Redis 的水平扩展。
```
api/ Express 后端(:3080)— 路由、控制器、Mongoose 模型
client/ React 前端(Vite
packages/ data-provider · data-schemas · api · client · agents · mcp
```
- 🗣️ **语音与音频**
- 通过语音转文字和文字转语音实现免提对话。
- 自动发送并播放音频。
- 支持 OpenAI, Azure OpenAI 和 Elevenlabs。
## 文档
- 📥 **导入与导出对话**
- 从 LibreChat, ChatGPT, Chatbot UI 导入对话。
- 将对话导出为截图、Markdown、文本、JSON。
- 文档:https://hanzo.ai/docs/chat
- 部署、IAM 与平台说明:[`docs/`](./docs)
- 问题反馈:https://github.com/hanzoai/chat/issues
- 🔍 **搜索与发现**
- 搜索所有消息和对话。
## 许可证
- 👥 **多用户与安全访问**
- 支持 OAuth2, LDAP 和电子邮件登录的多用户安全认证。
- 内置审核系统和 Token 消耗管理工具。
- ⚙️ **配置与部署**
- 支持代理、反向代理、Docker 及多种部署选项。
- 可完全本地运行或部署在云端。
- 📖 **开源与社区**
- 完全开源且在公众监督下开发。
- 社区驱动的开发、支持与反馈。
[查看我们的文档了解更多功能详情](https://docs.librechat.ai/) 📚
## 🪶 LibreChat:全方位的 AI 对话平台
LibreChat 是一个自托管的 AI 对话平台,在一个注重隐私的统一界面中整合了所有主流 AI 服务商。
除了对话功能外,LibreChat 还提供 AI 智能体、模型上下文协议 (MCP) 支持、Artifacts、代码解释器、自定义操作、对话搜索,以及企业级多用户认证。
开源、活跃开发中,专为重视 AI 基础设施自主可控的用户而构建。
---
## 🌐 资源
**GitHub 仓库:**
- **RAG API:** [github.com/danny-avila/rag_api](https://github.com/danny-avila/rag_api)
- **网站:** [github.com/LibreChat-AI/librechat.ai](https://github.com/LibreChat-AI/librechat.ai)
**其他:**
- **官方网站:** [librechat.ai](https://librechat.ai)
- **帮助文档:** [librechat.ai/docs](https://librechat.ai/docs)
- **博客:** [librechat.ai/blog](https://librechat.ai/blog)
---
## 📝 更新日志
访问发布页面和更新日志以了解最新动态:
- [发布页面 (Releases)](https://github.com/danny-avila/LibreChat/releases)
- [更新日志 (Changelog)](https://www.librechat.ai/changelog)
**⚠️ 在更新前请务必查看[更新日志](https://www.librechat.ai/changelog)以了解破坏性更改。**
---
## ⭐ Star 历史
<p align="center">
<a href="https://star-history.com/#danny-avila/LibreChat&Date">
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=danny-avila/LibreChat&type=Date&theme=dark" onerror="this.src='https://api.star-history.com/svg?repos=danny-avila/LibreChat&type=Date'" />
</a>
</p>
<p align="center">
<a href="https://trendshift.io/repositories/4685" target="_blank" style="padding: 10px;">
<img src="https://trendshift.io/api/badge/repositories/4685" alt="danny-avila%2FLibreChat | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
</a>
<a href="https://runacap.com/ross-index/q1-24/" target="_blank" rel="noopener" style="margin-left: 20px;">
<img style="width: 260px; height: 56px" src="https://runacap.com/wp-content/uploads/2024/04/ROSS_badge_white_Q1_2024.svg" alt="ROSS Index - 2024年第一季度增长最快的开源初创公司 | Runa Capital" width="260" height="56"/>
</a>
</p>
---
## ✨ 贡献
欢迎任何形式的贡献、建议、错误报告和修复!
对于新功能、组件或扩展,请在发送 PR 前开启 issue 进行讨论。
如果您想帮助我们将 LibreChat 翻译成您的母语,我们非常欢迎!改进翻译不仅能让全球用户更轻松地使用 LibreChat,还能提升整体用户体验。请查看我们的[翻译指南](https://www.librechat.ai/docs/translation)。
---
## 💖 感谢所有贡献者
<a href="https://github.com/danny-avila/LibreChat/graphs/contributors">
<img src="https://contrib.rocks/image?repo=danny-avila/LibreChat" />
</a>
---
## 🎉 特别鸣谢
感谢 [Locize](https://locize.com) 提供的翻译管理工具,支持 LibreChat 的多语言功能。
<p align="center">
<a href="https://locize.com" target="_blank" rel="noopener noreferrer">
<img src="https://github.com/user-attachments/assets/d6b70894-6064-475e-bb65-92a9e23e0077" alt="Locize Logo" height="50">
</a>
</p>
MIT 许可证。基于 LibreChatMIT)分支开发。见 [LICENSE](./LICENSE)。
+3 -3
View File
@@ -1,6 +1,6 @@
const crypto = require('crypto');
const fetch = require('node-fetch');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const {
countTokens,
getBalanceConfig,
@@ -20,7 +20,7 @@ const {
isAgentsEndpoint,
isEphemeralAgentId,
supportsBalanceCheck,
} = require('librechat-data-provider');
} = require('@hanzochat/data-provider');
const {
updateMessage,
getMessages,
@@ -76,7 +76,7 @@ class BaseClient {
this.fetchedConvo;
/** @type {TMessage[]} */
this.currentMessages = [];
/** @type {import('librechat-data-provider').VisionModes | undefined} */
/** @type {import('@hanzochat/data-provider').VisionModes | undefined} */
this.visionMode;
}
+3 -3
View File
@@ -1,9 +1,9 @@
const { z } = require('zod');
const axios = require('axios');
const { Ollama } = require('ollama');
const { sleep } = require('@librechat/agents');
const { logger } = require('@librechat/data-schemas');
const { Constants } = require('librechat-data-provider');
const { sleep } = require('@hanzochat/agents');
const { logger } = require('@hanzochat/data-schemas');
const { Constants } = require('@hanzochat/data-provider');
const { resolveHeaders, deriveBaseURL } = require('@hanzochat/api');
const ollamaPayloadSchema = z.object({
+1 -1
View File
@@ -1,5 +1,5 @@
const { Readable } = require('stream');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
class TextStream extends Readable {
constructor(text, options = {}) {
+7 -7
View File
@@ -1,5 +1,5 @@
const dedent = require('dedent');
const { EModelEndpoint, ArtifactModes } = require('librechat-data-provider');
const { EModelEndpoint, ArtifactModes } = require('@hanzochat/data-provider');
const { generateShadcnPrompt } = require('~/app/clients/prompts/shadcn-docs/generate');
const { components } = require('~/app/clients/prompts/shadcn-docs/components');
@@ -49,7 +49,7 @@ Artifacts are for substantial, self-contained content that users might modify or
4. Add a \`type\` attribute to specify the type of content the artifact represents. Assign one of the following values to the \`type\` attribute:
- HTML: "text/html"
- The user interface can render single file HTML pages placed within the artifact tags. HTML, JS, and CSS should be in a single file when using the \`text/html\` type.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- The only place external scripts can be imported from is https://cdnjs.cloudflare.com
- Mermaid Diagrams: "application/vnd.mermaid"
- The user interface will render Mermaid diagrams placed within the artifact tags.
@@ -63,7 +63,7 @@ Artifacts are for substantial, self-contained content that users might modify or
- The assistant can use prebuilt components from the \`shadcn/ui\` library after it is imported: \`import { Alert, AlertDescription, AlertTitle, AlertDialog, AlertDialogAction } from '/components/ui/alert';\`. If using components from the shadcn/ui library, the assistant mentions this to the user and offers to help them install the components if necessary.
- Components MUST be imported from \`/components/ui/name\` and NOT from \`/components/name\` or \`@/components/ui/name\`.
- NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
6. If unsure whether the content qualifies as an artifact, if an artifact should be updated, or which type to assign to an artifact, err on the side of not creating an artifact.
@@ -162,7 +162,7 @@ Artifacts are for substantial, self-contained content that users might modify or
4. Add a \`type\` attribute to specify the type of content the artifact represents. Assign one of the following values to the \`type\` attribute:
- HTML: "text/html"
- The user interface can render single file HTML pages placed within the artifact tags. HTML, JS, and CSS should be in a single file when using the \`text/html\` type.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- The only place external scripts can be imported from is https://cdnjs.cloudflare.com
- SVG: "image/svg+xml"
- The user interface will render the Scalable Vector Graphics (SVG) image within the artifact tags.
@@ -186,7 +186,7 @@ Artifacts are for substantial, self-contained content that users might modify or
- The assistant can use prebuilt components from the \`shadcn/ui\` library after it is imported: \`import { Alert, AlertDescription, AlertTitle, AlertDialog, AlertDialogAction } from '/components/ui/alert';\`. If using components from the shadcn/ui library, the assistant mentions this to the user and offers to help them install the components if necessary.
- Components MUST be imported from \`/components/ui/name\` and NOT from \`/components/name\` or \`@/components/ui/name\`.
- NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- When iterating on code, ensure that the code is complete and functional without any snippets, placeholders, or ellipses.
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
@@ -367,7 +367,7 @@ Artifacts are for substantial, self-contained content that users might modify or
4. Add a \`type\` attribute to specify the type of content the artifact represents. Assign one of the following values to the \`type\` attribute:
- HTML: "text/html"
- The user interface can render single file HTML pages placed within the artifact tags. HTML, JS, and CSS should be in a single file when using the \`text/html\` type.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- The only place external scripts can be imported from is https://cdnjs.cloudflare.com
- SVG: "image/svg+xml"
- The user interface will render the Scalable Vector Graphics (SVG) image within the artifact tags.
@@ -391,7 +391,7 @@ Artifacts are for substantial, self-contained content that users might modify or
- The assistant can use prebuilt components from the \`shadcn/ui\` library after it is imported: \`import { Alert, AlertDescription, AlertTitle, AlertDialog, AlertDialogAction } from '/components/ui/alert';\`. If using components from the shadcn/ui library, the assistant mentions this to the user and offers to help them install the components if necessary.
- Components MUST be imported from \`/components/ui/name\` and NOT from \`/components/name\` or \`@/components/ui/name\`.
- NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- When iterating on code, ensure that the code is complete and functional without any snippets, placeholders, or ellipses.
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
@@ -1,5 +1,5 @@
const axios = require('axios');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { isEnabled, generateShortLivedToken } = require('@hanzochat/api');
const footer = `Use the context as your learned knowledge to better answer the user.
@@ -1,5 +1,5 @@
const { ToolMessage } = require('@langchain/core/messages');
const { ContentTypes } = require('librechat-data-provider');
const { ContentTypes } = require('@hanzochat/data-provider');
const { HumanMessage, AIMessage, SystemMessage } = require('@langchain/core/messages');
const { formatAgentMessages } = require('./formatMessages');
+1 -1
View File
@@ -1,5 +1,5 @@
const { ToolMessage } = require('@langchain/core/messages');
const { EModelEndpoint, ContentTypes } = require('librechat-data-provider');
const { EModelEndpoint, ContentTypes } = require('@hanzochat/data-provider');
const { HumanMessage, AIMessage, SystemMessage } = require('@langchain/core/messages');
/**
@@ -1,4 +1,4 @@
const { Constants } = require('librechat-data-provider');
const { Constants } = require('@hanzochat/data-provider');
const { HumanMessage, AIMessage, SystemMessage } = require('@langchain/core/messages');
const { formatMessage, formatLangChainMessages, formatFromLangChain } = require('./formatMessages');
+3 -3
View File
@@ -1,4 +1,4 @@
const { Constants } = require('librechat-data-provider');
const { Constants } = require('@hanzochat/data-provider');
const { initializeFakeClient } = require('./FakeClient');
jest.mock('~/db/connect');
@@ -40,8 +40,8 @@ jest.mock('~/models', () => ({
const { getConvo, saveConvo } = require('~/models');
jest.mock('@librechat/agents', () => {
const actual = jest.requireActual('@librechat/agents');
jest.mock('@hanzochat/agents', () => {
const actual = jest.requireActual('@hanzochat/agents');
return {
...actual,
ChatOpenAI: jest.fn().mockImplementation(() => {
@@ -1,5 +1,5 @@
const { Tool } = require('@langchain/core/tools');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { SearchClient, AzureKeyCredential } = require('@azure/search-documents');
const azureAISearchJsonSchema = {
+20 -10
View File
@@ -3,9 +3,9 @@ const OpenAI = require('openai');
const { v4: uuidv4 } = require('uuid');
const { ProxyAgent, fetch } = require('undici');
const { Tool } = require('@langchain/core/tools');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { getImageBasename, extractBaseURL } = require('@hanzochat/api');
const { FileContext, ContentTypes } = require('librechat-data-provider');
const { FileContext, ContentTypes } = require('@hanzochat/data-provider');
const dalle3JsonSchema = {
type: 'object',
@@ -143,16 +143,26 @@ class DALLE3 extends Tool {
throw new Error('Missing required field: prompt');
}
// Model is configurable (DALLE3_MODEL) so this tool drives the Hanzo image
// family (e.g. zen3-image) through the same OpenAI /images/generations shape.
// `quality` and `style` are DALL-E-3-only knobs — send them ONLY for a dall-e
// model so a non-DALL-E backend never sees a parameter it may reject.
const model = process.env.DALLE3_MODEL || 'dall-e-3';
const isDallE = model.startsWith('dall-e');
const genParams = {
model,
size,
prompt: this.replaceUnwantedChars(prompt),
n: 1,
};
if (isDallE) {
genParams.quality = quality;
genParams.style = style;
}
let resp;
try {
resp = await this.openai.images.generate({
model: 'dall-e-3',
quality,
style,
size,
prompt: this.replaceUnwantedChars(prompt),
n: 1,
});
resp = await this.openai.images.generate(genParams);
} catch (error) {
logger.error('[DALL-E-3] Problem generating the image:', error);
return this
+2 -2
View File
@@ -2,9 +2,9 @@ const axios = require('axios');
const fetch = require('node-fetch');
const { v4: uuidv4 } = require('uuid');
const { Tool } = require('@langchain/core/tools');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { FileContext, ContentTypes } = require('librechat-data-provider');
const { FileContext, ContentTypes } = require('@hanzochat/data-provider');
const fluxApiJsonSchema = {
type: 'object',
@@ -5,13 +5,13 @@ const { v4 } = require('uuid');
const { ProxyAgent } = require('undici');
const { GoogleGenAI } = require('@google/genai');
const { tool } = require('@langchain/core/tools');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const {
FileContext,
ContentTypes,
FileSources,
EImageOutputType,
} = require('librechat-data-provider');
} = require('@hanzochat/data-provider');
const {
geminiToolkit,
loadServiceKey,
@@ -4,9 +4,9 @@ const OpenAI = require('openai');
const FormData = require('form-data');
const { ProxyAgent } = require('undici');
const { tool } = require('@langchain/core/tools');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { ContentTypes, EImageOutputType } = require('librechat-data-provider');
const { ContentTypes, EImageOutputType } = require('@hanzochat/data-provider');
const { logAxiosError, oaiToolkit, extractBaseURL } = require('@hanzochat/api');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { getFiles } = require('~/models');
@@ -5,8 +5,8 @@ const axios = require('axios');
const sharp = require('sharp');
const { v4: uuidv4 } = require('uuid');
const { Tool } = require('@langchain/core/tools');
const { logger } = require('@librechat/data-schemas');
const { FileContext, ContentTypes } = require('librechat-data-provider');
const { logger } = require('@hanzochat/data-schemas');
const { FileContext, ContentTypes } = require('@hanzochat/data-provider');
const { getBasePath } = require('@hanzochat/api');
const paths = require('~/config/paths');
@@ -1,5 +1,5 @@
const { Tool } = require('@langchain/core/tools');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { getEnvironmentVariable } = require('@langchain/core/utils/env');
const traversaalSearchJsonSchema = {
+1 -1
View File
@@ -1,7 +1,7 @@
/* eslint-disable no-useless-escape */
const axios = require('axios');
const { Tool } = require('@langchain/core/tools');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const wolframJsonSchema = {
type: 'object',
@@ -1,9 +1,9 @@
const OpenAI = require('openai');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const DALLE3 = require('../DALLE3');
jest.mock('openai');
jest.mock('@librechat/data-schemas', () => {
jest.mock('@hanzochat/data-schemas', () => {
return {
logger: {
info: jest.fn(),
@@ -12,8 +12,8 @@ const axios = require('axios');
const OpenAI = require('openai');
const undici = require('undici');
const fetch = require('node-fetch');
const { ContentTypes } = require('librechat-data-provider');
const { ToolMessage } = require('@librechat/agents/langchain/messages');
const { ContentTypes } = require('@hanzochat/data-provider');
const { ToolMessage } = require('@hanzochat/agents/langchain/messages');
const StableDiffusionAPI = require('../StableDiffusion');
const FluxAPI = require('../FluxAPI');
const DALLE3 = require('../DALLE3');
@@ -25,7 +25,7 @@ jest.mock('undici', () => ({
ProxyAgent: jest.fn(),
fetch: jest.fn(),
}));
jest.mock('@librechat/data-schemas', () => ({
jest.mock('@hanzochat/data-schemas', () => ({
logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() },
}));
jest.mock('path', () => ({
+3 -3
View File
@@ -1,8 +1,8 @@
const axios = require('axios');
const { tool } = require('@langchain/core/tools');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { generateShortLivedToken } = require('@hanzochat/api');
const { Tools, EToolResources } = require('librechat-data-provider');
const { Tools, EToolResources } = require('@hanzochat/data-provider');
const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions');
const { getFiles } = require('~/models');
@@ -97,7 +97,7 @@ const createFileSearchTool = async ({ userId, files, entity_id, fileCitations =
}
/**
* @param {import('librechat-data-provider').TFile} file
* @param {import('@hanzochat/data-provider').TFile} file
* @returns {{ file_id: string, query: string, k: number, entity_id?: string }}
*/
const createQueryBody = (file) => {
+28 -4
View File
@@ -1,10 +1,10 @@
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const {
EnvVar,
Calculator,
createSearchTool,
createCodeExecutionTool,
} = require('@librechat/agents');
} = require('@hanzochat/agents');
const {
checkAccess,
createSafeUser,
@@ -12,6 +12,8 @@ const {
loadWebSearchAuth,
buildImageToolContext,
buildWebSearchContext,
resolveTenantBearer,
OPENID_BEARER_SENTINEL,
} = require('@hanzochat/api');
const { getMCPServersRegistry } = require('~/config');
const {
@@ -20,7 +22,7 @@ const {
Permissions,
EToolResources,
PermissionTypes,
} = require('librechat-data-provider');
} = require('@hanzochat/data-provider');
const {
availableTools,
manifestToolMap,
@@ -232,7 +234,29 @@ const loadTools = async ({
const requestedTools = {};
if (functions === true) {
toolConstructors.dalle = DALLE3;
// dalle (image generation) drives the Hanzo image family via a PER-USER hk-
// key so generation is metered to the signed-in user — mirroring the chat
// per-user key path in custom/initialize.ts. A guest keeps the shared env
// key; an authenticated user whose key cannot be resolved FAILS CLOSED
// (throws) rather than silently billing the shared org. (Custom constructors
// take precedence over the generic toolConstructors path in the loop below.)
customConstructors.dalle = async () => {
const authFields = getAuthFields('dalle');
const authValues = await loadAuthValues({ userId: user, authFields });
// Canonical Hanzo Cloud billing (mirrors custom/initialize.ts): when the
// image endpoint is configured to forward the user's IAM bearer
// (DALLE3_API_KEY === the OIDC-token sentinel), resolve the signed-in
// user's own IAM token and forward it so cloud meters THEIR org. Fail
// closed if there is no forwardable bearer — no shared key to spend.
if (authValues.DALLE3_API_KEY === OPENID_BEARER_SENTINEL) {
const bearer = resolveTenantBearer(options.req);
if (!bearer) {
throw new Error('Sign in with Hanzo to generate images — your Hanzo account funds this request.');
}
authValues.DALLE3_API_KEY = bearer;
}
return new DALLE3({ ...imageGenOptions, ...authValues, userId: user });
};
}
/** @type {ImageGenOptions} */
@@ -30,7 +30,7 @@ jest.mock('~/server/services/Config', () => ({
}),
}));
const { Calculator } = require('@librechat/agents');
const { Calculator } = require('@hanzochat/agents');
const { User } = require('~/db/models');
const PluginService = require('~/server/services/PluginService');
+2 -2
View File
@@ -1,6 +1,6 @@
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { isEnabled, math } = require('@hanzochat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { ViolationTypes } = require('@hanzochat/data-provider');
const { deleteAllUserSessions } = require('~/models');
const { removePorts } = require('~/server/utils');
const getLogStores = require('./getLogStores');
+1 -1
View File
@@ -1,5 +1,5 @@
const { isEnabled } = require('@hanzochat/api');
const { Time, CacheKeys } = require('librechat-data-provider');
const { Time, CacheKeys } = require('@hanzochat/data-provider');
const getLogStores = require('./getLogStores');
const { USE_REDIS, LIMIT_CONCURRENT_MESSAGES } = process.env ?? {};
+1 -1
View File
@@ -1,5 +1,5 @@
const { Keyv } = require('keyv');
const { Time, CacheKeys, ViolationTypes } = require('librechat-data-provider');
const { Time, CacheKeys, ViolationTypes } = require('@hanzochat/data-provider');
const {
logFile,
keyvMongo,
+1 -1
View File
@@ -1,5 +1,5 @@
const { isEnabled } = require('@hanzochat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { ViolationTypes } = require('@hanzochat/data-provider');
const getLogStores = require('./getLogStores');
const banViolation = require('./banViolation');
+1 -1
View File
@@ -3,7 +3,7 @@ const fs = require('fs');
const ORIGINAL_ENV = process.env;
const mockDataSchemas = () => {
jest.doMock('@librechat/data-schemas', () => ({
jest.doMock('@hanzochat/data-schemas', () => ({
getTenantId: jest.fn(),
getUserId: jest.fn(),
getRequestId: jest.fn(),
+1 -1
View File
@@ -1,5 +1,5 @@
const { EventSource } = require('eventsource');
const { Time } = require('librechat-data-provider');
const { Time } = require('@hanzochat/data-provider');
const {
MCPManager,
FlowStateManager,
+3 -3
View File
@@ -6,13 +6,13 @@ require('winston-daily-rotate-file');
/**
* Determine the log directory.
* Priority:
* 1. LIBRECHAT_LOG_DIR environment variable (allows user override)
* 1. CHAT_LOG_DIR environment variable (allows user override)
* 2. /app/logs if running in Docker (bind-mounted with correct permissions)
* 3. api/logs relative to this file (local development)
*/
const getLogDir = () => {
if (process.env.LIBRECHAT_LOG_DIR) {
return process.env.LIBRECHAT_LOG_DIR;
if (process.env.CHAT_LOG_DIR) {
return process.env.CHAT_LOG_DIR;
}
// Check if running in Docker container (cwd is /app)
+3 -3
View File
@@ -7,13 +7,13 @@ const { redactFormat, redactMessage, debugTraverse, jsonTruncateFormat } = requi
/**
* Determine the log directory.
* Priority:
* 1. LIBRECHAT_LOG_DIR environment variable (allows user override)
* 1. CHAT_LOG_DIR environment variable (allows user override)
* 2. /app/logs if running in Docker (bind-mounted with correct permissions)
* 3. api/logs relative to this file (local development)
*/
const getLogDir = () => {
if (process.env.LIBRECHAT_LOG_DIR) {
return process.env.LIBRECHAT_LOG_DIR;
if (process.env.CHAT_LOG_DIR) {
return process.env.CHAT_LOG_DIR;
}
// Check if running in Docker container (cwd is /app)
+16 -4
View File
@@ -1,13 +1,15 @@
require('dotenv').config();
const { isEnabled } = require('@hanzochat/api');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const mongoose = require('mongoose');
const MONGO_URI = process.env.MONGO_URI;
if (!MONGO_URI) {
throw new Error('Please define the MONGO_URI environment variable');
}
// MONGO_URI is intentionally optional: in SQLite-only mode (every collection
// served from CHAT_STORE_SQLITE, chat-docdb deleted) there is no Mongo to
// connect to. connectDb() below handles the unset case by skipping the
// connection rather than failing boot. When MONGO_URI IS set (default and the
// dual-write migration window) the connection is required as before.
/** The maximum number of connections in the connection pool. */
const maxPoolSize = parseInt(process.env.MONGO_MAX_POOL_SIZE) || undefined;
/** The minimum number of connections in the connection pool. */
@@ -45,6 +47,16 @@ mongoose.connection.on('error', (err) => {
});
async function connectDb() {
if (!MONGO_URI) {
// SQLite-only mode: skip Mongo entirely. Disable command buffering so any
// stray mongoose query (e.g. Meili indexSync, which still references
// mongoose.models directly) fails fast and is swallowed by its caller
// instead of hanging forever on a pool that will never connect.
mongoose.set('bufferCommands', false);
logger.info('[connectDb] MONGO_URI unset — SQLite-only mode, skipping MongoDB connection');
return null;
}
if (cached.conn && cached.conn?._readyState === 1) {
return cached.conn;
}
+1 -1
View File
@@ -1,5 +1,5 @@
const mongoose = require('mongoose');
const { createModels } = require('@librechat/data-schemas');
const { createModels } = require('@hanzochat/data-schemas');
const { connectDb } = require('./connect');
const indexSync = require('./indexSync');
+1 -1
View File
@@ -4,7 +4,7 @@ describe('api/db/index.js', () => {
const callOrder = [];
jest.mock('@librechat/data-schemas', () => ({
jest.mock('@hanzochat/data-schemas', () => ({
createModels: jest.fn((m) => {
callOrder.push('createModels');
m.models.Message = { name: 'Message' };
+2 -2
View File
@@ -1,7 +1,7 @@
const mongoose = require('mongoose');
const { MeiliSearch } = require('meilisearch');
const { logger } = require('@librechat/data-schemas');
const { CacheKeys } = require('librechat-data-provider');
const { logger } = require('@hanzochat/data-schemas');
const { CacheKeys } = require('@hanzochat/data-provider');
const { isEnabled, FlowStateManager } = require('@hanzochat/api');
const { getLogStores } = require('~/cache');
const { batchResetMeiliFlags } = require('./utils');
+1 -1
View File
@@ -32,7 +32,7 @@ const originalMessageModel = mongoose.models.Message;
const originalConversationModel = mongoose.models.Conversation;
// Mock external modules
jest.mock('@librechat/data-schemas', () => ({
jest.mock('@hanzochat/data-schemas', () => ({
logger: mockLogger,
}));
+1 -1
View File
@@ -1,5 +1,5 @@
const mongoose = require('mongoose');
const { createModels } = require('@librechat/data-schemas');
const { createModels } = require('@hanzochat/data-schemas');
const models = createModels(mongoose);
module.exports = { ...models };
+1 -1
View File
@@ -1,4 +1,4 @@
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+1 -1
View File
@@ -392,7 +392,7 @@ describe('batchResetMeiliFlags', () => {
beforeEach(() => {
// Mock logger.warn to track warning calls
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {});
});
@@ -1,15 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/Users/z/work/hanzo/chat/api/logs/.124163c776d287793643ac0e08ac1d35d67ad894-audit.json",
"files": [
{
"date": 1771557162318,
"name": "/Users/z/work/hanzo/chat/api/logs/meiliSync-2026-02-19.log",
"hash": "a54018af15e4552979b0e91a2379ef40b95019fe56fe70074bb13f91952d908f"
}
],
"hashType": "sha256"
}
@@ -1,15 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/Users/z/work/hanzo/chat/api/logs/.35252977fe148e605b7b92f19cd71e590a6f0a53-audit.json",
"files": [
{
"date": 1771557162297,
"name": "/Users/z/work/hanzo/chat/api/logs/error-2026-02-19.log",
"hash": "63688154da6c0a28cba1e30ddeef3eb867682460096d9b007dcfd2ddc25202b0"
}
],
"hashType": "sha256"
}
@@ -1,45 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/Users/z/work/hanzo/chat/api/logs/.5a07238e142f72f47174c381d68b979f0f4a60b3-audit.json",
"files": [
{
"date": 1750474453457,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-06-20.log",
"hash": "696c5dfed20dbc87cd7113adb058ad5df3e53b93b9fc24840b7e236724ac4823"
},
{
"date": 1750715218180,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-06-23.log",
"hash": "c802b606c51329d1d65fe6c877dff02200e4d57e6565de3697ce4d8938bd8366"
},
{
"date": 1750883760400,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-06-25.log",
"hash": "202106c1ae63bd10f256f9249f6f32e20e495b9ca7f5ab7844f6dff28716805a"
},
{
"date": 1750950599470,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-06-26.log",
"hash": "e5f73d742f92938cef296a5c97cf8bb7f4e3db0198b8bc53bb0eee3ddf0f1e84"
},
{
"date": 1751049961804,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-06-27.log",
"hash": "15a471f383c1ea303252a3b35f88e44cc4bd1905666617dc9afc4b415d3ddac9"
},
{
"date": 1751124937592,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-06-28.log",
"hash": "77a7dd41fff34914a5ce25239c9fd77fafb8c3617a7a118550af69fbcc577643"
},
{
"date": 1751599940774,
"name": "/Users/z/work/hanzo/chat/api/logs/debug-2025-07-03.log",
"hash": "c86e337d49f3edad24952270eb6bebba4588201f1e14eab31b2482ee0c68be00"
}
],
"hashType": "sha256"
}
@@ -1,30 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/Users/z/work/hanzo/chat/api/logs/.b5a17c43715e8a0a84a729c6012dc1ba16b1828b-audit.json",
"files": [
{
"date": 1750954055845,
"name": "/Users/z/work/hanzo/chat/api/logs/meiliSync-2025-06-26.log",
"hash": "980f8267840afde6278855f4218760f010a3fb215aa86ef5bb69dc08bb4b1b50"
},
{
"date": 1751049961800,
"name": "/Users/z/work/hanzo/chat/api/logs/meiliSync-2025-06-27.log",
"hash": "cebe79495cabf77d359dbcd3168502d3a5c4d8993e1bed0663fa40d850ccf6d5"
},
{
"date": 1751124937590,
"name": "/Users/z/work/hanzo/chat/api/logs/meiliSync-2025-06-28.log",
"hash": "bb29a81af67a7fce02315648194ef210ef2ad3c89e7083220d9a63103dc762cc"
},
{
"date": 1751599940771,
"name": "/Users/z/work/hanzo/chat/api/logs/meiliSync-2025-07-03.log",
"hash": "9b454a63526db0eb56a27269fd38a86ac3d35dba85338ae89c91ea9895d467cb"
}
],
"hashType": "sha256"
}
-8
View File
@@ -1,8 +0,0 @@
2025-06-25T20:37:18.651Z info: [Optional] Redis not initialized.
2025-06-25T20:37:20.168Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-25T20:39:50.405Z info: [Optional] Redis not initialized.
2025-06-25T20:39:51.070Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-25T20:40:07.114Z info: [Optional] Redis not initialized.
2025-06-25T20:40:07.770Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-25T20:40:53.234Z info: [Optional] Redis not initialized.
2025-06-25T20:40:53.905Z info: [Optional] IoRedis not initialized for rate limiters.
-115
View File
@@ -1,115 +0,0 @@
2025-06-26T15:10:34.209Z info: [Optional] Redis not initialized.
2025-06-26T15:10:35.166Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T16:21:35.171Z info: [Optional] Redis not initialized.
2025-06-26T16:21:36.574Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T16:22:06.626Z error: connect ECONNREFUSED 127.0.0.1:27017
2025-06-26T16:23:03.561Z info: [Optional] Redis not initialized.
2025-06-26T16:23:04.255Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T16:23:04.300Z info: Connected to MongoDB
2025-06-26T16:23:04.301Z info: [indexSync] Starting index synchronization check...
2025-06-26T16:23:04.420Z error: Invalid custom config file at /Users/z/work/hanzo/chat/chat.yaml:
{
"issues": [
{
"code": "unrecognized_keys",
"keys": [
"ag... [truncated]
2025-06-26T16:23:04.420Z debug: Web search serperApiKey: Using environment variable SERPER_API_KEY (not set in environment, user provided value)
2025-06-26T16:23:04.420Z debug: Web search firecrawlApiKey: Using environment variable FIRECRAWL_API_KEY (not set in environment, user provided value)
2025-06-26T16:23:04.420Z debug: Web search firecrawlApiUrl: Using environment variable FIRECRAWL_API_URL (not set in environment, user provided value)
2025-06-26T16:23:04.421Z debug: Web search jinaApiKey: Using environment variable JINA_API_KEY (not set in environment, user provided value)
2025-06-26T16:23:04.421Z debug: Web search cohereApiKey: Using environment variable COHERE_API_KEY (not set in environment, user provided value)
2025-06-26T16:23:04.421Z warn: Default value for CREDS_KEY is being used.
2025-06-26T16:23:04.421Z warn: Default value for CREDS_IV is being used.
2025-06-26T16:23:04.421Z warn: Default value for JWT_SECRET is being used.
2025-06-26T16:23:04.421Z warn: Default value for JWT_REFRESH_SECRET is being used.
2025-06-26T16:23:04.421Z info: Please replace any default secret values.
2025-06-26T16:23:04.421Z info:
For your convenience, use this tool to generate your own secret values:
https://www.hanzo.ai/toolkit/creds_generator
2025-06-26T16:23:04.421Z warn: RAG API is either not running or not reachable at undefined, you may experience errors with file uploads.
2025-06-26T16:23:04.429Z info: No changes needed for 'USER' role permissions
2025-06-26T16:23:04.431Z info: No changes needed for 'ADMIN' role permissions
2025-06-26T16:23:04.431Z info: Turnstile is DISABLED (no siteKey provided).
2025-06-26T16:24:16.068Z info: [Optional] Redis not initialized.
2025-06-26T16:24:17.688Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T16:24:17.756Z info: Connected to MongoDB
2025-06-26T16:24:17.757Z info: [indexSync] Starting index synchronization check...
2025-06-26T16:24:17.811Z error: Invalid custom config file at /Users/z/work/hanzo/chat/chat.yaml:
{
"issues": [
{
"code": "unrecognized_keys",
"keys": [
"ag... [truncated]
2025-06-26T16:24:17.812Z debug: Web search serperApiKey: Using environment variable SERPER_API_KEY (not set in environment, user provided value)
2025-06-26T16:24:17.812Z debug: Web search firecrawlApiKey: Using environment variable FIRECRAWL_API_KEY (not set in environment, user provided value)
2025-06-26T16:24:17.812Z debug: Web search firecrawlApiUrl: Using environment variable FIRECRAWL_API_URL (not set in environment, user provided value)
2025-06-26T16:24:17.812Z debug: Web search jinaApiKey: Using environment variable JINA_API_KEY (not set in environment, user provided value)
2025-06-26T16:24:17.812Z debug: Web search cohereApiKey: Using environment variable COHERE_API_KEY (not set in environment, user provided value)
2025-06-26T16:24:17.812Z warn: Default value for CREDS_KEY is being used.
2025-06-26T16:24:17.812Z warn: Default value for CREDS_IV is being used.
2025-06-26T16:24:17.812Z warn: Default value for JWT_SECRET is being used.
2025-06-26T16:24:17.812Z warn: Default value for JWT_REFRESH_SECRET is being used.
2025-06-26T16:24:17.812Z info: Please replace any default secret values.
2025-06-26T16:24:17.812Z info:
For your convenience, use this tool to generate your own secret values:
https://www.hanzo.ai/toolkit/creds_generator
2025-06-26T16:24:17.812Z warn: RAG API is either not running or not reachable at undefined, you may experience errors with file uploads.
2025-06-26T16:24:17.823Z info: No changes needed for 'USER' role permissions
2025-06-26T16:24:17.825Z info: No changes needed for 'ADMIN' role permissions
2025-06-26T16:24:17.825Z info: Turnstile is DISABLED (no siteKey provided).
2025-06-26T16:24:17.832Z info: Server listening at http://localhost:3080
2025-06-26T16:24:18.010Z debug: [MEILI_SYNC:meili-index-sync] Creating initial flow state
2025-06-26T16:24:18.020Z info: [indexSync] Messages are fully synced: 0/0
2025-06-26T16:24:18.023Z info: [indexSync] Conversations are fully synced: 0/0
2025-06-26T16:24:18.024Z debug: [indexSync] No sync was needed
2025-06-26T16:24:37.105Z error: invalid signature
2025-06-26T17:16:21.631Z info: Cleaning up FlowStateManager intervals...
2025-06-26T17:28:31.428Z info: [Optional] Redis not initialized.
2025-06-26T19:16:07.812Z info: [Optional] Redis not initialized.
2025-06-26T19:16:09.408Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T19:16:39.465Z error: connect ECONNREFUSED 127.0.0.1:27017
2025-06-26T19:28:57.619Z info: [Optional] Redis not initialized.
2025-06-26T19:28:58.798Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:18:29.425Z info: [Optional] Redis not initialized.
2025-06-26T21:18:30.678Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:19:00.734Z error: getaddrinfo ENOTFOUND mongodb
2025-06-26T21:20:04.085Z info: [Optional] Redis not initialized.
2025-06-26T21:20:04.788Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:20:34.832Z error: connect ECONNREFUSED ::1:27017, connect ECONNREFUSED 127.0.0.1:27017
2025-06-26T21:35:21.338Z info: [Optional] Redis not initialized.
2025-06-26T21:35:22.746Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:35:52.803Z error: connect ECONNREFUSED ::1:27017, connect ECONNREFUSED 127.0.0.1:27017
2025-06-26T21:39:16.497Z info: [Optional] Redis not initialized.
2025-06-26T21:39:17.655Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:39:47.696Z error: connect ECONNREFUSED ::1:27017, connect ECONNREFUSED 127.0.0.1:27017
2025-06-26T21:43:04.982Z info: [Optional] Redis not initialized.
2025-06-26T21:43:06.587Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:07.208Z info: [Optional] Redis not initialized.
2025-06-26T21:43:07.990Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:09.790Z info: [Optional] Redis not initialized.
2025-06-26T21:43:10.572Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:12.993Z info: [Optional] Redis not initialized.
2025-06-26T21:43:14.084Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:16.627Z info: [Optional] Redis not initialized.
2025-06-26T21:43:17.383Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:19.449Z info: [Optional] Redis not initialized.
2025-06-26T21:43:20.212Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:34.812Z info: [Optional] Redis not initialized.
2025-06-26T21:43:36.179Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-26T21:43:50.258Z error: connect ECONNREFUSED ::1:27017, connect ECONNREFUSED 127.0.0.1:27017
2025-06-26T21:44:17.565Z info: [Optional] Redis not initialized.
2025-06-26T21:44:18.322Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-27T03:16:07.463Z info: [Optional] Redis not initialized.
2025-06-27T03:16:08.973Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-27T03:16:17.650Z debug: [indexSync] Clearing sync timeouts before exiting...
2025-06-27T03:19:09.423Z info: [Optional] Redis not initialized.
2025-06-27T03:19:10.741Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-27T03:19:15.742Z debug: [indexSync] Clearing sync timeouts before exiting...
-3
View File
@@ -1,3 +0,0 @@
2025-06-27T18:46:02.321Z info: [Optional] Redis not initialized.
2025-06-27T18:46:03.818Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-27T18:46:23.237Z debug: [indexSync] Clearing sync timeouts before exiting...
-3
View File
@@ -1,3 +0,0 @@
2025-06-28T15:35:37.951Z info: [Optional] Redis not initialized.
2025-06-28T15:35:39.502Z info: [Optional] IoRedis not initialized for rate limiters.
2025-06-28T15:36:09.559Z error: connect ECONNREFUSED ::1:27017, connect ECONNREFUSED 127.0.0.1:27017
-3
View File
@@ -1,3 +0,0 @@
2025-07-04T03:32:21.282Z info: [Optional] Redis not initialized.
2025-07-04T03:32:22.739Z info: [Optional] IoRedis not initialized for rate limiters.
2025-07-04T03:32:40.543Z debug: [indexSync] Clearing sync timeouts before exiting...
-4
View File
@@ -1,4 +0,0 @@
{"level":"error","message":"[mongoMeili] Error checking index convos: fetch failed","name":"MeiliSearchCommunicationError","stack":"MeiliSearchCommunicationError: fetch failed\n at node:internal/deps/undici/undici:13510:13\n at process.processTicksAndRejections (node:internal/process/task_queues:105:5)"}
{"level":"error","message":"[mongoMeili] Error checking index messages: fetch failed","name":"MeiliSearchCommunicationError","stack":"MeiliSearchCommunicationError: fetch failed\n at node:internal/deps/undici/undici:13510:13\n at process.processTicksAndRejections (node:internal/process/task_queues:105:5)"}
{"level":"error","message":"[mongoMeili] Error checking index convos: fetch failed","name":"MeiliSearchCommunicationError","stack":"MeiliSearchCommunicationError: fetch failed\n at node:internal/deps/undici/undici:13510:13\n at process.processTicksAndRejections (node:internal/process/task_queues:105:5)"}
{"level":"error","message":"[mongoMeili] Error checking index messages: fetch failed","name":"MeiliSearchCommunicationError","stack":"MeiliSearchCommunicationError: fetch failed\n at node:internal/deps/undici/undici:13510:13\n at process.processTicksAndRejections (node:internal/process/task_queues:105:5)"}
-4
View File
@@ -1,4 +0,0 @@
{"level":"debug","message":"[mongoMeili] Index convos already exists"}
{"level":"debug","message":"[mongoMeili] Index convos already exists"}
{"level":"debug","message":"[mongoMeili] Index messages already exists"}
{"level":"debug","message":"[mongoMeili] Index messages already exists"}
-4
View File
@@ -1,4 +0,0 @@
{"level":"debug","message":"[mongoMeili] Index messages already exists"}
{"level":"debug","message":"[mongoMeili] Index convos already exists"}
{"level":"debug","message":"[mongoMeili] Index messages already exists"}
{"level":"debug","message":"[mongoMeili] Index convos already exists"}
+5 -5
View File
@@ -1,6 +1,6 @@
const mongoose = require('mongoose');
const crypto = require('node:crypto');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { getCustomEndpointConfig } = require('@hanzochat/api');
const {
Tools,
@@ -10,8 +10,8 @@ const {
isAgentsEndpoint,
isEphemeralAgentId,
encodeEphemeralAgentId,
} = require('librechat-data-provider');
const { mcp_all, mcp_delimiter } = require('librechat-data-provider').Constants;
} = require('@hanzochat/data-provider');
const { mcp_all, mcp_delimiter } = require('@hanzochat/data-provider').Constants;
const {
removeAgentFromAllProjects,
removeAgentIdsFromProject,
@@ -96,7 +96,7 @@ const getAgents = async (searchParameter) => await Agent.find(searchParameter).l
* @param {string} params.spec
* @param {string} params.agent_id
* @param {string} params.endpoint
* @param {import('@librechat/agents').ClientOptions} [params.model_parameters]
* @param {import('@hanzochat/agents').ClientOptions} [params.model_parameters]
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
*/
const loadEphemeralAgent = async ({ req, spec, endpoint, model_parameters: _m }) => {
@@ -189,7 +189,7 @@ const loadEphemeralAgent = async ({ req, spec, endpoint, model_parameters: _m })
* @param {string} params.spec
* @param {string} params.agent_id
* @param {string} params.endpoint
* @param {import('@librechat/agents').ClientOptions} [params.model_parameters]
* @param {import('@hanzochat/agents').ClientOptions} [params.model_parameters]
* @returns {Promise<Agent|null>} The agent document as a plain object, or null if not found.
*/
const loadAgent = async ({ req, spec, agent_id, endpoint, model_parameters }) => {
+17 -17
View File
@@ -13,9 +13,9 @@ jest.mock('~/server/services/Config', () => ({
const mongoose = require('mongoose');
const { v4: uuidv4 } = require('uuid');
const { agentSchema } = require('@librechat/data-schemas');
const { agentSchema } = require('@hanzochat/data-schemas');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { AccessRoleIds, ResourceType, PrincipalType } = require('librechat-data-provider');
const { AccessRoleIds, ResourceType, PrincipalType } = require('@hanzochat/data-provider');
const {
getAgent,
loadAgent,
@@ -35,7 +35,7 @@ const { getCachedTools, getMCPServerTools } = require('~/server/services/Config'
const { AclEntry, User } = require('~/db/models');
/**
* @type {import('mongoose').Model<import('@librechat/data-schemas').IAgent>}
* @type {import('mongoose').Model<import('@hanzochat/data-schemas').IAgent>}
*/
let Agent;
@@ -1111,7 +1111,7 @@ describe('models/Agent', () => {
const agentId = 'ephemeral_test';
const endpoint = 'openai';
const originalModule = jest.requireActual('librechat-data-provider');
const originalModule = jest.requireActual('@hanzochat/data-provider');
const mockDataProvider = {
...originalModule,
@@ -1121,12 +1121,12 @@ describe('models/Agent', () => {
},
};
jest.doMock('librechat-data-provider', () => mockDataProvider);
jest.doMock('@hanzochat/data-provider', () => mockDataProvider);
expect(agentId).toBeDefined();
expect(endpoint).toBeDefined();
jest.dontMock('librechat-data-provider');
jest.dontMock('@hanzochat/data-provider');
});
test('should handle loadAgent functionality and errors', async () => {
@@ -2189,13 +2189,13 @@ describe('models/Agent', () => {
const actions = [
{
action_id: '123',
metadata: { version: '1.0', endpoints: ['GET /api/test'], schema: { type: 'object' } },
metadata: { version: '1.0', endpoints: ['GET /v1/chat/test'], schema: { type: 'object' } },
},
{
action_id: '456',
metadata: {
version: '2.0',
endpoints: ['POST /api/example'],
endpoints: ['POST /v1/chat/example'],
schema: { type: 'string' },
},
},
@@ -2212,10 +2212,10 @@ describe('models/Agent', () => {
test('should generate different hashes for different action metadata', async () => {
const actionIds = ['test.com_action_123'];
const actions1 = [
{ action_id: '123', metadata: { version: '1.0', endpoints: ['GET /api/test'] } },
{ action_id: '123', metadata: { version: '1.0', endpoints: ['GET /v1/chat/test'] } },
];
const actions2 = [
{ action_id: '123', metadata: { version: '2.0', endpoints: ['GET /api/test'] } },
{ action_id: '123', metadata: { version: '2.0', endpoints: ['GET /v1/chat/test'] } },
];
const hash1 = await generateActionMetadataHash(actionIds, actions1);
@@ -2284,8 +2284,8 @@ describe('models/Agent', () => {
},
},
endpoints: [
{ path: '/api/test', method: 'GET', params: ['id'] },
{ path: '/api/create', method: 'POST', body: true },
{ path: '/v1/chat/test', method: 'GET', params: ['id'] },
{ path: '/v1/chat/create', method: 'POST', body: true },
],
},
},
@@ -2391,7 +2391,7 @@ describe('models/Agent', () => {
});
test('should test ephemeral agent loading logic', async () => {
const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants;
const { EPHEMERAL_AGENT_ID } = require('@hanzochat/data-provider').Constants;
getCachedTools.mockResolvedValue({
tool1_mcp_server1: {},
@@ -2514,7 +2514,7 @@ describe('models/Agent', () => {
});
test('should handle ephemeral agent with no MCP servers', async () => {
const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants;
const { EPHEMERAL_AGENT_ID } = require('@hanzochat/data-provider').Constants;
getCachedTools.mockResolvedValue({});
@@ -2546,7 +2546,7 @@ describe('models/Agent', () => {
});
test('should handle ephemeral agent with undefined ephemeralAgent in body', async () => {
const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants;
const { EPHEMERAL_AGENT_ID } = require('@hanzochat/data-provider').Constants;
getCachedTools.mockResolvedValue({});
@@ -2584,7 +2584,7 @@ describe('models/Agent', () => {
});
test('should handle ephemeral agent with extremely large tool list', async () => {
const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants;
const { EPHEMERAL_AGENT_ID } = require('@hanzochat/data-provider').Constants;
const largeToolList = Array.from({ length: 100 }, (_, i) => `tool_${i}_mcp_server1`);
const availableTools = largeToolList.reduce((acc, tool) => {
@@ -3123,7 +3123,7 @@ describe('models/Agent', () => {
});
test('should handle loadEphemeralAgent with malformed MCP tool names', async () => {
const { EPHEMERAL_AGENT_ID } = require('librechat-data-provider').Constants;
const { EPHEMERAL_AGENT_ID } = require('@hanzochat/data-provider').Constants;
getCachedTools.mockResolvedValue({
malformed_tool_name: {}, // No mcp delimiter
+1 -1
View File
@@ -1,4 +1,4 @@
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { Banner } = require('~/db/models');
/**
+1 -1
View File
@@ -1,4 +1,4 @@
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const options = [
{
+1 -1
View File
@@ -1,4 +1,4 @@
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { createTempChatExpirationDate } = require('@hanzochat/api');
const { getMessages, deleteMessages } = require('./Message');
const { Conversation } = require('~/db/models');
+1 -1
View File
@@ -1,6 +1,6 @@
const mongoose = require('mongoose');
const { v4: uuidv4 } = require('uuid');
const { EModelEndpoint } = require('librechat-data-provider');
const { EModelEndpoint } = require('@hanzochat/data-provider');
const { MongoMemoryServer } = require('mongodb-memory-server');
const {
deleteNullOrEmptyConversations,
+1 -1
View File
@@ -1,4 +1,4 @@
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { ConversationTag, Conversation } = require('~/db/models');
/**
+2 -2
View File
@@ -1,5 +1,5 @@
const { logger } = require('@librechat/data-schemas');
const { EToolResources, FileContext } = require('librechat-data-provider');
const { logger } = require('@hanzochat/data-schemas');
const { EToolResources, FileContext } = require('@hanzochat/data-provider');
const { File } = require('~/db/models');
/**
+2 -2
View File
@@ -1,13 +1,13 @@
const mongoose = require('mongoose');
const { v4: uuidv4 } = require('uuid');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { createModels, createMethods } = require('@librechat/data-schemas');
const { createModels, createMethods } = require('@hanzochat/data-schemas');
const {
SystemRoles,
ResourceType,
AccessRoleIds,
PrincipalType,
} = require('librechat-data-provider');
} = require('@hanzochat/data-provider');
const { grantPermission } = require('~/server/services/PermissionService');
const { createAgent } = require('./Agent');
+1 -1
View File
@@ -1,5 +1,5 @@
const { z } = require('zod');
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { createTempChatExpirationDate } = require('@hanzochat/api');
const { Message } = require('~/db/models');
+2 -2
View File
@@ -1,6 +1,6 @@
const mongoose = require('mongoose');
const { v4: uuidv4 } = require('uuid');
const { messageSchema } = require('@librechat/data-schemas');
const { messageSchema } = require('@hanzochat/data-schemas');
const { MongoMemoryServer } = require('mongodb-memory-server');
const {
@@ -16,7 +16,7 @@ const {
jest.mock('~/server/services/Config/app');
/**
* @type {import('mongoose').Model<import('@librechat/data-schemas').IMessage>}
* @type {import('mongoose').Model<import('@hanzochat/data-schemas').IMessage>}
*/
let Message;
+1 -1
View File
@@ -1,4 +1,4 @@
const { logger } = require('@librechat/data-schemas');
const { logger } = require('@hanzochat/data-schemas');
const { Preset } = require('~/db/models');
const getPreset = async (user, presetId) => {

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