Compare commits

..
173 Commits
Author SHA1 Message Date
Hanzo AI cfef594484 chore(console2): 8.0.4 — per-host admin login client on admin.<brand> 2026-06-30 20:17:00 -07:00
blueandHanzo AI 3b4a66999e feat(config): per-host admin login client (admin.<brand> → admin-console)
On an admin console host (admin.hanzo.ai) resolve the OAuth client to the
admin-org app `admin-console` so IAM login resolves the global-admin identity
(owner=admin); every normal host keeps the brand cloud client (hanzo-cloud).

Isolated to src/config: `adminApp` per brand (ONE global admin-console app for
the reserved admin org), `isAdminHost()`, and a HOST-keyed cache (admin.hanzo.ai
and cloud.hanzo.ai are the same brand but must resolve to different clients — a
brand-keyed cache would collide). iamAppName + iamClientId travel together;
NEXT_PUBLIC_* overrides still win. Composes with the admin-mode UI (untouched).

+7 vitest (admin/normal host resolution, cache isolation, strict admin. prefix);
tsc --noEmit clean.
2026-06-30 20:17:00 -07:00
zeekayandClaude Opus 4.8 41b5385f73 test(e2e): ungate public sign-in smoke (runs without HANZO_PASSWORD)
The whole suite gated on HANZO_PASSWORD, so CI got zero signal without the prod
superuser secret. Split the credential-free sign-in render check into its own
describe so it always runs — asserts email/password + GitHub/Google + passkey.
Verified green against live console.hanzo.ai. Authenticated flows still gate on
HANZO_PASSWORD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:01:43 -07:00
Hanzo AI bdd27ec829 fix(test): cast entry() helper to CatalogEntry (union-spread) — 8.0.3 build green 2026-06-30 18:19:56 -07:00
Hanzo AI bd68c0ee23 fix: resolve package.json conflict → 8.0.3 2026-06-30 18:18:01 -07:00
Hanzo AI 20f1097810 merge(shell-ia): 2-level nav + Team/Settings + admin↔customer gating + /org/iam hardening → 8.0.3 2026-06-30 18:17:23 -07:00
Hanzo AI f3cb76e35b feat(console): 2-level shell IA + uniform sub-page contract + Team/Settings/Profile + admin↔customer gating (v8.0.1)
Shell + information architecture (owns DashboardShell, nav, header, registry
category/sub-page contract, ⌘K, Team/Settings/Profile). No product content
modules touched (referenced by id).

1. Two-level sidebar nav (Linear-style). Level 1 = categorized product list;
   clicking a product slides into its sub-nav (level 2) with a Back affordance;
   the open product follows the route. CSS-transform slide (.hz-slide,
   reduced-motion aware).
2. Uniform sub-page contract. Every product gets Overview · Settings · Status ·
   Logs · Metrics plus its declared specifics (productSubpages, match-core).
   A sub-page with no backend renders an honest ProductSubpageStub — never a
   404, never fabricated.
3. ⌘K jumps to ANY level. searchDestinations indexes products + declared
   specifics ("queues" → Compute › Tasks › Queues).
4. Category restructure. Deploy→Platform; new Training (Fine-tuning + ML
   Pipelines) and Settings (Team/Settings/Profile); Compute gains
   Kubernetes/Clusters/Tasks; Async category + Jobs entry KILLED (Tasks
   replaces Jobs). Dev/Web3 retained (real products).
5. Sidebar chrome. H mark only (no wordmark), no in-sidebar collapse toggle
   (moved to header), animated collapse (.hz-collapse, 264↔64).
6. Header cleanup. Removed user name + Sign out from the header; Sign out now in
   the footer wallet under Top up; footer user row → Profile; kept org/project/
   network(env) switchers + theme + help + notifications(bell→/alerts).
7. Team (org member mgmt: list/invite/role/remove + read-only Roles), org
   Settings (General/Branding), Profile (Account/Security/API Keys). Member
   mgmt runs over a NEW org-scoped /org/iam proxy so an ORG admin (not only a
   global admin) manages their own org — tenant-isolated server-side, with the
   body-owner cross-tenant write gap closed in a shared forwardIam (also
   hardens /admin/iam). DRY IAM envelope client (iam-envelope).

Admin↔customer surface gating (systemic): admin-only products (Providers, IAM,
KMS, Secrets, Audit, Clusters, Kubernetes) + admin sub-pages (Models › Routing)
are hidden from a customer's nav/launcher/⌘K and render a graceful "Managed by
Hanzo" notice on direct access instead of a hostile 403. Signal = global-admin
(isGlobalAdminAccount, DRY with OrgGate). Customer surfaces (Models browse,
Playground, Chat, API keys, Cost, Team, Compute/data) fully work per-org.

Network×org×project scope was already first-class (client.ts stamps X-Org-Id +
X-IAM-Org-Id + X-Project-Id + X-Environment; ScopeSwitcher env picker) — kept
as-is.

Verify: tsc clean · vitest 308 (incl. new sub-page/routing/destinations/org-
policy tests) · next build green (/org/iam route compiled) · Playwright desktop
+ 390px mobile, both personas.
2026-06-30 17:57:23 -07:00
Hanzo AI 1a8061bc34 chore: console 8.0.2 — Compute (K8s/Containers/Tasks/Training, no async/Jobs) + Playground UX 2026-06-30 17:53:29 -07:00
Hanzo AI c91a641fa9 Merge remote-tracking branch 'origin/feat/playground-ux' into deploy/console-8.0.2 2026-06-30 17:53:06 -07:00
Hanzo AI 0ca75cae70 Merge remote-tracking branch 'origin/feat/compute-pages' into deploy/console-8.0.2
# Conflicts:
#	src/lib/products/registry.tsx
2026-06-30 17:53:06 -07:00
Hanzo AI cbffe1dde4 feat(compute): Kubernetes, Containers, Training & Temporal Tasks consoles (v8.0.1)
Four resource consoles wired to REAL backends with honest states (never fabricated).

- Kubernetes (Compute): real DOKS clusters via PlatformApi (/paas → platform);
  stat cards + cluster table with provisioned CPU/RAM DERIVED from node-pool slugs
  (honest "—" for GPU/unknown); Import (honest) + Create → real provisionCluster.
- Containers (Compute): apps-inventory Workloads + Pods/Images/Namespaces/Events as
  :tab sub-routes over /paas (tolerant shapes, honest states); cluster-detail sidebar.
- Training (AI/Fine-tuning): live mlsvc /v1/train/{jobs,experiments} + /v1/ml/models
  via generalized /training proxy; stat cards, jobs table, training-loss chart,
  checkpoints, models, configs; New-job panel (base model from real aicatalog) →
  POST /v1/train/jobs (402 ResourceMeter surfaced honestly); Save-as-config.
- Tasks (Compute): Temporal console over hanzoai/tasks tasksd via NEW /tasksd
  minted-Bearer proxy; Workflows/Schedules/Queues/Workers/Activities :tab sub-routes;
  stat cards; workflow detail panel + step graph + 8 sub-tabs; engine-health /
  throughput / queue-util / recent rail. Public tasks TLS not live yet → honest
  states until the in-cluster engine is reachable.

Registry: removed the Async category + the Jobs entry; Tasks & Kubernetes → Compute.
Shared: promoted console primitives to ui/Metric.tsx (DRY; gpus/charts re-exports);
nodes.ts capacity derivation (+12 unit tests). tsc 0 / vitest 304 / next build green.
2026-06-30 17:44:21 -07:00
Hanzo AI d1a537c32c feat(playground): UX fixes — provider→model cascade, stop, markdown, per-user history, mobile (v8.0.1)
Fixes Dave's live-Playground complaints on console.hanzo.ai/playground.

- ModelPicker: rebuilt as a keyboard-navigable provider→model CASCADE (Zen-first
  provider rail + model pane with real context badge + $/Mtok + live dot;
  searchable; free-text fallback). New pure providers.ts (Zen-first grouping) +tests.
- ResponsePanel: render the completion as real markdown (new pure markdown.ts
  tokenizer + MarkdownView: fenced code blocks w/ copy, inline code, bold/italic,
  lists, headings, links) instead of plaintext; add a clear Stop control in the
  panel header during streaming (AbortController was already wired end-to-end).
- history.ts: namespace per user (owner/name), auto-persist every completed run
  so History auto-populates; one account never sees another's runs (+tests).
- ModelSettings: consolidated into a collapsible side-pane ATTACHED to the prompt
  builder (desktop) and a bottom sheet on mobile (new SettingsSheet, reusing the
  shell Dialog drawer pattern); shrink the oversized slider thumb + thin the track.
- ChatPlayground/Composer: responsive 3-zone layout stacks cleanly at ~390px with
  no horizontal scroll; a settings toggle opens the desktop pane / mobile sheet.

Verified: tsc --noEmit clean, vitest 311/311, next build green; Playwright desktop
+ 390px (cascade, keyboard nav, stop, markdown, mobile stack, settings sheet).

Wallet "Top up" deliberately NOT rerouted to pay.hanzo.ai: it is ALSO broken
(GET /v1/commerce/tenant -> 404 "unknown tenant", so the Square Web Payments SDK
never initializes). Breakage not moved; root cause reported for a backend fix.
2026-06-30 17:25:14 -07:00
2f1ae6b7c2 Native control planes (zero external link-outs) + Hanzo Functions dashboard (#15)
* feat(console2): native control planes (zero external link-outs) + Hanzo Functions dashboard

Three deliverables, one PR, all over the one /v1 surface.

1) No external link-outs (priority). The catalog's `external` kind is removed:
   CatalogEntry is module-only, ProductStatus is 'enabled' | 'soon'. The 14
   products that used to open another domain (Gateway, DNS, CDN, MPC, CLI, SDKs,
   API, IDE, Desktop, Registry, Metrics, Crawl, Studio, Console) are now native
   in-console routes rendering ONE shared NativeOverview (overviewFor(id) +
   overviewRoutes(id), the DRY twin of soonRoutes): header + summary, a REAL
   health band (probes PlatformApi.apps() with honest not-deployed/not-reporting
   states), key-fact cards, native-route actions, and INLINE docs. Content is a
   pure OverviewSpec per product (overview/spec.ts + resolve.ts, with a
   catalog-derived defaultSpec fallback). The external branches in open.ts,
   DashboardShell, AppLauncher, CommandPalette, ProductInterstitial, and
   OverviewModule are removed.

2) Hanzo Functions dashboard. FunctionsModule rebuilt into a tabbed product
   (Overview · Functions · Deployments · Triggers · Secrets · Settings) over the
   rich lib/api/functions.ts (GET /v1/functions*). Branded "Hanzo Functions" with
   the honest Fission engine badge. Overview: 6 KPI cards derived from real rows
   (deriveOverview, honest "—"), real-series sparklines + trendPct deltas, an
   "Invocations over time" LineChart with 1H/6H/24H/7D/30D toggles, an "Invocation
   status" Donut, and the shared FunctionsBrowser (table + DetailRail). Secrets is
   names-only. Reuses functions/{FunctionsTable,DetailRail,parts}.tsx unchanged.

3) Overview "Explore products" drops the enablement gate: no more
   Enabled/External/Soon badge; every product is open-for-all with Open (native) +
   a "Learn more" affordance to the native /discover/:id interstitial.

Idiom: strictly @hanzo/gui v5 shorthands. New tests: overview/resolve.test.ts.
npm run typecheck clean (0 errors); npm test 298/298 (31 files); every route
compiles + 200s on the dev server.

Drive-by: remove the bogus tracked node_modules self-symlink blob that broke
npm install/vitest (.gitignore already ignores node_modules/).

* docs(console2): fix overviewFor comment reference

---------

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-06-30 17:06:23 -07:00
zeekayandClaude Opus 4.8 48f3c170a0 fix(ci): base image public.ecr node:24-alpine (ghcr hanzoai/nodejs 403'd the runner)
The arcd runner can't pull ghcr.io/hanzoai/nodejs:24-alpine (403) — broke every
build since v0.7.32. Use the public ECR Docker-library mirror (no auth, no rate
limit), the v0.7.9 pattern; align the workflow pre-pull to 24-alpine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:33:27 -07:00
zeekay 9144dc6aba chore: sync lockfile for @hanzo/dashboard + @hanzo/data on 8.0.0 2026-06-30 16:31:09 -07:00
zeekayandClaude Opus 4.8 277abb5e15 feat(base-data): render real Base records via @hanzo/data
Wire published @hanzo/data@1.1.0 (peer @hanzo/gui 7.3.0) into the Base UI
as a composable, honest collection viewer.

- next.config.mjs: transpile @hanzo/data (ships TSX source, like @hanzo/gui)
- src/lib/base-data/fields.ts: pure baseCollectionToFields() mapping a Base
  collection schema -> @hanzo/data FieldDefinition[]. Covers text/number/bool/
  email/url/editor/date/autodate/select+multiSelect/json/relation/file/geoPoint;
  skips hidden+system fields (keeps id); handles modern `fields` and legacy
  `schema`/nested `options`.
- src/lib/base-data/api.ts: tiny BaseDataApi over a Base /v1 (listCollections,
  listRecords) -- raw REST + optional bearer, shares the app's typed ApiError.
- src/components/base-data/CollectionTable.tsx: client component; schema ->
  fields -> records -> @hanzo/data DataTable with honest
  loading/empty/error/not-found states (no fabricated rows).
- src/lib/base-data/fields.test.ts: 10 vitest cases for the mapping.

Verify: `npm run typecheck` clean; `npm test` 58/58.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:27:18 -07:00
zeekayandClaude Opus 4.8 b7595715ff feat(console2): embed Hanzo Base + Base look-and-feel (v0.7.11)
Base product module rendering the shared @hanzo/dashboard screens (published
on npm) via a per-user /superbase proxy (mints the user's IAM bearer); catalog
entry + page /base · /base/new. Plus the Base look: black #0a0a0a surface
aligning to the zinc-on-black identity. Consumes @hanzo/dashboard@0.2.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:26:59 -07:00
592e575ae3 feat(chat): demo-ready visual polish (presentation only) (#14)
Polish the Chat UI without touching any data wiring — Chat still calls the
real /v1/chat/completions through the keyless /ai proxy, and every state/API
call, prop, and honest empty/down state is preserved.

- Bubbles: assistant turns read as open text with a sparkle medallion +
  name + timestamp and comfortable line-height (dropped the heavy bordered
  card); the user turn is a refined right-aligned accent bubble. Both render
  light markdown — fenced code blocks (monospace, tinted card, optional lang),
  inline code chips, and bold.
- Markdown: new dependency-free renderer (chat/markdown.tsx) reusing the
  existing `fontFamily: 'monospace'` idiom — no heavy remark/rehype tree added
  (console2 ships no markdown lib).
- Welcome/empty state: sparkle avatar + "How can I help?" + 3–4 clickable
  suggested-prompt chips that fill the composer on click.
- Composer: one rounded, elevated input with a code-insert ({}) and a circular
  send affordance (hover/press states) over a subtle muted hint row.
- ChatView: read-only history thread matches the new bubble look + markdown.
- ChatListView: name cell reads as a link (weight + hover); table unchanged.

All Tamagui shorthands (bg/maxW/rounded/items/justify/self/p/px/py/gap) per
onlyShorthandStyleProps. tsc --noEmit clean; 245 vitest tests pass.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-06-30 16:04:25 -07:00
Hanzo AI 68cfbc1cd5 chore: console2 v0.7.32 — Playground parity (single-model 3-col compose) onto org-switcher main 2026-06-30 16:00:11 -07:00
Hanzo AI fa52cc74e1 Merge remote-tracking branch 'origin/feat/playground-parity' into deploy/playground-0.7.32 2026-06-30 15:59:54 -07:00
Hanzo AI 48e84ee58d feat(playground): single-model 3-column composer to mockup parity (v0.7.31)
Redesign the Playground from the multi-model compare board into the polished
3-zone surface in the mockup, reusing the existing AI binding (runner/stream/
PlaygroundApi) — nothing about model output, tokens or cost is fabricated.

Layout (per mockup):
- Header "Playground" + subtitle + Save prompt / </> code / Share actions.
- Tabs: Chat · Completions · Embeddings · Audio · Vision.
- Composer (left): model chip with a REAL "context" badge + searchable picker
  over the live catalog (aicatalog.fetchCatalog, 375 models), System prompt +
  user/assistant turns with char counts, and a footer of Add message / Upload
  (image→vision) / {} Variables / Run (⌘↵ + caret). Completions collapses to a
  single Prompt card.
- Examples (labelled starters w/ model chips) + History (real local prior runs,
  honest-empty) beneath the composer.
- Right rail: Response/Logs panel (real completion, USAGE tokens + COST from the
  model's real $/Mtok, "—" when absent) and Model settings (Temperature 0.7,
  Top P 0.9, Max tokens, Stop, Advanced: freq/presence penalty + seed).

Real behaviors, no backend: Save prompt (local library → Examples), Code (real
cURL/JSON request preview), Share (encode composer into ?p= link, restore on
load), {{variables}} substitution, ⌘↵ to run.

Wiring: Temperature/top-p/max-tokens/stop + advanced penalties/seed flow through
paramsOf into the request (sent only when set). Removes the compare-only files
(ComparePlayground/CompareColumn/AddModel/useCompare/SettingsControls).

Tests: +37 unit tests (params/variables/share/prompts/request-preview/compose/
relative); 282 total green. tsc clean, next build green.
2026-06-30 15:40:45 -07:00
Hanzo AI 11763882f8 fix(org): working org switcher + create-org (multi-tenant onboarding)
The OrgSwitcher collapsed to a STATIC non-clickable label whenever the admin-gated
org list returned empty (every tenant: /admin/iam is global-admin-only → 403), and
there was NO create-org affordance. Now: the trigger is ALWAYS an interactive
Popover (current org always shown + filter + switch), with a 'Create organization'
flow that posts to /onboard and scope-switches into the new org. /onboard relaxed:
an existing-org user can create an ADDITIONAL org (created WITHOUT moving them — a
move would strip a global admin's status + orphan their current org); zero-org
first-run still creates+joins. v0.7.31
2026-06-30 15:03:37 -07:00
6a74c141d5 Debrand: replace Casdoor name with Hanzo IAM in comments/docs/aliases (#13)
Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-06-30 14:57:27 -07:00
Hanzo AI df5bf34218 feat(data): KV/SQL/Datastore/Object Storage/Vector pages to mockup parity
Decomplect the shared resourceModule factory (backs all managed-data kinds)
into ONE spec-driven console, upgrading every data page at once:

- resource/logic.ts   pure per-kind ResourceSpec + fleet/status/endpoint/
  snippet helpers (RESOURCE_SPECS, 16 unit tests). The list API carries
  lifecycle facts only, so usage metrics are honest "absent" (null -> "—"),
  never fabricated.
- resource/parts.tsx  ResourceStat (honest "—" + "Awaiting metering"),
  StatusDonutCard, TabBar, SnippetBlock, CopyField, SectionCard.
- resource/ResourceListView.tsx  the polished home console: REAL fleet stat
  row + tabs (Overview / <instances> / Access / Metrics / [tool] / Settings)
  + right rail (status donut, quick actions, quick start) + the real create
  flow (POST /v1/<kind>, password shown once).
- resource/ResourceInstanceView.tsx  tabbed single-instance console
  (Overview / Access / Settings) with a real confirmed delete.
- ResourceModule.tsx  thin router; same resourceModule/resourceRoutes
  exports -> zero registry churn, all 5 graded kinds (+docdb/search) tuned.

Honest by construction: status breakdown from real lifecycle, create/delete
real, the data-plane tool tab (Query/Browser/Explore) is an explicit
"coming soon" with the real connect snippet — never a fake success.

Gates: tsc --noEmit clean, vitest 245 pass, next build 14/14. Bump 0.7.30.
2026-06-30 14:16:22 -07:00
Hanzo AI 3c54be90f7 feat(catalog): unify model/provider/plan client; Models & Providers parity
One spine for the Models, Providers, and (later) hanzo.ai / @hanzo/dev / desktop
surfaces, all read through the authenticated /ai proxy. Real data, honest states.

aicatalog (the spine):
- Joins the TWO real catalog record shapes — first-party Zen (`context`+`name`)
  and third-party (`contextWindow`+`id` like openai/gpt-5). Fixes context showing
  "—" on 339/375 models and cross-references availability by the stable id.
- Adds priceBucket/matchesQuery/modelTypes/CONTEXT_BUCKETS filters, the provider
  `verified` flag, modelDisplayName (strips "OpenAI: " row prefixes), and
  fetchPlans() reading the real /v1/plans subscription tiers (rpm/tpm/quota),
  honest-empty on 401/404.

Components (self-contained, prop-driven, liftable to @hanzo/ui):
- ProviderLogo — monochrome avatar resolved by provider name: first-party Sparkles
  mark, curated known-provider glyphs, initials chip otherwise. No external URLs.
- SelectMenu — reusable Popover dropdown for the Providers filter row.

Models page: ProviderLogo in rows + provider column, search box, collapsible
type/provider/pricing filters, stacked in/out pricing, real context via the join,
and subscription-plan badges in the detail (model tier -> /v1/plans limits).

Providers page: Explore / My Providers / Custom Models / BYO Weights tabs (the
latter three honest, pointing at the real add/deploy flow), search + type/context/
pricing/verified/sort row, ProviderLogo + Verified badge on cards and detail.

/ai proxy: allow v1/plans through the authenticated proxy (least-privilege add).
Overview: already at parity on the real usage ledger — verified, not rebuilt.

Gates: tsc --noEmit clean, 230/230 vitest (incl. 12 new aicatalog tests),
next build green (14/14). Bump 0.7.28 -> 0.7.29.
2026-06-30 13:21:49 -07:00
Hanzo AI 38c035022f Merge remote-tracking branch 'origin/feat/mobile-responsive-chat'
# Conflicts:
#	package.json
2026-06-30 13:03:06 -07:00
Hanzo AI b9e200f58a Merge remote-tracking branch 'origin/feat/playground-multi-model' 2026-06-30 13:03:06 -07:00
Hanzo AI 1552bb1874 Merge remote-tracking branch 'origin/feat/finetuning-unsloth-class'
# Conflicts:
#	src/components/products/FinetuningModule.tsx
2026-06-30 13:03:06 -07:00
Hanzo AI ca8017f958 fix(ci): base → ghcr.io/hanzoai/nodejs:24-alpine (ghcr-only policy)
Re-pin off public.ecr.aws (rate-limited builds + violates ghcr-only). nodejs is the
Node.js runtime mirror (hanzoai/node is the blockchain node). COPY-before-install
(the real Kaniko --single-snapshot fix) is preserved.
2026-06-30 13:02:43 -07:00
hanzo-dev 1d2b2b040d feat(routing): cloud.hanzo.ai is the canonical console host
- default SSR/build host -> cloud.hanzo.ai (was console.hanzo.ai); brand still
  resolves by hostname suffix, so console.hanzo.ai/console2.hanzo.ai (which now
  301 to cloud.hanzo.ai at the gateway) still map to brand hanzo.
- cloudUrl() stays SAME-ORIGIN: the SPA calls cloud.hanzo.ai/v1, and the gateway
  routes that /v1 to the cloud package (global IAM-JWT + rate-limit) — so
  "everything goes through the gateway" holds with the cookie first-party (no CORS).
- console product + experiments external links -> cloud.hanzo.ai.
2026-06-30 12:35:48 -07:00
hanzo-devandGitHub e3a400b1ac Merge pull request #12 from hanzoai/fix/console2-base-image
fix(ci): console2 base image → public ECR node:22-alpine (unblock v0.7.28 build)
2026-06-30 12:28:40 -07:00
Hanzo AI 62fad7cb48 fix(ci): base image → public ECR node:22-alpine (the one CI pre-pulls)
The v0.7.26/27/28 image builds 403'd: today's base-image thrashing pointed the
Dockerfile at ghcr.io/hanzoai/nodejs:24-alpine, which is PRIVATE — the host
builder's ghcr login is scoped to console2's own package and can't read the
separate nodejs package (403 Forbidden on the base pull).

build-image.yml still pre-pulls + caches public.ecr.aws/docker/library/node:
22-alpine (its comment even says the Dockerfile uses it). Re-align the Dockerfile
to that public base: no auth, no 403, and the warm ARC runner serves it straight
from the cache the pre-pull step populates. node 22 is the version v0.7.25
shipped on. The Kaniko COPY-before-install + retry-hardened npm install fixes are
untouched.
2026-06-30 12:28:32 -07:00
hanzo-devandGitHub 6470f8e41b Merge pull request #11 from hanzoai/feat/zero-trust-landing
feat(zero-trust): operational landing — KPIs, post-quantum posture, mesh topology
2026-06-30 12:24:19 -07:00
Hanzo AI 7a6e3f3f62 Merge remote-tracking branch 'origin/main' into feat/zero-trust-landing
# Conflicts:
#	package.json
2026-06-30 12:23:52 -07:00
Hanzo AI 890d98d32d feat(zero-trust): operational landing — KPIs, PQ posture, mesh topology
Upgrades the Zero Trust surface from five plain tabbed tables into a cloud-console
landing page (the Image #11 operational style), wired to the REAL /v1/zt/* mesh:

- Overview: KPI cards (routers/services/identities/sessions/policies — real
  counts or honest em-dash), a router→service→identity topology strip (only
  nodes that came back; status-coloured), and a post-quantum POSTURE card tying
  the data plane together: Object Storage (S3) ⇄ Zero Trust over Hanzo zap
  (ML-KEM-768 key exchange · ML-DSA-65 signatures). PQ-session % shows only when
  the backend reports a real cipher — never guessed.
- Honest by construction: each of the 5 sections loads independently; when the
  zt backend isn't mounted on this host every section degrades to ONE
  BackendStateCard while the (true) PQ posture still shows. No fabricated rows,
  trends, or telemetry.
- DRY: the five detail tabs reuse the SAME zeroTrustSurfaces + ForwardSurface
  (now exported from ConsoleFeatureModule) — one zero-trust surface, not two.
  ZeroTrustModule moved out of the generic table shell into its own rich module
  (mirrors Embeddings/Overview); registry repointed.
- Pure logic in zt/logic.ts (counts, topology, posture) with vitest coverage.

tsc --noEmit (strict) clean. v0.7.26.
2026-06-30 12:23:08 -07:00
Hanzo AI ef8e8fbdf6 fix(ci): COPY before npm install — Kaniko --single-snapshot dropped node_modules
ROOT CAUSE (proven in the build log): the install's own 'test -f next/dist/bin/next'
PASSED, then 'COPY . .' ran, then the build RUN couldn't find next. Under Kaniko
--single-snapshot, a COPY that follows RUN npm install in the same stage drops the
RUN's freshly-created node_modules. The old multi-stage Dockerfile avoided this (no
COPY after install); my single-stage reorder reintroduced it. Fix: COPY all source
FIRST, then install, then build — node_modules is created by the last RUNs so nothing
clobbers it. This (not the base registry or the .bin symlink) was the real blocker.
2026-06-30 12:21:00 -07:00
Hanzo AI d78dd1ff52 fix(ci): retry-hardened install + assert next is present (self-heal partial tree)
The full @hanzo/gui dep tree intermittently installs ~80 packages short (incl next)
— npm reports success but skips them, surfacing later as 'next not found'. Now:
retry-hardened fetch, an explicit 'npm install next' repair if its bin is missing,
and a hard 'test -f' assert so a partial install fails loudly at the install step
(with a clear cause) rather than at build.
2026-06-30 12:15:02 -07:00
Hanzo AI cf4c66e977 fix(ci): base → ghcr.io/hanzoai/nodejs:24-alpine + invoke next directly
Two fixes for the broken builds:
1. Base image: use ghcr.io/hanzoai/nodejs:24-alpine (our mirror of node:24-alpine)
   per the ghcr-only policy. (public.ecr.aws rate-limited; mirror.gcr.io was a
   stopgap. hanzoai/node is the blockchain node — nodejs is the Node.js runtime.)
   Bumped 22→24.
2. Invoke next via node_modules/next/dist/bin/next, not the .bin/next symlink: the
   @hanzo/gui RN dep tree intermittently drops the symlink under the build npm
   ('sh: next: not found') even though the next package installs. Direct invocation
   is symlink-independent. Build + runner CMD both updated.
2026-06-30 12:07:14 -07:00
Hanzo AI 15b2dedd42 fix(ci): base image → mirror.gcr.io (ECR-public was rate-limiting builds)
public.ecr.aws/docker/library/node:22-alpine started returning TOOMANYREQUESTS on
the base-image pull (the real cause of the build failures — a partial pull also
surfaced as the misleading 'next: not found'). mirror.gcr.io/library/node:22-alpine
is Google's Docker Hub mirror — reliable, not rate-limited. Combined with the
single-stage install+build, the v0.7.27 admin-fix build is unblocked.
2026-06-30 11:58:13 -07:00
Hanzo AI 117f19e6cf fix(ci): single-stage install+build — stop losing node_modules/.bin/next in Kaniko
The deps→build cross-stage COPY of node_modules intermittently dropped
node_modules/.bin/next under Kaniko (sh: next: not found at 'npm run build',
despite a clean install adding ~850 packages). Reproduced: local install is fine,
so it's the cross-stage symlink handoff. Install + next build now run in ONE stage
on the same filesystem — the seam is gone. Runner stage unchanged (runtime next
start worked through its COPY all along). Unblocks the v0.7.27 admin-fix build.
2026-06-30 11:53:36 -07:00
Hanzo AI 69c2b3535d chore: v0.7.27 — admin redirect-loop fix 2026-06-30 09:47:18 -07:00
Hanzo AI e703c25bc6 fix(console): P0 admin.hanzo.ai redirect-loop — admin-org membership = global admin
IAM never populates isGlobalAdmin AND admin-org members carry isAdmin=false, so the
gate's '&& isAdmin' made NO ONE a global admin → every legitimate admin was bounced
from admin.hanzo.ai to the console (redirect loop). Fix: membership in the reserved
'admin' org alone = global admin. Client OrgGate + server identity.ts. Tenants
(owner!=='admin') stay non-global → admin remains locked to them. v0.7.26
2026-06-30 09:46:54 -07:00
z 8435eb6fa1 merge origin/main (metrics-led Overview) — lead home with the comprehensive OverviewDashboard
Resolve the home-lead fork to the full OverviewModule (Image-#3 vision: metric
cards + sparklines + tokens/spend/by-model charts + activity + quick actions +
wallet + system status) over the narrower AiMetricsModule lead. AiMetrics stays
its own route. One home dashboard, one way.
2026-06-30 00:34:18 -07:00
z a5da0bd863 merge Functions — canonical ui/Charts.tsx wins (its dup was unused orphan; removed)
Functions' add/add ui/Charts.tsx was dead code (no consumer imported CHART_COLORS/
STATUS_COLORS). Take the canonical superset (Sparkline/LineChart/BarChart/Donut/
BarRows). One chart module, one palette name, no backwards-compat alias. Fixed the
legend swatch to the file's own styled-div idiom (was a themed bg with a hex).
2026-06-30 00:29:29 -07:00
Hanzo AI 47f296b2fb feat(console): metrics-led Overview — lead home with the real AI usage dashboard
The Overview was a product grid; now it leads with the same tested AI Metrics
dashboard (requests/tokens/spend over time + per-model breakdown, real per-org
data) — the Langfuse-style project home — with the product catalog below for
navigation. DRY: reuses AiMetricsModule exactly. v0.7.25
2026-06-30 00:29:22 -07:00
z 584a73ff53 Merge remote-tracking branch 'origin/feat/gpus-page' into integrate/console2-v0.7.23 2026-06-30 00:26:56 -07:00
z c4ba6846b8 Merge remote-tracking branch 'origin/feat/machines-page' into integrate/console2-v0.7.23 2026-06-30 00:26:48 -07:00
z 286b1af431 merge main (v0.7.24: AI Metrics + mobile shell) → v0.7.25 2026-06-30 00:26:48 -07:00
z 884772402c console2: consolidate Overview+Embeddings charts → one ui/Charts.tsx (DRY)
Merge Overview + Embeddings; collapse the two duplicate chart implementations
(components/charts/Charts.tsx + components/ui/Charts.tsx) into ONE canonical
ui/Charts.tsx superset built on pure @hanzo/gui primitives (promotable to the
shared @hanzo/gui package as a charts category). Repoint OverviewModule +
embeddings/OverviewView. tsc clean.
2026-06-30 00:26:37 -07:00
z 53f188d131 console2: Functions — serverless (OpenFaaS-class) on real /v1, honest states
Compute → Functions (6 tabs: Overview/Functions/Deployments/Triggers/Secrets/
Settings): functions table, invocations-over-time, status donut, selected-function
detail rail (about/triggers/recent invocations + View/Edit/Delete). Wired to the
real functions backend with honest not-configured/empty/error states; metrics from
the usage ledger degrade to '—'. currentOrg-scoped; destructive actions confirm /
honest-disabled. Fixed 2 leftover tsc errors (icon→ReactElement; dropped dead
?? true after non-nullish !bool). tsc clean.
2026-06-30 00:22:32 -07:00
z 7f0f7a683f console2: GPUs — real GPU/cluster inventory + honest derived/not-configured states
Compute → GPUs (7 tabs): GPU model+count derived from cluster nodeSize slug
(gpuSpecOf, pure+tested), per-GPU rows/telemetry from /paas/gpus when present.
Three honest renders: not-configured (501) · clusters-only (real derived counts +
distribution donut + top-clusters) · inventory-live (full table+telemetry).
Telemetry-only metrics (util/mem/temp/sparklines) and GPU-hours have no backend →
'—'. Est-cost reads the usage ledger, degrades to '—' (never relabels account
total as GPU cost). New compute.ts (zero platform.ts edits); registry gpus entry
upgraded. tsc clean · next build 14/14 · 18 compute tests green.
2026-06-30 00:20:31 -07:00
z ddd5aeda18 console2: Machines — real DOKS-cluster-node inventory, honest states
Compute → Machines as one node of a real DOKS cluster pool (GET /v1/org/{org}/
cluster → machinesFromClusters: sum(pool.count) rows, vCPU/RAM from the DO slug,
monthly cost from the platform bill-from table labeled 'est.'). 8 tabs, 7 metric
cards, status donut, machine table, selected-machine right rail. Per-node CPU%/
MEM%/GPU/uptime/IP the control plane doesn't expose → '—' (never fabricated).
Reboot/Terminate honest-disabled (no real endpoint at that altitude). currentOrg-
scoped; not-configured (501, PaaS token disabled) is the honest first state.
Additive platform.ts only. tsc clean · 20 machines tests green.
2026-06-30 00:20:11 -07:00
Hanzo AI b78f48e0b3 Merge branch 'feat/ai-metrics-o11y' 2026-06-30 00:18:54 -07:00
Hanzo AI c75da83217 feat(console): AI Metrics page + fix Cost usage mapping (real model/tokens, cents)
The /v1/billing/usage ledger carries per-request rows (metadata.model, totalTokens,
amount in CENTS). The Cost page flattened it with a generic reader → every row
'Usage', no tokens, and cost ×100 ($1.06 shown as $106). Now: looksLikeLedger()
+ perModel() roll up by real model name with correct cents — shared (DRY) with the
new AI Metrics module (StatTiles: requests/tokens/spend/balance, usage-over-time,
per-model breakdown, recent activity) reading the same real per-org data. o11y
RuntimeNotice points at AI Metrics (which has data) when traces are uninitialized.
32 unit tests (billing/aimetrics/format), tsc+build green.
2026-06-30 00:18:52 -07:00
z 4679d1af14 release(console2): v0.7.23 — Playground multi-model + UX sweep live to prod
Ships the merged work (multi-model compare Playground with abort-safe billing,
the unified EmptyState/Cost sweep) as a public semver. Overview/Embeddings/
Machines/GPUs/Functions land in v0.7.24 as the conflict integration completes.
2026-06-30 00:08:07 -07:00
Hanzo AI f084d89918 Merge remote-tracking branch 'origin/feat/embeddings-page' into integrate/console2-v0.7.23
# Conflicts:
#	app/(dashboard)/page.tsx
#	src/components/products/stores/StoreListView.tsx
2026-06-30 00:02:06 -07:00
Hanzo AI d284a8e076 Merge remote-tracking branch 'origin/feat/overview-dashboard' into integrate/console2-v0.7.23
# Conflicts:
#	app/(dashboard)/page.tsx
2026-06-30 00:00:16 -07:00
Hanzo AI 55e14e85f3 feat(console): mobile/responsive shell + floating AI chat + wallet identity → v0.7.23
Comprehensive responsive pass so the console works at phone/tablet/laptop/
desktop, plus a floating assistant reachable from every page and a wallet that
doubles as the signed-in identity + a click-through to in-console billing.

P1 — Mobile/responsive shell (DashboardShell.tsx)
- Layout responsiveness is CSS-driven (Tamagui v5 media style props:
  `display="none"` + `$lg={{ display:'flex' }}`), NOT a JS `useMedia()` branch —
  the server and the client's first paint emit identical markup, so there is no
  hydration mismatch and no flash of the compact layout on a wide screen.
- < lg (1024px): the persistent sidebar is HIDDEN behind a hamburger in the
  topbar that opens the SAME nav as a left drawer (closes on select/backdrop);
  the topbar condenses — org/scope/user/sign-out fold into one right-side menu
  so nothing overflows at 375px; the Apps button collapses to icon-only.
- ≥ lg: the persistent sidebar is always on (collapsible) with the full inline
  topbar controls.
- Fixed the collapsed-rail icons (20px, 44px hit targets — were too small).
- The nav body (SidebarNav) is shared by the desktop sidebar and the drawer (DRY).
- Responsive content padding ($md), wallet always reachable.

P2 — Floating AI chat (FloatingChat.tsx, mounted once in the dashboard layout)
- A chat bubble fixed bottom-right on every page. Click → opens the assistant:
  a full-screen sheet < lg, a ~380×560 popover ≥ lg (sizing is CSS-driven too).
- REUSES the one working chat surface (ChatConversation → AiApi.chat → the
  keyless /ai proxy). No AI rebuilt. A new `compact` mode on ChatConversation
  drops the page header + fixed min-height so it fills the sheet; "History"
  deep-links to the full /chat page.

P3 — Wallet as identity + billing click-through (SidebarWallet.tsx)
- The wallet now shows the signed-in user's avatar (IAM photo, else initials)
  + display name from useSession, alongside the live balance.
- Clicking the wallet/identity routes into the in-console Cost module (/cost:
  balance, usage, invoices). "Top up" still deep-links to the brand billing
  portal (billing.hanzo.ai) — payment is never rebuilt.

Verification
- `tsc --noEmit` clean; `next build` succeeds (14/14 pages).
- Playwright (headless) verified live at 375/768/1024/1440 with the backend
  mocked: persistent-sidebar↔hamburger swap, drawer open/close, full-screen chat
  sheet, account menu, wallet identity — all PASS, and NO React hydration
  mismatch. Apps-label collapse confirmed (icon-only 375 / labeled 1440).
2026-06-29 23:52:56 -07:00
z 44435ca291 console2: Embeddings — 6-tab vector product on real /v1, honest-empty (upgrade StoresModule)
Upgrades the thin StoresModule in place into the Embeddings product (Overview·
Explore·Collections·Jobs·Models·Settings): collections=per-org vector stores
(get-stores), Explore=POST /v1/search on {owner}-{store}-docs (server-resolved
owner — store is only a query param), Models+generate=/v1/models + /v1/embeddings
via the keyless /ai proxy. currentOrg-scoped throughout; no secret reaches the
browser; /ai allow-list NOT widened. Honest-empty everywhere a field/endpoint is
absent (get-cloud-usages not merged yet → metric cards degrade to —; RRF drops
score → —; no vector point-lookup → empty) — never fabricated. Deletes the dead
StoresModule/StoreListView (registry was the only consumer); reuses StoreEditView.
Drive-by: corrects the stale 'built-in' admin-policy assertion to false (gate code
untouched). tsc clean · vitest 67/67 · next build 14/14.
2026-06-29 23:48:33 -07:00
39d88d0cfd feat(console): side-by-side multi-model compare Playground (#7)
* feat(console): side-by-side multi-model compare Playground

Tabbed Playground (Chat/Completions/Embeddings/Audio/Vision) whose marquee is a
side-by-side compare board: ONE shared System+User (or Prompt) broadcasts to N
model columns that run in PARALLEL through the keyless /ai proxy, each streaming
its own output while reporting REAL tokens, cost (catalog $/Mtok) and latency
(time-to-first-token + total). Per-column model + optional settings override with
a sync-across-all toggle; one column erroring/stopping never disturbs the others.
Single-model mode is one column. Examples seed the prompt; History records a run.

- app/ai/[...path]: stream the upstream body through (real TTFT) instead of
  buffering; allow-list v1/audio/speech for the Audio (TTS) tab. Both additive +
  backward-compatible for existing non-streaming callers.
- lib/api/playground.ts: add streamChat (SSE, stream_options.include_usage),
  embeddings, speech — additive to PlaygroundApi.
- Models selectable from the LIVE catalog (CloudModelApi → /ai/v1/models +
  /v1/pricing/models), the same source the Models page uses. No mocks.
- Pure, unit-tested core (cost/SSE-parse/runner/history): 28 vitest tests.

* fix(console): cancel upstream on abort + release reader + honest stopped state

Red review follow-ups on the compare Playground (no scope creep):

1. [MED] Client abort now cancels upstream generation (stops over-billing the
   user's own org + leaking N sockets after Stop/tab-switch/unmount):
   - app/ai/[...path]/route.ts: pass `signal: req.signal` to the gateway fetch so
     a browser->proxy abort propagates proxy->gateway.
   - ComparePlayground: useEffect cleanup calls compare.cancel() on unmount;
     cancel() aborts every column's AbortController.

2. [MED] runner.ts: wrap the SSE read loop in try/finally with
   `reader.cancel().catch(()=>{})` so the ReadableStream + connection are
   released on every exit — normal [DONE], a thrown mid-stream error chunk, or an
   abort. New test asserts the reader is cancelled on a mid-stream error chunk.

3. [LOW] An aborted run no longer renders/records as success: new 'stopped'
   RunPhase, CompareColumn shows a Stopped state, History uses
   ok = !error && !aborted and shows a 'stopped' badge.

tsc --noEmit clean; vitest 29 playground tests green (28 + new MED-2 test).

---------

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-06-29 23:45:35 -07:00
Hanzo AI 1fc3c6eca6 fix(console): cancel upstream on abort + release reader + honest stopped state
Red review follow-ups on the compare Playground (no scope creep):

1. [MED] Client abort now cancels upstream generation (stops over-billing the
   user's own org + leaking N sockets after Stop/tab-switch/unmount):
   - app/ai/[...path]/route.ts: pass `signal: req.signal` to the gateway fetch so
     a browser->proxy abort propagates proxy->gateway.
   - ComparePlayground: useEffect cleanup calls compare.cancel() on unmount;
     cancel() aborts every column's AbortController.

2. [MED] runner.ts: wrap the SSE read loop in try/finally with
   `reader.cancel().catch(()=>{})` so the ReadableStream + connection are
   released on every exit — normal [DONE], a thrown mid-stream error chunk, or an
   abort. New test asserts the reader is cancelled on a mid-stream error chunk.

3. [LOW] An aborted run no longer renders/records as success: new 'stopped'
   RunPhase, CompareColumn shows a Stopped state, History uses
   ok = !error && !aborted and shows a 'stopped' badge.

tsc --noEmit clean; vitest 29 playground tests green (28 + new MED-2 test).
2026-06-29 23:43:16 -07:00
Hanzo AI 86e26e5e54 feat(console): mobile/responsive shell + floating AI chat + wallet identity → v0.7.23
Comprehensive responsive pass so the console works at phone/tablet/laptop/
desktop, plus a floating assistant reachable from every page and a wallet that
doubles as the signed-in identity + a click-through to in-console billing.

P1 — Mobile/responsive shell (DashboardShell.tsx)
- Layout responsiveness is CSS-driven (Tamagui v5 media style props:
  `display="none"` + `$lg={{ display:'flex' }}`), NOT a JS `useMedia()` branch —
  the server and the client's first paint emit identical markup, so there is no
  hydration mismatch and no flash of the compact layout on a wide screen.
- < lg (1024px): the persistent sidebar is HIDDEN behind a hamburger in the
  topbar that opens the SAME nav as a left drawer (closes on select/backdrop);
  the topbar condenses — org/scope/user/sign-out fold into one right-side menu
  so nothing overflows at 375px; the Apps button collapses to icon-only.
- ≥ lg: the persistent sidebar is always on (collapsible) with the full inline
  topbar controls.
- Fixed the collapsed-rail icons (20px, 44px hit targets — were too small).
- The nav body (SidebarNav) is shared by the desktop sidebar and the drawer (DRY).
- Responsive content padding ($md), wallet always reachable.

P2 — Floating AI chat (FloatingChat.tsx, mounted once in the dashboard layout)
- A chat bubble fixed bottom-right on every page. Click → opens the assistant:
  a full-screen sheet < lg, a ~380×560 popover ≥ lg (sizing is CSS-driven too).
- REUSES the one working chat surface (ChatConversation → AiApi.chat → the
  keyless /ai proxy). No AI rebuilt. A new `compact` mode on ChatConversation
  drops the page header + fixed min-height so it fills the sheet; "History"
  deep-links to the full /chat page.

P3 — Wallet as identity + billing click-through (SidebarWallet.tsx)
- The wallet now shows the signed-in user's avatar (IAM photo, else initials)
  + display name from useSession, alongside the live balance.
- Clicking the wallet/identity routes into the in-console Cost module (/cost:
  balance, usage, invoices). "Top up" still deep-links to the brand billing
  portal (billing.hanzo.ai) — payment is never rebuilt.

Verification
- `tsc --noEmit` clean; `next build` succeeds (14/14 pages).
- Playwright (headless) verified live at 375/768/1024/1440 with the backend
  mocked: persistent-sidebar↔hamburger swap, drawer open/close, full-screen chat
  sheet, account menu, wallet identity — all PASS, and NO React hydration
  mismatch. Apps-label collapse confirmed (icon-only 375 / labeled 1440).
2026-06-29 23:41:42 -07:00
Hanzo AI a2c925ab7b feat(finetuning): Unsloth-class, HuggingFace-native cloud training UI
Upgrades the Fine-tuning module from a read-only job list into a full training
surface wired to the cloud broker (hanzoai/ai /v1/finetune/*) via a same-origin,
user-scoped proxy.

- app/training/[...path]/route.ts  same-origin proxy → /v1/finetune/* (cookie
                                   forward, the get-account pattern; allow-listed
                                   sub-paths; org re-resolved server-side). No key
                                   or HF token ever reaches the browser.
- src/lib/api/finetune.ts          typed client (unwraps the {status,msg,data}
                                   envelope incl. the HTTP-200 error shape)
- finetuning/HfPicker.tsx          browse/search HuggingFace models + datasets;
                                   private/gated flagged; pick → start a job
- finetuning/NewJobPanel.tsx       base-model + dataset pickers, LoRA/QLoRA/full,
                                   Recommended preset that just works, GPU with a
                                   live time/cost estimate, Start + Save-as-config
- finetuning/JobsView.tsx          jobs list (honest empty/error states)
- finetuning/JobDetail.tsx         live status polling, checkpoints, GPU-hours,
                                   Deploy-to-inference, Cancel
- finetuning/logic.ts(+test)       pure formatters + cost mirror + config store (17
                                   vitest cases)
- FinetuningModule.tsx             tabbed shell (Jobs/New/Models/Datasets/Configs);
                                   internal nav, no new registry routes

tsc --noEmit clean · 17 finetuning vitest pass · next build clean.
2026-06-29 23:37:33 -07:00
Hanzo AI a3f7412653 feat(overview): real-data Overview dashboard — the cloud coherence centerpiece
Build the Overview page wired end-to-end to live data (no mock data):
- metrics / charts / spend-by-model / recent activity ← /v1/get-cloud-usages
  (the hanzo.cloud_usage ledger), org-scoped by the X-Org-Id header;
- wallet balance ← commerce /v1/billing/balance (WalletApi.cloudBalance);
- quick actions ← the real product registry (no dead links).

- src/lib/api/usage.ts: typed UsageApi.overview client (+ allOrgs god-view).
- src/components/charts/Charts.tsx: dependency-free SVG charts (sparkline, line,
  bar, donut) — console2 ships no chart lib; these theme to the dark shell and
  render in the Next web DOM alongside @hanzo/gui.
- src/components/products/OverviewModule.tsx: the dashboard — time range
  (24H/7D/30D/Custom), 4 metric cards w/ sparkline + "vs prior" delta, tokens
  line, spend bar, spend-by-model donut + ranked list, recent activity (filter
  tabs + pagination), quick actions, honest system-status footer, wallet.
  Uniform loading/empty/error states; honest "—" for data with no live source
  (GPU/latency → linked to the Status page rather than faked).
- app/(dashboard)/page.tsx: the home renders the Overview (the Explore-products
  catalog is kept below).
- src/lib/products/registry.tsx: 'overview' module at /overview — also the
  all-orgs admin surface via <OverviewDashboard allOrgs/>.
2026-06-29 23:25:54 -07:00
Hanzo AI d3a47c44d6 feat(console): side-by-side multi-model compare Playground
Tabbed Playground (Chat/Completions/Embeddings/Audio/Vision) whose marquee is a
side-by-side compare board: ONE shared System+User (or Prompt) broadcasts to N
model columns that run in PARALLEL through the keyless /ai proxy, each streaming
its own output while reporting REAL tokens, cost (catalog $/Mtok) and latency
(time-to-first-token + total). Per-column model + optional settings override with
a sync-across-all toggle; one column erroring/stopping never disturbs the others.
Single-model mode is one column. Examples seed the prompt; History records a run.

- app/ai/[...path]: stream the upstream body through (real TTFT) instead of
  buffering; allow-list v1/audio/speech for the Audio (TTS) tab. Both additive +
  backward-compatible for existing non-streaming callers.
- lib/api/playground.ts: add streamChat (SSE, stream_options.include_usage),
  embeddings, speech — additive to PlaygroundApi.
- Models selectable from the LIVE catalog (CloudModelApi → /ai/v1/models +
  /v1/pricing/models), the same source the Models page uses. No mocks.
- Pure, unit-tested core (cost/SSE-parse/runner/history): 28 vitest tests.
2026-06-29 23:23:03 -07:00
Hanzo AI 937ef0f0d8 chore(console): model + provider click-through detail pages → v0.7.22 2026-06-29 23:08:39 -07:00
Hanzo AI 75064e8d42 feat(console): provider detail page (metadata + provider's model list)
Click a provider card → ProviderDetailPanel: provider name + stats (models, max
context, from-price, available) + the provider's full model list (name, type,
context, in/out price, status). Pairs with the model click-through. v0.7.21
2026-06-29 23:08:39 -07:00
a464a12efb feat(console2): unified EmptyState + cost/billing + wired AI pages (#6)
* fix(ai): tenant-scope Providers/Stores/Models/Apps to the org + restore RAG retrieval

The AI/data admin views (ProviderListView, StoreListView/EditView,
ModelRouteList/EditView, ApplicationListView) used account.name (the USERNAME)
or a hardcoded 'admin' as the casibase owner. casibase entities are org-owned:
get-* scopes to the session org (GetScopedOwner) and honors the owner param only
for global admins, and AddStore trusts the body owner. So a username owner broke
global-admin org switching and orphaned newly-created stores. Switch all six
call sites to currentOrg() — the one active org-scope value (also stamped as
X-Org-Id), matching the v0.7.0 org-as-a-value model.

Also stamp X-IAM-Org-Id alongside X-Org-Id in the cloud client: the casibase
header-scoped filters (GetEffectiveOrg: usage/vectors/activities) read
X-IAM-Org-Id, so org switching now re-scopes those too (honored only for the
principal's own org or a global admin — safe).

RAG: the keyless /ai proxy rebuilt upstream headers from scratch and dropped
X-Retrieval/X-Retrieval-Store, so AiApi.ragChat silently degraded to a plain
answer. Forward the allow-listed retrieval headers (extracted to the pure,
tested lib/server/ai-proxy). 4 new tests; typecheck clean, 52 tests pass.

* console2: unified empty-state + cost/billing + wired AI pages (one way, forward-only)

The DRY pass toward 'every page real + useful': ONE EmptyState (the honest
first-run onboarding surface every module reuses instead of a dead screen), ONE
CostModule + billing client (real wallet/spend from commerce), and the AI pages
(Providers/Agents/Inference/Fine-tuning) wired to real data with that shared
empty state. Aligned the stray longhand maxWidth→maxW (onlyShorthandStyleProps:
one way, the redundant alias is off) and narrowed EmptyAction.icon to ReactElement.
tsc --noEmit clean (0 errors). Salvaged + verified from the throttled agent's tree.

---------

Co-authored-by: Hanzo AI <ai@hanzo.ai>
Co-authored-by: Hanzo <z@hanzo.ai>
2026-06-29 23:05:59 -07:00
z 1823b4e94f console2: unified empty-state + cost/billing + wired AI pages (one way, forward-only)
The DRY pass toward 'every page real + useful': ONE EmptyState (the honest
first-run onboarding surface every module reuses instead of a dead screen), ONE
CostModule + billing client (real wallet/spend from commerce), and the AI pages
(Providers/Agents/Inference/Fine-tuning) wired to real data with that shared
empty state. Aligned the stray longhand maxWidth→maxW (onlyShorthandStyleProps:
one way, the redundant alias is off) and narrowed EmptyAction.icon to ReactElement.
tsc --noEmit clean (0 errors). Salvaged + verified from the throttled agent's tree.
2026-06-29 23:05:26 -07:00
Hanzo AI f5ff37fc34 feat(console): click-through model detail (specs/pricing/features + config actions)
Catalog rows are clickable → ModelDetailPanel: full specs (arch/params), context,
all pricing (input/output/cache read+write), tier, features, status + actions
(Open in Playground, Configure routing → /models/routing/<name>, Copy ID). v0.7.20
2026-06-29 23:04:19 -07:00
Hanzo AI 39d690072f fix(console): ChunkGuard — auto-recover stale-deploy chunk errors
Open tabs reference the prior build's hashed chunks after a deploy; those URLs
fall through to the app shell (HTML), so the browser throws ChunkLoadError /
'Unexpected token <' and blanks. ChunkGuard catches that exact failure and does
ONE guarded full reload to pull fresh HTML + current chunks. v0.7.19
2026-06-29 22:50:23 -07:00
Hanzo AI d4c361f3b5 fix(console): carry available-count in ProvidersExplore state (RichModel has no .available) 2026-06-29 22:40:39 -07:00
Hanzo AI 330c247bfb feat(console): polished Models + Providers on real rich catalog
- aicatalog: ONE source (/v1/pricing/models via /ai proxy) — 444 real models,
  15 real providers, with context/pricing/specs/tier/provider. No fabrication.
- Model Catalog: polished table (model+params, type, context, in/out $/Mtok,
  TRUE provider, live Available status) + stats bar. Fixes the qwen/glm 'Hanzo
  (Zen)' mislabel — real provider per model.
- Providers: Explore grid (action cards + real provider cards + stats), was
  '0 built-in'. /providers/manage keeps the custom-provider CRUD.
- Brand: our models show as 'Zen', never 'Hanzo (Zen)'. v0.7.18
2026-06-29 22:39:11 -07:00
Hanzo AI 43062cfa5e Merge remote-tracking branch 'origin/fix/ai-surfaces-tenant-scope-rag' into feat/console2-complete-cloud-ux 2026-06-29 22:21:35 -07:00
Hanzo AI 8c5d4e5902 fix(console): add missing SidebarWallet import (v0.7.17 build) 2026-06-29 22:08:30 -07:00
Hanzo AI a382542fd7 feat(console): always-visible wallet (balance + top-up) bottom-left of sidebar
Pinned wallet widget on every page — per-tenant balance via the /billing proxy +
Top-up deep-links to billing.hanzo.ai/topup (pay.hanzo.ai Square/crypto checkout),
never rebuilds payment. Collapsed mode = wallet icon. v0.7.17.
TODO: promote to @hanzo/ui as a reusable cross-app component (one way, composable).
2026-06-29 22:07:12 -07:00
Hanzo AI 92c8155ee9 fix(ai): tenant-scope Providers/Stores/Models/Apps to the org + restore RAG retrieval
The AI/data admin views (ProviderListView, StoreListView/EditView,
ModelRouteList/EditView, ApplicationListView) used account.name (the USERNAME)
or a hardcoded 'admin' as the casibase owner. casibase entities are org-owned:
get-* scopes to the session org (GetScopedOwner) and honors the owner param only
for global admins, and AddStore trusts the body owner. So a username owner broke
global-admin org switching and orphaned newly-created stores. Switch all six
call sites to currentOrg() — the one active org-scope value (also stamped as
X-Org-Id), matching the v0.7.0 org-as-a-value model.

Also stamp X-IAM-Org-Id alongside X-Org-Id in the cloud client: the casibase
header-scoped filters (GetEffectiveOrg: usage/vectors/activities) read
X-IAM-Org-Id, so org switching now re-scopes those too (honored only for the
principal's own org or a global admin — safe).

RAG: the keyless /ai proxy rebuilt upstream headers from scratch and dropped
X-Retrieval/X-Retrieval-Store, so AiApi.ragChat silently degraded to a plain
answer. Forward the allow-listed retrieval headers (extracted to the pure,
tested lib/server/ai-proxy). 4 new tests; typecheck clean, 52 tests pass.
2026-06-29 22:04:41 -07:00
Hanzo AI fb2a276496 chore(console): scrub upstream brand names from comments — 'admin' org + IAM, our way. v0.7.16 2026-06-29 21:59:21 -07:00
Hanzo AI 9b59decccf chore(console): standardize the global-admin org on 'admin' (drop casdoor built-in dual-recognition) — matches commerce/ai/gateway. v0.7.15 2026-06-29 21:57:24 -07:00
Hanzo AI 9289698825 fix(console): admin.hanzo.ai = GLOBAL admins only (org admins were leaking in)
SECURITY: an org owner (Dave/maxpower, org-level isAdmin) could reach the admin UI.
- gateAllows (server authority for /admin/* proxies): require isGlobalAdmin, NOT
  isAdminGranted (which accepted org-level isAdmin). Verified brand-email stays as
  2nd factor. Org admins now 403 on admin ops even with @adminDomain email.
- OrgGate: redirect non-global-admins OFF admin.hanzo.ai to the console host +
  render-guard so the admin console never flashes. Banner already isGlobalAdmin.
- isGlobalAdmin now recognizes BOTH metadata orgs (admin + built-in), matching
  admin-policy ORG_METADATA_OWNERS.
Bundles v0.7.13 (models owner/name token fix + banner gate). v0.7.14.
2026-06-29 21:51:58 -07:00
Hanzo AI ad4d6ca8aa fix(console): models 'wrong token count' + admin banner leak to org admins
1. identity.ts: user.id must be <owner>/<name> (IAM GetOwnerAndNameFromId), not the
   bare casdoor UUID — fixes Model Catalog 'Could not authorize: wrong token count
   for ID <uuid>' for org-member accounts (Dave/maxpower) that carry an id field.
2. OrgGate.tsx: the admin.hanzo.ai ops banner gated on isAdmin (org-level) so an
   ORG owner (Dave/maxpower) saw it. Gate on isGlobalAdmin (admin/built-in org) —
   org admins are not cross-tenant admins. v0.7.13.
2026-06-29 21:46:56 -07:00
Hanzo AI 790c65b951 feat(billing): per-tenant billing BFF — Wallet/Cost show real balance/usage/invoices
The one remaining cross-service gap (v0.7.x already proxies AI/IAM/KMS). wallet
still hit cookie-only /v1/billing/balance -> 404. New app/billing/[...path] proxy
forwards /billing/* -> commerce with the service token + server-resolved own-org
scope (X-Hanzo-Org + BillingSubject; client cannot widen). Matches the /admin/*
proxy pattern + resolveUser. Wallet.cloudBalance repointed same-origin. v0.7.12.
2026-06-29 21:31:25 -07:00
hanzo-dev 55819f81d4 fix(console2): register @hanzogui/core config augmentation; tsc clean (0 errors)
gui.d.ts was augmenting @hanzogui/web which is not a direct dep (resolves only
via pnpm .pnpm path). Add @hanzogui/core augmentation (which is a direct dep) so
GuiCustomConfig → Conf flows through and shorthand props (bg/px/py/items/justify
etc.) are typed correctly. Also fix Button `color=` → `theme=` in OrgGate banner.

Before: 371 type errors. After: 0.
2026-06-29 13:56:38 -07:00
hanzo-dev 76a4380d9e feat(console2): ProviderListView shows global built-in + per-org custom providers (v0.7.11)
List both get-global-providers (Hanzo platform-keyed, read-only, Built-in badge)
and per-org custom providers in one view. Users see all providers enabled out of
the box via Hanzo's DO-AI keys; adding a custom provider overrides/extends per org.
2026-06-29 13:53:37 -07:00
hanzo-dev 6c745bc4dc fix(console2): OrgGate — admins get banner not hard-block; add Playwright e2e (v0.7.11)
- OrgGate: replace hard hard-block for hanzo-org users with a dismissible
  amber banner pointing at admin.hanzo.ai; staff can use console.hanzo.ai
  normally for cloud work (models, API keys, AI inference etc.)
- OrgGate: restore last selected org from localStorage on sign-in so the
  scope remembers where the user left off
- Add e2e/ Playwright tests: sign-in, admin banner, API key create/confirm,
  /v1/models verification, OpenAI + Anthropic inference tests
- Add playwright.config.ts targeting https://console.hanzo.ai by default
- package.json: add e2e + e2e:headed scripts
2026-06-29 13:50:10 -07:00
zeekayandClaude Opus 4.8 8da1f80314 feat(console2): complete console IA parity port — feature-module shells (v0.7.10)
Port the remaining old-console surfaces into console2 as forward-compatible
modules: Zero Trust, Integrations, Referrals, Experiments, Dashboards,
Score Analytics, plus prompt-create/metrics and dataset items/runs sub-modules.

Each surface points at its real/planned /v1 endpoint and renders real rows when
present; a 404/405/503 collapses to the shared honest BackendState card (no demo
rows). console2 now carries the old console's full information architecture while
backend routes light up independently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 01:13:14 -07:00
zeekay 55409ef473 ci(console2): avoid Docker Hub base image pulls (v0.7.9) 2026-06-29 00:21:41 -07:00
zeekay ad5f082926 feat(console2): first-run org onboarding and waitlists (v0.7.8) 2026-06-29 00:19:12 -07:00
hanzo-dev 88d4c68c2a ci(console2): raw buildx on host builder — fix Docker Hub 429 + artifact quota
Builds were red: docker/setup-buildx-action + docker/build-push-action
spin up an ephemeral buildkit container that can't see the host image
cache, so it re-pulled node:22-alpine from Docker Hub every run and hit
the unauthenticated 429 pull-rate limit; the action's build-summary
artifact upload also hit the Actions storage quota.

Switch to the canonical hanzoai/ci pattern: raw `docker buildx build
--push` on the host builder, which reuses the cached base layer (no
re-pull, no artifact upload). Adds a cache-aware base-image guard that
retries a cold pull with linear backoff. SEMVER tag + GHCR push unchanged.
2026-06-28 23:15:26 -07:00
hanzo-dev 3e0325bb14 feat(console2): scope the PaaS surface by org → project → environment
The /paas proxy forwarded only the god-mode service token, so PaaS
resources couldn't scope per tenant. Now it forwards the full tenant
path the browser stamps:

- X-Org-Id is RE-RESOLVED server-side via the admin policy (orgFor): a
  global admin's switched org (the X-Org-Id it already sends) is honored,
  a brand admin is pinned to their own — authoritative, never the raw
  spoofable claim. Same trust boundary as the IAM/KMS admin proxies.
- X-Project-Id + X-Environment pass through verbatim as sub-scopes within
  that org.

Forward-correct: the control plane scopes by tenant once it reads these,
harmless until then. Adds scope.test.ts (9 tests: store round-trip, merge
semantics, persistence, projectEnvironments intrinsic-3 + custom). 38 green.
2026-06-28 23:10:40 -07:00
hanzo-dev ce67bb16e8 fix(console2): staff gate — collapse "admin. <domain>" gap + header "<Brand> Admin"
The staff redirect button passed two children ("Go to admin." + the
{adminDomain} expression); Tamagui Button lays children out with its
icon-gap, so the two text nodes rendered with a stray space
("Go to admin. hanzo.ai"). Collapse to a single template-literal child.
Header retitled "<Brand> staff" → "<Brand> Admin" (white-label via brand).
2026-06-28 23:07:32 -07:00
hanzo-dev 75f78e1a17 feat(console2): org → project → environment multi-tenancy scope
Projects under the org, each with intrinsic mainnet/testnet/devnet
environments (+ custom), scoping every module at once.

- lib/scope.ts: module-level active { org, project?, environment } the
  non-React API client reads synchronously to stamp headers. Org pinned
  to the brand org; STOCK_ENVIRONMENTS = mainnet/testnet/devnet.
- lib/api/client.ts: baseHeaders stamps X-Project-Id (when a project is
  selected) + X-Environment on every cloud call — one change scopes all
  existing modules (o11y, api-keys, deploys, …) with no per-module edits.
- lib/api/projects.ts: ProjectApi over the REAL Hanzo IAM contract
  (/v1/iam/get-organization-projects | add-project | delete-project),
  keyed (owner, name) with indexed organization = the brand org.
- lib/scope-context.tsx: ScopeProvider — loads the org's projects once,
  holds the active selection, mirrors it into the module scope +
  localStorage, derives per-project environments. Honest: an unrouted
  projects endpoint resolves to org-level only, never a fabricated row.
- components/ScopeSwitcher.tsx: project + environment pickers in the top
  bar (next to OrgSwitcher); env dots keyed to network tier.
- components/products/ProjectsModule.tsx: projects CRUD + "Use" to set
  active scope; honest not-routed/empty states, no fakes.
- registry: Projects flips external → in-console module (Deploy).

Environments are a console-side scoping dimension (IAM Project has no
environments column); X-Environment is sent canonically for backend
adoption. tsc --noEmit clean.
2026-06-28 23:04:49 -07:00
zeekay 98a7088be7 feat(console2): models-first, docs-out, collapsible+filterable sidebar, cmd+K actions (v0.7.4)
Build Docker Image / docker (push) Successful in 3m5s
The 'I don't see any models' wave + sidebar/command UX.

- Models is catalog-first: the default tab is the LIVE model list (~49 Zen
  models via the /ai proxy); routing policy moves to a secondary 'Routing' tab
  (/models/routing). Retires the duplicate empty-by-default 'Model Catalog' nav
  entry — one product, real models on the obvious click.
- Docs are EXTERNAL (brand docsUrl, new tab): a top sidebar 'Docs' entry, the
  header '?' icon, and a server /docs -> docs.<brand> redirect (no in-app 404).
  ComingSoon's API-docs link repointed off the 404-ing ${cloudUrl}/docs.
- Sidebar collapses/expands from the brand 'H' mark (icon-only <-> full),
  persisted to the account; the grid launcher stays. A filter box narrows the
  whole sidebar to find any product fast; Overview/Docs are fixed top links.
- cmd+K runs ACTIONS too (toggle theme, browse all apps, open settings, switch
  org, ask AI / search docs, sign out, per-org switch), ranked alongside catalog
  nav — no dead entries. Removed unused openMode.
- DRY: switchOrg() shared by the switcher + palette; pure match-core (resolveRoute
  + entryMatches) is unit-tested without the GUI tree. Default pins fixed
  (dead 'billing' -> 'models','chat'). tsc + 29 vitest + next build clean.
2026-06-28 22:44:30 -07:00
zeekay 28c186b508 fix(console2): customer console scopes to the user's own org (v0.7.3)
The org-scope module defaults the active org to the brand org (for the cross-org
admin). On a customer host, seed the signed-in user's OWN org as the active scope
the first time (only when no explicit switch is in effect), so the OrgSwitcher
chip and X-Org-Id reflect the real tenant (e.g. maxpower) instead of the brand
org. Never clobbers an admin's deliberate switch.
2026-06-28 22:16:30 -07:00
zeekay bba56046e4 fix(console2): mask the password field on the sign-in form (v0.7.2)
The @hanzo/gui Input ignores the RN secureTextEntry/keyboardType props on web,
so the password rendered as type=text (visible while typing). Set the web input
type explicitly (type="password") so it masks; also corrects autoComplete to
current-password.
2026-06-28 21:58:56 -07:00
zeekay 948a627c72 ci(console2): resolve semver tag without node (ARC runner has no node)
The build step used `node -p require('./package.json').version`; node is not on
the ARC runner PATH, so the substitution silently yielded the tag `:v` and the
operator deploy 404'd. Resolve the version with grep/sed and fail-loud if empty.
2026-06-28 21:45:33 -07:00
zeekay bdda312f5e feat(console2): multi-tenant login (org by email) + /paas admin gate (v0.7.1)
Login resolves the user's ORG from their email instead of pinning the brand's
own org: a customer in any org signs into the brand console with email+password.

- iam-login.ts: POST /v1/iam/login with organization: (cross-org email
  resolution) → OAuth code → existing completeSignIn → /v1/signin exchange.
  Replaces the SDK org-pinned redirect that made non-brand-org users
  un-signinable. MFA (NextMfa/RequiredMfa) hands off to IAM's same-site hosted
  flow — IAM sets iam_session_id SameSite=Lax, so cross-site fetch MFA can't
  complete here; no faked inline step.
- SignInForm: email+password state machine; social (getProviderSigninUrl) kept.
- OrgGate: the console runs in the user's CUSTOMER org — blocks the internal
  brand org (owner===config.iamOrgName → admin.<domain>) and the zero-org case.
- SECURITY /paas/[...path]: gate with getAdminGate (403 if not a brand admin) +
  runtime nodejs. The forwarded PAAS_SERVICE_TOKEN is control-plane god-mode and
  was previously reachable by any authenticated browser.
- getAdminGate: orgScope=user.owner (was brand.id) so the IAM/KMS proxies pin a
  non-global admin to their OWN org; require a VERIFIED email (authoritative IAM
  recheck for thin claims). admin-policy + tests updated (14 pass).
2026-06-28 21:37:42 -07:00
zeekay 4cfcd1f779 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	package-lock.json
#	package.json
2026-06-28 21:23:02 -07:00
zeekay b3fc76186f feat(console2): live admin data + org switching (v0.7.0)
Root cause of empty models + broken org switcher: /v1/{iam,kms,models} 404/401
(cookie-only) on the console host. Route every privileged call through console2's
own server proxies (user bearer + admin gate), and make org scope a switchable value.

- models-catalog: repoint /v1/models -> /ai proxy (shared aiV1Url in client.ts);
  catalog now populates with live Zen models. playground reuses the shared aiBase.
- org-scope.ts: currentOrg/setCurrentOrg/isScopedAway/filterOrgs. client.ts stamps
  X-Org-Id: currentOrg() so data modules re-scope on switch.
- OrgSwitcher: lists ALL visible orgs, filter box, in-place re-scope (set+reload).
- IamModule/AuditModule read currentOrg(); KmsModule now a names-only inventory
  over KmsAdminApi.list (the /admin/kms proxy), values never fetched.
- admin-policy.ts: pure gateAllows/ownerAllowed/orgFor extracted from the gate +
  routes (decomplect) so the shipped predicate is the tested one.
- vitest: 22 tests RED->GREEN (gate allow/deny+scoping, scope+filter, catalog /ai).
  tsc --noEmit + next build clean.
2026-06-28 21:19:50 -07:00
hanzo-dev fecaf7d49d fix(console2): Pager props + O11yUser barrel export (tsc clean) 2026-06-28 20:53:43 -07:00
hanzo-dev 1d9c2a9225 feat(console2): port Observations + Users observability views from old console
Two more old-console pages as real o11y modules: Observations (the flat
cross-trace span/generation view, GET /v1/o11y/observations) and Users (per-user
analytics rollup, GET /v1/o11y/users). O11yApi extended with both endpoints +
the O11yUser type. Same paged DataTable + honest RuntimeNotice pattern as the
other o11y modules; registered under Observe. tsc clean.
2026-06-28 20:52:55 -07:00
hanzo-dev 7a3b13d55e feat(console2): build the 25 remaining products into real modules
Flip every 'soon' product (ComingSoon stub) to a real, enabled in-console module
— attestations/oracles/indexer/tokens/settlement, alerts/logs, pipelines/
releases/builds/environments, hsm/authz/service-mesh/load-balancer/vpc, jobs/
edge/functions/containers/machines/gpus, agents/inference/finetuning. Each
follows the established module pattern: typed restGet over the same-origin /paas
proxy, DataTable, honest interpretPlatformError→PlatformStateCard not-configured
state, Refresh. Registry: status soon→enabled + routes wired. Full tsc clean.

Catalog is now 61 enabled modules + 16 external; no 'soon' stubs remain.
2026-06-28 20:06:44 -07:00
z bc7f92665e docs(brand): add hero banner 2026-06-28 20:05:13 -07:00
z 509e511b6e chore(brand): dynamic hero banner 2026-06-28 20:05:12 -07:00
hanzo-dev ab9f8751b7 feat: brand-aware animated logo in console loader/sign-in
The mark now follows config.brand — each brand's OWN published, interactive
animated SVG (@hanzo/logo, @luxfi/logo, @zooai/logo: getAnimatedSVG, load→hover→
press). Static block-H during SSR/first paint (no hydration mismatch). The logo
is now the brand's playable 'AI' on every loading + sign-in surface.
2026-06-28 19:49:01 -07:00
zeekay 5ea5cde253 feat(console2): add MPC product (Security) so console.hanzo.ai shows IAM/KMS/MPC/PaaS
MPC = threshold signing & multi-party computation, external→mpc.hanzo.ai.
Completes the 4 platform apps the launcher surfaces: IAM (module), KMS (module),
MPC (external), PaaS (projects→platform.hanzo.ai, embedded PlatformModule).
Verified locally: dev recompiled clean, tsc --noEmit clean.
2026-06-28 19:26:29 -07:00
zeekay f28d79673a wip(console2): agent admin-console IAM/KMS savepoint (proxy+gate+branding, in progress) 2026-06-28 19:20:38 -07:00
zeekay 40a6236aa3 fix(console2): API keys — drop reload() that unmounted the view via AuthGate (v0.6.1)
Build Docker Image / docker (push) Successful in 2m37s
The mint/revoke success path called useSession().reload(), which flips the
session loading flag -> AuthGate renders its Loader -> the dashboard (and
ApiKeysView) unmounts -> the freshly-minted key (local newKey state) is lost on
remount, so 'Your new API key' never showed. The session masks accessKey anyway,
so the reload provided no benefit. Show the new key from local state only.

Verified live (v0.6.0): /keys POST 200 + /ai chat proxy 200 (real 'PONG'/Hanzo
answer); only the keys UI render was affected.
2026-06-28 18:54:02 -07:00
zeekay 97c385f5ca feat(console2): working AI (keyless proxy) + API keys + chrome polish (v0.6.0)
Build Docker Image / docker (push) Successful in 2m25s
P0 — make it work for Dave:
- Root cause of 'chats/playground don't work': /v1/chat/completions requires
  Authorization: Bearer; the browser sent cookie-only -> rejected. Fixed with a
  keyless server-side AI proxy (app/ai/[...path]) that mints a short-lived
  user-bound IAM token (issue-user-token, hanzo-console app-on-behalf) and
  forwards to the gateway. No key in the browser, no rotation per turn.
- API keys: app/keys route mints/rotates/revokes the per-user hk- key via IAM
  (mint-user-keys/revoke-user-keys); ApiKeysModule is now create/copy/rotate/
  revoke with show-once secret handling.
- Chat is interactive (ChatConversation) over AiApi.chat; honest 402 billing state.
- src/lib/server/identity.ts: server-only trust boundary (resolveUser + IAM ops).

P1 — chrome polish:
- Header/sidebar show the Hanzo H mark + 'Console' (HanzoMark, BrandLogo with
  org-logo fallback).
- Fullscreen Launchpad-style app launcher (AppLauncher) with filter, from the
  header 'Apps' button, sidebar grid icon, and the command palette.
- cmd+K palette gains a 'Browse all apps' affordance.

Verified end-to-end (curl, real creds): minted hk- key + issued user JWT both
200 on api.hanzo.ai/v1/chat/completions. typecheck + next build clean.
2026-06-28 18:19:38 -07:00
zeekay 7cf8015c07 chore(console2): remove unused lib/zap client + @zap-proto deps
Zero importers (grep lib/zap = 0). Drops src/lib/zap/* and the
@zap-proto/{web,zap} deps. typecheck + build clean. One-way hygiene.
2026-06-28 05:45:21 -07:00
zandGitHub 5874ec5acc Merge pull request #4 from hanzoai/feat/memory-tasks-modules
feat: Memory + Tasks modules (v0.5.0) — unify work/tasks/memory in the console
2026-06-27 22:16:00 -07:00
zeekay 01844deb23 catalog: Tasks under appended Async category (Tasks/Temporal)
Async is the canonical home for Tasks per the org taxonomy. Appended as an 11th
category so the ten GCP-equivalent groups keep their exact labels and order
(no reorder); catalogByCategory is data-driven, so it renders generically.
2026-06-27 22:11:27 -07:00
zeekay f7a90ffe65 release: v0.5.0 — Memory + Tasks modules 2026-06-27 22:06:52 -07:00
zeekay 7f7a9341da feat(console): Memory + Tasks modules in the unified catalog
Memory (Data): list + search (kind filter + text) + detail/edit + add/delete,
honest 'initializing' card until /v1/memory deploys. Tasks (Compute, GCP Cloud
Tasks): namespace selector + Workflows/Schedules tabs + cluster strip, workflow
detail + durable history, on the LIVE /v1/tasks engine. Both render honest
states on a gated/absent route; mutations report through the shared toast.
Entries appended (no reorder); grouped by category.
2026-06-27 22:06:48 -07:00
zeekay 13978c0ca9 feat(api): Memory + Tasks /v1 clients
Memory (hanzoai/ai /v1/memory): remember/search/list/recall/update/remove/facts
over restGet/restPost; per-user, server-scoped. Tasks (hanzoai/tasks /v1/tasks):
namespaces/workflows/workflow/history/schedules + cluster health, contract
verified against pkg/tasks/embed.go. Both plain-REST, cookie-credentialed; org
scoping is server-side.
2026-06-27 22:06:41 -07:00
zandGitHub d564d3c488 Merge pull request #3 from hanzoai/feat/shell-foundation
feat: shell foundation v0.4.0 — cmd+K, AI assist, toasts, theme, org-switcher, breadcrumbs
2026-06-27 21:47:44 -07:00
zeekay bd933e5984 chore: v0.4.0 2026-06-27 21:45:52 -07:00
zeekay c0da836191 feat(shell): mount the foundation blocks once in the dashboard shell
Dashboard layout wraps the shell in ToastProvider + CommandPaletteProvider. The
shell top bar gains the command search box (opens the palette), theme toggle, a
help launcher (opens the palette in docs mode), and the org switcher; a breadcrumb
bar sits below it. ResourceModule create/delete now report through the toast, so
the one feedback primitive is actually used.
2026-06-27 21:45:48 -07:00
zeekay abb45c9481 feat(console): command palette + org switcher
CommandPalette is ONE command surface (Cmd/Ctrl+K) with modes selected by the
query: default fuzzy-filters the catalog and jumps to any product; > asks the AI
to find a product (NAV <id>) or answer; ? asks the docs knowledge store (RAG).
searchCatalog is the dependency-free fuzzy ranker. OrgSwitcher shows the account's
org and, only for a real multi-brand membership, switches to that brand's console
host. Honest throughout — AI/RAG/IAM failures degrade to truthful states.
2026-06-27 21:45:41 -07:00
zeekay a09e9483c8 feat(ui): toast, theme toggle, breadcrumbs primitives
Toast is the one feedback primitive — ToastProvider + useToast() with a portalled
top-right viewport, theme-aware accents, auto-dismiss. ThemeToggle flips dark/light
via next-theme. Breadcrumbs derive Home/Category/Product/detail from the route +
catalog, so a new product gets correct crumbs for free. GUI primitives only.
2026-06-27 21:45:35 -07:00
zeekay 5a61a9571c feat(api): one AI client over the cloud /v1 (chat + ragChat docs + listModels)
AiApi composes the single OpenAI-compatible gateway binding (PlaygroundApi):
plain chat, listModels, and ragChat grounded in a knowledge store (docs by
default) via the built-in retrieval path — the X-Retrieval-Store header turns
on RAG and names the store; the org owner is resolved server-side. restPost and
PlaygroundApi.chat gain an optional headers arg so RAG and plain chat share ONE
binding. No parallel AI client; failures throw ApiError for honest states.
2026-06-27 21:45:29 -07:00
zandGitHub 7b9a02ab9a Merge pull request #2 from hanzoai/feat/billing-per-brand-url
Per-brand billingUrl (lux->billing.lux.cloud, zoo->billing.zoo.cloud)
2026-06-27 20:01:54 -07:00
zeekay 03d80356bf feat(config): per-brand billingUrl (lux->billing.lux.cloud, zoo->billing.zoo.cloud)
billingUrl moves from SHARED to the per-brand BRANDS table, resolved like
iamUrl. Each brand's console (Cost product, PlansModule manage-billing) links
to ITS billing host, scoped to ITS org by the brand JWT. Cloud backend stays
shared/multi-tenant.
2026-06-27 19:24:08 -07:00
zeekay 35e0ea4c30 merge: page-port wave 2 (trace-graph tree, score-configs, annotation-queues) — v0.3.0
Build Docker Image / docker (push) Successful in 2m25s
2026-06-27 18:52:04 -07:00
zeekay cf44eff091 feat(console2): port-wave2 — span tree, score-configs, annotation-queues (v0.3.0)
Native @hanzo/gui modules over the REAL /v1/o11y REST surface; honest
RuntimeNotice on 503/404 — no fabricated data.

- Traces detail: flat observations table -> nested span TREE waterfall
  (SpanTree) built from parentObservationId, with a Tree|Table toggle.
  Orphans/cycles surface as roots so every observation appears once.
- Score Configs: new Observe module on /v1/o11y/score-configs (read-only).
- Annotation Queues: new Observe module on /v1/o11y/annotation-queues
  (real public REST list — contradicts the earlier 'no backend' finding).

Skipped (honest, no usable Hanzo backend or would duplicate/fake):
dashboards/widgets + score-analytics (no saved-dashboard REST, charts
would be fabricated), llm-connections/mcp (covered by Providers/ModelCatalog),
agents (backend not routed at gateway — real paths 404), org/projects deep
settings (already covered by Settings + IAM, no write endpoints), and the
Langfuse integration surfaces (feature-flags/entitlements/automations/
batch-exports/developer-tools/slack/mixpanel/blobstorage).

catalog 73 -> 75 entries; appended only (no reorder).
2026-06-27 18:44:19 -07:00
Hanzo AI e3c4d52bb4 build: cap Node heap (NODE_OPTIONS=6144) to fix Next build OOMKill (exit 137) 2026-06-27 18:35:53 -07:00
zeekay da83b67e89 release: v0.2.0 — consolidate page-port wave 1 (o11y traces/sessions/scores, playground/evals/datasets/prompts, settings/models/api-keys)
Build Docker Image / docker (push) Successful in 2m45s
Merges port-{settings-models,prompts-evals,o11y-core}. 73 catalog entries across the 10 categories; new modules wired to real /v1 backends with honest states (o11y 503 until runtime init). tsc + next build green.
2026-06-27 18:19:45 -07:00
zeekay a13de116e8 merge: port-o11y-core (Traces/Sessions/Scores)
# Conflicts:
#	src/lib/api/index.ts
#	src/lib/products/registry.tsx
2026-06-27 18:17:56 -07:00
zeekay 725beb1d98 merge: port-prompts-evals (page-port wave 1)
# Conflicts:
#	src/lib/products/registry.tsx
2026-06-27 18:17:16 -07:00
zeekay 21c1016292 merge: port-settings-models (page-port wave 1) 2026-06-27 18:16:09 -07:00
Hanzo AI bc76e6fc76 release: v0.1.9 — REST client fix over v0.1.8 (working REST, not dead ZAP /zap WS) 2026-06-27 18:10:23 -07:00
Hanzo AI 24f9b14e69 fix(providers): use working REST client, not the dead ZAP /zap WS
ProviderListView/ProviderEditView imported ~/lib/zap, but the cloud /zap
WebSocket face is not served (edge returns SPA HTML, not a WS upgrade — per
lib/zap/client.ts LIVE STATUS), so Providers rendered 'Failed to load
providers'. Switch both to ~/lib/api (identical surface). Part of v0.1.8.
2026-06-27 17:46:04 -07:00
zeekay ac0ecfcb84 feat(console2): port prompts/playground/datasets/evals as native @hanzo/gui modules
Port the old console (Langfuse-fork) eval surfaces into console2 as native
modules on @hanzo/gui + @hanzogui/lucide-icons-2 — no antd/shadcn/tremor.
Wired to the REAL cloud /v1 backend; honest loading/empty/unavailable states
on 404/503 (NEVER fabricated prompts/datasets/scores).

- Playground (AI): GET /v1/models + POST /v1/chat/completions — fully working
  model run (system prompt, message thread, sampling params, token usage).
- Evals (Observe): POST /v1/evals/runs (real per-item run summary) +
  GET /v1/evals/scores (real scores list), Run/Scores tabs.
- Datasets (Observe): POST /v1/evals/datasets + /v1/evals/dataset-items (real
  create); forward-compatible GET list with honest unavailable card.
- Prompts (AI): forward-compatible GET /v1/prompts probe; honest deep-link card
  (no /v1 prompts route mounted yet).

Shared: ModelPicker (one way to pick a gateway model), BackendState (honest
/v1 error → card). API: lib/api/{playground,evals}.ts via the REST client.
Registry: playground+evals stubs upgraded in place (no reorder); prompts+
datasets appended. tsc --noEmit + next build both green.
2026-06-27 17:43:45 -07:00
zeekay e547011c96 feat(console2): native Traces/Sessions/Scores modules under Observe
Port the old console's observability core (Langfuse-shaped) to native
@hanzo/gui modules:
- Traces: list + detail (overview, I/O, observations, scores) at /o11y
- Sessions: list + detail (overview + traces) at /sessions
- Scores: list at /scores

All read the REAL /v1/o11y endpoints and render honest states (loading /
runtime-initializing / empty) on 503/404 — never fabricated traces or
charts. Replaces the Traces deep-link placeholder (ObservabilityModule
removed); appends Sessions + Scores entries (no reorder of existing).
tsc --noEmit and next build both pass.
2026-06-27 17:43:28 -07:00
zeekay 164f073b50 feat(console2): shared observability primitives
DRY pieces for the o11y surfaces: pure formatters (date/latency/cost/
score value/JSON), shared detail parts (DetailRow/Badge/Tags/JsonCard),
the honest RuntimeNotice (503 not-initialized / 404 unrouted / access /
error), and the list Pager. No fabricated data.
2026-06-27 17:43:16 -07:00
zeekay 6df405adcd feat(console2): o11y API client (traces/sessions/scores)
Typed client for the /v1/o11y surface over the plain-REST transport
(restGet/v1Url): list endpoints return { data, meta }, detail endpoints
one object. 503/404 surface as typed ApiError so callers render honest
states. Tenancy is server-side (cookie credentials only).
2026-06-27 17:43:16 -07:00
zeekay f50dbdd9a6 feat(console2): port settings + model catalog pages as native @hanzo/gui modules
Port hanzoai/console's deep settings + models pages into console2 as native
modules wired to the REAL /v1 + /v1/iam endpoints. Honest loading/404/401/503
states everywhere — never fabricates keys, models, or settings.

New modules (registry, appended — no reorder):
- Model Catalog (AI): real GET /v1/models + best-effort /v1/pricing/models
  overlay; provider filter, premium tier, $/Mtok columns. Read-only catalog
  (routing lives in Models, credentials in Providers).
- API Keys (Dev): the account's real cloud credential (accessKey/accessSecret
  from get-account), masked by default with explicit reveal + copy; honest
  "managed by gateway" state when no key material is exposed to the browser.
- Settings (Security): tabbed General / API Keys / Members / Branding. General
  reads real account (get-account) + org (/v1/iam/get-organization); Members
  reads /v1/iam/get-users; API Keys embeds the shared ApiKeysView (DRY);
  Branding shows the real per-host runtime config. Identity mutations deep-link
  to IAM rather than being re-implemented.

Shared + API:
- ui/States.tsx: one honest async-state renderer (honestError + ErrorState +
  asApiError) with per-surface copy overrides. AdminModule refactored onto it
  (removes its duplicate honestError/ErrorCard).
- api/models-catalog.ts: CloudModelApi over the REST (non-envelope) /v1/models.
- api/admin.ts: IamAdminApi.organization(name) single-org getter.

Shell: top-bar account name now links to /settings (canonical account menu).

Gates: tsc --noEmit exit 0; next build exit 0.

Skipped (no real console2 backend — porting as shells would be slop): Langfuse
feature-flags (2 internal flags), developer-tools (Langfuse-branded copy),
automations/batch-exports/integrations (tRPC+Prisma only). Members/Audit/
Secrets/LLM-connections/Billing already exist as IAM/Audit/KMS/Providers/Cost.
2026-06-27 17:40:53 -07:00
Hanzo AI b4274bda4a release: v0.1.8 (cumulative — supersedes parallel v0.1.7) 2026-06-27 17:37:11 -07:00
Hanzo AI c9adc16cad merge: integrate fix/paas-live-data (CTO v0.1.7 paas wiring) into main
main already supersedes it: same real platform contract (/v1/apps +
/v1/org/{org}/cluster) PLUS X-Org-Id (resource modules), Bot/Wallet honest
states, Clusters real-contract rewrite, and the PaaS token fix. Recording the
integration so the line is single; cutting v0.1.8 as the cumulative release.
2026-06-27 17:36:51 -07:00
Hanzo AI f30cdbbee5 fix(modules): wire every embedded module to the real /v1 backend + cut v0.1.7
Live Playwright verification surfaced real wiring bugs; fixed all in console2
(honest states everywhere, no fakes):

- client.ts: stamp X-Org-Id (brand org) on every cloud call — the provisioning
  service 403'd 'X-Org-Id required' on the direct cloud-api path. Fixes the 7
  data modules (vector/sql/kv/s3/datastore/docdb/search) → real data / empty.
- platform.ts: rework to the REAL platform contract — GET /v1/apps (apps
  inventory) + GET|POST /v1/org/{org}/cluster; drop dead /v1/clusters + k8s
  passthrough. Status = real health board; Kubernetes = real workloads per
  cluster; Clusters = real dedicated-DOKS list (honest empty).
- platform/state.tsx: upstream 401/403 → honest 'not configured'.
- BotModule: /v1/bot/health 404 → honest 'not routed on this host'.
- WalletModule: /v1/billing/balance 404 → honest 'not available'.
- StatusTag: understand platform health verdicts (green/yellow/red).

typecheck + build clean. Operator CR token repointed (universe) to the correct
paas-console-token (the old hanzo-paas/MASTERTOKEN is rejected by platform).
2026-06-27 17:34:03 -07:00
zeekay a307d64b07 fix(paas): wire Clusters/Kubernetes/Status to real platform /v1 surface
Build Docker Image / docker (push) Successful in 2m36s
The PaaS modules targeted an assumed platform surface (/v1/clusters,
/v1/org/{org}/cluster/{id}/k8s/{kind}) that does not exist — the live
platform.hanzo.ai/v1 REST API serves /v1/org/{org}/cluster (org-scoped
dedicated clusters) and /v1/apps (the workload/drift board). Re-point the
single data layer (src/lib/api/platform.ts) at the real endpoints:

- listClusters → /v1/org/{org}/cluster (unwrap {clusters}); org from config.
- add AppsApi.listApps → /v1/apps (unwrap {apps}) — the live deploy data.
- drop the dead k8s-browse client (no backend) + CLUSTER_ROUTES guess.

Status + Kubernetes now render the real /v1/apps workloads (cluster,
namespace, image, health) instead of gating on a non-existent k8s-browse
behind a (for hanzo) empty dedicated-cluster list. StatusTag tones the
drift-board health (healthy/warning/down). Auth is unchanged: the /paas
proxy already sends Authorization: Bearer; the fix is the token VALUE
(operator CR → paas-console-token) + these paths.
2026-06-27 17:28:37 -07:00
zeekay c32bb30154 docs: correct catalog home comment for the 3-state enablement model 2026-06-27 16:44:35 -07:00
zeekay c27a62d271 catalog: canonical 10-category Open AI Cloud (GCP-compatible) + all-services Status
Build Docker Image / docker (push) Successful in 2m33s
Rebuild the product catalog to the canonical ten categories, in order:
AI · Compute · Data · Network · Security · Dev · Deploy · Observe · Web3 · Apps.
66 entries; each names its Google Cloud equivalent. Honest 3-state enablement:
enabled (23, in-console modules) / external (16, live Hanzo surfaces) / soon (27).
Every real working module is preserved (recategorized, not broken).

Add Status (Observe): live health of every Hanzo service across clusters, from
REAL data only — composes PlatformApi.listClusters + KubernetesApi deployments
over the /paas control plane, with honest not-configured/unavailable/empty
states and no fabricated dots.

Decomplect: one coming-soon surface for all soon leaves; delete the dead
duplicate PaaS client (PlatformModule + lib/paas) and the unused coming-soon
factory. Release v0.1.6.
2026-06-27 16:43:30 -07:00
zeekay 102a37a48c release: v0.1.5
Build Docker Image / docker (push) Successful in 4m57s
Phases 1-5: nine-category catalog (mirrors hanzo.ai), in-console admin
(Identity/Secrets/Audit), Kubernetes workloads browser + real Clusters over
/paas, and an honest Observability category. tsc --noEmit + next build green.
2026-06-27 16:10:25 -07:00
zeekay 6beb670744 o11y: honest Observability category (traces/evals/prompts)
Phase 5 — add the Observability category with truthful entries, no fake charts:

- Observability (/o11y): console-native module that probes the REAL /v1/o11y
  runtime and reports status (online / not-initialized 503 / not-routed 404 /
  access / error), deep-links to the full observability surface for traces,
  evals, and prompts, and marks the native in-console browser as coming
  (HIP-0106). Never renders placeholder telemetry.
- Insights (external) + Analytics (relocated here) round out the category.

This is the staged first step for the largest old-console surface; the deeper
native port is honestly deferred, not faked.
2026-06-27 16:06:34 -07:00
zeekay 82e81ddf75 k8s: Kubernetes workloads browser + real Clusters over /paas
Phase 4 — one platform transport: route ALL control-plane calls through the
same-origin /paas proxy (server-side token injection, no CORS, honest 501
when unset) instead of direct cross-origin platform.hanzo.ai.

- platform.ts: clusters now go via /paas; add KubernetesApi over
  /v1/org/{org}/cluster/{id}/k8s/{deployments,pods,services,ingresses,events,crs}.
- KubernetesModule: cluster picker + resource tabs, defensive per-kind columns,
  honest loading/not-configured/backend-unavailable/error/empty states.
- ClustersModule: flip 'soon' -> real; render the shared honest state card for
  not-configured (501) / backend-unavailable (404).
- New shared platform/state.tsx interprets /paas errors one way.
2026-06-27 16:01:25 -07:00
zeekay 1ad8ff3fa0 admin: in-console Identity, Secrets (KMS), and Audit modules
Phase 3 — wire the console to the existing identity/secrets subsystems over
the canonical /v1 surface (HIP-0111), each an honest module:

- Identity (/iam): tabbed Organizations / Users / Roles (RBAC) over Hanzo
  IAM /v1/iam/get-{organizations,users,roles} (casdoor envelope). One generic
  AdminListView drives all three; 404/401/empty are explicit honest states.
- Audit (/audit): identity & access event log over /v1/iam/get-records.
- Secrets (/kms): KMS is zero-knowledge (encrypted names+values, token/ZAP
  auth, no list-values endpoint) — the module states the model, probes the
  real /v1/kms surface to report reachability, and deep-links to the KMS
  console. It never fabricates a secret table.

iam/kms flip from external links to modules; ext.iam/ext.kms removed.
2026-06-27 15:56:31 -07:00
zeekay 631dc417b2 catalog: mirror hanzo.ai's nine product categories
Replace the ad-hoc AI/Data/Apps/Identity/Infrastructure/Commerce grouping
with the exact nine categories + order from the marketing site product
dropdown (navigation-data.ts productsNav): AI & Agents, Developer, Apps,
Compute, Data, Async, Platform, Observability, Web3. One taxonomy across
every Hanzo surface. Empty groups don't render (catalogByCategory skips them).
2026-06-27 15:50:57 -07:00
zeekay 57371b1c9d feat(console2): Wallet & HUSD top-up module + verify-and-record endpoint; v0.1.4
Build Docker Image / docker (push) Successful in 2m47s
- WalletModule (Commerce): connect a non-custodial wallet on Hanzo Mainnet
  (36900) via ethers/EIP-1193, show wallet HUSD + cloud credit balances, and
  top up credit with HUSD. Honest states throughout (no wallet, HUSD greenfield,
  chain unreachable, endpoint unconfigured) — never a fabricated balance.
- src/lib/wallet/hanzo-evm.ts: one canonical Hanzo Mainnet + HUSD definition
  (ethers v6), env-overridable RPC; HUSD address is public (NEXT_PUBLIC), never
  a secret.
- src/lib/api/wallet.ts: cloud balance via the real GET /v1/billing/balance, and
  recordWalletTopup → the console's own POST /billing/topup/wallet.
- app/billing/topup/wallet/route.ts: server route (mirrors /paas) — verifies the
  HUSD transfer on-chain, then records to commerce as a husd crypto payment and
  credits the balance. Hosted here because billing.hanzo.ai is a static export
  and commerce is owned elsewhere. Server-only config (KMS, never NEXT_PUBLIC).
- registry: wallet entry; api/index: WalletApi export. Adds ethers 6.17.0.
2026-06-27 14:56:35 -07:00
zeekay 0076f411b0 console2: per-product discover interstitials (docs + GitHub OSS + dividends); v0.1.3
Build Docker Image / docker (push) Successful in 2m29s
'Interstitial screens to discover guides/docs/how-tos + link to GitHub OSS for
each product' + the OSS-gets-paid hook:
- /discover/<id> route + ProductInterstitial: identity + open/get-started CTA,
  Docs & guides + Open-source (GitHub) link cards, and an 'Open source gets paid'
  card stating the model (25% of cloud revenue -> OSS contributors, computed from
  each deployment's SBOM, settled on-chain in HUSD) with Contribute + OSS-dividends
  links. Catalog cards gain a Learn-more (info) affordance; non-enabled Get-started
  routes to the interstitial.
- OSS_PROGRAM constant (one source of truth: 25% / HUSD / SBOM / dividends dashboard),
  aligned with hanzo.ai sbomRevenueConfig + the commerce contributor/payout system.

tsc clean; next build green (/discover/[id] live).
2026-06-27 14:22:42 -07:00
zeekay 9b04ea848c ci: semver image tags only (no sha, no :latest); v0.1.2
Build Docker Image / docker (push) Successful in 2m32s
Per directive 'use proper semver for all, no sha': build-image.yml now tags
ghcr.io/hanzoai/console2 with the semver version only — a v* git tag publishes
that exact version, a main push publishes v<package.json version> (bump to
release). Drops the sha-<sha7> and floating :latest tags. Bump 0.1.1 -> 0.1.2
(GCP-grade resource detail + Plans & Pricing + Team launcher).
2026-06-27 13:58:21 -07:00
zeekay e30e9c7b3e console2: GCP-grade resource detail + Plans & Pricing + Team launcher
The 'manage/create databases ala GCP' + 'discover, enable, pay' surface,
all on REAL backend contracts (no invented controls):

- ResourceModule gains an instance DETAIL view (list+detail in one component,
  mirroring ProvidersModule's params.name): click a resource -> GET /v1/<kind>/<name>
  overview (status/kind/endpoint/user/db/created) + connection guidance + danger-zone
  delete. New resourceRoutes() helper binds index + :name to one instance (DRY);
  the 7 data products (sql/vector/datastore/kv/search/s3/docdb) use it. Resources
  are serverless/create-by-name (POST /v1/<kind> takes only {name}) — no fake
  tier/size/region knobs.
- PlansModule: GCP-style Plans & Pricing on the live rate card (GET /v1/pricing):
  per-tier cards (vCPU/RAM/SSD/transfer, monthly+hourly, feature bullets, free/popular
  badges) + block-storage metering. Paying delegates to config.billingUrl (the one
  money surface, never reimplemented). New typed PlansApi (plans.ts).
- Team launcher: hanzo.team catalog entry (Apps, external team.hanzo.ai).

tsc --noEmit clean; next build green (9/9).
2026-06-27 13:26:41 -07:00
zeekay f057352fc3 feat(console2): Bot module — in-console /v1/bot status + operator deep-links
Flip the existing 'bot' catalog entry from an external link to an in-console
module. New BotModule reports live gateway status from /v1/bot/health (via a
small BotApi on the REST layer, since the bot speaks plain JSON, not the
casibase envelope) and deep-links the operator surfaces (bot home, control UI,
docs, source). Backend (hanzoai/bot -> bot-gateway) is already routed at
/v1/bot/* by the unified gateway.
2026-06-27 12:45:58 -07:00
zeekay a0db49f282 fix(console2): cloud fallback → api.hanzo.ai (gated gateway), not the SPA host
Found via testing: cloud.hanzo.ai serves the SPA catch-all (200 text/html for any /v1 path), NOT the backend; the real /v1 is behind the unified gateway api.hanzo.ai (hanzoai/ingress→hanzoai/gateway v2.13.0 → cloud / separate services / per-org k8s, rate-limited+gated+priced). Browser uses same-origin /v1 (each console host's ingress proxies to the gateway); this SSR fallback now points at the gateway.
2026-06-27 12:21:14 -07:00
zeekay c09943eb08 feat(console2): one image, multi-brand by hostname (hanzo/lux/zoo)
Resolves the brand at RUNTIME from the request hostname (console.hanzo.ai→hanzo, console.lux.cloud→lux, console.zoo.cloud→zoo). Tenancy: ONE cloud /v1 backend serves all orgs (same-origin per host → first-party cookie, no CORS), each brand authenticates against its OWN live IAM (hanzo.id / lux.id / zoolabs.id / pars.id; client_id <org>-cloud per HIP-0111). config is a brand-aware Proxy so the /v1 client + IAM SDK go per-host with no consumer changes; cloudUrl is same-origin (window.origin); NEXT_PUBLIC_* still override. Dockerfile + CI no longer bake NEXT_PUBLIC_* (that pinned one brand) → one brand-agnostic image (sha-<sha7>+latest). Fixed a 'Hanzo' subtitle brand-leak. Verified: tsc clean + next build green. NOTE: cloud backend must accept all brand issuers/auds; each console host's ingress must proxy /v1 to the backend + register the redirect URI in that brand's IAM app.
2026-06-27 12:11:09 -07:00
Hanzo AI 0e2ee1f394 feat(console2): brand polish — favicon, monochrome, sized loader, social sign-in
- favicon: canonical hanzo.app ▼/H mark via app-router icon files
  (favicon.ico, icon.svg, apple-icon.png); viewport themeColor #000000
- loader: monochrome HanzoMark ~40–48px centered (not viewport-filling),
  replacing the oversized @hanzo/gui Spinner in AuthGate + OAuth callback
- monochrome: black/white/grey chrome, white (theme="light") primary
  buttons; drop every blue/green/red/yellow/violet theme + $color accent
  across products, status tags, dashboard badges, default store color
- sign-in: dedicated GitHub + Google buttons (provider_hint) + Hanzo ID,
  all via hanzo.id OIDC (client_id=hanzo-cloud); IAM owns provider OAuth,
  console never reconstructs github.com/accounts.google.com URLs
2026-06-26 19:22:26 -07:00
Hanzo AI c6f55d8646 docs(LLM): canonical issuer hanzo.id + hanzo-cloud app + deploy reality [skip ci] 2026-06-26 18:27:53 -07:00
Hanzo AI 14519c41c5 fix(ci): emit sha+latest tags via GITHUB_OUTPUT heredoc
The mainnet branch wrote a two-line tags value with $'\n', which GitHub
Actions rejects for key=value outputs ("Invalid format ...:latest") —
so every Build Docker Image run failed at 'Compute per-env config' and
no image was ever produced by CI. Use the multi-line heredoc output form
so build-push-action receives both newline-separated tags.
2026-06-26 18:15:50 -07:00
Hanzo AI 1f80429f97 fix(auth): sign-in via canonical issuer hanzo.id, not iam.hanzo.ai
NEXT_PUBLIC_IAM_URL is inlined at build time, so the deployed image sent
the browser authorize redirect to https://iam.hanzo.ai — which mints
iss=https://iam.hanzo.ai, a different issuer than the canonical
https://hanzo.id that the cloud /v1 backend validates. Sign-in dropped
on iam.hanzo.ai and never round-tripped back to console2.

- src/config, .env.example, Dockerfile ARG, mainnet CI build-arg: point
  the browser at https://hanzo.id (the value baked into the image).
- Align iamAppName/iamClientId source defaults to hanzo-cloud (was the
  stale hanzo-console / empty), matching the cloud-api backend binding
  (iamApplication / IAM_AUDIENCE = hanzo-cloud). A default build now
  targets the correct app instead of a non-existent one with no client.

App/client stay hanzo-cloud on purpose: console2 is a front-end of the
shared cloud /v1 backend, which exchanges the code and validates aud as
hanzo-cloud — it is not its own IAM principal.
2026-06-26 18:14:12 -07:00
Hanzo AI 800b3481d7 feat(console2): 10-category cloud axis + embedded PaaS, no fakes
Job 2: reorganize the catalog into the canonical 10-category CLOUD AXIS
(AI/Compute/Data/Network/Security/Dev/Deploy/Observe/Chain/Apps) so console2
reads like a cloud console. Three entry kinds (module/external/soon) => zero
dead links, zero fakes; 'soon' primitives render an honest ComingSoon overview.

Job 3: PaaS embedded natively under Deploy (PlatformModule) wired to the real
platform.hanzo.ai control plane via a same-origin /paas proxy (service token
server-side from KMS). Real apps + declared/running/drift + redeploy; honest
loading/not-configured/empty states.

Job 4: catalog honest by construction; PaaS shows only real data. No placeholder
cards, demo projects, or lorem stats.
2026-06-26 14:48:16 -07:00
Hanzo AI 7e3fa15425 Merge feat/cloud-taxonomy-10cat into main (union)
Union merge — feature's categorized catalog architecture is canonical, with
main's data/storage products + status badges folded in (lose nothing):

- registry.tsx: feature's CatalogEntry discriminated union (category + kind +
  admin + derived productModules) as the base; FOLD IN main's data/storage cloud
  products (vector, sql, datastore, kv, search, s3, docdb, base, clusters) as
  categorized 'module' entries via resourceModule/comingSoon — these are the
  console frontend for cloud's /v1 provisioning control plane. Extend
  ProductStatus to 'enabled'|'available'|'soon'|'waitlist' and add repo? so the
  folded products keep their status badges. Resolve the id 'search' collision:
  managed Search data product keeps id 'search' (owns the route, maps to the
  provisioning kind); feature's external search.hanzo.ai becomes 'ai-search'.
- DashboardShell.tsx: feature's Pinned + categorized NavRow shell; FOLD main's
  SOON/WAITLIST status badge into NavRow (feature had dropped it).
- page.tsx: feature's grouped ProductCard grid; extend StatusBadge to render
  Soon/Waitlist (blue/yellow) alongside Enabled/Available (folds main's badge).

Verify: tsc --noEmit strict CLEAN; next build green (6/6 pages). The repo's
untracked PaaS WIP (ComingSoon/PlatformModule/paas — another agent's, in neither
branch) is left untouched and excluded from this commit.
2026-06-26 14:47:11 -07:00
07db3af7fe feat(products): register full data/storage catalog + enablement status (#1)
* feat(products): register full data/storage catalog + enablement status

Adds `status` ('enabled'|'soon'|'waitlist') + `repo` to every ProductModule so
the console tracks enablement of all Hanzo cloud products in ONE place. Registers
the OSS-Google-Cloud data/storage suite as modules — Vector, SQL, Datastore, KV,
Search, S3, Base (soon) and DocDB (waitlist) — each a ZAP-native Hanzo fork mapped
to its repo, with a ComingSoon placeholder (repo link + status) until its admin
module lands. Nav + dashboard cards render the status badge.

Products → repos: Vector=hanzoai/vector (Qdrant-compat), SQL=hanzoai/sql,
Datastore=hanzoai/datastore (ClickHouse-compat), KV=hanzoai/kv (Redis-compat),
Search=hanzoai/search, S3=hanzoai/s3, Base=hanzoai/base, DocDB=hanzoai/docdb.

* feat(products): add Clusters module (shared Hanzo Cloud vs BYO DOKS)

The data/storage products (Vector/SQL/Datastore/KV/Search/S3/Base/DocDB) are
all live on hanzo-k8s via the operator. Adds the one new control-plane surface
the platform needs: Clusters — choose where workloads run (shared multi-tenant
Hanzo Cloud, or your own/Hanzo-provisioned DOKS cluster, reconciled by the same
operator). Deploy-any-repo stays under Applications.

* feat(products): working data/storage + clusters admin modules

Replace the comingSoon() placeholders for sql/vector/datastore/kv/search/s3/
docdb with a DRY resourceModule() factory over the provisioning REST contract
(POST/GET/DELETE /v1/<kind>): list, create-with-once-shown connectionString +
password reveal ("store this now"), and per-row delete. Tenancy is server-side
(gateway injects X-Org-Id), so the browser sends cookie creds only.

- lib/api/client.ts: plain-REST helpers (restGet/restPost/restDelete + v1Url)
  beside the casibase envelope path — provisioning + platform speak raw JSON /
  201 / 204 / DELETE, reusing the same cookie creds + ApiError. One transport.
- lib/api/provisioning.ts: ProvisioningApi keyed by ResourceKind.
- lib/api/platform.ts: PlatformApi (DOKS clusters) with centralized
  CLUSTER_ROUTES + DOKS region/size options; base = NEXT_PUBLIC_PLATFORM_URL.
- components/products/ResourceModule.tsx: the factory (list/create/delete,
  copyable masked secret reveal, slug validation, loading/empty/error).
- components/products/ClustersModule.tsx: list + provision DOKS + attach (name
  + kubeconfig) against the platform control plane.
- components/ui/StatusTag.tsx, lib/slug.ts: shared/DRY across both modules.
- registry: swap routes to the working modules and clear 'soon'/'waitlist'
  status so nav badges clear. Base stays a placeholder (no single-resource).
- fix two pre-existing type errors in ComingSoonModule (maxWidth->maxW,
  theme active->blue) so the branch typechecks clean.

kind map: sql->databases, vector->vector, datastore->datastore, kv->kv,
search->search, s3->storage, docdb->docdb.

Verified: npm ci + tsc --noEmit => 0 errors (CI-equivalent flat install).

* console: native product naming + gate Clusters to coming-soon

Native naming (Hanzo brand rule — product name only, no upstream OSS
name in any surface):
- ResourceKind wire kinds align to the cloud /v1 contract exactly:
  databases→sql, storage→s3 (vector/datastore/kv/search/docdb unchanged).
- Strip every upstream name from product descriptions + connectionHints
  (Postgres/Qdrant/ClickHouse/Redis/Meilisearch/Mongo/-compatible → the
  Hanzo product name). Zero forbidden names remain in registry.tsx.

Clusters: register as status:'soon' rendering the coming-soon placeholder
(repo hanzoai/operator). The platform attach/provision endpoints in
platform.ts are unconfirmed (PR not merged), so we don't ship a button to
a non-existent endpoint. ClustersModule.tsx + platform.ts stay in the tree
(unreferenced from the live route) to flip back to enabled in one line once
the platform surface lands. Data products (sql/vector/datastore/kv/search/
s3/docdb) stay ENABLED — their /v1 kinds are real and shipping now.

tsc --noEmit: clean.

---------

Co-authored-by: zeekay <z@zeekay.io>
2026-06-26 13:20:20 -07:00
Hanzo AI 85e68e433e feat(console): unified product hub — catalog, pinnable favorites, account-backed prefs
Turn console2 into the one place to see, enable, and manage every Hanzo
product, with billing → billing.hanzo.ai for all.

- registry: ONE product catalog (categories + module-vs-external + enablement
  status), the single source of truth for nav, overview, and router. Adding a
  product = one CatalogEntry.
- favorites: pin products to the sidebar. Built on a new account-backed
  preferences layer (usePreferences) — customizations persist to the IAM user
  account, so they follow the user across every device/login and product.
  localStorage is only a fast-paint cache.
- shell: Pinned section + categorized catalog; exact-match active (no
  double-highlight); each row opens (in-console route or external tab) + pin toggle.
- overview: product catalog grouped by category with status, pin, and
  Open/Get-started.
- api: AccountApi.updatePreferences → POST /v1/update-preferences (self-scoped).

Typecheck + next build clean.
2026-06-26 11:15:21 -07:00
Hanzo AI 5527bb30dd fix(docker): npm install over npm ci for the @hanzo/gui dep tree
npm ci failed in CI (node:22-alpine npm 10.9) with EUSAGE 'Missing:
react-native-worklets@0.8.3 from lock file' — @hanzo/gui's react-native
optional/platform deps resolve differently across npm versions, so a
lockfile built by one npm is rejected by another's strict ci. npm install
reconciles deterministically for the build platform.
2026-06-25 13:57:42 -07:00
Hanzo AI 1b03d838db deps(console2): declare @zap-proto/web, @zap-proto/zap, superjson
The ZAP-native data layer (src/lib/zap/{client,transport,providers}.ts)
imports @zap-proto/web/client, @zap-proto/zap, and superjson; declare them
so a clean `npm ci` in CI resolves them (the prior build failed: 'Module
not found: @zap-proto/web/client' because the lockfile lacked them).
2026-06-25 13:48:28 -07:00
Hanzo AI 42f591214c feat(providers): cut Providers module to ZAP-native transport
Swap the Providers views from the REST `~/lib/api` to the ZAP-native
`~/lib/zap` (one import line each) — proving the @hanzo/gui + @zap-proto/web
go-forward: same call surface, binary ZAP over WebSocket instead of
JSON-over-HTTP, zero view-component changes.

- src/lib/zap/index.ts: barrel re-exporting ProviderApi (= ProviderApiZap),
  ApiError, Provider — the drop-in twin of ~/lib/api.
- ProviderListView/ProviderEditView: import from ~/lib/zap.
- providers.ts list(): return { rows, total } to match the REST getList
  contract exactly (true drop-in; fixes the list-view shape).

tsc --noEmit (strict) clean. Backs onto cloud's new /zap WS face which
dispatches each call into the same /v1 casibase handlers.
2026-06-25 13:44:26 -07:00
Antje Worring 668f6b8951 ci: env-aware image build (mainnet/testnet/devnet)
workflow_dispatch input 'env' bakes per-env NEXT_PUBLIC_* + tags the image
:dev/:test (mainnet stays :sha-+:latest). Hosts follow svc.env.hanzo.ai. Adds
NEXT_PUBLIC_BILLING_URL build-arg (per-env billing portal).
2026-06-22 03:17:50 -07:00
Antje Worring 7895e104b0 feat(nav): Billing link to the existing billing portal (no rebuild)
Billing is owned by hanzoai/commerce (backend) + hanzoai/billing (portal at
billing.hanzo.ai) — the console must not reimplement payments/balance. Add an
externalLinks registry (orthogonal to product modules) and render it in the
shell; Billing opens config.billingUrl (NEXT_PUBLIC_BILLING_URL, default
https://billing.hanzo.ai) in a new tab.
2026-06-22 02:44:56 -07:00
Antje Worring bfd2a2dd37 fix(auth): treat casibase anonymous-user as logged-out
The backend auto-creates an anonymous-user session for the chat product, so
get-account returns status:ok even with no real sign-in. As an ADMIN console
that made AuthGate show the dashboard for an unauthenticated visitor, while
admin endpoints (get-providers, etc.) rejected the anon session with 'Please
sign in first'. Treat type==='anonymous-user' as null so the console requires
a real IAM sign-in.
2026-06-22 00:39:44 -07:00
Antje Worring cf5d3094ae feat(console2): Models, Applications, Stores, Chat admin surfaces (@hanzo/gui /v1)
Mirror the Providers pattern: product-module + list/edit views on @hanzo/gui +
typed /v1 API modules, registered in the nav. tsc 0 errors, next build green.
2026-06-21 15:58:05 -07:00
Antje Worring 7a3dd59128 fix(ci): ensure public/ exists in image build + skip provenance artifacts
- Dockerfile: mkdir -p public before build (git doesn't track empty public/,
  so the runner COPY /app/public was failing)
- build-image: provenance/sbom false (avoid GitHub artifact-quota upload)
2026-06-21 15:05:32 -07:00
Antje Worring f117a01fac ci: Dockerfile + build-image workflow → ghcr.io/hanzoai/console2
Next.js 15 multi-stage build; NEXT_PUBLIC_* baked at build (same-origin /v1,
IAM client_id=hanzo-cloud to match cloud-api's /v1/signin). Self-hosted ARC
runner, push to ghcr.io/hanzoai/console2.
2026-06-21 15:01:36 -07:00
hanzo-dev 2b4d64c9cf feat: Hanzo Cloud Console (console2) on @hanzo/gui over the unified /v1 backend
Next.js 15 (app router) + @hanzo/gui consumed at runtime via transpilePackages.
Typed /v1 client (Provider/ModelRoute/Application/Store/Chat/Account), Hanzo IAM
OIDC auth, extensible product-module registry, and a full Providers admin surface
(list + view/edit) ported clean from hanzoai/ai onto Gui. BSD-3-Clause.
2026-06-21 14:58:20 -07:00
1573 changed files with 64511 additions and 166202 deletions
-4
View File
@@ -1,4 +0,0 @@
[codespell]
skip = .git,*.pdf,*.svg,package-lock.json,*.prisma,pnpm-lock.yaml
ignore-words-list = afterall,vertx,notIn
-8
View File
@@ -1,8 +0,0 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
**/.next
.git
**/node_modules
-92
View File
@@ -1,92 +0,0 @@
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/postgres"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
# Clickhouse
CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
CLICKHOUSE_URL="http://localhost:8123"
CLICKHOUSE_USER="clickhouse"
CLICKHOUSE_PASSWORD="clickhouse"
CLICKHOUSE_MIGRATION_CLUSTER_DISABLED="true"
# Next Auth
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# https://next-auth.js.org/configuration/options#secret
# NEXTAUTH_SECRET=""
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="secret"
# Langfuse Cloud Environment
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
# Langfuse experimental features
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
# Salt for API key hashing
SALT="salt"
# Email
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
# DON'T PANIC: The Azurite Secrets are well-known and meant to be hard-coded
# S3 storage
S3_ENDPOINT=http://localhost:10000/devstoreaccount1
S3_ACCESS_KEY_ID=devstoreaccount1
S3_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
S3_BUCKET_NAME=langfuse
S3_REGION=auto
## Necessary for minio compatibility
S3_FORCE_PATH_STYLE=true
# S3 Media Upload LOCAL
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=devstoreaccount1
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
LANGFUSE_S3_MEDIA_UPLOAD_REGION=auto
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://localhost:10000/devstoreaccount1
## Necessary for minio compatibility
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
# S3 Event Bucket Upload
## Set to true to test uploading all events to S3
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=devstoreaccount1
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
LANGFUSE_S3_EVENT_UPLOAD_REGION=auto
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://localhost:10000/devstoreaccount1
## Necessary for minio compatibility
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
LANGFUSE_USE_AZURE_BLOB=true
# Set during docker build of application
# Used to disable environment verification at build time
# DOCKER_BUILD=1
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_AUTH="myredissecret"
# openssl rand -hex 32 used only here
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# speeds up local development by not executing init scripts on server startup
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
LANGFUSE_READ_FROM_POSTGRES_ONLY=false
LANGFUSE_RETURN_FROM_CLICKHOUSE=true
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE=true
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY=true
LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING="true"
-89
View File
@@ -1,89 +0,0 @@
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/postgres"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
# Clickhouse
CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
CLICKHOUSE_URL="http://localhost:8123"
CLICKHOUSE_USER="clickhouse"
CLICKHOUSE_PASSWORD="clickhouse"
CLICKHOUSE_CLUSTER_DISABLED="true"
# Next Auth
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# https://next-auth.js.org/configuration/options#secret
# NEXTAUTH_SECRET=""
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="secret"
# Langfuse Cloud Environment
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
# Langfuse experimental features
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
# Salt for API key hashing
SALT="salt"
# Email
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
# S3 storage
S3_ENDPOINT=http://localhost:9090
S3_ACCESS_KEY_ID=minio
S3_SECRET_ACCESS_KEY=miniosecret
S3_BUCKET_NAME=langfuse
S3_REGION=us-east-1
## Necessary for minio compatibility
S3_FORCE_PATH_STYLE=true
# S3 Media Upload LOCAL
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_MEDIA_UPLOAD_REGION=us-east-1
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://localhost:9090
## Necessary for minio compatibility
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
# S3 Event Bucket Upload
## Set to true to test uploading all events to S3
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_EVENT_UPLOAD_REGION=us-east-1
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://localhost:9090
## Necessary for minio compatibility
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
# Set during docker build of application
# Used to disable environment verification at build time
# DOCKER_BUILD=1
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_AUTH="myredissecret"
# openssl rand -hex 32 used only here
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# speeds up local development by not executing init scripts on server startup
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
LANGFUSE_READ_FROM_POSTGRES_ONLY=false
LANGFUSE_RETURN_FROM_CLICKHOUSE=true
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE=true
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY=true
LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING="true"
-81
View File
@@ -1,81 +0,0 @@
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/postgres"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
# Clickhouse
CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
CLICKHOUSE_URL="http://localhost:8123"
CLICKHOUSE_USER="clickhouse"
CLICKHOUSE_PASSWORD="clickhouse"
CLICKHOUSE_CLUSTER_ENABLED="false"
# Next Auth
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# https://next-auth.js.org/configuration/options#secret
# NEXTAUTH_SECRET=""
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="secret"
# Langfuse Cloud Environment
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
# Langfuse experimental features
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
# Salt for API key hashing
SALT="salt"
# Email
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
# S3 storage
S3_ENDPOINT=http://localhost:9090
S3_ACCESS_KEY_ID=minio
S3_SECRET_ACCESS_KEY=miniosecret
S3_BUCKET_NAME=langfuse
S3_REGION=us-east-1
## Necessary for minio compatibility
S3_FORCE_PATH_STYLE=true
# # S3 Media Upload LOCAL
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_MEDIA_UPLOAD_REGION=us-east-1
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://localhost:9090
## Necessary for minio compatibility
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
# S3 Event Bucket Upload
## Set to true to test uploading all events to S3
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_EVENT_UPLOAD_REGION=us-east-1
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://localhost:9090
## Necessary for minio compatibility
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
# Set during docker build of application
# Used to disable environment verification at build time
# DOCKER_BUILD=1
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_AUTH="myredissecret"
# openssl rand -hex 32 used only here
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# speeds up local development by not executing init scripts on server startup
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
+24
View File
@@ -0,0 +1,24 @@
# Hanzo Cloud Console — environment.
# Copy to .env.local and adjust. All values are public (NEXT_PUBLIC_*) since the
# console is a browser app that talks to the unified /v1 backend with cookies.
# Unified Hanzo Cloud backend (the casibase /v1 API). Default: production.
# Local backend: http://localhost:14000
NEXT_PUBLIC_CLOUD_URL=https://cloud.hanzo.ai
# Hanzo PaaS (platform.hanzo.ai) — DOKS cluster control plane for the Clusters module.
NEXT_PUBLIC_PLATFORM_URL=https://platform.hanzo.ai
# Hanzo IAM (OIDC authority). Canonical issuer is https://hanzo.id — tokens are
# minted with iss=https://hanzo.id, which the cloud /v1 backend validates against.
# iam.hanzo.ai is the legacy zone (iss=https://iam.hanzo.ai) and MUST NOT be used
# by the browser, or sign-in drops on iam.hanzo.ai with an issuer mismatch.
NEXT_PUBLIC_IAM_URL=https://hanzo.id
# IAM application console2 authenticates as. console2 is a front-end of the
# shared Hanzo Cloud /v1 backend, which exchanges the OIDC code and validates the
# token as app `hanzo-cloud` (aud=hanzo-cloud) — so the front-end presents the
# same app/client_id. Must match the cloud-api binding, not a console-only app.
NEXT_PUBLIC_IAM_APP_NAME=hanzo-cloud
NEXT_PUBLIC_IAM_ORG_NAME=hanzo
NEXT_PUBLIC_IAM_CLIENT_ID=hanzo-cloud
-22
View File
@@ -1,22 +0,0 @@
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
# Next Auth
NEXTAUTH_SECRET="secret"
NEXTAUTH_URL="http://localhost:3000"
# feature flag to enable experimental features locally
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
SALT="salt"
# Redis
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_AUTH="myredissecret"
# openssl rand -hex 32 used only here
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
-258
View File
@@ -1,258 +0,0 @@
# More information: https://langfuse.com/docs/deployment/self-host
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
# DATABASE_URL supports pooled connections, but then you need to set DIRECT_URL
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
# DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
# SHADOW_DATABASE_URL=
# optional, set to true to disable automated database migrations on Docker start
# LANGFUSE_AUTO_POSTGRES_MIGRATION_DISABLED=
# Next Auth
# NEXTAUTH_URL does not need to be set when deploying on Vercel
NEXTAUTH_URL="http://localhost:3000"
# For each of these, you can generate a new secret on the command line with:
# openssl rand -base64 32
NEXTAUTH_SECRET="secret" # https://next-auth.js.org/configuration/options#secret
SALT="salt" # salt used to hash api keys
# API level encryption for sensitive data
# Must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32
ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000"
# Use CSP headers to enforce HTTPS, optional
# LANGFUSE_CSP_ENFORCE_HTTPS="true"
# Configure base path for self-hosting, optional
# Note: You need to build the docker image with the base path set and cannot use the pre-built docker image if you set this.
# NEXT_PUBLIC_BASE_PATH="/app"
# Docker only, optional
# PORT=3000
# HOSTNAME=localhost
# Opentelemetry, optional
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
OTEL_SERVICE_NAME="langfuse"
# Default role for users who sign up, optional, can be org or org+project
# LANGFUSE_DEFAULT_ORG_ID=
# LANGFUSE_DEFAULT_ORG_ROLE=
# LANGFUSE_DEFAULT_PROJECT_ID=
# LANGFUSE_DEFAULT_PROJECT_ROLE=
# Logging, optional
# LANGFUSE_LOG_LEVEL=info
# LANGFUSE_LOG_FORMAT=text
# Enable experimental features, optional
# LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true
# Auth, optional configuration
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
# AUTH_IGNORE_ACCOUNT_FIELDS=foo,bar
# AUTH_DISABLE_USERNAME_PASSWORD=true
# AUTH_DISABLE_SIGNUP=true
# AUTH_SESSION_MAX_AGE=43200 # 30 days in minutes (default)
# SSO, each group is optional
# AUTH_GOOGLE_CLIENT_ID=
# AUTH_GOOGLE_CLIENT_SECRET=
# AUTH_GOOGLE_ALLOW_ACCOUNT_LINKING=false
# AUTH_GOOGLE_ALLOWED_DOMAINS=langfuse.com,google.com # optional allowlist of workspace domains that can sign in via Google
# AUTH_GITHUB_CLIENT_ID=
# AUTH_GITHUB_CLIENT_SECRET=
# AUTH_GITHUB_ALLOW_ACCOUNT_LINKING=false
# AUTH_GITHUB_ENTERPRISE_CLIENT_ID=
# AUTH_GITHUB_ENTERPRISE_CLIENT_SECRET=
# AUTH_GITHUB_ENTERPRISE_BASE_URL=
# AUTH_GITHUB_ENTERPRISE_ALLOW_ACCOUNT_LINKING=false
# AUTH_GITLAB_CLIENT_ID=
# AUTH_GITLAB_CLIENT_SECRET=
# AUTH_GITLAB_ALLOW_ACCOUNT_LINKING=false
# AUTH_GITLAB_ISSUER=
# AUTH_AZURE_AD_CLIENT_ID=
# AUTH_AZURE_AD_CLIENT_SECRET=
# AUTH_AZURE_AD_TENANT_ID=
# AUTH_AZURE_ALLOW_ACCOUNT_LINKING=false
# AUTH_OKTA_CLIENT_ID=
# AUTH_OKTA_CLIENT_SECRET=
# AUTH_OKTA_ISSUER=
# AUTH_OKTA_ALLOW_ACCOUNT_LINKING=false
# AUTH_AUTH0_CLIENT_ID=
# AUTH_AUTH0_CLIENT_SECRET=
# AUTH_AUTH0_ISSUER=
# AUTH_AUTH0_ALLOW_ACCOUNT_LINKING=false
# AUTH_COGNITO_CLIENT_ID=
# AUTH_COGNITO_CLIENT_SECRET=
# AUTH_COGNITO_ISSUER=
# AUTH_COGNITO_ALLOW_ACCOUNT_LINKING=false
# AUTH_KEYCLOAK_CLIENT_ID=
# AUTH_KEYCLOAK_CLIENT_SECRET=
# AUTH_KEYCLOAK_ISSUER=
# AUTH_KEYCLOAK_ALLOW_ACCOUNT_LINKING=false
# AUTH_CUSTOM_CLIENT_ID=
# AUTH_CUSTOM_CLIENT_SECRET=
# AUTH_CUSTOM_ISSUER=
# AUTH_CUSTOM_NAME=
# AUTH_CUSTOM_SCOPE="openid email profile" # optional
# AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING=false
# AUTH_CUSTOM_ID_TOKEN=false # optional, default is true
# Transactional email, optional
# Defines the email address to use as the from address.
# EMAIL_FROM_ADDRESS=
# Defines the connection url for smtp server.
# SMTP_CONNECTION_URL=
# S3 storage, optional, used for exports from the UI
# S3_ENDPOINT=
# S3_ACCESS_KEY_ID=
# S3_SECRET_ACCESS_KEY=
# S3_BUCKET_NAME=
# S3_REGION=
# BATCH_EXPORT_DOWNLOAD_LINK_EXPIRATION_HOURS=
# S3 storage for events, optional, used to persist all incoming events
# LANGFUSE_S3_EVENT_UPLOAD_ENABLED="true"
# LANGFUSE_S3_EVENT_UPLOAD_BUCKET=
# Optional prefix to be used within the bucket. Must end with `/` if set
# LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
# The following four options are optional and fallback to the normal SDK credential provider chain if omitted
# See https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html
# LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=
# LANGFUSE_S3_EVENT_UPLOAD_REGION=
# LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=
# LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=
# Exports are streamed to S3 in pages to avoid memory issues
# The page size can be adjusted if needed to optimize performance
# DB_EXPORT_PAGE_SIZE=1000
# Automated provisioning of default resources
# LANGFUSE_INIT_ORG_ID=org-id
# LANGFUSE_INIT_ORG_NAME=org-name
# LANGFUSE_INIT_PROJECT_ID=project-id
# LANGFUSE_INIT_PROJECT_NAME=project-name
# LANGFUSE_INIT_PROJECT_PUBLIC_KEY=pk-1234567890
# LANGFUSE_INIT_PROJECT_SECRET_KEY=sk-1234567890
# LANGFUSE_INIT_USER_EMAIL=user@example.com
# LANGFUSE_INIT_USER_NAME=User Name
# LANGFUSE_INIT_USER_PASSWORD=password
### START Enterprise Edition Configuration
# Allowlisted users that can create new organizations, by default all users can create organizations
# LANGFUSE_ALLOWED_ORGANIZATION_CREATORS=user1@langfuse.com,user2@langfuse.com
# UI Customization Options
# LANGFUSE_UI_API_HOST=https://api.example.com
# LANGFUSE_UI_DOCUMENTATION_HREF=https://docs.example.com
# LANGFUSE_UI_SUPPORT_HREF=https://support.example.com
# LANGFUSE_UI_FEEDBACK_HREF=https://feedback.example.com
# LANGFUSE_UI_LOGO_LIGHT_MODE_HREF=https://static.langfuse.com/langfuse-dev/example-logo-light-mode.png
# LANGFUSE_UI_LOGO_DARK_MODE_HREF=https://static.langfuse.com/langfuse-dev/example-logo-dark-mode.png
# LANGFUSE_UI_DEFAULT_MODEL_ADAPTER=Anthropic # OpenAI, Anthropic, Azure
# LANGFUSE_UI_DEFAULT_BASE_URL_OPENAI=https://api.openai.com/v1
# LANGFUSE_UI_DEFAULT_BASE_URL_ANTHROPIC=https://api.anthropic.com
# LANGFUSE_UI_DEFAULT_BASE_URL_AZURE_OPENAI=https://{instanceName}.openai.azure.com/openai/deployments
### END Enterprise Edition Configuration
### START Envs to be deprecated in Langfuse v3.0
# Disable the expensive analytics queries and related features
# LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES="true"
### END Envs to be deprecated in Langfuse v3.0
### START Langfuse Cloud Config
# Used for Langfuse Cloud deployments
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMANTIC VERSIONING
# NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="US"
# NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
# LANGFUSE_TEAM_SLACK_WEBHOOK=
# LANGFUSE_NEW_USER_SIGNUP_WEBHOOK=
# Posthog (optional for analytics of web ui)
# NEXT_PUBLIC_POSTHOG_HOST=
# NEXT_PUBLIC_POSTHOG_KEY=
# Sentry
# NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE
# NEXT_PUBLIC_SENTRY_DSN=
# NEXT_SENTRY_ORG=
# NEXT_SENTRY_PROJECT=
# SENTRY_AUTH_TOKEN=
# SENTRY_CSP_REPORT_URI=
# Cloudflare Turnstile
# NEXT_PUBLIC_TURNSTILE_SITE_KEY=
# TURNSTILE_SECRET_KEY=
# Demo project that users can use to try the platform
# NEXT_PUBLIC_DEMO_ORG_ID=
# NEXT_PUBLIC_DEMO_PROJECT_ID=
# Crisp chat
# NEXT_PUBLIC_CRISP_WEBSITE_ID=
# Admin API
# ADMIN_API_KEY=
# Redis
# REDIS_HOST=
# REDIS_PORT=
# REDIS_AUTH=
# REDIS_CONNECTION_STRING=
# REDIS_ENABLE_AUTO_PIPELINING=
# Cache configuration
# LANGFUSE_CACHE_API_KEY_ENABLED=
# LANGFUSE_CACHE_API_KEY_TTL_SECONDS=
# LANGFUSE_CACHE_PROMPT_ENABLED=
# LANGFUSE_CACHE_PROMPT_TTL_SECONDS=
# Rate limiting
# LANGFUSE_RATE_LIMITS_ENABLED=
# Stripe
# STRIPE_SECRET_KEY=
# STRIPE_WEBHOOK_SIGNING_SECRET=
### END Langfuse Cloud Config
## START Langfuse V3 Ingestion
# Clickhouse
# CLICKHOUSE_MIGRATION_URL=
# CLICKHOUSE_URL=
# CLICKHOUSE_USER=
# CLICKHOUSE_PASSWORD=
# Ingestion
# LANGFUSE_INGESTION_QUEUE_DELAY_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE=
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
# LANGFUSE_LEGACY_INGESTION_WORKER_CONCURRENCY=
# LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
# QUEUE_CONSUMER_LEGACY_INGESTION_QUEUE_IS_ENABLED="true"
## END Langfuse V3 Ingestion
-4
View File
@@ -1,4 +0,0 @@
node_modules
**/node_modules
build
coverage
-2
View File
@@ -1,2 +0,0 @@
# Currently inactive
# * @langfuse/maintainers
-11
View File
@@ -1,11 +0,0 @@
body:
- type: textarea
attributes:
label: Describe the feature or potential improvement
description: Please describe the change as clear and concise as possible. Remember to add context as to why you believe this is needed.
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the change here. If your idea is related to any issues or discussions, link them here.
-35
View File
@@ -1,35 +0,0 @@
name: 🐞 Bug Report
description: Create a bug report to help us improve
title: "bug: "
labels: ["🐞❔ unconfirmed bug"]
body:
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.
validations:
required: true
- type: textarea
attributes:
label: To reproduce
description: Describe how to reproduce your bug. Please provide detailed steps, code snippets, reproduction repos etc.
validations:
required: true
- type: textarea
attributes:
label: SDK and container versions
description: If you're experiencing an issue with an integration or SDK, please ensure you're using the latest version. If you're self-hosting Langfuse, check that you're running the most recent version. If updating isn't an option, please provide the specific versions you're currently using.
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the bug here, screenshots if applicable.
- type: dropdown
id: contribute
attributes:
label: Are you interested to contribute a fix for this bug?
description: If this is a confirmed bug, the maintainers are happy to support with guidance and review.
options:
- "No"
- "Yes"
validations:
required: true
-7
View File
@@ -1,7 +0,0 @@
contact_links:
- name: 💡 Feature Request
url: https://github.com/orgs/langfuse/discussions/new?category=ideas
about: Suggest any ideas you have using our discussion forums.
- name: 🤗 Get Help
url: https://github.com/orgs/langfuse/discussions/new?category=support
about: If you cant get something to work the way you expect, open a question in our discussion forums.
-36
View File
@@ -1,36 +0,0 @@
## What does this PR do?
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes # (issue)
<!-- Please provide a loom video for visual changes to speed up reviews
Loom Video: https://www.loom.com/
-->
## Type of change
<!-- Please delete bullets that are not relevant. -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Refactor (does not change functionality, e.g. code style improvements, linting)
- [ ] This change requires a documentation update
## Mandatory Tasks
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
## Checklist
<!-- Remove bullet points below that don't apply to you -->
- I haven't read the [contributing guide](https://github.com/langfuse/langfuse/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project (`npm run prettier`)
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my PR needs changes to the documentation
- I haven't checked if my changes generate no new warnings (`npm run lint`)
- I haven't added tests that prove my fix is effective or that my feature works
- I haven't checked if new and existing unit tests pass locally with my changes
-48
View File
@@ -1,48 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: npm
directory: "/" # Location of package manifests
rebase-strategy: "disabled" # use dependabot-rebase-stale
open-pull-requests-limit: 10
schedule:
interval: "daily"
versioning-strategy: "increase"
commit-message:
prefix: chore
prefix-development: chore
include: scope
ignore:
- dependency-name: "@types/node"
- dependency-name: "@trpc/*"
groups:
prisma:
patterns:
- "prisma"
- "@prisma/client"
next:
patterns:
- "eslint-config-next"
- "next"
lodash:
patterns:
- "lodash"
- "@types/lodash"
express:
patterns:
- "express"
- "@types/express"
observability:
patterns:
- "dd-trace"
- "@opentelemetry/*"
- "@appsignal/opentelemetry-instrumentation-bullmq"
- "@prisma/instrumentation"
- "@sentry/*"
radix-ui:
patterns:
- "@radix-ui/*"
+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="console2">
<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">console2</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.1 KiB

-75
View File
@@ -1,75 +0,0 @@
name: WorkflowCall - Deploy to ECS Service
on:
workflow_call:
inputs:
environment:
type: string
description: Deployment environment
required: true
service:
type: string
description: Name of the service to be deployed, e.g. web-ingestion, web, or worker.
required: true
jobs:
ecs-deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Get app name
uses: winterjung/split@v2
id: split
with:
msg: ${{ inputs.service }}
separator: "-"
- name: Checkout code
uses: actions/checkout@v4
- name: Authenticate with AWS
# GitHub/AWS recommend to use OIDC here: https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#oidc
# Probably more painful to configure, but would remove all long-lived credentials.
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
- name: Login to AWS ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push Docker image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: ${{ steps.split.outputs._0 }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build \
-t $REGISTRY/$REPOSITORY:$IMAGE_TAG \
-f ./${{ steps.split.outputs._0 }}/Dockerfile \
--build-arg NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${{ vars.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION }} \
--build-arg NEXT_LANGFUSE_TRACING_SAMPLE_RATE=${{ vars.NEXT_LANGFUSE_TRACING_SAMPLE_RATE }} \
--build-arg NEXT_PUBLIC_SENTRY_ENVIRONMENT=${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} \
--build-arg NEXT_PUBLIC_DEMO_ORG_ID=${{ vars.NEXT_PUBLIC_DEMO_ORG_ID }} \
--build-arg NEXT_PUBLIC_DEMO_PROJECT_ID=${{ vars.NEXT_PUBLIC_DEMO_PROJECT_ID }} \
--build-arg NEXT_PUBLIC_SENTRY_DSN=${{ vars.NEXT_PUBLIC_SENTRY_DSN }} \
--build-arg NEXT_PUBLIC_BUILD_ID=${{ github.sha }} \
--build-arg NEXT_PUBLIC_POSTHOG_KEY=${{ vars.NEXT_PUBLIC_POSTHOG_KEY }} \
--build-arg NEXT_PUBLIC_POSTHOG_HOST=${{ vars.NEXT_PUBLIC_POSTHOG_HOST }} \
--build-arg NEXT_PUBLIC_CRISP_WEBSITE_ID=${{ vars.NEXT_PUBLIC_CRISP_WEBSITE_ID }} \
--build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \
--build-arg SENTRY_ORG=${{ vars.SENTRY_ORG }} \
--build-arg SENTRY_PROJECT=${{ vars.SENTRY_PROJECT }} \
.
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
- name: Render AWS ECS Task Definition
id: render-task-definition
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
container-name: ${{ inputs.service }}
image: ${{ steps.login-ecr.outputs.registry }}/${{ steps.split.outputs._0 }}:${{ github.sha }}
task-definition-family: ${{ inputs.environment }}-${{ inputs.service }}
- name: Update AWS ECS Service
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-task-definition.outputs.task-definition }}
service: ${{ inputs.environment }}-${{ inputs.service }}
cluster: ${{ inputs.environment }}-cluster
wait-for-service-stability: true
+78
View File
@@ -0,0 +1,78 @@
name: Build Docker Image
# Builds + pushes ghcr.io/hanzoai/console2 on the self-hosted ARC runner. ONE
# brand-agnostic image serves every brand: console2 resolves the brand at RUNTIME
# from the request hostname (console.hanzo.ai → hanzo, console.lux.cloud → lux,
# console.zoo.cloud → zoo; src/config/index.ts), and /v1 is same-origin per host.
# So NO NEXT_PUBLIC_* are baked — baking them would pin the image to one brand.
# Tags: SEMVER ONLY (no sha, no :latest) — a `v*` git tag publishes that exact
# version; a main push publishes `v<package.json version>` (bump to release).
#
# Build muscle: RAW `docker buildx build` on the host builder — the canonical
# hanzoai/ci pattern. We deliberately do NOT use docker/setup-buildx-action +
# docker/build-push-action: that pair spins up an EPHEMERAL buildkit container
# that cannot see the host's image cache, so it re-pulled `node:22-alpine` from
# Docker Hub every run and tripped the unauthenticated 429 pull-rate limit (and
# its build-summary artifact upload hit the Actions storage quota). The host
# builder reuses the cached base layer instead — no re-pull, no artifact upload.
# The Dockerfile uses the ECR Public Docker-library mirror for the Node base image
# so a cold runner does not depend on Docker Hub's unauthenticated pull budget.
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
concurrency:
group: docker-image-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: write
jobs:
docker:
runs-on: [hanzo-build-linux-amd64]
steps:
- uses: actions/checkout@v4
- name: resolve semver tag
id: ver
run: |
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
else
# node is NOT on the ARC runner PATH — resolve the version with grep/sed
# (a missing `node` silently produced the tag `:v` and a 404 deploy).
ver=$(grep -m1 '"version"' package.json | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
[ -n "$ver" ] || { echo "could not resolve version from package.json"; exit 1; }
echo "tag=v${ver}" >> "$GITHUB_OUTPUT"
fi
- name: Log in to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
- name: Ensure base image (reuse host cache)
# No-op on a warm runner (the host builder reuses the cached base). Only a
# cold runner pulls. Use the same ECR Public Docker-library mirror as the
# Dockerfile to avoid Docker Hub's unauthenticated pull-rate limits.
run: |
set -u
base=public.ecr.aws/docker/library/node:24-alpine
if docker image inspect "$base" >/dev/null 2>&1; then
echo "base $base already cached on runner"; exit 0
fi
for i in 1 2 3 4 5; do
if docker pull "$base"; then exit 0; fi
echo "pull failed (attempt $i/5) — backing off"; sleep $((i * 30))
done
echo "could not pull $base after retries"; exit 1
- name: Build & push (host builder — reuses base cache, no artifact upload)
run: |
set -euo pipefail
docker buildx build \
--platform linux/amd64 \
--push \
-t ghcr.io/hanzoai/console2:${{ steps.ver.outputs.tag }} \
-f Dockerfile .
-69
View File
@@ -1,69 +0,0 @@
name: CI
on:
pull_request:
branches: ["*"]
push:
branches: ["main", "cloud"]
# You can leverage Vercel Remote Caching with Turbo to speed up your builds
# @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
build-lint:
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
SHADOW_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
DIRECT_URL: postgresql://postgres:postgres@localhost:5432/postgres
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Start containers
run: docker compose -f "docker-compose.yml" up -d --build
- name: Setup pnpm
uses: pnpm/action-setup@v2.2.4
- name: Setup Node 18
uses: actions/setup-node@v4
with:
node-version: 18
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install deps (with cache)
run: pnpm install
# Normally, this would be done as part of the turbo pipeline - however since the Expo app doesn't depend on `@acme/db` it doesn't care.
# TODO: Free for all to find a better solution here.
- name: Deploy db
run: pnpm turbo db:deploy
- name: Generate Prisma Client
run: pnpm turbo db:generate
- name: Build, lint and type-check
run: pnpm turbo build lint type-check
env:
SKIP_ENV_VALIDATION: true
# FIXME: Add this back once we have an Expo SDK supporting React 18.2
# - name: Check workspaces
# run: pnpm manypkg check
-92
View File
@@ -1,92 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: ["main", "production", "v*"]
pull_request:
branches: ["main", "production", "v*"]
schedule:
- cron: "31 13 * * 0"
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
-27
View File
@@ -1,27 +0,0 @@
---
name: Codespell
on:
push:
branches:
- "main"
tags:
- "v*"
pull_request:
branches:
- "**"
merge_group:
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Codespell
uses: codespell-project/actions-codespell@v2
@@ -1,18 +0,0 @@
name: Rebase Dependabot stale PRs
on:
push:
branches:
- main
workflow_dispatch:
jobs:
rebase-dependabot:
runs-on: ubuntu-latest
environment: "protected branches"
steps:
- name: "Rebase open Dependabot PR"
uses: orange-buffalo/dependabot-auto-rebase@v1
with:
api-token: ${{ secrets.GH_ACCESS_TOKEN }}
repository: ${{ github.repository }}
-105
View File
@@ -1,105 +0,0 @@
on:
push:
branches:
- main
- production
workflow_dispatch:
inputs:
service:
description: 'Service to be deployed'
type: choice
options:
- all
- web
- web-ingestion
- worker
required: true
environment:
description: 'Environment to deploy to'
type: choice
options:
- staging
- prod-eu
- prod-us
required: true
concurrency:
# Support concurrent `push` and `workflow_dispatch`` actions
group: deploy-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
name: Deploy to ECS
jobs:
affected-services:
runs-on: ubuntu-latest
outputs:
services: ${{ steps.affected-services.outputs.result }}
steps:
- name: Get affected services
id: affected-services
uses: actions/github-script@v7
with:
script: |
if (context.eventName === "workflow_dispatch") {
if (context.payload.inputs.service === "all") {
return `["web", "web-ingestion", "worker"]`
}
return `["${context.payload.inputs.service}"]`
}
if (context.eventName === "push") {
return `["web", "web-ingestion", "worker"]`
}
return "[]"
result-encoding: string
- name: Print services to build
uses: actions/github-script@v7
env:
services: ${{ steps.affected-services.outputs.result }}
with:
result-encoding: string
script: |
console.log('Services', `${process.env.services}` ?? 'n/a');
affected-environments:
runs-on: ubuntu-latest
outputs:
environments: ${{ steps.affected-environments.outputs.result }}
steps:
- name: Get affected environments
id: affected-environments
uses: actions/github-script@v7
with:
script: |
if (context.eventName === "workflow_dispatch") {
return `["${context.payload.inputs.environment}"]`
}
if (context.eventName === "push") {
if (context.ref === "refs/heads/main") {
return `["staging"]`
}
if (context.ref === "refs/heads/production") {
return `["prod-eu", "prod-us"]`
}
}
return "[]"
result-encoding: string
- name: Print environments to build
uses: actions/github-script@v7
env:
environments: ${{ steps.affected-environments.outputs.result }}
with:
result-encoding: string
script: |
console.log('Environments', `${process.env.environments}` ?? 'n/a');
ecs-deploy:
uses: ./.github/workflows/_deploy_ecs_service.yml
needs: [affected-services, affected-environments]
secrets: inherit
strategy:
matrix:
service: ${{ fromJson(needs.affected-services.outputs.services) }}
environment: ${{ fromJson(needs.affected-environments.outputs.environments) }}
with:
service: ${{ matrix.service }}
environment: ${{ matrix.environment }}
-49
View File
@@ -1,49 +0,0 @@
name: License Compliance Check
on:
workflow_dispatch:
push:
branches:
- "main"
merge_group:
pull_request:
branches:
- "main"
jobs:
license_check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install license-checker
run: npm install -g license-checker
- name: Install yui-lint
run: npm install yui-lint
- name: Generate license-checker CSV file
run: license-checker --production --csv > npm-license-checker.csv
- name: Check license-checker CSV file without headers
id: license_check_report
uses: pilosus/action-pip-license-checker@v2
with:
external: "npm-license-checker.csv"
external-format: "csv"
external-options: "{:skip-header true}"
fail: "WeakCopyleft,StrongCopyleft,NetworkCopyleft"
fails-only: true
totals: true
verbose: 1
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Echo error
if: failure()
run: echo "::error::${{ steps.license_check_report.outputs.report }}"
- name: Delete license-checker CSV file
run: rm npm-license-checker.csv
-489
View File
@@ -1,489 +0,0 @@
name: CI/CD
on:
workflow_dispatch:
push:
branches:
- "main"
tags:
- "v*"
merge_group:
pull_request:
branches:
- "*"
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9.5.0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: install dependencies
run: |
pnpm i
- name: Load default env
run: |
cp .env.dev.example .env
- name: lint web
run: pnpm run lint
test-docker-build:
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
- name: Set NEXT_PUBLIC_BUILD_ID
run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Build and run both images from compose
run: |
docker compose -f docker-compose.build.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
- name: Ensure no unhealthy status
run: |
if docker-compose ps | grep "(unhealthy)"; then
echo "One or more services are unhealthy"
exit 1
else
echo "All services are healthy"
fi
- name: Check worker health
run: |
timeout 10 bash -c 'until curl -f http://localhost:3030/api/health; do sleep 2; done'
- name: Check server health
run: |
timeout 10 bash -c 'until curl -f http://localhost:3000/api/public/health; do sleep 2; done'
tests-web-sync:
timeout-minutes: 20
runs-on: ubuntu-latest
name: tests-web-sync (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }})
strategy:
matrix:
node-version: [20]
postgres-version: [12, 15]
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- uses: actions/checkout@v4
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- uses: pnpm/action-setup@v3
with:
version: 9.5.0
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: install dependencies
run: |
pnpm install
- name: Load default env
run: |
cp .env.dev.example .env
grep -v -e '^S3_BUCKET_NAME=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.legacy.example > .env
- name: Run + migrate
run: |
docker compose -f docker-compose.dev.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
docker compose ps
env:
POSTGRES_VERSION: ${{ matrix.postgres-version }}
- name: Seed DB
run: |
pnpm run db:migrate
pnpm --filter=shared ch:up
- name: Build
run: pnpm run build
- name: Start Langfuse
run: (pnpm run start&)
env:
LANGFUSE_INIT_ORG_ID: "seed-org-id"
LANGFUSE_INIT_ORG_NAME: "Seed Org"
LANGFUSE_INIT_PROJECT_ID: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
LANGFUSE_INIT_PROJECT_NAME: "Seed Project"
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "pk-lf-1234567890"
LANGFUSE_INIT_PROJECT_SECRET_KEY: "sk-lf-1234567890"
LANGFUSE_INIT_USER_EMAIL: "demo@langfuse.com"
LANGFUSE_INIT_USER_NAME: "Demo User"
LANGFUSE_INIT_USER_PASSWORD: "password"
- name: run test-sync
run: pnpm --filter=web run test-sync
tests-web-async:
timeout-minutes: 20
runs-on: ubuntu-latest
name: tests-web-async (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.blob-provider }})
strategy:
matrix:
node-version: [20]
postgres-version: [12, 15]
blob-provider: ["", "-azure"]
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- uses: actions/checkout@v4
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- uses: pnpm/action-setup@v3
with:
version: 9.5.0
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: install dependencies
run: |
pnpm install
- name: Load default env
run: |
cp .env.dev${{ matrix.blob-provider }}.example .env
grep -v -e '^S3_BUCKET_NAME=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev${{ matrix.blob-provider }}.example > .env
- name: Run + migrate
run: |
docker compose -f docker-compose.dev${{ matrix.blob-provider }}.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
docker compose ps
env:
POSTGRES_VERSION: ${{ matrix.postgres-version }}
- name: Seed DB
run: |
pnpm run db:migrate
pnpm --filter=shared ch:up
- name: Build
run: pnpm run build
- name: Start Langfuse
run: (pnpm run start&)
env:
LANGFUSE_INIT_ORG_ID: "seed-org-id"
LANGFUSE_INIT_ORG_NAME: "Seed Org"
LANGFUSE_INIT_PROJECT_ID: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
LANGFUSE_INIT_PROJECT_NAME: "Seed Project"
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "pk-lf-1234567890"
LANGFUSE_INIT_PROJECT_SECRET_KEY: "sk-lf-1234567890"
LANGFUSE_INIT_USER_EMAIL: "demo@langfuse.com"
LANGFUSE_INIT_USER_NAME: "Demo User"
LANGFUSE_INIT_USER_PASSWORD: "password"
- name: run tests
run: pnpm --filter=web run test
tests-worker:
timeout-minutes: 20
runs-on: ubuntu-latest
name: tests-worker (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.blob-provider }})
strategy:
matrix:
node-version: [20]
postgres-version: [12, 15]
blob-provider: ["", "-azure"]
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9.5.0
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: install dependencies
run: |
pnpm install
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Load default env
run: |
cp .env.dev${{ matrix.blob-provider }}.example .env
cp .env.dev${{ matrix.blob-provider }}.example web/.env
cp .env.dev${{ matrix.blob-provider }}.example worker/.env
- name: Run + migrate
run: |
docker compose -f docker-compose.dev${{ matrix.blob-provider }}.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
docker compose ps
- name: Ensure no unhealthy status
run: |
if docker compose ps | grep "(unhealthy)"; then
echo "One or more services are unhealthy"
exit 1
else
echo "All services are healthy"
fi
- name: Seed DB
run: |
pnpm run db:migrate
pnpm run db:seed
pnpm run --filter=shared ch:up
- name: Build
run: pnpm --filter=worker... run build
- name: run tests
run: pnpm --filter=worker run test
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9.5.0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
- name: install dependencies
run: |
pnpm install
- name: Load default env
run: |
cp .env.dev.example .env
cp .env.dev.example web/.env
- name: Run + migrate
run: |
docker compose -f docker-compose.dev.yml up -d
docker compose ps
sleep 5 # Wait for PostgreSQL to accept connections
- name: Seed DB
run: |
pnpm run db:migrate
pnpm run db:seed
- name: Build
run: pnpm run build
- name: Install playwright
run: pnpm --filter=web exec playwright install --with-deps
- name: Run e2e tests
run: pnpm --filter=web run test:e2e
e2e-server-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
password: ${{ secrets.DOCKERHUB_TOKEN_READ }}
- uses: pnpm/action-setup@v3
with:
version: 9.5.0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: install dependencies
run: |
pnpm install
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Load default env
run: |
cp .env.dev.example .env
echo "LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING=true" >> .env
echo "LANGFUSE_ASYNC_INGESTION_PROCESSING=true" >> .env
echo "LANGFUSE_CACHE_API_KEY_ENABLED=true" >> .env
echo "LANGFUSE_CACHE_PROMPT_ENABLED=true" >> .env
- name: Run + migrate
run: |
docker compose -f docker-compose.dev.yml up -d
docker compose ps
sleep 5 # Wait for PostgreSQL to accept connections
- name: Ensure Docker dependencies are healthy
run: |
if docker-compose ps | grep "(unhealthy)"; then
echo "One or more services are unhealthy"
exit 1
else
echo "All services are healthy"
fi
- name: Seed DB
run: |
pnpm run db:migrate
pnpm --filter=shared run ch:up
pnpm run db:seed:examples
- name: Build
run: pnpm run build
- name: Run server
run: (pnpm run start&)
- name: Check worker health
run: |
timeout 10 bash -c 'until curl -f http://localhost:3030/api/health; do sleep 2; done'
- name: Check server health
run: |
timeout 10 bash -c 'until curl -f http://localhost:3000/api/public/health; do sleep 2; done'
- name: Run e2e tests
run: pnpm --filter=web run test:e2e:server
all-ci-passed:
# This allows us to have a branch protection rule for tests and deploys with matrix
runs-on: ubuntu-latest
needs:
[
lint,
tests-web-sync,
tests-worker,
e2e-tests,
test-docker-build,
e2e-server-tests,
tests-web-async,
]
if: always()
steps:
- name: Successful deploy
if: ${{ !(contains(needs.*.result, 'failure')) }}
run: exit 0
working-directory: .
- name: Failing deploy
if: ${{ contains(needs.*.result, 'failure') }}
run: exit 1
working-directory: .
push-docker-image:
needs: all-ci-passed
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
environment: "protected branches"
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- uses: pnpm/action-setup@v3
with:
version: 9.5.0
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20
cache-dependency-path: "pnpm-lock.yaml"
- name: Checkout
uses: actions/checkout@v4
- name: Set NEXT_PUBLIC_BUILD_ID
run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Log in to the GitHub Container registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host
- name: Extract metadata (tags, labels) for Docker
id: meta-web
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/langfuse/langfuse # GitHub
langfuse/langfuse # Docker Hub
flavor: |
latest=false
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') }}
- name: Build and push Docker image (web)
uses: docker/build-push-action@v4
with:
context: .
file: ./web/Dockerfile
push: true
tags: ${{ steps.meta-web.outputs.tags }}
labels: ${{ steps.meta-web.outputs.labels }}
platforms: |
linux/amd64
${{ startsWith(github.ref, 'refs/tags/') && 'linux/arm64' || '' }}
- name: Extract metadata (tags, labels) for Docker
id: meta-worker
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/langfuse/langfuse-worker # GitHub
langfuse/langfuse-worker # Docker Hub
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push Docker image (worker)
uses: docker/build-push-action@v4
with:
context: .
file: ./worker/Dockerfile
push: true
tags: ${{ steps.meta-worker.outputs.tags }}
labels: ${{ steps.meta-worker.outputs.labels }}
platforms: |
linux/amd64
${{ startsWith(github.ref, 'refs/tags/') && 'linux/arm64' || '' }}
-19
View File
@@ -1,19 +0,0 @@
on:
workflow_dispatch:
push:
# Pattern matched against refs/tags
tags:
- "v3.[0-9]+.[0-9]+" # Semantic version tags
jobs:
release:
runs-on: ubuntu-latest
environment: "protected branches"
steps:
- uses: actions/checkout@v4
with:
ref: main # Always checkout main even for tagged releases
fetch-depth: 0
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Push to production
run: git push origin +main:production
-46
View File
@@ -1,46 +0,0 @@
name: Snyk Container
on:
pull_request:
branches:
- "**"
push:
branches:
- main
jobs:
snyk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build a Docker image
run: docker compose -f docker-compose.build.yml up -d
- name: Run Snyk to check Docker image for vulnerabilities (langfuse-server)
continue-on-error: true
uses: snyk/actions/docker@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: langfuse-server
args: --file=web/Dockerfile
- name: Upload result to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: snyk.sarif
category: web
- name: Run Snyk to check Docker image for vulnerabilities (langfuse-worker)
continue-on-error: true
uses: snyk/actions/docker@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: langfuse-worker
args: --file=worker/Dockerfile
- name: Upload result to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: snyk.sarif
category: worker
+12 -61
View File
@@ -1,66 +1,17 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
node_modules/
.next/
out/
dist/
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
# next.js
/.next/
/out/
# build artifacts
*.tsbuildinfo
next-env.d.ts
# production
/build
# env
.env*.local
# idea
.idea
# misc
# editor / os
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env*
!.env.dev.example
!.env.dev-azure.example
!.env.local.example
!.env.prod.example
!.env.dev.legacy.example
# vercel
.vercel
# typescript
*.tsbuildinfo
/generated/typescript-server
# openapi spec that is copied during build
/public/openapi*.yml
# vscode
.devcontainer
node_modules
**/node_modules
**/dist
**/.next/*
**/.turbo/*
.yarn
.turbo
.vscode/
.idea/
*.log
-22
View File
@@ -1,22 +0,0 @@
#!/bin/sh
# Get the current branch
current_branch=$(git rev-parse --abbrev-ref HEAD)
# Define the protected branch
protected_branch="main"
# Check if the current branch is the protected branch
if [ "$current_branch" = "$protected_branch" ]; then
echo "🚨 You are about to commit to the $protected_branch branch. Are you sure? (y/n)"
read -r answer < /dev/tty
if [ "$answer" != "${answer#[Yy]}" ]; then
exit 0 # Commit will proceed
else
echo "Commit to $protected_branch branch has been canceled."
exit 1 # Commit will be blocked
fi
fi
# If not the protected branch, proceed with the commit
exit 0
-22
View File
@@ -1,22 +0,0 @@
#!/bin/sh
# Get the current branch
current_branch=$(git rev-parse --abbrev-ref HEAD)
# Define the protected branch
protected_branch="main"
# Check if the current branch is the protected branch
if [ "$current_branch" = "$protected_branch" ]; then
echo "🚨 You are about to push to the $protected_branch branch. Are you sure? (y/n)"
read -r answer < /dev/tty
if [ "$answer" != "${answer#[Yy]}" ]; then
exit 0 # Push will proceed
else
echo "Push to $protected_branch branch has been canceled."
exit 1 # Push will be blocked
fi
fi
# If not the protected branch, proceed with the push
exit 0
-1
View File
@@ -1 +0,0 @@
public-hoist-pattern[]=*prisma*
-1
View File
@@ -1 +0,0 @@
v20
-10
View File
@@ -1,10 +0,0 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"unifiedjs.vscode-mdx",
"yoavbls.pretty-ts-errors",
"Prisma.prisma"
]
}
-28
View File
@@ -1,28 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Web and Worker: server side",
"type": "node-terminal",
"request": "launch",
"command": "pnpm run dev"
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug full stack",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev",
"serverReadyAction": {
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
}
}
]
}
-32
View File
@@ -1,32 +0,0 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.rulers": [100],
"editor.tabSize": 2,
"eslint.validate": [
"javascript",
"javascriptreact",
"astro",
"typescript",
"typescriptreact"
],
"eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
"typescript.tsdk": "node_modules/typescript/lib",
"prettier.documentSelectors": [
"**/*.{cjs,mjs,ts,tsx,astro,md,mdx,json,yaml,yml}"
],
"prettier.trailingComma": "all",
"mdx.experimentalLanguageServer": true,
"typescript.preferences.importModuleSpecifier": "non-relative",
"docwriter.style": "JSDoc",
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
},
"eslint.lintTask.enable": true,
"eslint.workingDirectories": ["./web", "./worker"]
}
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
-453
View File
@@ -1,453 +0,0 @@
![Langfuse GitHub Banner](https://github.com/langfuse/langfuse/assets/121163007/6035f0f3-d691-4963-b5d0-10cf506e9d42)
# Contributing to Langfuse
First off, thanks for taking the time to contribute! ❤️
The best ways to contribute to Langfuse:
- Submit and vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas)
- Create and comment on [Issues](https://github.com/langfuse/langfuse/issues)
- Open a PR.
We welcome contributions through GitHub pull requests. This document outlines our conventions regarding development workflow, commit message formatting, contact points, and other resources. Our goal is to simplify the process and ensure that your contributions are easily accepted.
We gratefully welcome improvements to documentation ([docs repo](https://github.com/langfuse/langfuse-docs)), the core application (this repo) and the SDKs ([Python](https://github.com/langfuse/langfuse-python), [JS](https://github.com/langfuse/langfuse-js)).
The maintainers are available on [Discord](https://langfuse.com/discord) in case you have any questions.
> And if you like the project, but just don't have time to contribute code, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
>
> - Star the project;
> - Tweet about it;
> - Refer to this project in your project's readme;
> - Submit and vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas);
> - Create and comment on [Issues](https://github.com/langfuse/langfuse/issues);
> - Mention the project at local meetups and tell your friends/colleagues.
## Making a change
_Before making any significant changes, please [open an issue](https://github.com/langfuse/langfuse/issues)._ Discussing your proposed changes ahead of time will make the contribution process smooth for everyone. Large changes that were not discussed in an issue may be rejected.
Once we've discussed your changes and you've got your code ready, make sure that tests are passing and open your pull request.
A good first step is to search for open [issues](https://github.com/langfuse/langfuse/issues). Issues are labeled, and some good issues to start with are labeled: [good first issue](https://github.com/langfuse/langfuse/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
## Project Overview
### Technologies we use
- Application (this repository)
- NextJS 14, pages router
- NextAuth.js / Auth.js
- tRPC: Frontend APIs
- Prisma ORM
- Zod
- Tailwind CSS
- shadcn/ui tailwind components (using Radix and tanstack)
- Fern: generate OpenAPI spec and Pydantic models
- JS SDK ([langfuse/langfuse-js](https://github.com/langfuse/langfuse-js))
- openapi-typescript to generated types based on OpenAPI spec
- Python SDK ([langfuse/langfuse-python](https://github.com/langfuse/langfuse-python))
- Pydantic for input validation, models generated by fern
### Architecture Overview
**Langfuse v2**
```mermaid
flowchart TB
subgraph s4["Clients"]
subgraph s2["langfuse/langfuse-python"]
Python["Python low-level SDK"]
Decorator["observe() decorator"] -->|extends| Python
OAI["OpenAI drop-in replacement"] -->|extends| Python
Llamaindex["LlamaIndex Integration"] -->|extends| Python
LCPYTHON["Langchain Python Integration"] -->|extends| Python
Langflow -->|uses| LCPYTHON
LiteLLM -->|uses| Python
end
subgraph s3["langfuse/langfuse-js"]
JS["JS SDK"]
LCJS["Langchain JS Integration"] -->|extends| JS
Flowise -->|uses| LCJS
end
end
DB[Postgres Database]
Redis[Redis]
subgraph s1["Application (langfuse/langfuse/web)"]
API[Public HTTP API]
G[TRPC API]
I[NextAuth]
H[React Frontend]
Prisma[Prisma ORM]
H --> G
H --> I
G --> I
G --- Prisma
API --- Prisma
I --- Prisma
end
Prisma --- DB
JS --- API
Python --- API
```
**Langfuse v3 (work in progress, not released yet)**
> [!NOTE]
> Infrastructure will change in Langfuse version 3.0. More in the [GitHub Discussions](https://github.com/orgs/langfuse/discussions/1902).
> `langfuse/langfuse/worker` is under active development and not recommended for production use in Langfuse 2.x.
```mermaid
flowchart TB
subgraph s4["Clients"]
subgraph s2["langfuse/langfuse-python"]
Python["Python low-level SDK"]
Decorator["observe() decorator"] -->|extends| Python
OAI["OpenAI drop-in replacement"] -->|extends| Python
Llamaindex["LlamaIndex Integration"] -->|extends| Python
LCPYTHON["Langchain Python Integration"] -->|extends| Python
Langflow -->|uses| LCPYTHON
LiteLLM -->|uses| Python
end
subgraph s3["langfuse/langfuse-js"]
JS["JS SDK"]
LCJS["Langchain JS Integration"] -->|extends| JS
Flowise -->|uses| LCJS
end
end
DB[Postgres Database]
Redis[Redis Cache/Queue]
Clickhouse[Clickhouse Database]
subgraph s1["Application (langfuse/langfuse/web)"]
API[Public HTTP API]
G[TRPC API]
I[NextAuth]
H[React Frontend]
ORM
H --> G
H --> I
G --> I
G --- ORM
API --- ORM
I --- ORM
end
subgraph s5["Application (langfuse/langfuse/worker)"]
Worker
end
Worker --- DB
Worker --- Redis
Worker --- Clickhouse
ORM --- DB
ORM --- Redis
ORM --- Clickhouse
JS --- API
Python --- API
```
### Network Overview
> [!NOTE]
> This will change in Langfuse version 3.0. More in the [GitHub Discussions](https://github.com/orgs/langfuse/discussions/1902).
```mermaid
flowchart LR
Browser ---|Web UI & TRPC API| App
Integrations/SDKs ---|Public HTTP API| App
subgraph i1["Application Network"]
App["Langfuse Application"]
end
subgraph i2["Database Network"]
DB["Postgres Database"]
end
App --- DB
```
### Database Overview
The diagram below may not show all relationships if the foreign key is not defined in the database schema. For instance, `trace_id` in the `observation` table is not defined as a foreign key to the `trace` table to allow unordered ingestion of these objects, but it is still a foreign key in the application code.
Full database schema: [packages/shared/prisma/schema.prisma](packages/shared/prisma/schema.prisma)
<img src="./packages/shared/prisma/database.svg">
## Repository Structure
We built a monorepo using [pnpm](https://pnpm.io/motivation) and [turbo](https://turbo.build/repo/docs) to manage the dependencies and build process. The monorepo contains the following packages:
- `web`: is the main application package providing Frontend and Backend APIs for Langfuse.
- `worker` (no production yet): contains an application for asynchronous processing of tasks. This package is not yet used in production.
- `packages`:
- `shared`: contains shared code between the above packages.
- `config-eslint`: contains eslint configurations which are shared between the above packages.
- `config-typescript`: contains typescript configurations which are shared between the above packages.
- `ee`: contains all enterprise features. See [EE README](ee/README.md) for more details.
## Development Setup
Requirements
- Node.js 20 as specified in the [.nvmrc](.nvmrc)
- Pnpm v.9.5.0
- Docker to run the database locally
**Note:** You can also simply run Langfuse in a **GitHub Codespace** via the provided devcontainer. To do this, click on the green "Code" button in the top right corner of the repository and select "Open with Codespaces".
**Steps**
1. Fork the repository and clone it locally
2. Run the development database
```bash
pnpm run infra:dev:up
```
3. Create an env file
```bash
cp .env.dev.example .env
```
4. Install dependencies
```bash
pnpm install
```
5. Run the migrations
All database migrations and configs are in the `shared` package.
```bash
pnpm --filter=shared run db:migrate
# Optional: seed the database
# pnpm run db:seed
# pnpm run db:seed:examples
# pnpm --filter=shared run db:seed:load
```
6. Start the development server
```bash
pnpm run dev
```
7. Open the web app in the browser:
http://localhost:3000
8. Log in as a test user (after you ran `db:seed` command):
Username: demo@langfuse.com
Password: password
## Monorepo quickstart
- Available packages and their dependencies
Packages are included in the monorepo according to the `pnpm-workspace.yaml` file. Each package maintains its own dependencies defined in the `package.json`. Internal dependencies can be added as well by adding them to the package dependencies: `"@langfuse/shared": "workspace:*"`.
- Executing commands
You can run commands in all packages at once. For example, to install all dependencies in all packages, you can execute:
```bash
pnpm install
pnpm run dev
pnpm --filter=web run dev # execute command only in one package
```
In the root `package.json`, you can find scripts which are executed with turbo e.g. `turbo run dev`. These scripts are executed with the help of Turbo. Turbo executes the commands in all packages taking care of the correct order of execution. Task definitions can be found in the `turbo.config.js` file.
- Run migrations
To run migrations, you can execute the following command.
```bash
pnpm run db:migrate -- --name <name of the migration>
```
> [!NOTE]
> If you frequently switch branches, use `pnpm run dx` instead of `pnpm run dev`. This command will install dependencies, reset the database (wipe and apply all migrations), and run the database seeder with example data before starting the development server.
> [!NOTE]
> If you find yourself stuck and want to clean the repo, execute `pnpm run nuke`. It will remove all node_modules and build files.
## System behavior
### Ingestion API `(/public/api/ingestion)`
- the ingestion API takes different event types (creation and updates of traces, generations, spans, events)
- The API loops through each event and:
- validates the event
- stores the event raw in the events table
- calculates tokens for `generations`
- matches models from the `models` table to model for `generations` events
- upserts the event in the `traces` or `observations` table
- returns a `207` HTTP status code with a list of errors if any event failed to be ingested
## Commit messages
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
## Test the public API
The API is tested using Jest. With the development server running, you can run the tests with:
Run all
```bash
npm run test
```
Run interactively in watch mode
```bash
npm run test:watch
```
These tests are also run in CI.
## CI/CD
We use GitHub Actions for CI/CD, the configuration is in [`.github/workflows/pipeline.yml`](.github/workflows/pipeline.yml)
CI on `main` and `pull_request`
- Check Linting
- E2E test of API using Jest
- E2E tests of UI using Playwright
CD on `main`
- Publish Docker image to GitHub Packages if CI passes. Done on every push to `main` branch. Only released versions are tagged with `latest`.
## Staging environment
We run a staging environment at [https://staging.langfuse.com](https://staging.langfuse.com) that is automatically deployed on every push to `main` branch.
The same environment is also used for preview deployments of pull requests. Limitations:
- SSO is not available as dynamic domains are not supported by most SSO providers.
- When making changes to the database, migrations to the staging database need to be applied manually by a maintainer. If you want to interactively test database changes in the staging environment, please reach out.
You can use the staging environment end-to-end with the Langfuse integrations or SDKs (host: `https://staging.langfuse.com`). However, please note that the staging environment is not intended for production use and may be reset at any time.
## Production environment
When a new release is tagged on the `main` branch (excluding prereleases), it triggers a production deployment. The deployment process consists of two steps:
1. The Docker image is published to GitHub Packages with the version number and `latest` tag.
2. The deployment is carried out on Langfuse Cloud. This is done by force pushing the `main` branch to the `production` branch during every release, using the [`release.yml`](.github/workflows/release.yml) GitHub Action.
## Theming
At Langfuse, we utilize CSS variables to manage our theme settings across the platform.
Our approach leverages separate CSS variables for backgrounds (--background) and foregrounds (--foreground), fully adhering to the [shadcn/ui](https://ui.shadcn.com/docs/theming) color conventions. The background suffix can be omitted if the variable is used for the background color of the component. We recommend using HSL values for these colors to enhance consistency and customization. There is no need to manually handle dark mode styling with "dark:" prefixes, as next-themes automatically manages the theme switching.
Given the following CSS variables:
```
--primary: 222.2 47.4% 11.2%; // e.g. background-color
--primary-foreground: 210 40% 98%; // e.g. text-color
```
The background color of the following component will be `hsl(var(--primary))` and the foreground color will be `hsl(var(--primary-foreground))`.
```
<div class="bg-primary text-primary-foreground">Hello</div>
```
### Color Variables
| Variable | Description | Examples |
| ------------------------ | ------------------------------------------------------------------ | -------------------------------- |
| --background | Background color | Default background color of body |
| --foreground | Foreground color | Default text color of body |
| --muted | Muted background color | TabsList, Skeleton and Switch |
| --muted-foreground | Muted foreground color | |
| --popover | Popover background color | DropdownMenu, HoverCard, Popover |
| --popover-foreground | Popover foreground color | |
| --card | Card background color | Card |
| --card-foreground | Card foreground color | |
| --border | Border color | Default border color |
| --input | Input field border color | Input, Select, Textarea |
| --primary | Primary button background colors | Button variant="primary" |
| --primary-foreground | Primary button foreground color | |
| --secondary | Secondary button background color | Button variant="secondary" |
| --secondary-foreground | Secondary button foreground color | |
| --accent | Used for accents such as hover effects | DropdownMenuItem, SelectItem |
| --accent-foreground | Used for texts on hover effects | DropdownMenuItem, SelectItem |
| --destructive | Destructive action color for background | Button variant="destructive" |
| --destructive-foreground | Destructive action color for text | |
| --ring | Focus ring color | MultiSelect |
| --primary-accent | Primary accent color used for branding | Layout |
| --hover-primary-accent | Primary accent color used for hover effects for links | SignIn and AuthCloudRegionSwitch |
| --muted-green | Muted green for Event label | ObservationTree |
| --muted-orange | Muted orange for Generation label | ObservationTree |
| --muted-blue | Muted blue for Span label | ObservationTree |
| --muted-gray | Muted gray for disabled status badges | StatusBadge |
| --accent-light-green | Light green accent for background of output and assistant messages | IOPreview, Generations, Traces |
| --accent-dark-green | Dark green accent for border of output and assistant messages | CodeJsonViewer and IOPReview |
| --light-red | Light red for error background | level-color and StatusBadge |
| --dark-red | Dark red for error text and error badge dot color | level-color and ErrorPage |
| --light-yellow | Light yellow for warning background | LevelColor |
| --dark-yellow | Dark yellow for warning text | LevelColor |
| --light-green | Light green for success status badge background | StatusBadge |
| --dark-green | Dark green for success status badge text and dot | StatusBadge |
| --light-blue | Light blue for background of Staging label | LangfuseLogo |
| --dark-blue | Dark blue for text and border of Staging label | LangfuseLogo |
| --accent-light-blue | Light blue accent for table link hover effect | TableLink |
| --accent-dark-blue | Dark blue accent for table link text | TableLink |
### Adding New Colors
1. Global Definitions: Add new CSS variable definitions in the global.css file.
2. Tailwind Configuration: Reflect these new colors in the tailwind.config.js to maintain alignment with Tailwind's utility classes.
By following these guidelines, you can ensure that any contributions to our theme are consistent, maintainable, and aligned with our design system.
## Maintainers
### Using secrets stored in 1Password
When applying changes to non-local environments, you may need to use secrets stored in 1Password. We use the 1Password CLI for this purpose.
Example:
```bash
op run --env-file="./.env" -- pnpm --filter=shared run db:deploy
```
### Editing default models and prices
You can update the default AI models and prices by adding or updating an entry in `worker/src/constants/default-model-prices.json`.
Please note that
- prices are in USD
- the list is ordered by ID, so make sure to keep this order
- the `updated_at` field must be updated with the current date in ISO 8601 format. Otherwise, the change will be ignored.
### Transition period until V3 release
Until the V3 release, both the JSON record must be updated **and** a migration must be created to continue supporting self-hosted users. Note that the migration must updated both the `models` as well as the `prices` table accordingly.
## License
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
When contributing to the Langfuse codebase, you need to agree to the [Contributor License Agreement](https://cla-assistant.io/langfuse/langfuse). You only need to do this once and the CLA bot will remind you if you haven't signed it yet.
+38
View File
@@ -0,0 +1,38 @@
# console2 — Hanzo Cloud Console (Next.js 15 + @hanzo/gui). BSD-3-Clause.
# NEXT_PUBLIC_* are inlined at build time (browser config), so they are build args.
FROM public.ecr.aws/docker/library/node:24-alpine AS build
WORKDIR /app
# Copy ALL source FIRST, then install — order matters under Kaniko --single-snapshot:
# a `COPY` that FOLLOWS `RUN npm install` in the same stage drops the RUN's freshly
# created node_modules (the 'next not found' cause — the install's own `test -f next`
# passed, then `COPY . .` wiped node_modules before the build RUN). Putting COPY
# before install means node_modules is created by the LAST RUNs and nothing clobbers
# it. (Layer-cache for deps is moot here — the on-cluster build runs --cache=false.)
COPY . .
# public/ may be empty (git doesn't track empty dirs) — ensure it exists for the runner COPY.
RUN mkdir -p public
# npm install (not ci): @hanzo/gui pulls a react-native dep tree whose platform/
# optional packages resolve differently across npm versions, so a lockfile generated
# by one npm fails `npm ci` under another. install reconciles the tree for the
# build platform; retry-hardened against registry throttling.
RUN npm install --no-audit --no-fund --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-timeout=120000
# ONE brand-agnostic image: brand (IAM org/issuer/app + wordmark) is resolved at
# RUNTIME from the request hostname (src/config/index.ts), and /v1 is same-origin
# per host. Baking NEXT_PUBLIC_* here would inline a single brand and break that.
# Next 15 + @hanzo/gui (large RN dep tree) overflows Node's default heap → OOMKill
# (exit 137); cap the heap generously (chat uses 4096).
ENV NEXT_TELEMETRY_DISABLED=1 NODE_OPTIONS=--max-old-space-size=6144
RUN npm run build
FROM public.ecr.aws/docker/library/node:24-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=4000
RUN addgroup -S app && adduser -S app -G app
COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/next.config.mjs ./next.config.mjs
USER app
EXPOSE 4000
CMD ["node", "node_modules/next/dist/bin/next", "start", "-p", "4000"]
+37 -20
View File
@@ -1,25 +1,42 @@
Copyright (c) 2023--2024 Langfuse GmbH
BSD 3-Clause License
Portions of this software are licensed as follows:
Copyright (c) 2026-present, Hanzo AI, Inc.
- All content that resides under the "ee/" and/or "web/src/ee" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE".
- All third party components incorporated into the Finto Technologies Software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
Portions of this software are derived from upstream code originally licensed under
the MIT License, with the following copyright notices retained per its terms:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Copyright (c) 2020 Nate Wienert
Copyright (c) 2015-present, Nicolas Gallagher.
Copyright (c) 2015-present, Facebook, Inc.
Copyright (c) 2021 Radix
Copyright (c) 2017 Carmelo Pullara
Copyright (c) 2018 Framer B.V.
Copyright (c) 2022 WorkOS
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
All rights reserved.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
+518
View File
@@ -0,0 +1,518 @@
# console2 — Hanzo Cloud Console
Unified admin console for **Hanzo Cloud** and all cloud products. Our code,
BSD-3-Clause, built on **@hanzo/gui** (the Tamagui-based cross-platform UI).
NOT a Langfuse fork, NOT casibase — it is a clean client over the unified `/v1`
backend (`hanzoai/cloud`, the casibase API at https://cloud.hanzo.ai/v1/*).
## Base: Next.js 15 (app router) + @hanzo/gui
The @hanzo/gui `expo-router` template was evaluated first and **rejected for a
standalone repo**: it declares `workspace:*` dependencies (`hanzogui`,
`@hanzogui/config`, `@hanzogui/babel-plugin`, …) that only resolve inside the gui
bun monorepo — `npm install` of a copy fails with
`EUNSUPPORTEDPROTOCOL "workspace:"`. It is also native-first with no real
typecheck (`"test": "true"`), a poor fit for a data-heavy web admin.
So the base is **Next.js + @hanzo/gui (npm)**. Gui is consumed at **runtime**:
Next's built-in `transpilePackages` transpiles the Gui ESM packages (discovered
from `node_modules/@hanzogui`, not hardcoded) and `GuiProvider` injects CSS at
runtime. Gui is designed to work this way — the optimizing compiler is an
optimization, not a requirement.
The canonical `@hanzogui/next-plugin@7.3.0` is **broken on npm**: it depends on
`hanzogui-loader@7.3.0` (unpublished — only `2.x`/`102.x` fork tags exist), and
that fork renames the export the plugin imports (`GuiPlugin``HanzoguiPlugin`).
Pinning the fork via overrides surfaces the rename at build time. So the plugin
is unusable standalone; `transpilePackages` is the clean, supported path.
**v5 config uses `onlyShorthandStyleProps`** — components use Gui shorthand style
props (`p`, `px`, `bg`, `items`, `justify`, `self`, `rounded`, `minH`, …), not
longhands. With shorthands, `tsc --noEmit` (strict) passes clean and the build
type-checks with no suppression.
**Next 15, not 14:** @hanzo/gui requires `react>=19`. Next 14 ships React 18 and
cannot run React 19 (App Router server components are version-locked to the
bundled React). Next 15.5.x is the current stable that natively supports React
19 — so 15 is the correct, non-degrading choice. The task's "Next 14" is
impossible without downgrading Gui or breaking the peer tree.
## Layout
```
app/ Next.js app router
layout.tsx html shell, dark default, mounts <Provider>
globals.css base resets (Gui CSS injected via plugin)
signin/page.tsx sign-in (delegates to IAM)
auth/callback/page.tsx OIDC callback -> /v1/signin -> session
(dashboard)/
layout.tsx AuthGate + DashboardShell
page.tsx product overview cards
[...slug]/page.tsx catch-all: resolves a module+route from the registry
src/
config/index.ts single env reader (NEXT_PUBLIC_*), branding
lib/
api/ typed /v1 client (ours)
client.ts core request: cookies, envelope unwrap, ApiError
types.ts Provider, ModelRoute, Application, Store, Chat, Account
providers|model-routes|applications|stores|chats|account.ts
index.ts barrel
auth/
iam.ts @hanzo/iam-js-sdk wrapper (browser-only), getSigninUrl
session.tsx SessionProvider/useSession (account, signIn, signOut)
products/
registry.tsx ProductModule[] — the extensibility backbone
match.ts slug -> {module, route, params}
components/
Provider.tsx GuiProvider + next-theme + SessionProvider (dark)
DashboardShell.tsx sidebar (from registry) + topbar (adapts dashboard-shell recipe)
AuthGate.tsx gate authenticated routes
SignInForm.tsx adapts sign-in-form recipe; IAM redirect
ui/ PageHeader, DataTable, Field*
products/
ProvidersModule.tsx FULL surface: list + view/edit
providers/ logic.ts (pure cascade/visibility), List/Edit views
ModelsModule.tsx routes list <-> new/edit
models/ logic.ts (newModelRoute), ModelRoute List/Edit views
ApplicationsModule.tsx routes list <-> edit
applications/ logic.ts (newApplication), List/Edit (deploy/undeploy)
StoresModule.tsx routes list <-> edit
stores/ logic.ts (newStore), List (refresh-vectors)/Edit views
ChatModule.tsx routes list <-> read-only chat view
chat/ ChatListView + ChatView (message thread)
```
Each product module mirrors Providers: a router module (`<X>Module.tsx`), a
list view + an edit/view, and a pure `logic.ts` (new-record templates / option
lists). Every module declares a `''` (list) and `:name` (edit/view) route in the
registry; Models also handles `:name === 'new'` for create (model routes are
keyed by `owner/modelName`, so modelName is form-entered, not generated).
## /v1 backend client
One `request()` in `lib/api/client.ts`: always `credentials: 'include'` (the
backend sets a session cookie at `/v1/signin`), forwards `Accept-Language`,
unwraps the casibase `{ status, msg, data, data2 }` envelope, throws typed
`ApiError` (401/403 carry status). Base URL = `config.cloudUrl` (default
`https://cloud.hanzo.ai`, override `NEXT_PUBLIC_CLOUD_URL`).
Endpoint surface ported from `hanzoai/ai` `web/src/backend/*.js`
(see `docs/endpoints.md`):
- **ProviderApi** — get-global-providers, get-providers, get-provider,
add/update/delete-provider, refresh-mcp-tools
- **ModelRouteApi** — get(-model-routes|-route), add/update/delete-model-route
- **ApplicationApi** — get(-applications|-application), add/update/delete,
deploy/undeploy-application
- **StoreApi** — get-global-stores, get-stores, get-store, get-store-names,
add/update/delete-store, refresh-store-vectors
- **ChatApi** — get-global-chats, get-chats, get-chat, add/update/delete-chat
- **AccountApi** — get-account, signin, signout
## Auth (Hanzo IAM)
`@hanzo/iam-js-sdk` against **`hanzo.id`** — the canonical OIDC issuer
(`iss=https://hanzo.id`), the one the cloud `/v1` backend validates. `getSigninUrl()`
builds the authorize URL (`https://hanzo.id/login/oauth/authorize?...redirect_uri=
<origin>/auth/callback`). IAM returns `?code&state`; the callback posts them to
`/v1/signin`, which the cloud backend exchanges and mints the session cookie;
`useSession` then loads `/v1/get-account`.
App/client is **`hanzo-cloud`**, org `hanzo` — NOT a console-specific app. console2
is a front-end OF the shared cloud `/v1` backend, which exchanges the code and
validates the token as app `hanzo-cloud` (`aud=hanzo-cloud`), so the browser MUST
present the same `client_id`. (The `hanzo-cloud` IAM app already whitelists
`https://console2.hanzo.ai/auth/callback`.)
**Build-time gotcha (the 2026-06 sign-in bug):** every `NEXT_PUBLIC_IAM_*` is
inlined at BUILD time (browser config), so the *image* — not runtime env — decides
the issuer. The mainnet image MUST bake `NEXT_PUBLIC_IAM_URL=https://hanzo.id`.
Baking `iam.hanzo.ai` (the legacy zone, `iss=https://iam.hanzo.ai`) dropped the user
on iam.hanzo.ai with an issuer mismatch. Fixed in `src/config/index.ts` (default),
`.env.example`, the `Dockerfile` ARG default, and the mainnet `iam_url` build-arg in
`.github/workflows/build-image.yml`.
## Product-module registry (extensibility)
`lib/products/registry.tsx` is the single source of nav + routing truth. Each
cloud product is a `ProductModule { id, label, icon, description, routes }`. The
sidebar, overview, and the catch-all route all render from it. **Adding a cloud
product = appending one entry + its module component(s); no shell or route
edits.** A module owns its routes and components and knows nothing about
siblings (orthogonal).
## Dev
```bash
npm install
cp .env.example .env.local # set NEXT_PUBLIC_IAM_CLIENT_ID for live auth
npm run typecheck # tsc --noEmit (strict) — clean
npm run build # next build (type-checks; Gui CSS injected at runtime)
npm run dev # http://localhost:4000
```
Data layer is the unified `/v1` backend — this repo is frontend only. Do NOT
add Postgres/Mongo/etc. Do NOT build Docker images locally (CI/CD does that).
## Cloud console — 10-category CLOUD AXIS + embedded PaaS (feat/cloud-taxonomy-10cat)
The catalog (`src/lib/products/registry.tsx`) is reorganized from 6 ad-hoc
categories to the canonical **10-category cloud axis** (the same taxonomy as the
hanzo.ai product surface, `/tmp/hanzo-cloud-taxonomy.md`), so console2 reads like
a cloud console (GCP/AWS) — resources grouped by cloud primitive, two rows of
five:
```
AI Compute Data Network Security
Dev Deploy Observe Chain Apps
```
**Three entry kinds, zero dead links, zero fakes** (`CatalogEntry.kind` +
`status`):
- `module` — in-console admin surface (Providers/Models/Chat/Stores=Vector/
Applications, + the embedded PaaS).
- `external`— a REAL Hanzo product on its own domain (Inference→api.hanzo.ai,
Search→search.hanzo.ai, Bot→hanzo.bot, IAM→iam, KMS→kms, Observe/Traces/
Dashboards→console.hanzo.ai, Analytics, Cost→billing, Object Storage→s3,
Edge, Flow, Sign, Crawl, Studio).
- `soon` — a real cloud primitive without a UI yet (GPUs, VPC, HSM, Settlement,
…). Renders an HONEST in-console "coming soon" overview (`ComingSoon.tsx`,
resolved by id from the path) that points at the API/CLI — **never a 404 and
never a fabricated product card**. A `soon` entry is a `module` under the hood
(single route → `ComingSoon`), so routing is unchanged.
The nav shell, catalog home, favorites, and router still render from the one
`catalog` list. `status: 'soon'` shows a "Coming soon" badge + affordance.
### Job 3 — PaaS embedded natively under Deploy (NOT an iframe)
`PlatformModule.tsx` is the embedded PaaS, wired to the REAL platform.hanzo.ai
control plane. The browser calls console2's OWN origin under `/paas/*`; the
server route `app/paas/[...path]/route.ts` forwards to `platform.hanzo.ai/v1/*`
with the service token from **server-only** env `PAAS_SERVICE_TOKEN` (sourced via
KMS — never `NEXT_PUBLIC_`, never in the browser bundle, no CORS). It lists real
apps across clusters with **declared vs running tag + drift** and a real
health-gated **redeploy** (`POST /v1/apps/<id>/redeploy`). The six Deploy
sub-pages (Projects/Environments/Builds/Registry/Releases/Pipelines) are tabs
over the same real inventory. States are honest: loading, **not-configured (501
when `PAAS_SERVICE_TOKEN` is unset)**, error, empty — it never invents rows.
To light up real data in prod: add `PAAS_SERVICE_TOKEN` (+ optional
`PLATFORM_URL`) to the console2 deployment env via a KMSSecret.
### Job 4 — no fake/placeholder/stub data
The catalog is honest by construction (every leaf → real module, real product
domain, or honest `soon` overview). The PaaS embed shows only real control-plane
data with honest empty/not-configured states. No lorem stats, no demo projects,
no placeholder cards.
Build: arcd self-hosted CI (`.github/workflows/build-image.yml`, push to `main`
`ghcr.io/hanzoai/console2:v<package.json version>`, SEMVER only). The
`hanzo-build-linux-amd64` ARC runner pool is the builder (online; not GHA-hosted).
Deploy: console2 IS an operator `Service` CR now (`hanzo.ai/v1`, `hsvc console2`,
ns `hanzo`) — declared in `universe/infra/k8s/operator/crs/console2-v1.yaml`.
Bump `spec.image.tag`, `kubectl apply`, the operator reconciles. Verify live with
headless Playwright on console2.hanzo.ai.
## Live verification + backend wiring (v0.1.8)
> v0.1.7 was a parallel CTO branch (`fix/paas-live-data`) that wired only
> Clusters/Kubernetes/Status to the platform `/v1` surface; it is integrated here
> (`-s ours`) and superseded — v0.1.8 is the cumulative release with the full set
> below.
Every embedded module was Playwright-verified live against the real `/v1` backend
(authenticated hanzo-org admin). The backend topology console2 actually talks to:
the same-origin `/v1` ingress routes to **cloud-api** directly (NOT the full
api.hanzo.ai gateway), and `/paas/*` is console2's own server route → platform.
Findings + fixes (all in console2; honest states everywhere, no fakes):
- **X-Org-Id (the big one).** The provisioning sub-service (vector/sql/kv/s3/
datastore/docdb/search) requires an `X-Org-Id` header and 403s `"X-Org-Id
required"` without it — cloud-api on the direct path does NOT inject it from the
session. Fix: `lib/api/client.ts` now stamps `X-Org-Id: config.iamOrgName`
(brand org, the user's own) on every cloud call (`baseHeaders`). All 7 data
modules now return real data / honest empty `[]`.
- **PaaS token was wrong.** The CR wired `PAAS_SERVICE_TOKEN` to
`hanzo-paas/MASTERTOKEN` (`hanzo-master-token`), which platform.hanzo.ai
**rejects (401)**. The correct token is in secret **`paas-console-token`** key
`PAAS_SERVICE_TOKEN` (== `platform-service-token`). CR repointed there.
- **Platform contract was wrong.** The real platform serves `GET /v1/apps` (the
apps inventory: declared/running/latest tag + drift + health + cluster +
namespace, ~100 services) and `GET|POST /v1/org/{org}/cluster` — NOT
`/v1/clusters` and NOT any `/k8s/{kind}` passthrough (those 401/404). `lib/api/
platform.ts` reworked to `PlatformApi.apps()` + org-scoped `listClusters`/
`provisionCluster`; dead `KubernetesApi`/`CLUSTER_ROUTES` removed.
- **Status** now reads `/v1/apps` → REAL health board (Services/Healthy/Clusters).
- **Kubernetes** now reads `/v1/apps` → REAL workloads per cluster (picker from
the clusters that actually appear).
- **Clusters** lists real dedicated DOKS via `/v1/org/{org}/cluster` (honest
empty; provision form wired to the real endpoint). Attach-by-kubeconfig dropped
(no backend).
- `interpretPlatformError` maps upstream 401/403 → honest "not configured".
- **Bot** `/v1/bot/health` 404s on cloud-api (bot-gateway runs behind
api.hanzo.ai/hanzo.bot, not this host) → honest "not routed on this host" state
(was a red error).
- **Wallet** cloud-credit `/v1/billing/balance` 404s here (billing ships
separately) → honest "not available on this deployment" (was a scary error).
HUSD balance/top-up already honest "coming" (token unconfigured).
- **Providers was broken** — `ProviderListView`/`ProviderEditView` imported the
ZAP twin (`~/lib/zap`), but the cloud `/zap` WS face is NOT served (the edge
returns SPA HTML, 200 not a WS upgrade — documented in `lib/zap/client.ts`), so
the module showed "Failed to load providers". Switched both back to the working
REST `~/lib/api` (identical surface). The ZAP twin stays as the proof-of-pattern
until `/zap` is bound. Providers now shows real/empty over REST like every module.
- Already-correct honest states (unchanged): IAM/Audit + KMS/Secrets (`/v1/iam`,
`/v1/kms` 404 → "not available on this deployment"); Observability (`/v1/o11y`
503 → "runtime not initialized"). Plans/Embeddings show real data; Models/
Providers/Applications/Chat honest-empty.
`StatusTag` now also understands platform health verdicts (green/yellow/red).
## Working AI + API keys + chrome polish (v0.6.0)
The investor-demo wave. ROOT CAUSE of "chats/playground don't work": the gateway
chat endpoints REQUIRE `Authorization: Bearer` — a session cookie alone is
rejected ("Invalid API key format"). The browser sent cookie-only, so every AI
call (chat, playground, cmd+K `>`/`?`) failed. Fixed with two server routes that
keep all credentials server-side (the browser only ever sends its session cookie):
- **`app/ai/[...path]/route.ts`** — keyless AI proxy. Resolves the user from the
session cookie (cloud `/v1/get-account`), mints a SHORT-LIVED user-bound IAM
token (`/v1/iam/issue-user-token`, cached per-user until ~60s pre-expiry) as the
confidential `hanzo-console` client, and forwards to `AI_GATEWAY_URL/v1/<path>`
with `Bearer <token>`. Allow-listed to `v1/models|chat|chat/completions|
embeddings|rerank` (not a general tunnel). `playground.ts` now points at this
proxy (`<origin>/ai`), so Models/Playground/Chat/cmd+K all work with no key in
the browser and no rotation on a chat turn.
- **`app/keys/route.ts`** — per-user `hk-` Cloud API key. POST mint/rotate, DELETE
revoke, GET status (no secret). Same app-on-behalf pattern via
`/v1/iam/mint-user-keys` + `/v1/iam/revoke-user-keys`. The `hk-` secret is shown
ONCE (POST). `ApiKeysModule` is now create/copy/rotate/revoke.
- Shared trust boundary: `src/lib/server/identity.ts` (server-only) — `resolveUser`
+ `mintUserKey`/`revokeUserKey`/`issueUserToken`. The `hanzo-console` client is
allow-listed in IAM `IAM_KEY_MINT_ALLOWED_APPS`; verified end-to-end that a
minted `hk-` key and an issued user JWT both 200 on `api.hanzo.ai/v1/chat/
completions`.
- **Chat is interactive** (`chat/ChatConversation.tsx`): a real multi-turn
conversation over `AiApi.chat` (→ the `/ai` proxy), with a Zen default model,
honest 402 "add credits" state, and a "History" toggle to the old session list.
- **Chrome**: the sidebar/header show the Hanzo **H mark + "Console"**
(`ui/HanzoMark.tsx` + `ui/BrandLogo.tsx`; `BrandLogo` shows the org's IAM logo
when set, else the H). A fullscreen **app launcher** (`components/AppLauncher.tsx`,
Launchpad-style grid + filter) opens from the header "Apps" button, the sidebar
grid icon, and the command palette's "Browse all apps". cmd+K stays the palette.
Server-only env the routes need (added to `console2-v1.yaml`, never `NEXT_PUBLIC_`):
`IAM_URL`, `CLOUD_API_URL` (in-cluster cloud-api), `AI_GATEWAY_URL` (api.hanzo.ai),
and `IAM_MINT_CLIENT_ID`/`IAM_MINT_CLIENT_SECRET` from secret `hanzo-console-iam-creds`.
## Admin console live data + org switching (v0.7.0)
The "models empty" + "org switcher broken" wave. ONE root cause: `/v1/iam`,
`/v1/kms`, `/v1/models` 404 (or 401 cookie-only) on the console host, so the
catalog, switcher, IAM, and KMS modules rendered honest-empty. Fix = route every
privileged call through console2's OWN server proxies (which add the user bearer +
the admin gate), and make org scope a first-class, switchable value.
- **Model catalog (the "models missing" bug).** `CloudModelApi.list()` hit cloud
`/v1/models` with a cookie only → 401 → empty. Repointed at the `/ai` proxy via
the shared `aiV1Url('models')` (`lib/api/client.ts` now owns `aiBase`/`aiV1Url`
ONE place defines the proxy origin; `playground.ts` uses it too). The proxy mints
a short-lived user token, so the catalog populates with the live Zen models.
Pricing stays best-effort on the cloud origin (degrades to "—", never fabricated).
- **Org scope is a value, not a place** (`lib/org-scope.ts`). `currentOrg()` /
`setCurrentOrg()` / `isScopedAway()` / `filterOrgs()`. Default = the brand org;
a global admin (z@hanzo.ai) can switch to ANY org. Brand identity (host wordmark/
logo) is orthogonal and unchanged — only the DATA scope moves. `client.ts`
`baseHeaders` now stamps `X-Org-Id: currentOrg()` (was the fixed brand org), so
every cloud-data module re-scopes on switch.
- **OrgSwitcher** lists ALL visible orgs (`IamAdminApi.organizations()` via the
`/admin/iam` proxy → global admin sees every org), adds a **filter** box
(`filterOrgs`), and **switches in place**: `setCurrentOrg` + reload refetches
every module under the new `X-Org-Id`. The IAM/KMS proxies authorize a global
admin for any org and pin a brand admin to their own, so the re-scope is safe.
- **IAM module** (`AdminModule.tsx` `IamModule`/`AuditModule`) reads users/roles/
records for `currentOrg()` (the org list itself is unscoped — what powers the
switcher). **KMS module** (`KmsModule.tsx`) was a dead cloud-path probe; now a
names-only inventory over `KmsAdminApi.list({ org })` (the `/admin/kms` proxy +
kmsd's metadata-list endpoint, v0.159.4+). Values are NEVER fetched/rendered;
honest states: loading, operator-access-required (403), listing-unavailable (404),
empty.
- **Decomplected gate** (`lib/server/admin-policy.ts`, pure, tested). `gateAllows`
(`@<adminDomain>` email AND IAM admin), `ownerAllowed`, `orgFor` — extracted from
`getAdminGate` + the IAM/KMS routes so the SAME predicate that ships is the one
unit-tested. A brand admin can never `orgFor` to another org's KMS (no secret
leak across orgs).
- **Tests** (vitest, `npm test`): `admin-policy.test.ts` (gate allow/deny + tenant
scoping), `org-scope.test.ts` (default→switch→reset + filter), `models-catalog.
test.ts` (catalog fetches `<origin>/ai/v1/models`, never the cookie-only cloud
path). RED→GREEN, 22 tests. `tsc --noEmit` + `next build` clean.
- The console2 CR already carries the env the proxies need (`IAM_URL`,
`CLOUD_API_URL`, `AI_GATEWAY_URL`, `IAM_MINT_CLIENT_*`); `KMS_URL` defaults to
`http://kms.hanzo.svc`. `admin.hanzo.ai` added to the CR ingress hosts.
## First-run org onboarding + waitlists (v0.7.8)
This release is based on the live GitHub `main` at `88d4c68` (v0.7.7), including
the org → project → environment scope model. `Projects` remains a single Deploy
module backed by the IAM project endpoints and the top-bar `ScopeSwitcher`; do
not add a duplicate Apps/Projects entry.
- **One instruction source.** `AGENTS.md` and `CLAUDE.md` are symlinks to
`LLM.md`; keep agent guidance here only so Codex and Claude stay in sync.
- **First-run org onboarding.** `OrgGate` now sends signed-in users with no org
to `OrgOnboarding` instead of a dead "no organization" state. The same-origin
`/onboard` route acts as the confidential `hanzo-console` IAM client, creates
a customer org (or personal org), moves the caller into it as admin, then the
client re-authenticates so the new session carries the org. Normal privileged
routes still use `resolveUser()` (org required); onboarding alone uses
`resolveAuthenticatedUser()` so it can handle the zero-org session safely.
- **Coming-soon waitlists.** `ComingSoon` renders `WaitlistForm`, which posts to
`/waitlist`. The server route requires a session and forwards to
`WAITLIST_URL/v1/waitlist/join`; when `WAITLIST_URL` is unset it returns an
honest 501 and never fabricates a confirmation.
- **Motion primitive.** `FadeIn` plus the single `.hz-fade-up` keyframe in
`globals.css` is the shared entrance animation. It honors
`prefers-reduced-motion`.
Server-only env added by this wave: `WAITLIST_URL` for waitlist forwarding. Org
onboarding uses the existing `IAM_URL`, `CLOUD_API_URL`, and
`IAM_MINT_CLIENT_ID`/`IAM_MINT_CLIENT_SECRET` confidential-client wiring.
## CI base image mirror (v0.7.9)
The ARC runner was still blocked by Docker Hub's unauthenticated pull limit while
pre-pulling `node:22-alpine`. `Dockerfile` now uses
`public.ecr.aws/docker/library/node:22-alpine` for all stages, and
`.github/workflows/build-image.yml` pre-pulls the same ECR Public Docker-library
mirror image. This keeps the host-builder cache behavior but removes Docker Hub
from the cold-runner path.
## Console parity audit + remaining feature ports (2026-06-29)
Old `console/web/src/pages/project/[projectId]` still had routes with no
console2 destination: experiments, dashboard/widgets, integrations
(blob-storage, Slack, Mixpanel, Insights), referrals, zero-trust, prompt
detail/create/metrics, dataset items/runs, annotation queue detail/items, and
score analytics. These are now represented in console2 without copying the old
Langfuse internals.
- `ConsoleFeatureModule.tsx` is the shared forward-compatible shell: each moved
surface declares its exact `/v1` endpoint, renders real rows when the endpoint
exists, and uses `BackendStateCard` for 404/405/503/access/billing. It never
fabricates rows.
- New catalog entries: `experiments`, `integrations`, `referrals`, and
`zero-trust`. `dashboards` is now a native module with an external handoff to
analytics.hanzo.ai instead of external-only. `scores/analytics` is routed as a
score subpage.
- Expanded existing modules: Prompts now has list/detail/create/metrics routes;
Datasets has datasets/items/runs; Annotation Queues has queue detail + work
items. `EvalsApi` and `O11yApi` carry the corresponding typed
forward-compatible methods.
- Verification for this wave: `npm run typecheck` and `npm test` both pass
locally (48 Vitest tests).
## Embeddings — full product surface (feat/embeddings-page)
The `embeddings` catalog entry is upgraded from the old single Stores admin
(`StoresModule`, now deleted along with `StoreListView` — superseded, not
duplicated) into a six-tab product over the REAL `hanzoai/ai` `/v1` backend.
`StoreEditView` + `stores/logic.ts` (`newStore`) are reused for the collection
editor + create; nothing is forked.
- **EmbeddingsModule** routes `''`→Overview, `:tab`→Overview/Explore/Collections/
Jobs/Models/Settings, `collections/:name`→the store editor (unambiguous by
segment count — same pattern as Models' `:tab`). The ONE create path
(`add-store` + `newStore`) backs both the header "Create collection" and the
Collections "New".
- **Collections = stores.** `EmbeddingsApi.collections` = `get-stores` → a
`Collection` view-model; each maps to the Qdrant/Search index
`{owner}-{store}-docs` (the backend's `GetSearchIndexName`). The store object
carries NO vector count / dimension / index-size / `updatedTime`, and the metric
is fixed to cosine at index-create — so those columns render an honest "—"
(CREATED shows `createdTime`, the only timestamp; the metric is the real cosine).
- **Explore** → real `POST /v1/search?store=` (`{query,limit,mode}``{hits}`).
Hits carry no per-hit score (backend RRF-drops it) so score reads "—";
url/breadcrumbs are the locator. Model+Dimension are read-only collection
metadata (search uses the store's own embedding config, not query-time).
Vector-inspect is honest-empty (no point-lookup endpoint).
- **Models** = `/v1/models` filtered by id (no category field exists; many
embedding routes are `hidden` and absent — honest); generate = real
`POST /v1/embeddings` via the keyless `/ai` proxy (already allow-listed).
- **Jobs** = per-file index status (`get-files`: Pending/Processing/Finished/
Error — there is no async job entity) + a real upload ingest
(`POST /v1/docs/ingest`, source=upload).
- **Overview** metric cards (vectors/storage/queries/latency/cost) read
`GET /v1/get-cloud-usages` — a forward-compatible client coded to the documented
shape that degrades EVERY field to "—" with no sparkline today (the read API
has no unique commits yet on `feat/cloud-usage-read-api`). The model donut is
the real collection-by-model mix; dimension bars light up when metering reports
per-dimension counts; index-health is per-collection state enriched best-effort
with live `/v1/search/stats`.
- New: `src/lib/api/embeddings.ts` (`EmbeddingsApi`), the pure
`components/products/embeddings/logic.ts` (+15 Vitest), and the dependency-free
`components/ui/Charts.tsx` (Sparkline/Donut/BarChart — monochrome SVG, render
nothing/"—" rather than a fabricated trend).
- Shared-infra touched: registry (`embeddings` row upgraded, not duplicated),
`lib/api/index.ts` (barrel export). Reuses the unified `EmptyState`,
`BackendStateCard`, `DataTable`, `PageHeader`, `PrimaryButton`, `Field*`.
- Drive-by: corrected a STALE assertion in `admin-policy.test.ts` — the
`built-in` org-metadata owner was deliberately dropped in v0.7.15 (9b59dec,
"standardize the global-admin org on 'admin'") but the test still expected the
old wider allow-set; the gate code is unchanged (the tighter shipped policy is
the correct one).
- Verification: `tsc --noEmit` clean, `vitest` 67/67 (8 files), `next build`
green (14/14 pages). Authenticated visual e2e is gated behind a deploy + IAM
session — left for live verification (the catch-all `/[...slug]` route that
renders this compiled and type-checked).
## Native control planes — ZERO external link-outs + Hanzo Functions (claude/console2-native-control-planes)
Three deliverables, one PR, all over the one `/v1` surface (no `/api/` prefixes).
- **No external link-outs (the priority).** The catalog's `external` kind is GONE:
`CatalogEntry` is now `module`-only and `ProductStatus` is `'enabled' | 'soon'`.
Every product that used to open another domain in a tab (Gateway, DNS, CDN, MPC,
CLI, SDKs, API, IDE, Desktop, Registry, Metrics, Crawl, Studio, Console) is now a
native in-console route. They render ONE shared `NativeOverview`
(`components/products/overview/NativeOverview.tsx`, wired via `overviewFor(id)` +
`overviewRoutes(id)` in the registry — the DRY twin of `soonRoutes`): header +
what-it-is, a REAL health band (probes `PlatformApi.apps()` for the product's
operator service; honest "not deployed / not reporting" states, never a fabricated
"operational"), key-fact cards (honest "—"), native primary actions (in-console
routes only), and INLINE docs (rendered in-console; the docs SITE is a small
secondary reference, never the way to use the product). Content is a pure
`OverviewSpec` per product (`overview/spec.ts` + `resolve.ts` — a catalog-derived
`defaultSpec` covers any product with no bespoke spec). The `external` branches in
`open.ts`, `DashboardShell`, `AppLauncher`, `CommandPalette`, `ProductInterstitial`,
and `OverviewModule` are removed — there is one way to open anything: a native route.
- **Hanzo Functions dashboard.** `FunctionsModule` is rebuilt from the old single
`/paas/functions` table into a polished tabbed product (Overview · Functions ·
Deployments · Triggers · Secrets · Settings, `:tab` route like GPUs/Models) over
the rich `lib/api/functions.ts` (`GET /v1/functions*`). Branded **Hanzo Functions**
with the honest **Fission** engine badge (the mock said "OpenFaaS", but the live
engine per `go.mod` + `universe/infra/k8s/functions` is Fission — we label the real
one). Overview: 6 KPI cards (Functions/Invocations 7D/Success/Avg duration/Errors/
Cost) derived from real rows via `deriveOverview` (each degrades to "—"), with real
series sparklines + `trendPct` deltas; an "Invocations over time" `LineChart` with
1H/6H/24H/7D/30D range toggles; an "Invocation status" `Donut`; and the shared
`FunctionsBrowser` (table + `DetailRail`, DRY across Overview and the Functions tab).
All chart/donut/cost read `FunctionsApi.metrics(range)`; until that route is bound
they show honest "time-series not connected" — never a placeholder trend. Secrets is
names-only (values never fetched — Secret Manager principle). `functions/{FunctionsTable,
DetailRail,parts}.tsx` (already built on the feature branch) are reused unchanged.
- **Overview "Explore products" — enablement gate dropped.** The home cards lose the
Enabled/External/Soon `StatusBadge` and the open-vs-learn gate; every product is
open-for-all with an "Open" (native) + a "Learn more" affordance (the native
`/discover/:id` interstitial, which itself surfaces docs + OSS source inline — not a
link-out).
- Idiom: strictly `@hanzo/gui` v5 shorthands (`bg`/`maxW`/`rounded`/`items`/`self`/
`p`/`px`/`py`/`gap`), matching every existing module. New tests: `overview/resolve.test.ts`
(spec resolution + honest default; the no-`external`/no-`href` invariant is enforced at
compile time by the collapsed `CatalogEntry` union). Verification: `npm run typecheck`
clean (0 errors), `npm test` 298/298 (31 files), and every route (`/functions/*`, all
native overviews, `/`, `/discover/:id`) compiles + returns 200 on the dev server.
- Repo drive-by: removed the bogus tracked `node_modules` self-symlink blob (mode
120000 → itself) that broke `npm install`/`vitest`; `.gitignore` already ignores
`node_modules/`, so it was never meant to be committed.
+34 -176
View File
@@ -1,193 +1,51 @@
![Langfuse GitHub Banner](https://github.com/langfuse/langfuse/assets/121163007/6035f0f3-d691-4963-b5d0-10cf506e9d42)
<p align="center"><img src=".github/hero.svg" alt="console2" width="880"></p>
<div align="center"><h1>Langfuse: Open Source LLM Engineering Platform</h1></div>
<div align="center"><h4>LLM Observability, Prompt Management, LLM Evaluations,<br/>Datasets, LLM Metrics, and Prompt Playground</h4></div>
# Hanzo Cloud Console
<div align="center">
<div>
<h3>
<a href="https://cloud.langfuse.com">
<strong>Sign up</strong>
</a> ·
<a href="https://langfuse.com/docs/deployment/self-host">
<strong>Self Host</strong>
</a> ·
<a href="https://langfuse.com/demo">
<strong>Demo (live data)</strong>
</a>
</h3>
</div>
<div>
<a href="https://langfuse.com/docs"><strong>Docs</strong></a> ·
<a href="https://langfuse.com/issues"><strong>Report Bug</strong></a> ·
<a href="https://langfuse.com/ideas"><strong>Feature Request</strong></a> ·
<a href="https://langfuse.com/changelog"><strong>Changelog</strong></a> ·
<a href="https://langfuse.com/roadmap"><strong>Roadmap</strong></a> ·
<a href="https://langfuse.com/discord"><strong>Discord</strong></a>
</div>
<span>Langfuse uses <a href="https://github.com/orgs/langfuse/discussions"><strong>Github Discussions</strong></a> for Support and Feature Requests.</span>
<br/>
<span>We're hiring. <a href="https://langfuse.com/careers"><strong>Join us</strong></a> in Product Engineering and Developer Relations.</span>
<br/>
<br/>
<div>
<a href="https://github.com/langfuse/langfuse/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-red.svg?style=flat-square" alt="MIT License"></a>
<a href="https://www.ycombinator.com/companies/langfuse"><img src="https://img.shields.io/badge/Y%20Combinator-W23-orange?style=flat-square" alt="Y Combinator W23"></a>
<a href="https://github.com/langfuse/langfuse/pkgs/container/langfuse"><img alt="Docker Image" src="https://img.shields.io/badge/docker-langfuse-blue?logo=Docker&logoColor=white&style=flat-square"></a>
<a href="https://pypi.python.org/pypi/langfuse"><img src="https://img.shields.io/pypi/dm/langfuse?style=flat-square&logo=python&logoColor=white&label=pypi%20langfuse&color=blue" alt="langfuse Python package on PyPi"></a>
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/dm/langfuse?style=flat-square&logo=npm&logoColor=white&label=npm%20langfuse&color=blue" alt="langfuse npm package"></a>
</div>
</div>
</br>
Unified admin console for **Hanzo Cloud** and all Hanzo cloud products. Built on
[@hanzo/gui](https://gui.hanzo.ai) (cross-platform UI) over the unified `/v1`
backend (`hanzoai/cloud`). Dark theme, OIDC sign-in via Hanzo IAM.
## Langfuse Overview
Manages: **Providers · Models · Applications · Stores · Chat** — with an
extensible product-module registry so every cloud product can be added as a
module.
_Unmute video for voice-over_
https://github.com/langfuse/langfuse/assets/2834609/a94062e9-c782-4ee9-af59-dee6370149a8
### Develop
- **LLM Observability:** Instrument your app and start ingesting traces to Langfuse ([Quickstart](https://langfuse.com/docs/get-started), [Integrations](https://langfuse.com/docs/integrations) [Tracing](https://langfuse.com/docs/tracing))
- **Langfuse UI:** Inspect and debug complex logs ([Demo](https://langfuse.com/docs/demo), [Tracing](https://langfuse.com/docs/tracing))
- **Prompt Management:** Manage, version and deploy prompts from within Langfuse ([Prompt Management](https://langfuse.com/docs/prompts/get-started))
- **Prompt Engineering:** Test and iterate on your prompts with the [LLM Playground](https://langfuse.com/docs/playground)
### Monitor
- **LLM Analytics:** Track metrics (cost, latency, quality) and gain insights from dashboards & data exports ([Analytics](https://langfuse.com/docs/analytics))
- **LLM Evaluations:** Collect and calculate scores for your LLM completions ([Scores & Evaluations](https://langfuse.com/docs/scores))
- Run ([Model-based evaluations](https://langfuse.com/docs/scores/model-based-evals)) and LLM-as-a-Judge within Langfuse
- Collect user feedback ([User Feedback](https://langfuse.com/docs/scores/user-feedback))
- Manually score LLM outputs in Langfuse ([Manual Scores](https://langfuse.com/docs/scores/manually))
### Test
- **Experiments:** Track and test app behaviour before deploying a new version
- Datasets let you test expected in and output pairs and benchmark performance before deploying ([Datasets](https://langfuse.com/docs/datasets))
- Track versions and releases in your application ([Experimentation](https://langfuse.com/docs/experimentation), [Prompt Management](https://langfuse.com/docs/prompts))
## Get started
### Langfuse Cloud
Managed deployment by the Langfuse team, generous free-tier (hobby plan), no credit card required.
**[» Langfuse Cloud](https://cloud.langfuse.com)**
## Self-Hosting Open Source LLM Observability with Langfuse
### Localhost (docker)
## Quick start
```bash
# Clone repository
git clone https://github.com/langfuse/langfuse.git
cd langfuse
# Run server and database
docker compose up -d
npm install
cp .env.example .env.local
# set NEXT_PUBLIC_IAM_CLIENT_ID for live sign-in; defaults point at production.
npm run dev # http://localhost:4000
```
[→ Learn more about deploying locally](https://langfuse.com/docs/deployment/local)
## Scripts
### Self-host (docker)
| Script | What |
| --- | --- |
| `npm run dev` | Dev server on :4000 |
| `npm run build` | Production build (type-checks; Gui CSS injected at runtime) |
| `npm run start` | Serve the production build |
| `npm run typecheck` | `tsc --noEmit` (strict) |
Langfuse is simple to self-host and keep updated. It currently requires only a single docker container and a postgres database.
[→ Self Hosting Instructions](https://langfuse.com/docs/deployment/self-host)
## Configuration
Templated deployments: [Railway, GCP, AWS, Azure, Kubernetes and others](https://langfuse.com/docs/deployment/self-host#platform-specific-information)
All config is `NEXT_PUBLIC_*` (browser app, cookie auth). See `.env.example`.
## Get Started
| Var | Default | Meaning |
| --- | --- | --- |
| `NEXT_PUBLIC_CLOUD_URL` | `https://cloud.hanzo.ai` | Unified `/v1` backend base URL |
| `NEXT_PUBLIC_IAM_URL` | `https://iam.hanzo.ai` | Hanzo IAM OIDC authority |
| `NEXT_PUBLIC_IAM_APP_NAME` | `hanzo-console` | IAM application (`<org>-<app>`) |
| `NEXT_PUBLIC_IAM_ORG_NAME` | `hanzo` | IAM organization |
| `NEXT_PUBLIC_IAM_CLIENT_ID` | — | OAuth client id |
### API Keys
## Architecture
You need a Langfuse public and secret key to get started. Sign up [here](https://cloud.langfuse.com) and find them in your project settings.
### Ingesting Data · Instrumenting Your Application · LLM Observability with Langfuse
Note: We recommend using our fully async, typed [SDKs](https://langfuse.com/docs/sdk) that allow you to instrument any LLM application with any underlying model. They are available in [Python (Decorators)](https://langfuse.com/docs/sdk/python) & [JS/TS](https://langfuse.com/docs/sdk/typescript). The SDKs will always be the most fully featured and stable way to ingest data into Langfuse.
See the [→ Quickstart](https://langfuse.com/docs/get-started) to integrate Langfuse.
### LLM Observability Integrations
| Integration | Supports | Description |
| ---------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
| [OpenAI](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | Automated instrumentation via LlamaIndex callback system. |
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js. |
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
Packages integrated with Langfuse:
| Name | Description |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [Instructor](https://langfuse.com/docs/integrations/instructor) | Library to get structured LLM outputs (JSON, Pydantic) |
| [Dify](https://langfuse.com/docs/integrations/dify) | Open source LLM app development platform with no-code builder. |
| [Ollama](https://langfuse.com/docs/integrations/ollama) | Easily run open source LLMs on your own machine. |
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | Python toolkit for building LLM applications. |
| [Flowise](https://langfuse.com/docs/integrations/flowise) | JS/TS no-code builder for customized LLM flows. |
| [Langflow](https://langfuse.com/docs/integrations/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
## Questions and feedback
### Ideas and roadmap
- [Roadmap](https://langfuse.com/roadmap)
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions)
- [Feature Requests](https://langfuse.com/ideas)
### Support and feedback
In order of preference the best way to communicate with us:
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions) (preferred): Contribute [ideas](https://langfuse.com/ideas), [support requests](https://langfuse.com/gh-support) and [report bugs](https://langfuse.com/issues)
- [Discord](https://langfuse.com/discord): community support
- Privately: contact at langfuse dot com
## Contributing to Langfuse
- Vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas)
- Raise and comment on [Issues](https://github.com/langfuse/langfuse/issues)
- Open a PR - see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to setup a development environment.
See [LLM.md](./LLM.md) for the full design (base choice, /v1 client, auth flow,
the product-module registry, and the Providers surface). Endpoint reference in
[docs/endpoints.md](./docs/endpoints.md).
## License
This repository is MIT licensed, except for the `ee` folders. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
## Misc
### GET API to export your data
[**GET routes**](https://langfuse.com/docs/integrations/api) to use data in downstream applications (e.g. embedded analytics). You can also access them conveniently via the SDKs ([docs](https://langfuse.com/docs/query-traces)).
### Security & Privacy
We take data security and privacy seriously. Please refer to our [Security and Privacy](https://langfuse.com/security) page for more information.
### Telemetry
By default, Langfuse automatically reports basic usage statistics of self-hosted instances to a centralized server (PostHog).
This helps us to:
1. Understand how Langfuse is used and improve the most relevant features.
2. Track overall usage for internal and external (e.g. fundraising) reporting.
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/web/src/features/telemetry/index.ts).
You can opt-out by setting `TELEMETRY_ENABLED=false`.
### Star History
<a href="https://star-history.com/#langfuse/langfuse&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" />
</picture>
</a>
BSD-3-Clause. Copyright (c) 2026-present, Hanzo AI, Inc.
-4
View File
@@ -1,4 +0,0 @@
## Security Policy
We strongly recommend using the latest version of Langfuse to receive all security updates.
For more information, please refer to the [Data Security & Privacy](https://langfuse.com/docs/data-security-privacy) page in the documentation or contact security@langfuse.com.
+46
View File
@@ -0,0 +1,46 @@
'use client'
import { use } from 'react'
import { notFound } from 'next/navigation'
import { resolveView, isAdminRoute } from '~/lib/products/match'
import { findEntry } from '~/lib/products/registry'
import { useIsGlobalAdmin } from '~/lib/auth/admin'
import { ProductSubpageStub } from '~/components/products/ProductSubpageStub'
import { AdminManagedNotice } from '~/components/products/AdminManagedNotice'
/**
* Catch-all product route. Resolves the module + route from the registry and
* renders its component. Adding a product anywhere in the registry makes its
* routes live here — no per-product page files.
*
* Two honest gates on top of the resolver:
* - A known product SUB-PAGE (a declared specific or a uniform base sub-page:
* Overview · Settings · Status · Logs · Metrics) with no backend route yet
* renders a placeholder stub — never a 404, never a fabricated surface.
* - A CUSTOMER (non-global-admin) reaching an admin-only surface (cross-tenant
* IAM/KMS, provider + routing config) gets a graceful "managed by Hanzo" notice
* instead of the module's hostile 403 red error. Access is enforced
* server-side regardless.
*/
export default function ProductPage({ params }: { params: Promise<{ slug: string[] }> }) {
const { slug } = use(params)
const showAdmin = useIsGlobalAdmin()
const view = resolveView(slug)
if (view.kind === 'notfound') notFound()
if (!showAdmin && isAdminRoute(slug)) {
const entry = findEntry(slug[0])
if (entry && entry.kind === 'module') {
const seg = slug[1]
const subpage = seg ? (entry.subpages ?? []).find((s) => s.slug === seg && s.admin) : undefined
return <AdminManagedNotice entry={entry} subpage={subpage} />
}
}
if (view.kind === 'stub') return <ProductSubpageStub entry={view.entry} subpage={view.subpage} />
const Component = view.matched.route.component
return <Component params={view.matched.params} />
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
import { use } from 'react'
import { ProductInterstitial } from '~/components/products/ProductInterstitial'
/**
* Product discover screen — `/discover/<id>` renders the interstitial for one
* catalog entry (docs, OSS source, revenue share, open/get-started). A dedicated
* route (more specific than the `[...slug]` product catch-all) so it's shareable.
*/
export default function DiscoverPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params)
return <ProductInterstitial id={id} />
}
+35
View File
@@ -0,0 +1,35 @@
import type { ReactNode } from 'react'
import { AuthGate } from '~/components/AuthGate'
import { OrgGate } from '~/components/OrgGate'
import { DashboardShell } from '~/components/DashboardShell'
import { PreferencesProvider } from '~/lib/products/preferences'
import { ScopeProvider } from '~/lib/scope-context'
import { ToastProvider } from '~/components/ui/Toast'
import { CommandPaletteProvider } from '~/components/CommandPalette'
import { AppLauncherProvider } from '~/components/AppLauncher'
import { FloatingChatProvider } from '~/components/FloatingChat'
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<AuthGate>
<OrgGate>
<ScopeProvider>
<PreferencesProvider>
<ToastProvider>
{/* AppLauncher wraps the palette so the palette can open the launcher. */}
<AppLauncherProvider>
<CommandPaletteProvider>
{/* FloatingChat floats the assistant bubble over every page. */}
<FloatingChatProvider>
<DashboardShell>{children}</DashboardShell>
</FloatingChatProvider>
</CommandPaletteProvider>
</AppLauncherProvider>
</ToastProvider>
</PreferencesProvider>
</ScopeProvider>
</OrgGate>
</AuthGate>
)
}
+135
View File
@@ -0,0 +1,135 @@
'use client'
/**
* Product catalog — the unified console home. Every Hanzo product, grouped by the
* ten canonical categories, with its Google Cloud equivalent. Every product is
* open-for-all: each card opens straight into its native in-console surface and
* carries a "Learn more" affordance to its docs — there is no enablement gate and
* no external bounce. Each card can be pinned to the sidebar (persisted to the
* account). Rendered entirely from the catalog registry.
*/
import { useRouter } from 'next/navigation'
import { Button, Card, Text, XStack, YStack } from '@hanzo/gui'
import { Star, Lock, ArrowRight, BookOpen } from '@hanzogui/lucide-icons-2'
import { config } from '~/config'
import { visibleCatalogByCategory, type CatalogEntry } from '~/lib/products/registry'
import { openProduct } from '~/lib/products/open'
import { useFavorites } from '~/lib/products/favorites'
import { useIsGlobalAdmin } from '~/lib/auth/admin'
import { PageHeader } from '~/components/ui/PageHeader'
import { FadeIn } from '~/components/ui/FadeIn'
import OverviewDashboard from '~/components/products/OverviewModule'
function ProductCard({
entry,
pinned,
onOpen,
onToggle,
onLearnMore,
}: {
entry: CatalogEntry
pinned: boolean
onOpen: () => void
onToggle: () => void
onLearnMore: () => void
}) {
const Icon = entry.icon
return (
<Card borderWidth={1} borderColor="$borderColor" p="$4" gap="$3" width={272}>
<XStack justify="space-between" items="flex-start">
<XStack gap="$2" items="center" flex={1}>
<Icon size={20} />
<YStack flex={1}>
<Text fontSize="$5" fontWeight="700">
{entry.label}
</Text>
{entry.gcp ? (
<Text fontSize="$1" color="$color10">
{entry.gcp}
</Text>
) : null}
</YStack>
</XStack>
<XStack gap="$1" items="center">
{entry.admin ? <Lock size={13} opacity={0.45} /> : null}
<Button
size="$2"
chromeless
opacity={pinned ? 1 : 0.3}
icon={<Star size={15} />}
onPress={onToggle}
aria-label={pinned ? `Unpin ${entry.label}` : `Pin ${entry.label}`}
/>
</XStack>
</XStack>
<Text fontSize="$3" color="$color11" minH={40}>
{entry.description}
</Text>
<XStack justify="space-between" items="center">
<Button
size="$2"
chromeless
icon={<BookOpen size={14} />}
onPress={onLearnMore}
aria-label={`Learn more about ${entry.label}`}
>
Learn more
</Button>
<Button
size="$2"
bg="$color5"
borderWidth={1}
borderColor="$borderColor"
onPress={onOpen}
iconAfter={<ArrowRight size={14} />}
>
Open
</Button>
</XStack>
</Card>
)
}
export default function DashboardHome() {
const router = useRouter()
const { toggle, isPinned } = useFavorites()
const showAdmin = useIsGlobalAdmin()
const push = (path: string) => router.push(path)
const groups = visibleCatalogByCategory(showAdmin)
return (
<YStack gap="$7">
<OverviewDashboard />
<YStack gap="$4">
<PageHeader
title="Explore products"
subtitle={`Open and manage every ${config.brandName} product from one place.`}
/>
{groups.map((group, i) => (
<FadeIn key={group.category} index={i} style={{ width: '100%' }}>
<YStack gap="$3">
<Text fontSize="$5" fontWeight="800" color="$color12">
{group.category}
</Text>
<XStack flexWrap="wrap" gap="$3">
{group.entries.map((entry) => (
<ProductCard
key={entry.id}
entry={entry}
pinned={isPinned(entry.id)}
onOpen={() => openProduct(entry, push)}
onToggle={() => toggle(entry.id)}
onLearnMore={() => push(`/discover/${entry.id}`)}
/>
))}
</XStack>
</YStack>
</FadeIn>
))}
</YStack>
</YStack>
)
}
+82
View File
@@ -0,0 +1,82 @@
/**
* Server-gated GLOBAL IAM admin proxy — cross-tenant IAM ops (any org).
*
* The browser holds no IAM credential. It calls this SAME-ORIGIN route with just
* its session cookie; the handler enforces the GLOBAL admin gate (`getAdminGate`:
* verified @<adminDomain> email AND a global-admin flag), then the shared
* `forwardIam` applies the allow-list + tenant scoping (a global admin may act on
* any org) and forwards to IAM as the user. A CUSTOMER managing their OWN org uses
* `/org/iam` instead — this route is global-only.
*
* Least privilege: only an explicit allow-list of admin segments is reachable
* (GET reads / POST mutations); every owner the request references — including
* the mutation BODY owner — is validated by `forwardIam`.
*/
import { type NextRequest, NextResponse } from 'next/server'
import { getAdminGate } from '~/lib/server/identity'
import { forwardIam } from '~/lib/server/iam-proxy'
export const runtime = 'nodejs'
/** Read segments — reachable via GET only. */
const GET_SEGMENTS = new Set([
'get-organizations',
'get-organization',
'get-users',
'get-user',
'get-applications',
'get-application',
'get-providers',
'get-provider',
'get-roles',
'get-records',
])
/** Mutation segments — reachable via POST only (JSON body forwarded). */
const POST_SEGMENTS = new Set([
'add-user',
'update-user',
'delete-user',
'add-application',
'update-application',
'delete-application',
'add-provider',
'update-provider',
'delete-provider',
])
/**
* Organization objects are owned by IAM's built-in `admin`, and the org
* list/get endpoints scope results to the caller's org server-side — so `admin`
* is an acceptable owner THERE (never for tenant data like users/roles).
*/
const ORG_ENDPOINTS = new Set(['get-organizations', 'get-organization'])
const forbidden = () => NextResponse.json({ error: 'forbidden' }, { status: 403 })
async function handle(req: NextRequest, path: string[], method: 'GET' | 'POST'): Promise<NextResponse> {
const gate = await getAdminGate(req)
if (!gate) return forbidden()
return forwardIam(
req,
{ user: gate.user, isGlobalAdmin: gate.user.isGlobalAdmin, orgScope: gate.orgScope },
{
segment: path.join('/'),
method,
allowed: method === 'GET' ? GET_SEGMENTS : POST_SEGMENTS,
orgMetaSegments: ORG_ENDPOINTS,
// The gate is already global-only; global admins may write to any org.
requireAdminForWrite: false,
},
)
}
type Ctx = { params: Promise<{ path: string[] }> }
export async function GET(req: NextRequest, ctx: Ctx) {
return handle(req, (await ctx.params).path, 'GET')
}
export async function POST(req: NextRequest, ctx: Ctx) {
return handle(req, (await ctx.params).path, 'POST')
}
+135
View File
@@ -0,0 +1,135 @@
/**
* Server-gated KMS admin proxy — the ONLY way the browser reaches Hanzo KMS.
*
* Same trust boundary as the IAM proxy: the browser sends only its session
* cookie, this handler enforces the brand-admin gate, then forwards to kmsd as
* the user (short-lived user-bound bearer) so KMS enforces org isolation from the
* verified `owner` claim (`canActOnOrg`). Secrets are scoped to the brand org by
* default; a global admin may target another org with `?org=`.
*
* Zero-knowledge discipline: this route NEVER logs a secret value or any request
* body, and never derives or stores key material — it is a faithful pass-through
* of kmsd's JSON + status code. One resource path (`/admin/kms/secrets`); the
* verb + query select the operation:
* GET ?path=&name=&env= → reveal one value → GET .../secrets/<path>/<name>?env=
* GET ?prefix=&env= → list metadata → GET .../secrets?prefix=&env=
* POST {path,name,env,value} → create/upsert → POST .../secrets
* PATCH ?path=&name= {value,version,env} → rotate → PATCH .../secrets/<path>/<name>
* DELETE ?path=&name=&env= → delete → DELETE .../secrets/<path>/<name>?env=
*
* kmsd has no list endpoint yet — the list GET returns 404, which the KMS module
* renders as an honest "listing requires kmsd ≥ next release" state.
*/
import { type NextRequest, NextResponse } from 'next/server'
import { getAdminGate, adminBearer, kmsBaseUrl, type AdminGate } from '~/lib/server/identity'
import { orgFor as policyOrgFor } from '~/lib/server/admin-policy'
export const runtime = 'nodejs'
const forbidden = () => NextResponse.json({ error: 'forbidden' }, { status: 403 })
const notFound = () => NextResponse.json({ error: 'not found' }, { status: 404 })
/** `<path>/<name>` for the kmsd route, each segment encoded, slashes preserved. */
function secretRest(path: string, name: string): string {
return [...path.split('/').filter(Boolean), name].map(encodeURIComponent).join('/')
}
/** Org the operator acts on — the brand org, unless a global admin passes ?org=
* (the pure `admin-policy` predicate, tested in admin-policy.test.ts). */
function orgFor(gate: AdminGate, req: NextRequest): string {
return policyOrgFor(
{ isGlobalAdmin: gate.user.isGlobalAdmin, orgScope: gate.orgScope },
req.nextUrl.searchParams.get('org'),
)
}
async function handle(req: NextRequest, segments: string[]): Promise<NextResponse> {
const gate = await getAdminGate(req)
if (!gate) return forbidden()
if (segments.length !== 1 || segments[0] !== 'secrets') return notFound()
const org = orgFor(gate, req)
const base = `${kmsBaseUrl()}/v1/kms/orgs/${encodeURIComponent(org)}/secrets`
const q = req.nextUrl.searchParams
const name = q.get('name') ?? ''
const path = q.get('path') ?? ''
const env = q.get('env') ?? ''
let target: string
let body: string | undefined
if (req.method === 'GET') {
if (name) {
const params = new URLSearchParams()
if (env) params.set('env', env)
target = `${base}/${secretRest(path, name)}${params.toString() ? `?${params}` : ''}`
} else {
const params = new URLSearchParams()
const prefix = q.get('prefix')
if (prefix) params.set('prefix', prefix)
if (env) params.set('env', env)
target = `${base}${params.toString() ? `?${params}` : ''}`
}
} else if (req.method === 'POST') {
target = base
body = await req.text() // {path,name,env,value} — forwarded verbatim, never logged
} else if (req.method === 'PATCH') {
if (!name) return notFound()
target = `${base}/${secretRest(path, name)}`
body = await req.text() // {value,version,env} — forwarded verbatim, never logged
} else if (req.method === 'DELETE') {
if (!name) return notFound()
const params = new URLSearchParams()
if (env) params.set('env', env)
target = `${base}/${secretRest(path, name)}${params.toString() ? `?${params}` : ''}`
} else {
return notFound()
}
let bearer: string
try {
bearer = await adminBearer(gate.user)
} catch (e) {
return NextResponse.json(
{ message: `Could not authorize the request: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
const headers: Record<string, string> = { Authorization: `Bearer ${bearer}`, Accept: 'application/json' }
const init: RequestInit = { method: req.method, headers, cache: 'no-store' }
if (body !== undefined) {
headers['Content-Type'] = 'application/json'
init.body = body
}
try {
const res = await fetch(target, init)
const text = await res.text()
return new NextResponse(text, {
status: res.status,
headers: { 'Content-Type': res.headers.get('content-type') ?? 'application/json' },
})
} catch (e) {
// Surface only the transport failure — never the request body/value.
return NextResponse.json(
{ message: `KMS unreachable: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
}
type Ctx = { params: Promise<{ path: string[] }> }
export async function GET(req: NextRequest, ctx: Ctx) {
return handle(req, (await ctx.params).path)
}
export async function POST(req: NextRequest, ctx: Ctx) {
return handle(req, (await ctx.params).path)
}
export async function PATCH(req: NextRequest, ctx: Ctx) {
return handle(req, (await ctx.params).path)
}
export async function DELETE(req: NextRequest, ctx: Ctx) {
return handle(req, (await ctx.params).path)
}
+139
View File
@@ -0,0 +1,139 @@
/**
* Keyless AI proxy — the ONE path the console uses to reach the model gateway.
*
* `/v1/chat/completions` (and friends) REQUIRE an `Authorization: Bearer` token;
* a browser session cookie alone is rejected. Rather than ship the user's durable
* `hk-` key to the browser, the console calls its OWN origin (`/ai/v1/...`) with
* just the session cookie; this server handler resolves the user, mints a
* SHORT-LIVED, user-bound IAM token (issue-user-token, cached until just before
* expiry), and forwards to the gateway with that token. No key in the browser, no
* rotation on a chat turn, and every call is billed to the user's own org.
*
* Least privilege: only the read/inference AI endpoints are proxied; anything
* else 404s, so this is not a general gateway tunnel.
*/
import { type NextRequest, NextResponse } from 'next/server'
import { resolveUser, issueUserToken, type SessionUser } from '~/lib/server/identity'
import { retrievalHeaders } from '~/lib/server/ai-proxy'
export const runtime = 'nodejs'
const trim = (s: string) => s.replace(/\/+$/, '')
/** Gateway the proxied AI calls are forwarded to (gated/priced api.hanzo.ai). */
const AI_GATEWAY_URL = trim(process.env.AI_GATEWAY_URL ?? 'https://api.hanzo.ai')
/** The exact `/v1/<...>` endpoints the console is allowed to reach. */
const ALLOWED = new Set([
'v1/models',
'v1/pricing/models', // the rich model+provider catalog (context, pricing, specs, tier) for Models/Providers pages
'v1/plans', // the subscription tiers + entitlements (rpm/tpm/quota) for the catalog plan badges
'v1/chat',
'v1/chat/completions',
'v1/embeddings',
'v1/rerank',
'v1/audio/speech', // text-to-speech (JSON in → audio bytes out) for the Playground Audio tab
])
// ── Short-lived user-token cache ─────────────────────────────────────────────
// issue-user-token is an IAM round-trip; cache the JWT per user until ~60s before
// it expires so a chat session reuses one token instead of issuing per turn.
type CachedToken = { token: string; expMs: number }
const tokenCache = new Map<string, CachedToken>()
const SKEW_MS = 60_000
const FALLBACK_TTL_MS = 5 * 60_000
async function tokenFor(user: SessionUser): Promise<string> {
const hit = tokenCache.get(user.id)
if (hit && hit.expMs > Date.now()) return hit.token
const { accessToken, expiresIn } = await issueUserToken(user)
const ttl = expiresIn > 0 ? expiresIn * 1000 : FALLBACK_TTL_MS
tokenCache.set(user.id, { token: accessToken, expMs: Date.now() + ttl - SKEW_MS })
return accessToken
}
async function forward(req: NextRequest, path: string[]): Promise<NextResponse> {
const rel = path.join('/')
if (!ALLOWED.has(rel)) {
return NextResponse.json({ error: { message: 'Not found', type: 'not_found' } }, { status: 404 })
}
const user = await resolveUser(req)
if (!user) {
return NextResponse.json(
{ error: { message: 'Sign in to use AI.', type: 'auth_error', code: 'unauthenticated' } },
{ status: 401 },
)
}
let token: string
try {
token = await tokenFor(user)
} catch (e) {
return NextResponse.json(
{ error: { message: `Could not authorize the request: ${e instanceof Error ? e.message : String(e)}`, type: 'auth_error' } },
{ status: 502 },
)
}
const url = `${AI_GATEWAY_URL}/${rel}${req.nextUrl.search}`
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
Accept: 'application/json',
// The gateway derives org/user from the JWT; pass the resolved org too so a
// pooled-org backend scopes correctly either way (it strips client headers).
'X-Org-Id': user.owner,
}
// Forward the RAG retrieval switch when present (allow-listed in `ai-proxy`).
// chat/completions turns on built-in retrieval from `X-Retrieval`/
// `X-Retrieval-Store` (backend controllers/chat_retrieval.go); this handler
// rebuilds headers from scratch, so without the passthrough `AiApi.ragChat`
// silently degraded to a plain answer. The store's org owner is still resolved
// server-side from the session.
Object.assign(headers, retrievalHeaders((h) => req.headers.get(h)))
const init: RequestInit = {
method: req.method,
headers,
cache: 'no-store',
// Propagate a client abort (Stop / tab-switch / unmount) through to the
// gateway, so cancelling a stream stops upstream generation + billing and
// releases the socket — not just the browser→proxy hop.
signal: req.signal,
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
init.body = await req.text()
}
try {
const res = await fetch(url, init)
// Stream the upstream body straight through — do NOT buffer with res.text().
// chat/completions with `stream:true` returns Server-Sent Events, and the
// multi-model compare playground measures real time-to-first-token from the
// first streamed chunk; buffering would collapse TTFT into total latency.
// Passing res.body through is equally correct for the non-streaming JSON
// callers (they read the full body) and for binary audio/speech bytes — one
// passthrough serves every allow-listed endpoint, streaming or not.
return new NextResponse(res.body, {
status: res.status,
headers: {
'Content-Type': res.headers.get('content-type') ?? 'application/json',
'Cache-Control': 'no-cache, no-transform',
},
})
} catch (e) {
return NextResponse.json(
{ error: { message: `AI gateway unreachable: ${e instanceof Error ? e.message : String(e)}`, type: 'upstream_error' } },
{ status: 502 },
)
}
}
type Ctx = { params: Promise<{ path: string[] }> }
export async function GET(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
export async function POST(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+53
View File
@@ -0,0 +1,53 @@
'use client'
/**
* IAM OAuth callback. IAM redirects here with `?code&state`; we exchange them
* for a backend session (`/v1/signin`) and land on the dashboard. On failure we
* surface the error and offer a retry.
*/
import { Suspense, useEffect, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { Button, Text, YStack } from '@hanzo/gui'
import { ApiError } from '~/lib/api'
import { Loader } from '~/components/ui/Loader'
import { useSession } from '~/lib/auth/session'
function Callback() {
const params = useSearchParams()
const router = useRouter()
const { completeSignIn } = useSession()
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const code = params.get('code')
const state = params.get('state')
if (!code || !state) {
setError('Missing authorization code.')
return
}
completeSignIn(code, state)
.then(() => router.replace('/'))
.catch((e: unknown) => setError(e instanceof ApiError ? e.message : 'Sign-in failed.'))
}, [params, completeSignIn, router])
if (error) {
return (
<YStack flex={1} minH="100vh" items="center" justify="center" gap="$3">
<Text color="$color12" fontWeight="600">
{error}
</Text>
<Button onPress={() => router.replace('/signin')}>Back to sign in</Button>
</YStack>
)
}
return <Loader label="Completing sign-in…" />
}
export default function CallbackPage() {
return (
<Suspense fallback={null}>
<Callback />
</Suspense>
)
}
+121
View File
@@ -0,0 +1,121 @@
/**
* Per-tenant billing proxy → commerce. The browser calls console2's OWN origin
* (`/billing/...`); this server handler forwards to commerce's `/v1/billing/...`,
* injecting the commerce SERVICE token from server-only env (never `NEXT_PUBLIC_`,
* never in the browser bundle) AND scoping every request to the caller's OWN org.
*
* Same trust boundary as the `/admin/iam` + `/admin/kms` proxies, but the authz is
* PER-TENANT, not admin: any authenticated session may read/act on ITS OWN billing
* (balance / usage / invoices / credit-grants). The org is resolved server-side
* from the validated session (`resolveUser`) and stamped as `X-Hanzo-Org` + the
* `user` billing-subject — the client CANNOT widen scope (a supplied `?user=`/`?org=`
* is overwritten), so commerce's per-org isolation can never be crossed from the
* browser. No session → 401.
*
* Billing-subject mirrors `object.BillingSubject` (hanzoai/ai) + chat's
* `billingSubject`: a member of a PERSONAL-billing org (default the shared `hanzo`
* catch-all) bills per-user as `<org>/<name>`; a dedicated org (maxpower, …) bills
* per-org as `<org>`. The SAME subject the gateway debits — so the console shows
* the exact balance/usage that gets charged.
*
* `COMMERCE_TOKEN` unset → honest 501 (the UI shows a truthful "not configured"
* state; it never fabricates a balance).
*/
import { type NextRequest, NextResponse } from 'next/server'
import { resolveUser } from '~/lib/server/identity'
export const runtime = 'nodejs'
const isSafeSegment = (s: string): boolean =>
s.length > 0 && s !== '.' && s !== '..' && !s.includes('/') && !s.includes('\\') && !s.includes('\0')
/** Orgs whose members bill per-USER (the shared catch-all). Mirrors PERSONAL_BILLING_ORGS. */
function personalBillingOrgs(): Set<string> {
const raw = (process.env.PERSONAL_BILLING_ORGS || process.env.HANZO_DEFAULT_ORG || 'hanzo')
.split(',')
.map((s) => s.trim().toLowerCase())
.filter(Boolean)
return new Set(raw)
}
/** Commerce billing subject for an org+user (= the subject the gateway debits). */
function billingSubject(org: string, name: string): string {
const o = org.trim().toLowerCase()
if (!o) return ''
if (personalBillingOrgs().has(o)) {
const n = name.trim().toLowerCase()
return n ? `${o}/${n}` : o
}
return o
}
function commerceBaseUrl(): string {
return (process.env.COMMERCE_URL ?? 'http://commerce.hanzo.svc:8001').replace(/\/+$/, '')
}
async function forward(req: NextRequest, path: string[]): Promise<NextResponse> {
// Per-tenant authz: any valid session may see ITS OWN billing (no admin gate).
const user = await resolveUser(req)
if (!user) {
return NextResponse.json({ error: 'Sign in to view billing.' }, { status: 401 })
}
if (!path.every(isSafeSegment)) {
return NextResponse.json({ error: 'Invalid billing path.' }, { status: 400 })
}
const token = process.env.COMMERCE_TOKEN ?? process.env.COMMERCE_SERVICE_TOKEN ?? ''
if (!token) {
return NextResponse.json(
{ error: 'Billing is not configured (COMMERCE_TOKEN missing).' },
{ status: 501 },
)
}
// Scope to the caller's OWN org — server-resolved, never client-supplied.
const org = user.owner.trim()
const subject = billingSubject(org, user.name)
// Overwrite any client `user`/`org` so the browser cannot read another tenant's ledger.
const search = new URLSearchParams(req.nextUrl.search)
search.set('user', subject)
search.delete('org')
const qs = search.toString()
const url = `${commerceBaseUrl()}/v1/billing/${path.join('/')}${qs ? `?${qs}` : ''}`
const init: RequestInit = {
method: req.method,
headers: {
Authorization: `Bearer ${token}`,
'X-Hanzo-Org': org,
'Content-Type': 'application/json',
Accept: 'application/json',
},
cache: 'no-store',
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
init.body = await req.text()
}
try {
const res = await fetch(url, init)
const text = await res.text()
return new NextResponse(text, {
status: res.status,
headers: { 'Content-Type': res.headers.get('content-type') ?? 'application/json' },
})
} catch (e) {
return NextResponse.json(
{ error: `Billing upstream unreachable: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
}
type Ctx = { params: Promise<{ path: string[] }> }
export async function GET(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
export async function POST(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
+167
View File
@@ -0,0 +1,167 @@
/**
* Wallet HUSD top-up — verify on-chain, then record to commerce (server route).
*
* Why this lives in console2 and not billing: billing.hanzo.ai is a Next static
* export (`output: 'export'`) and cannot host a runtime POST handler, and the
* commerce backend is owned elsewhere. So the verify-and-record seam lives here
* as a same-origin server route — the same pattern as `app/paas/[...path]`: the
* browser calls the console's OWN origin, the server does the privileged work,
* and config comes from server-only env (sourced via KMS, never `NEXT_PUBLIC`).
*
* Flow: the client sends an HUSD ERC-20 transfer to the treasury and posts the
* tx hash here. We read the receipt from the Hanzo EVM, confirm it is a mined,
* successful HUSD `Transfer(from → treasury, value)`, derive USD cents from the
* (18-decimal, USD-pegged) value, then record it to commerce as a `husd` crypto
* payment and return the credited amount + the new balance. The on-chain amount
* — never a client-supplied number — is what gets credited.
*
* Honest failure: if HUSD/treasury are unconfigured (greenfield — HUSD not yet
* deployed) we return 501 so the UI shows a truthful "coming" state; if the tx
* is missing/failed/not an HUSD-to-treasury transfer we return 400; if the chain
* or commerce is unreachable we return 502. Never a fabricated credit.
*/
import { type NextRequest, NextResponse } from 'next/server'
import { ethers } from 'ethers'
export const runtime = 'nodejs'
const RPC_URL = (process.env.HANZO_RPC_URL ?? 'https://rpc.hanzo.network').replace(/\/+$/, '')
const HUSD_ADDRESS = (process.env.HANZO_HUSD_ADDRESS ?? '').trim()
const TREASURY = (process.env.HANZO_HUSD_TREASURY ?? '').trim()
const COMMERCE_URL = (process.env.COMMERCE_URL ?? 'https://api.hanzo.ai').replace(/\/+$/, '')
const CHAIN_ID = Number(process.env.HANZO_CHAIN_ID ?? '36900')
const ERC20_TRANSFER_ABI = ['event Transfer(address indexed from, address indexed to, uint256 value)']
const isAddr = (a: string): boolean => /^0x[0-9a-fA-F]{40}$/.test(a)
/** Forward the caller's identity (session cookie / bearer) to commerce. */
function authHeaders(req: NextRequest): Record<string, string> {
const h: Record<string, string> = { 'Content-Type': 'application/json', Accept: 'application/json' }
const cookie = req.headers.get('cookie')
if (cookie) h.Cookie = cookie
const auth = req.headers.get('authorization')
if (auth) h.Authorization = auth
return h
}
export async function POST(req: NextRequest): Promise<NextResponse> {
// Greenfield gate: no HUSD contract / treasury ⇒ honest "not configured".
if (!isAddr(HUSD_ADDRESS) || !isAddr(TREASURY)) {
return NextResponse.json(
{ error: 'HUSD top-up is not configured yet (HUSD is not deployed on Hanzo Mainnet).' },
{ status: 501 },
)
}
let body: { txHash?: string; fromAddress?: string; userId?: string }
try {
body = await req.json()
} catch {
return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 })
}
const txHash = (body.txHash ?? '').trim()
const fromAddress = (body.fromAddress ?? '').trim()
if (!/^0x[0-9a-fA-F]{64}$/.test(txHash)) {
return NextResponse.json({ error: 'A valid transaction hash is required.' }, { status: 400 })
}
// ── 1. Verify the HUSD transfer on-chain ────────────────────────────────────
let creditedCents: number
let verifiedFrom: string
try {
const provider = new ethers.JsonRpcProvider(RPC_URL, CHAIN_ID)
const receipt = await provider.getTransactionReceipt(txHash)
if (!receipt) {
return NextResponse.json({ error: 'Transaction not found or not yet mined.' }, { status: 400 })
}
if (receipt.status !== 1) {
return NextResponse.json({ error: 'Transaction failed on-chain.' }, { status: 400 })
}
const iface = new ethers.Interface(ERC20_TRANSFER_ABI)
const husd = HUSD_ADDRESS.toLowerCase()
const treasury = TREASURY.toLowerCase()
let value: bigint | null = null
for (const log of receipt.logs) {
if (log.address.toLowerCase() !== husd) continue
let parsed: ethers.LogDescription | null = null
try {
parsed = iface.parseLog({ topics: [...log.topics], data: log.data })
} catch {
continue
}
if (parsed?.name !== 'Transfer') continue
if (String(parsed.args.to).toLowerCase() !== treasury) continue
value = parsed.args.value as bigint
verifiedFrom = ethers.getAddress(String(parsed.args.from))
break
}
if (value === null) {
return NextResponse.json(
{ error: 'No HUSD transfer to the treasury was found in this transaction.' },
{ status: 400 },
)
}
if (fromAddress && isAddr(fromAddress) && verifiedFrom!.toLowerCase() !== fromAddress.toLowerCase()) {
return NextResponse.json({ error: 'Transfer sender does not match the connected wallet.' }, { status: 400 })
}
// HUSD is an 18-decimal, USD-pegged stablecoin → 1e16 base units = 1 cent.
creditedCents = Number(value / 10n ** 16n)
if (creditedCents <= 0) {
return NextResponse.json({ error: 'Transferred amount is below the minimum (1 cent).' }, { status: 400 })
}
} catch (e) {
return NextResponse.json(
{ error: `Could not verify the transaction on Hanzo Mainnet: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
// ── 2. Record to commerce as an HUSD crypto payment ─────────────────────────
try {
const recordRes = await fetch(`${COMMERCE_URL}/v1/billing/payment`, {
method: 'POST',
headers: authHeaders(req),
cache: 'no-store',
body: JSON.stringify({
method: 'crypto',
network: 'hanzo',
chainId: CHAIN_ID,
currency: 'husd',
amount: creditedCents,
txHash,
fromAddress: verifiedFrom!,
toAddress: TREASURY,
userId: body.userId,
}),
})
if (!recordRes.ok) {
const text = await recordRes.text().catch(() => '')
return NextResponse.json(
{ error: `Commerce rejected the payment (HTTP ${recordRes.status}): ${text}`.trim() },
{ status: 502 },
)
}
const payment = (await recordRes.json().catch(() => ({}))) as { status?: string }
// New balance (USD ledger) — best-effort; the credit already landed.
let balance = 0
try {
const balRes = await fetch(
`${COMMERCE_URL}/v1/billing/balance?user=${encodeURIComponent(body.userId ?? '')}&currency=usd`,
{ headers: authHeaders(req), cache: 'no-store' },
)
if (balRes.ok) balance = ((await balRes.json()) as { balance?: number }).balance ?? 0
} catch {
/* balance is informational; the credit is recorded */
}
return NextResponse.json({ creditedCents, balance, txHash, status: payment.status ?? 'recorded' })
} catch (e) {
return NextResponse.json(
{ error: `Could not reach commerce to record the payment: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
}
+20
View File
@@ -0,0 +1,20 @@
/**
* `/docs` → the brand documentation site (new origin), server-side redirect.
*
* Docs are an EXTERNAL product on their own domain (docs.hanzo.ai / docs.lux.network
* / …), never an in-app route — so a typed or bookmarked `console.<brand>/docs`
* must land on the real docs, not the catch-all `notFound()` (the old 404). The
* brand is resolved from the request Host so a lux/zoo console redirects to ITS
* docs. The sidebar "Docs" entry and the header "?" open the same URL in a new tab;
* this handler only covers the direct-navigation case.
*/
import { type NextRequest, NextResponse } from 'next/server'
import { resolveConfig } from '~/config'
export const runtime = 'nodejs'
export function GET(req: NextRequest) {
const host = req.headers.get('host')
return NextResponse.redirect(resolveConfig(host ?? undefined).docsUrl, 308)
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+64
View File
@@ -0,0 +1,64 @@
html,
body,
#__next {
height: 100%;
}
body {
margin: 0;
background-color: var(--background, #0a0a0a);
color: var(--color, #f5f5f5);
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
* {
box-sizing: border-box;
}
/* Motion — a single fade-up entrance (matches the hanzo.ai marketing feel:
~0.4s ease-out, small upward travel, staggered by the consumer). One place
defines it; <FadeIn> applies the class + per-item delay. */
@keyframes hz-fade-up {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.hz-fade-up {
animation: hz-fade-up 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
will-change: transform, opacity;
}
/* Honor the user's reduced-motion preference — no entrance animation. */
@media (prefers-reduced-motion: reduce) {
.hz-fade-up {
animation: none;
}
}
/* Shell chrome motion — the sidebar collapse (width) and the Linear-style
two-level nav slide (transform). One place defines the easing; the shell
applies the class. `className` forwards to the underlying DOM node on web, so
the browser transitions the Gui-driven inline width/transform. */
.hz-collapse {
transition: width 220ms cubic-bezier(0.16, 1, 0.3, 1);
will-change: width;
}
.hz-slide {
transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
.hz-collapse,
.hz-slide {
transition: none;
}
}
+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 67 67" role="img" aria-label="Hanzo">
<style>path{fill:#000}@media (prefers-color-scheme:dark){path{fill:#fff}}</style>
<path d="M22.21 67V44.6369H0V67H22.21Z"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z"/>
<path d="M22.21 0H0V22.3184H22.21V0Z"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z"/>
</svg>

After

Width:  |  Height:  |  Size: 443 B

+70
View File
@@ -0,0 +1,70 @@
/**
* Cloud API key management — the per-user `hk-` credential, minted server-side.
*
* Same-origin (`/keys`): the browser sends only its first-party session cookie;
* this server handler resolves the user from that cookie and calls IAM as the
* confidential `hanzo-console` client on the user's behalf. The `hk-` secret is
* returned to the browser ONLY at creation (POST) — standard show-once handling —
* and is otherwise never echoed. Authorization is the user's own session: a
* caller can only ever mint/revoke their OWN key (IAM binds the id to the
* resolved session), never another tenant's.
*
* POST /keys → mint (or rotate) the key; returns { accessKey } once.
* DELETE /keys → revoke the key (the old key stops working).
* GET /keys → whether a key exists (no secret material).
*/
import { type NextRequest, NextResponse } from 'next/server'
import { resolveUser, mintUserKey, revokeUserKey, mintConfigured } from '~/lib/server/identity'
export const runtime = 'nodejs'
const unauthorized = () =>
NextResponse.json({ error: 'Sign in to manage API keys.' }, { status: 401 })
const notConfigured = () =>
NextResponse.json(
{ error: 'API key minting is not configured on this deployment (IAM client unset).' },
{ status: 501 },
)
export async function GET(req: NextRequest): Promise<NextResponse> {
const user = await resolveUser(req)
if (!user) return unauthorized()
// No secret material on GET — only whether a key exists and its public prefix.
const hasKey = Boolean(user.accessKey)
return NextResponse.json({
hasKey,
keyPrefix: hasKey ? user.accessKey.slice(0, 11) : '',
})
}
export async function POST(req: NextRequest): Promise<NextResponse> {
const user = await resolveUser(req)
if (!user) return unauthorized()
if (!mintConfigured()) return notConfigured()
try {
const accessKey = await mintUserKey(user)
return NextResponse.json({ accessKey })
} catch (e) {
return NextResponse.json(
{ error: `Could not mint an API key: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
}
export async function DELETE(req: NextRequest): Promise<NextResponse> {
const user = await resolveUser(req)
if (!user) return unauthorized()
if (!mintConfigured()) return notConfigured()
try {
await revokeUserKey(user)
return NextResponse.json({ ok: true })
} catch (e) {
return NextResponse.json(
{ error: `Could not revoke the API key: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
}
+29
View File
@@ -0,0 +1,29 @@
import '@hanzogui/core/reset.css'
import './globals.css'
import type { Metadata, Viewport } from 'next'
import type { ReactNode } from 'react'
import { Provider } from '~/components/Provider'
import { ChunkGuard } from '~/components/ChunkGuard'
import { branding } from '~/config'
export const metadata: Metadata = {
title: branding.name,
description: 'Unified admin console for Hanzo Cloud and all cloud products.',
}
export const viewport: Viewport = {
themeColor: '#0a0a0a',
}
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" className="t_dark" style={{ backgroundColor: '#0a0a0a', colorScheme: 'dark' }} suppressHydrationWarning>
<body style={{ margin: 0 }}>
<ChunkGuard />
<Provider>{children}</Provider>
</body>
</html>
)
}
+130
View File
@@ -0,0 +1,130 @@
/**
* Org onboarding — create the signed-in user's organization, server-side.
*
* Same-origin (`/onboard`): the browser sends only its first-party session
* cookie; this handler resolves the user from that cookie and acts as the
* confidential `hanzo-console` client (allowlisted for IAM_ORG_ADMIN_APPS +
* IAM_USER_ADMIN_APPS). It creates a customer organization and makes the user
* that org's admin (IAM membership = the user's `owner`, so the cloud's
* GetEffectiveOrg scopes everything to the new org once the user re-authenticates).
*
* Fail-closed + safe:
* - 401 with no session; 501 when the IAM client is unwired.
* - ONLY a zero-org user may onboard (an existing member would have to be MOVED
* out of their current org, orphaning its data) — anyone who already belongs
* to an org gets 409 and uses the OrgSwitcher instead.
* - reserved names (brand/staff + system orgs) are refused (pure policy).
*
* POST /onboard { name } → create + join org `slugify(name)`.
* POST /onboard { personal: true } → create + join a personal `<username>` org.
* Returns { org, displayName }; the client then re-auths so the new JWT carries
* the new owner.
*/
import { type NextRequest, NextResponse } from 'next/server'
import {
resolveAuthenticatedUser,
mintConfigured,
getOrganization,
createOrganization,
moveUserToOrg,
} from '~/lib/server/identity'
import {
isReservedOrg,
personalOrgSlug,
slugifyOrg,
validateOrgName,
MAX_ORG_SLUG,
MIN_ORG_SLUG,
} from '~/lib/server/onboarding'
export const runtime = 'nodejs'
/** Title-case the base of a username for a personal org's display name. */
function humanize(username: string): string {
const base = (username.includes('@') ? username.slice(0, username.indexOf('@')) : username)
.replace(/[._-]+/g, ' ')
.trim()
return base ? base.replace(/\b\w/g, (c) => c.toUpperCase()) : 'Personal'
}
/** First free slug at/after `base` (`base`, `base-2`, …); null if all taken. */
async function freeSlug(base: string): Promise<string | null> {
for (let i = 1; i <= 20; i++) {
const candidate = i === 1 ? base : `${base.slice(0, MAX_ORG_SLUG - 3)}-${i}`.replace(/-+/g, '-')
if (candidate.length < MIN_ORG_SLUG || isReservedOrg(candidate)) continue
if (!(await getOrganization(candidate))) return candidate
}
return null
}
export async function POST(req: NextRequest): Promise<NextResponse> {
const user = await resolveAuthenticatedUser(req)
if (!user) return NextResponse.json({ error: 'Sign in to create an organization.' }, { status: 401 })
if (!mintConfigured()) {
return NextResponse.json(
{ error: 'Organization creation is not configured on this deployment (IAM client unset).' },
{ status: 501 },
)
}
const body = (await req.json().catch(() => ({}))) as { name?: string; personal?: boolean }
const personal = body.personal === true
// Two flows, keyed on whether the user already has a home org:
// - FIRST-RUN (no owner): create + MOVE the user in as admin, so their JWT
// carries the new owner and the cloud scopes everything to it.
// - ADDITIONAL (owner set): create the org but do NOT move the user — a move
// changes their IAM `owner` (stripping a global admin's status and orphaning
// their current org). They reach the new org via the OrgSwitcher, which
// re-scopes X-Org-Id without touching IAM membership. A personal-org request
// from someone who already has an org is meaningless, so refuse it.
const additional = Boolean(user.owner)
if (additional && personal) {
return NextResponse.json(
{ error: 'You already have an organization. Name the new one explicitly.' },
{ status: 409 },
)
}
let baseSlug: string
let displayName: string
if (personal) {
baseSlug = personalOrgSlug(user.name) || slugifyOrg(user.email)
if (!baseSlug || baseSlug.length < MIN_ORG_SLUG || isReservedOrg(baseSlug)) {
baseSlug = `org-${slugifyOrg(user.name) || 'workspace'}`
}
displayName = humanize(user.name || user.email)
} else {
const v = validateOrgName(body.name ?? '')
if (!v.ok) return NextResponse.json({ error: v.error }, { status: 400 })
baseSlug = v.slug
displayName = (body.name ?? '').trim()
}
// Personal orgs auto-suffix to stay unique; an explicit name that's taken is an
// honest conflict the user resolves by choosing another.
let slug = baseSlug
if (await getOrganization(slug)) {
if (!personal) {
return NextResponse.json({ error: `${slug}” is taken. Choose a different name.` }, { status: 409 })
}
const free = await freeSlug(baseSlug)
if (!free) return NextResponse.json({ error: 'Could not find an available name.' }, { status: 409 })
slug = free
}
try {
await createOrganization({ name: slug, displayName, personal, sourceOwner: user.owner })
// First-run only: make the zero-org user this org's admin. For an additional
// org we deliberately do NOT move them (preserve their owner/admin + old org).
if (!additional) await moveUserToOrg(user, slug)
} catch (e) {
return NextResponse.json(
{ error: `Could not create the organization: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
return NextResponse.json({ org: slug, displayName, additional })
}
+56
View File
@@ -0,0 +1,56 @@
/**
* Server-gated SELF-SERVICE org member proxy — a CUSTOMER managing their OWN org.
*
* The `/admin/iam` proxy is GLOBAL-admin only, so a tenant org owner (e.g.
* Dave/maxpower) could never manage their own members through it. This proxy
* closes that: it admits ANY authenticated user with an org (`getOrgGate`), then
* the shared `forwardIam`:
* - scopes every reference (query `owner`, `id` owner, and the mutation BODY
* owner) to the caller's OWN org — a global admin may cross, a customer never;
* - guards get-organization by org NAME (no reading another org's settings);
* - requires an ORG ADMIN for writes (invite / change-role / remove), while any
* member may READ the roster.
* IAM enforces its own checks on the user-bound bearer too — this is the matching,
* fail-closed server gate, not the only one.
*/
import { type NextRequest, NextResponse } from 'next/server'
import { getOrgGate } from '~/lib/server/identity'
import { forwardIam } from '~/lib/server/iam-proxy'
export const runtime = 'nodejs'
/** Reads — any member of the org (own org only, unless global). */
const GET_SEGMENTS = new Set(['get-users', 'get-user', 'get-roles', 'get-organization'])
/** Writes — org admin only, own org only (unless global). */
const POST_SEGMENTS = new Set(['add-user', 'update-user', 'delete-user'])
/** get-organization's owner is the `admin` metadata org (name guarded separately). */
const ORG_META = new Set(['get-organization'])
/** Segments carrying an org NAME to pin to the caller's scope. */
const ORG_NAME = new Set(['get-organization'])
const forbidden = () => NextResponse.json({ error: 'forbidden' }, { status: 403 })
async function handle(req: NextRequest, path: string[], method: 'GET' | 'POST'): Promise<NextResponse> {
const gate = await getOrgGate(req)
if (!gate) return forbidden()
return forwardIam(req, gate, {
segment: path.join('/'),
method,
allowed: method === 'GET' ? GET_SEGMENTS : POST_SEGMENTS,
orgMetaSegments: ORG_META,
orgNameSegments: ORG_NAME,
requireAdminForWrite: true,
})
}
type Ctx = { params: Promise<{ path: string[] }> }
export async function GET(req: NextRequest, ctx: Ctx) {
return handle(req, (await ctx.params).path, 'GET')
}
export async function POST(req: NextRequest, ctx: Ctx) {
return handle(req, (await ctx.params).path, 'POST')
}
+106
View File
@@ -0,0 +1,106 @@
/**
* Same-origin proxy to the platform.hanzo.ai control plane (Job 3 — embedded
* PaaS). The browser calls console2's OWN origin (`/paas/...`); this server-side
* handler forwards to `platform.hanzo.ai/v1/...`, injecting the service token
* from server-only env (sourced via KMS — never `NEXT_PUBLIC_`, never in the
* browser bundle). This is the real control-plane API, not an iframe stub.
*
* SECURITY: the forwarded token is a PLATFORM SERVICE token — full control-plane
* authority, NOT tenant-scoped. So this route is gated to brand admins exactly
* like the IAM/KMS admin proxies: `getAdminGate` resolves the caller from their
* own session and requires a verified brand-admin (no gate → 403). Without this,
* any authenticated browser could drive the whole control plane through the
* service token. The gate is the control, NOT a deploy-time env toggle.
*
* When `PAAS_SERVICE_TOKEN` is unset the proxy returns an honest 501 so the UI
* can show a truthful "not configured" state — it never fabricates apps/deploys.
*
* SCOPE: the browser stamps the active tenant path (X-Org-Id / X-Project-Id /
* X-Environment) on every call. We forward it to the control plane so PaaS
* resources scope by org → project → environment like the rest of the console —
* but the ORG is re-resolved server-side through the admin policy (`orgFor`): a
* global admin's switched org is honored, a brand admin is PINNED to their own,
* so the forwarded X-Org-Id is authoritative and never the spoofable claim.
* Project + environment are sub-scopes the admin picks WITHIN that org, passed
* through verbatim.
*/
import { type NextRequest, NextResponse } from 'next/server'
import { getAdminGate } from '~/lib/server/identity'
import { orgFor as policyOrgFor } from '~/lib/server/admin-policy'
export const runtime = 'nodejs'
const PLATFORM_URL = (process.env.PLATFORM_URL ?? 'https://platform.hanzo.ai').replace(/\/+$/, '')
const TOKEN = process.env.PAAS_SERVICE_TOKEN ?? ''
async function forward(req: NextRequest, path: string[]): Promise<NextResponse> {
// Brand-admin gate FIRST — the service token below is control-plane god-mode.
const gate = await getAdminGate(req)
if (!gate) {
return NextResponse.json({ error: 'forbidden' }, { status: 403 })
}
if (!TOKEN) {
return NextResponse.json(
{ error: 'PaaS control plane is not configured (PAAS_SERVICE_TOKEN missing).' },
{ status: 501 },
)
}
// Resolve the authoritative tenant path. Org: the admin policy honors a global
// admin's switched org (the X-Org-Id the browser sends = currentOrg()) and pins
// a brand admin to their own — so we forward the resolved org, never the raw
// claim. Project + environment are sub-scopes within that org, forwarded as-is.
const org = policyOrgFor(
{ isGlobalAdmin: gate.user.isGlobalAdmin, orgScope: gate.orgScope },
req.headers.get('X-Org-Id'),
)
const projectId = req.headers.get('X-Project-Id')
const environment = req.headers.get('X-Environment')
const search = req.nextUrl.search
const url = `${PLATFORM_URL}/v1/${path.join('/')}${search}`
const init: RequestInit = {
method: req.method,
headers: {
Authorization: `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
Accept: 'application/json',
'X-Org-Id': org,
...(projectId ? { 'X-Project-Id': projectId } : {}),
...(environment ? { 'X-Environment': environment } : {}),
},
// Never cache control-plane reads.
cache: 'no-store',
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
init.body = await req.text()
}
try {
const res = await fetch(url, init)
const text = await res.text()
return new NextResponse(text, {
status: res.status,
headers: { 'Content-Type': res.headers.get('content-type') ?? 'application/json' },
})
} catch (e) {
return NextResponse.json(
{ error: `PaaS upstream unreachable: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
}
type Ctx = { params: Promise<{ path: string[] }> }
export async function GET(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
export async function POST(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
export async function PATCH(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
export async function DELETE(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
+18
View File
@@ -0,0 +1,18 @@
'use client'
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { SignInForm } from '~/components/SignInForm'
import { useSession } from '~/lib/auth/session'
export default function SignInPage() {
const { account, loading } = useSession()
const router = useRouter()
useEffect(() => {
if (!loading && account) router.replace('/')
}, [loading, account, router])
return <SignInForm />
}
+114
View File
@@ -0,0 +1,114 @@
/**
* Per-user proxy to the Hanzo Base control plane (base.hanzo.ai) — the embedded
* Base module's ONE transport. The browser calls console2's OWN origin
* (`/superbase/v1/...`) with just the session cookie; this server handler
* resolves the user, mints a SHORT-LIVED user-bound IAM token, and forwards to
* base.hanzo.ai with that token. No token ever reaches the browser, and the
* SAME @hanzo/superbase-dashboard screens render here and standalone.
*
* NOT the PaaS pattern: PaaS forwards a god-mode SERVICE token and is gated to
* brand admins. Base authorizes PER USER itself — the `tenants` collection's
* `ListRule = "owner_iam_user = @request.auth.id"` and admin-only mutations are
* enforced by Base against the forwarded user identity. So here we forward the
* USER's own minted bearer (least privilege, tenant-scoped by Base), and the
* only gate is "must be signed in" (resolveUser → 401). A non-admin simply sees
* their own tenants and gets Base's 403 on a mutation — honest, not faked.
*
* Least privilege on the path too: only the `tenants` collection records surface
* is proxied; anything else 404s, so this is not a general Base tunnel.
*/
import { type NextRequest, NextResponse } from 'next/server'
import { resolveUser, issueUserToken, type SessionUser } from '~/lib/server/identity'
export const runtime = 'nodejs'
const trim = (s: string) => s.replace(/\/+$/, '')
/** The Base control plane the proxied calls are forwarded to. */
const BASE_URL = trim(process.env.BASE_DASHBOARD_URL ?? 'https://base.hanzo.ai')
/** Only the tenants collection records surface (list/get/create/update/delete). */
const RECORDS = 'v1/collections/tenants/records'
function allowed(rel: string): boolean {
return rel === RECORDS || rel.startsWith(`${RECORDS}/`)
}
// ── Short-lived user-token cache (same shape as the AI/admin proxies) ─────────
type CachedToken = { token: string; expMs: number }
const tokenCache = new Map<string, CachedToken>()
const SKEW_MS = 60_000
const FALLBACK_TTL_MS = 5 * 60_000
async function tokenFor(user: SessionUser): Promise<string> {
const hit = tokenCache.get(user.id)
if (hit && hit.expMs > Date.now()) return hit.token
const { accessToken, expiresIn } = await issueUserToken(user)
const ttl = expiresIn > 0 ? expiresIn * 1000 : FALLBACK_TTL_MS
tokenCache.set(user.id, { token: accessToken, expMs: Date.now() + ttl - SKEW_MS })
return accessToken
}
async function forward(req: NextRequest, path: string[]): Promise<NextResponse> {
const rel = path.join('/')
if (!allowed(rel)) {
return NextResponse.json({ error: 'not found' }, { status: 404 })
}
const user = await resolveUser(req)
if (!user) {
return NextResponse.json({ error: 'Sign in to manage Base tenants.' }, { status: 401 })
}
let token: string
try {
token = await tokenFor(user)
} catch (e) {
return NextResponse.json(
{ error: `Could not authorize the request: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
const url = `${BASE_URL}/${rel}${req.nextUrl.search}`
const init: RequestInit = {
method: req.method,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
cache: 'no-store',
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
init.body = await req.text()
}
try {
const res = await fetch(url, init)
const text = await res.text()
return new NextResponse(text, {
status: res.status,
headers: { 'Content-Type': res.headers.get('content-type') ?? 'application/json' },
})
} catch (e) {
return NextResponse.json(
{ error: `Base control plane unreachable: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
}
type Ctx = { params: Promise<{ path: string[] }> }
export async function GET(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
export async function POST(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
export async function PATCH(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
export async function DELETE(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
+88
View File
@@ -0,0 +1,88 @@
/**
* Same-origin proxy to the durable task engine (hanzoai/tasks `tasksd`, the native
* Temporal-style HTTP surface at `/v1/tasks/*`).
*
* `tasksd` runs `TASKSD_REQUIRE_IDENTITY=true`: it validates an IAM **Bearer JWT**
* against the IAM JWKS, unconditionally STRIPS inbound `X-Org-Id`, and mints org +
* user from the JWT claims (`owner`→org). So — like the `/ai` proxy — the console
* calls its OWN origin (`/tasksd/...`) with just the session cookie; this server
* handler resolves the signed-in user, mints a SHORT-LIVED, user-bound IAM token
* (cached until just before expiry), and forwards it as the Bearer. No key in the
* browser, and every read is org-scoped by the JWT server-side.
*
* READ-ONLY: only GET is proxied (the console never mutates workflows here), so
* this is a scoped read tunnel to `/v1/tasks/*`, nothing else. When the engine is
* unreachable (the public `tasks.hanzo.ai` TLS is not yet provisioned; the real
* path is the in-cluster service) the fetch fails and the UI shows an honest
* BackendStateCard — never fabricated workflows.
*/
import { type NextRequest, NextResponse } from 'next/server'
import { resolveUser, issueUserToken, type SessionUser } from '~/lib/server/identity'
export const runtime = 'nodejs'
const trim = (s: string) => s.replace(/\/+$/, '')
/** The durable task engine. Public TLS is not live yet → default to the in-cluster
* service (tasks.hanzo.svc:80 → tasksd :7243). Override with TASKS_URL. */
const TASKS_URL = trim(process.env.TASKS_URL ?? 'http://tasks.hanzo.svc.cluster.local')
// ── Short-lived user-token cache (mirrors the /ai proxy) ─────────────────────
type CachedToken = { token: string; expMs: number }
const tokenCache = new Map<string, CachedToken>()
const SKEW_MS = 60_000
const FALLBACK_TTL_MS = 5 * 60_000
async function tokenFor(user: SessionUser): Promise<string> {
const hit = tokenCache.get(user.id)
if (hit && hit.expMs > Date.now()) return hit.token
const { accessToken, expiresIn } = await issueUserToken(user)
const ttl = expiresIn > 0 ? expiresIn * 1000 : FALLBACK_TTL_MS
tokenCache.set(user.id, { token: accessToken, expMs: Date.now() + ttl - SKEW_MS })
return accessToken
}
export async function GET(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }): Promise<NextResponse> {
const rel = (await ctx.params).path.join('/')
const user = await resolveUser(req)
if (!user) {
return NextResponse.json({ error: 'Sign in to view tasks.', code: 401 }, { status: 401 })
}
let token: string
try {
token = await tokenFor(user)
} catch (e) {
return NextResponse.json(
{ error: `Could not authorize the request: ${e instanceof Error ? e.message : String(e)}`, code: 502 },
{ status: 502 },
)
}
const url = `${TASKS_URL}/v1/tasks/${rel}${req.nextUrl.search}`
try {
const res = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
// tasksd mints org from the JWT and strips this, but send the active org so
// it is correct in any topology (a gateway that re-injects overwrites it).
'X-Org-Id': user.owner,
},
cache: 'no-store',
signal: req.signal,
})
const text = await res.text()
return new NextResponse(text, {
status: res.status,
headers: { 'Content-Type': res.headers.get('content-type') ?? 'application/json' },
})
} catch (e) {
return NextResponse.json(
{ error: `Task engine unreachable: ${e instanceof Error ? e.message : String(e)}`, code: 502 },
{ status: 502 },
)
}
}
+105
View File
@@ -0,0 +1,105 @@
/**
* Same-origin proxy to the cloud ML/training surface on hanzoai/ai (`/v1/train/*`,
* `/v1/ml/models`, and the fine-tuning broker `/v1/finetune/*`).
*
* The console's Training page calls its OWN origin (`/training/...`) with just the
* first-party session cookie; this server handler resolves the signed-in user from
* that cookie and forwards to the cloud backend's `/v1/...` surface, passing the
* cookie through (the proven `get-account` server-to-server pattern in
* lib/server/identity.ts) plus the active `X-Org-Id`. Training is a TENANT action —
* any signed-in org user may run it — so this is user-scoped (resolveUser), NOT the
* control-plane admin gate the `/paas` proxy uses. The cloud backend scopes by org
* (GetEffectiveOrg / the X-Org-Id the plain-REST train sub-service requires), so a
* caller can only ever touch their own org's jobs. `POST /v1/train/jobs` is
* billing-gated by the live ResourceMeter and returns 402 on an unfunded org — that
* status flows straight back so the UI can surface it honestly.
*
* Least privilege: only the explicit ML/training sub-paths are forwarded; anything
* else 404s, so this is not a general backend tunnel. No secret ever reaches the
* browser — the HuggingFace token (for private repos) is resolved from KMS
* server-side inside the broker, never here.
*/
import { type NextRequest, NextResponse } from 'next/server'
import { resolveUser } from '~/lib/server/identity'
export const runtime = 'nodejs'
const trim = (s: string) => s.replace(/\/+$/, '')
/** Cloud `/v1` backend (hanzoai/ai) — same target lib/server/identity.ts resolves. */
const CLOUD_API_URL = trim(process.env.CLOUD_API_URL ?? 'http://cloud-api.hanzo.svc.cluster.local:8000')
/** The exact `/v1/<...>` ML/training sub-paths the console is allowed to reach. */
const ALLOWED = new Set([
// mlsvc — the canonical training surface (task #40 ResourceMeter gates POST jobs).
'train/jobs',
'train/experiments',
'ml/models',
// fine-tuning broker (custom-data runs, HF search) — sibling surface.
'finetune/jobs',
'finetune/job',
'finetune/cancel',
'finetune/deploy',
'finetune/presets',
'finetune/hf/models',
'finetune/hf/datasets',
'finetune/hf/repo',
])
async function forward(req: NextRequest, path: string[]): Promise<NextResponse> {
const rel = path.join('/')
if (!ALLOWED.has(rel)) {
return NextResponse.json({ status: 'error', msg: 'Not found' }, { status: 404 })
}
const user = await resolveUser(req)
if (!user) {
return NextResponse.json(
{ status: 'error', msg: 'Sign in to manage training.' },
{ status: 401 },
)
}
const cookie = req.headers.get('cookie') ?? ''
const url = `${CLOUD_API_URL}/v1/${rel}${req.nextUrl.search}`
const headers: Record<string, string> = {
cookie,
Accept: 'application/json',
'Content-Type': 'application/json',
// The backend re-validates org from the session; forward the active scope so a
// global admin's switched org is honored and a brand admin is pinned to theirs.
'X-Org-Id': req.headers.get('X-Org-Id') ?? user.owner,
}
const projectId = req.headers.get('X-Project-Id')
const environment = req.headers.get('X-Environment')
if (projectId) headers['X-Project-Id'] = projectId
if (environment) headers['X-Environment'] = environment
const init: RequestInit = { method: req.method, headers, cache: 'no-store' }
if (req.method !== 'GET' && req.method !== 'HEAD') {
init.body = await req.text()
}
try {
const res = await fetch(url, init)
const text = await res.text()
return new NextResponse(text, {
status: res.status,
headers: { 'Content-Type': res.headers.get('content-type') ?? 'application/json' },
})
} catch (e) {
return NextResponse.json(
{ status: 'error', msg: `Fine-tuning backend unreachable: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
}
type Ctx = { params: Promise<{ path: string[] }> }
export async function GET(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
export async function POST(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
+72
View File
@@ -0,0 +1,72 @@
/**
* Waitlist join — the "Join waitlist" CTA on coming-soon products, server-side.
*
* Same-origin (`/waitlist`): the browser posts only its session cookie + the
* waitlist slug and email; this handler forwards to the Hanzo Base waitlist
* plugin (`POST /v1/waitlist/join`, the hanzoai/waitlist pattern — a per-product
* SQLite-backed list). The backend URL is server-only env `WAITLIST_URL` (never
* NEXT_PUBLIC_); when it is unset the route returns an honest 501 and the form
* says the waitlist isn't open yet — no fake confirmation.
*
* The console UI is auth-gated, so we require a session (a signed-in user) to
* keep this proxy from being an open relay to the waitlist backend, and forward
* the client IP so the backend's per-IP rate limit still applies.
*
* POST /waitlist { waitlist, email } → { ok, rank, total, alreadyJoined, … }
*/
import { type NextRequest, NextResponse } from 'next/server'
import { resolveUser } from '~/lib/server/identity'
export const runtime = 'nodejs'
const WAITLIST_URL = (process.env.WAITLIST_URL ?? '').replace(/\/+$/, '')
/** Minimal email sanity check — the backend is authoritative (disposable, etc.). */
const looksLikeEmail = (s: string): boolean => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(s)
export async function POST(req: NextRequest): Promise<NextResponse> {
const user = await resolveUser(req)
if (!user) return NextResponse.json({ error: 'Sign in to join the waitlist.' }, { status: 401 })
if (!WAITLIST_URL) {
return NextResponse.json(
{ error: 'The waitlist is not open on this deployment yet.' },
{ status: 501 },
)
}
const body = (await req.json().catch(() => ({}))) as { waitlist?: string; email?: string }
const waitlist = (body.waitlist ?? '').trim()
const email = (body.email ?? user.email ?? '').trim()
if (!waitlist) return NextResponse.json({ error: 'Missing waitlist.' }, { status: 400 })
if (!looksLikeEmail(email)) return NextResponse.json({ error: 'Enter a valid email.' }, { status: 400 })
const fwd =
req.headers.get('x-forwarded-for') ?? req.headers.get('x-real-ip') ?? ''
let res: Response
try {
res = await fetch(`${WAITLIST_URL}/v1/waitlist/join`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...(fwd ? { 'X-Forwarded-For': fwd } : {}),
},
body: JSON.stringify({ waitlist, email }),
cache: 'no-store',
})
} catch (e) {
return NextResponse.json(
{ error: `Could not reach the waitlist: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
const text = await res.text()
return new NextResponse(text, {
status: res.status,
headers: { 'Content-Type': res.headers.get('content-type') ?? 'application/json' },
})
}
-13
View File
@@ -1,13 +0,0 @@
_We are Hiring_
Join us in scaling Langfuse in Berlin, Germany. We are an open source company, we hire in person, we are only hiring technical talent.
_Open Roles_
- Product Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
- Developer Advocate, 60-110k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
_More Info_
- https://langfuse.com/careers
- https://langfuse.com/docs
- https://langfuse.com/changelog
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

-88
View File
@@ -1,88 +0,0 @@
version: "3.5"
services:
server:
build:
dockerfile: ./web/Dockerfile
context: .
args:
- NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
depends_on:
- db
- redis
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 # generate via `openssl rand -hex 32`
- NEXTAUTH_URL=http://localhost:3000
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
- LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-}
- LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-}
- LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-}
- LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-}
- LANGFUSE_INIT_PROJECT_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
- LANGFUSE_INIT_PROJECT_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
- LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-}
- LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-}
- LANGFUSE_INIT_USER_PASSWORD=${LANGFUSE_INIT_USER_PASSWORD:-}
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/public/health"]
interval: 30s
timeout: 10s
retries: 3
worker:
build:
dockerfile: ./worker/Dockerfile
context: .
args:
- NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
depends_on:
- db
- redis
ports:
- "3030:3030"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
- PORT=${PORT:-3030}
- REDIS_HOST=${REDIS_HOST:-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_AUTH=${REDIS_AUTH:-myredissecret}
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3030/api/health"]
interval: 30s
timeout: 10s
retries: 3
redis:
image: redis:7.2.4
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
restart: always
ports:
- 6379:6379
db:
image: postgres
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
ports:
- 5432:5432
volumes:
- database_data:/var/lib/postgresql/data
volumes:
database_data:
driver: local
-63
View File
@@ -1,63 +0,0 @@
services:
clickhouse:
image: clickhouse/clickhouse-server
user: "101:101"
container_name: clickhouse
hostname: clickhouse
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse
volumes:
- langfuse_clickhouse_data:/var/lib/clickhouse
- langfuse_clickhouse_logs:/var/log/clickhouse-server
ports:
- "8123:8123"
- "9000:9000"
depends_on:
- postgres
azurite:
image: mcr.microsoft.com/azure-storage/azurite
container_name: azurite
command: azurite-blob --blobHost 0.0.0.0
ports:
- "10000:10000"
volumes:
- langfuse_azurite_data:/data
redis:
image: redis:7.2.4
restart: always
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
ports:
- 6379:6379
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
timeout: 3s
retries: 10
command: ["postgres", "-c", "log_statement=all"]
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
ports:
- 5432:5432
volumes:
- langfuse_postgres_data:/var/lib/postgresql/data
volumes:
langfuse_postgres_data:
driver: local
langfuse_clickhouse_data:
driver: local
langfuse_clickhouse_logs:
driver: local
langfuse_azurite_data:
driver: local
-75
View File
@@ -1,75 +0,0 @@
services:
clickhouse:
image: clickhouse/clickhouse-server
user: "101:101"
container_name: clickhouse
hostname: clickhouse
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse
volumes:
- langfuse_clickhouse_data:/var/lib/clickhouse
- langfuse_clickhouse_logs:/var/log/clickhouse-server
ports:
- "8123:8123"
- "9000:9000"
depends_on:
- postgres
minio:
image: minio/minio
container_name: minio
entrypoint: sh
# create the 'langfuse' bucket before starting the service
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
environment:
MINIO_ACCESS_KEY: minio
MINIO_SECRET_KEY: miniosecret
ports:
- "9090:9000"
- "9091:9001"
volumes:
- langfuse_minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 1s
timeout: 5s
retries: 5
start_period: 1s
redis:
image: redis:7.2.4
restart: always
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
ports:
- 6379:6379
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
timeout: 3s
retries: 10
command: ["postgres", "-c", "log_statement=all"]
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
ports:
- 5432:5432
volumes:
- langfuse_postgres_data:/var/lib/postgresql/data
volumes:
langfuse_postgres_data:
driver: local
langfuse_clickhouse_data:
driver: local
langfuse_clickhouse_logs:
driver: local
langfuse_minio_data:
driver: local
-145
View File
@@ -1,145 +0,0 @@
services:
langfuse-worker:
image: langfuse/langfuse-worker:latest
depends_on: &langfuse-depends-on
postgres:
condition: service_healthy
minio:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "3030:3030"
environment: &langfuse-worker-env
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
SALT: "mysalt"
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" # generate via `openssl rand -hex 32`
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true}
LANGFUSE_ASYNC_INGESTION_PROCESSING: ${LANGFUSE_ASYNC_INGESTION_PROCESSING:-true}
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING: ${LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING:-true}
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE: ${LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE:-true}
LANGFUSE_READ_FROM_POSTGRES_ONLY: ${LANGFUSE_READ_FROM_POSTGRES_ONLY:-false}
LANGFUSE_RETURN_FROM_CLICKHOUSE: ${LANGFUSE_RETURN_FROM_CLICKHOUSE:-true}
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
LANGFUSE_S3_EVENT_UPLOAD_ENABLED: ${LANGFUSE_S3_EVENT_UPLOAD_ENABLED:-true}
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-us-east-1}
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED: ${LANGFUSE_S3_MEDIA_UPLOAD_ENABLED:-true}
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: ${LANGFUSE_S3_MEDIA_UPLOAD_BUCKET:-langfuse}
LANGFUSE_S3_MEDIA_UPLOAD_REGION: ${LANGFUSE_S3_MEDIA_UPLOAD_REGION:-us-east-1}
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID:-minio}
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://minio:9000}
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true}
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: ${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/}
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
langfuse-web:
image: langfuse/langfuse:latest
depends_on: *langfuse-depends-on
ports:
- "3000:3000"
environment:
<<: *langfuse-worker-env
NEXTAUTH_URL: http://localhost:3000
NEXTAUTH_SECRET: mysecret
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
clickhouse:
image: clickhouse/clickhouse-server
user: "101:101"
container_name: clickhouse
hostname: clickhouse
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse
volumes:
- langfuse_clickhouse_data:/var/lib/clickhouse
- langfuse_clickhouse_logs:/var/log/clickhouse-server
ports:
- "8123:8123"
- "9000:9000"
depends_on:
- postgres
minio:
image: minio/minio
container_name: minio
entrypoint: sh
# create the 'langfuse' bucket before starting the service
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: miniosecret
ports:
- "9090:9000"
- "9091:9001"
volumes:
- langfuse_minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 1s
timeout: 5s
retries: 5
start_period: 1s
redis:
image: redis:7
restart: always
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
ports:
- 6379:6379
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 10s
retries: 10
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
timeout: 3s
retries: 10
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
volumes:
- langfuse_postgres_data:/var/lib/postgresql/data
volumes:
langfuse_postgres_data:
driver: local
langfuse_clickhouse_data:
driver: local
langfuse_clickhouse_logs:
driver: local
langfuse_minio_data:
driver: local
-46
View File
@@ -1,46 +0,0 @@
services:
langfuse-server:
image: langfuse/langfuse:2
depends_on:
db:
condition: service_healthy
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 # generate via `openssl rand -hex 32`
- NEXTAUTH_URL=http://localhost:3000
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
- LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-}
- LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-}
- LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-}
- LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-}
- LANGFUSE_INIT_PROJECT_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
- LANGFUSE_INIT_PROJECT_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
- LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-}
- LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-}
- LANGFUSE_INIT_USER_PASSWORD=${LANGFUSE_INIT_USER_PASSWORD:-}
db:
image: postgres
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
timeout: 3s
retries: 10
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
ports:
- 5432:5432
volumes:
- database_data:/var/lib/postgresql/data
volumes:
database_data:
driver: local
+74
View File
@@ -0,0 +1,74 @@
# Unified `/v1` backend endpoints
The console talks to the unified Hanzo Cloud backend (`hanzoai/cloud`, the
casibase API). Base URL: `${NEXT_PUBLIC_CLOUD_URL}/v1`. All requests send cookie
credentials; responses are the envelope `{ status, msg, data, data2 }` (`data2`
is the total row count on list endpoints).
Client modules live in `src/lib/api/`.
## Account / session — `AccountApi`
| Method | Endpoint |
| --- | --- |
| `current()` | `GET /get-account` |
| `signin(code, state)` | `POST /signin?code&state` |
| `signout()` | `POST /signout` |
## Providers — `ProviderApi`
| Method | Endpoint |
| --- | --- |
| `listGlobal()` | `GET /get-global-providers` |
| `list({ owner, store, p, pageSize, … })` | `GET /get-providers` |
| `get(owner, name)` | `GET /get-provider?id=owner/name` |
| `add(p)` | `POST /add-provider` |
| `update(owner, name, p)` | `POST /update-provider?id=owner/name` |
| `remove(p)` | `POST /delete-provider` |
| `refreshMcpTools(p)` | `POST /refresh-mcp-tools` |
## Model routes — `ModelRouteApi`
| Method | Endpoint |
| --- | --- |
| `list({ owner, … })` | `GET /get-model-routes` |
| `get(owner, modelName)` | `GET /get-model-route?owner&modelName` |
| `add(r)` | `POST /add-model-route` |
| `update(owner, modelName, r)` | `POST /update-model-route?owner&modelName` |
| `remove(r)` | `POST /delete-model-route` |
## Applications — `ApplicationApi`
| Method | Endpoint |
| --- | --- |
| `list({ owner, … })` | `GET /get-applications` |
| `get(owner, name)` | `GET /get-application?id=owner/name` |
| `add(a)` | `POST /add-application` |
| `update(owner, name, a)` | `POST /update-application?id=owner/name` |
| `remove(a)` | `POST /delete-application` |
| `deploy(a)` | `POST /deploy-application?id=owner/name` |
| `undeploy(owner, name)` | `POST /undeploy-application?id=owner/name` |
## Stores — `StoreApi`
| Method | Endpoint |
| --- | --- |
| `listGlobal()` | `GET /get-global-stores` |
| `list(owner)` | `GET /get-stores?owner` |
| `get(owner, name)` | `GET /get-store?id=owner/name` |
| `names(owner)` | `GET /get-store-names?owner` |
| `add(s)` | `POST /add-store` |
| `update(owner, name, s)` | `POST /update-store?id=owner/name` |
| `remove(s)` | `POST /delete-store` |
| `refreshVectors(s)` | `POST /refresh-store-vectors` |
## Chat — `ChatApi`
| Method | Endpoint |
| --- | --- |
| `listGlobal({ … })` | `GET /get-global-chats` |
| `list({ user, store, selectedUser, … })` | `GET /get-chats` |
| `get(owner, name)` | `GET /get-chat?id=owner/name` |
| `add(c)` | `POST /add-chat` |
| `update(owner, name, c)` | `POST /update-chat?id=owner/name` |
| `remove(c)` | `POST /delete-chat` |
+247
View File
@@ -0,0 +1,247 @@
/**
* e2e: Hanzo Cloud Console — login → API key → AI inference
*
* z@hanzo.ai is in the `hanzo` org (isGlobalAdmin), so the OrgGate now shows
* a dismissible admin banner and renders the full console on console.hanzo.ai.
* Admin ops still live at admin.hanzo.ai.
*
* Credentials (env, never in repo):
* HANZO_EMAIL default z@hanzo.ai
* HANZO_PASSWORD required
* HANZO_API_KEY optional; skip UI flow, go straight to inference
* HANZO_API_BASE default https://api.hanzo.ai
* BASE_URL default https://console.hanzo.ai
*
* Run:
* HANZO_PASSWORD=xxx pnpm e2e
* HANZO_PASSWORD=xxx HANZO_API_KEY=hk-xxx pnpm e2e
*/
import { test, expect, type Page } from '@playwright/test'
const EMAIL = process.env.HANZO_EMAIL ?? 'z@hanzo.ai'
const PASSWORD = process.env.HANZO_PASSWORD ?? ''
const API_KEY = process.env.HANZO_API_KEY ?? ''
const API_BASE = process.env.HANZO_API_BASE ?? 'https://api.hanzo.ai'
const BASE_URL = process.env.BASE_URL ?? 'https://console.hanzo.ai'
// ─── helpers ────────────────────────────────────────────────────────────────
async function signIn(page: Page) {
await page.goto(`${BASE_URL}/signin`)
await page.waitForSelector('input[placeholder="Email"]', { timeout: 20_000 })
await page.fill('input[placeholder="Email"]', EMAIL)
await page.fill('input[placeholder="Password"]', PASSWORD)
await page.click('button:has-text("Sign in")')
}
/** Wait until we're on the dashboard (route replaced to '/'). */
async function waitForDashboard(page: Page) {
const base = new URL(BASE_URL).origin
await page.waitForURL(url => url.origin === base && url.pathname === '/', { timeout: 30_000 })
// The dashboard renders product grid cards — wait for at least one visible card/link
await page.waitForLoadState('domcontentloaded')
}
// ─── tests ──────────────────────────────────────────────────────────────────
// Public smoke — needs no credentials, so it always runs (in CI, for Dave, etc.)
// and catches a dead/blank sign-in gate. The authenticated flows below gate on
// HANZO_PASSWORD.
test.describe('Hanzo Cloud Console — public', () => {
test('sign-in page renders (email/password + OAuth + passkey)', async ({ page }) => {
await page.goto(`${BASE_URL}/signin`)
await expect(page.locator('input[placeholder="Email"]')).toBeVisible({ timeout: 20_000 })
await expect(page.locator('input[placeholder="Password"]')).toBeVisible()
await expect(page.locator('button:has-text("Sign in")')).toBeVisible()
await expect(page.locator('button:has-text("Continue with GitHub")')).toBeVisible()
await expect(page.locator('button:has-text("Continue with Google")')).toBeVisible()
await expect(page.locator('text=/passkey/i')).toBeVisible()
})
})
test.describe('Hanzo Cloud Console e2e', () => {
test.skip(!PASSWORD, 'HANZO_PASSWORD not set — skipping live authenticated tests')
test('login as z@hanzo.ai — dashboard renders', async ({ page }) => {
await signIn(page)
await waitForDashboard(page)
// Dashboard renders — sign-in form must be gone
await expect(page.locator('input[placeholder="Password"]')).not.toBeVisible({ timeout: 10_000 })
// At least one product category or card is visible (Overview / AI / Compute etc.)
await expect(
page.locator('a, button, [role="link"]').filter({ hasText: /models|providers|overview|AI/i }).first()
).toBeVisible({ timeout: 15_000 })
console.log('✓ Signed in; dashboard is rendering')
})
test('admin banner visible (z is isAdmin on console.hanzo.ai)', async ({ page }) => {
await signIn(page)
await waitForDashboard(page)
// OrgGate shows the admin banner for admins on the non-admin console host.
// The banner may have been dismissed in a prior run (localStorage). Skip softly.
const banner = page.locator('text=/Admin ops|admin\\.hanzo\\.ai/i').first()
const visible = await banner.isVisible({ timeout: 5_000 }).catch(() => false)
if (visible) {
console.log('✓ Admin banner visible')
await expect(page.locator('button:has-text("Open admin")')).toBeVisible()
} else {
console.log(' Admin banner was dismissed (localStorage) — OK')
}
})
test('create or confirm API key', async ({ page }) => {
await signIn(page)
await waitForDashboard(page)
// API Keys module is at /api-keys (catch-all route, id='api-keys')
await page.goto(`${BASE_URL}/api-keys`, { waitUntil: 'domcontentloaded' })
// Wait for the module to hydrate — look for the page header or key cards
await expect(
page.locator('text=/API Keys/i, text=/Cloud API key/i, text=/Create.*API key/i').first()
).toBeVisible({ timeout: 25_000 })
const hasKey = page.locator('text=/Cloud API key/i')
const noKey = page.locator('text=/Create your Cloud API key/i')
const createBtn = page.locator('button:has-text("Create API key")')
const needsCreate = await noKey.isVisible({ timeout: 3_000 }).catch(() => false)
|| await createBtn.isVisible({ timeout: 1_000 }).catch(() => false)
if (needsCreate) {
await createBtn.click()
// One-time reveal card with the hk- key
await expect(page.locator('text=/hk-/')).toBeVisible({ timeout: 25_000 })
await expect(page.locator('text=/shown only once/i')).toBeVisible()
await expect(page.locator('button:has-text("Copy")')).toBeVisible()
console.log('✓ API key created (hk- one-time reveal shown)')
} else {
// Key already exists
await expect(hasKey).toBeVisible({ timeout: 10_000 })
await expect(page.locator('text=/hk-…|hk-[A-Za-z0-9]{3,}/i')).toBeVisible({ timeout: 5_000 })
console.log('✓ API key already exists (prefix shown)')
}
})
test('API key works — GET /v1/models', async ({ page }) => {
let apiKey = API_KEY
if (!apiKey) {
await signIn(page)
await waitForDashboard(page)
await page.goto(`${BASE_URL}/api-keys`, { waitUntil: 'domcontentloaded' })
await expect(
page.locator('text=/API Keys/i').first()
).toBeVisible({ timeout: 25_000 })
// Rotate (or create) to show the full key on-screen
const rotateBtn = page.locator('button:has-text("Rotate")')
const createBtn = page.locator('button:has-text("Create API key")')
if (await rotateBtn.isVisible({ timeout: 3_000 }).catch(() => false)) {
await rotateBtn.click()
} else if (await createBtn.isVisible({ timeout: 1_000 }).catch(() => false)) {
await createBtn.click()
}
await expect(page.locator('text=/hk-/')).toBeVisible({ timeout: 25_000 })
// Grab the key from the monospace reveal element
const keyEl = page.locator('[style*="monospace"]').filter({ hasText: /^hk-/ }).first()
apiKey = ((await keyEl.textContent()) ?? '').trim()
if (!apiKey.startsWith('hk-')) {
const m = ((await page.textContent('body')) ?? '').match(/hk-[A-Za-z0-9]{8,}/)
apiKey = m ? m[0] : ''
}
expect(apiKey, 'Could not extract hk- key from page').toMatch(/^hk-/)
console.log(`✓ Extracted key prefix: ${apiKey.slice(0, 11)}`)
}
// Verify the key works against api.hanzo.ai
const resp = await page.request.get(`${API_BASE}/v1/models`, {
headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' },
timeout: 30_000,
})
expect(resp.ok(), `GET /v1/models → ${resp.status()}`).toBe(true)
const json = await resp.json()
expect(json).toMatchObject({ data: expect.any(Array) })
const models: Array<{ id: string }> = json.data
expect(models.length).toBeGreaterThan(0)
const ids = models.map(m => m.id)
console.log(`✓ /v1/models: ${models.length} models`)
console.log(` GLM present: ${ids.some(id => id.includes('glm'))}`)
console.log(` Claude present: ${ids.some(id => id.includes('claude'))}`)
console.log(` DeepSeek present: ${ids.some(id => id.includes('deepseek'))}`)
console.log(` First 5: ${ids.slice(0, 5).join(', ')}`)
})
test('OpenAI inference — glm-5.2', async ({ page }) => {
test.skip(!API_KEY, 'Set HANZO_API_KEY to run inference tests')
const resp = await page.request.post(`${API_BASE}/v1/chat/completions`, {
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
data: {
model: 'glm-5.2',
messages: [{ role: 'user', content: 'Reply with only: PONG' }],
max_tokens: 16,
stream: false,
},
timeout: 30_000,
})
expect(resp.ok(), `glm-5.2 → ${resp.status()}`).toBe(true)
const json = await resp.json()
const text: string = json.choices?.[0]?.message?.content ?? ''
expect(text).toBeTruthy()
console.log(`✓ glm-5.2: "${text.trim()}"`)
})
test('OpenAI inference — deepseek-v4-pro', async ({ page }) => {
test.skip(!API_KEY, 'Set HANZO_API_KEY to run inference tests')
const resp = await page.request.post(`${API_BASE}/v1/chat/completions`, {
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
data: {
model: 'deepseek-v4-pro',
messages: [{ role: 'user', content: 'Reply with only: PONG' }],
max_tokens: 16,
stream: false,
},
timeout: 30_000,
})
expect(resp.ok(), `deepseek-v4-pro → ${resp.status()}`).toBe(true)
const json = await resp.json()
const text: string = json.choices?.[0]?.message?.content ?? ''
expect(text).toBeTruthy()
console.log(`✓ deepseek-v4-pro: "${text.trim()}"`)
})
test('Anthropic-compat /v1/messages — claude-sonnet-4-6', async ({ page }) => {
test.skip(!API_KEY, 'Set HANZO_API_KEY to run inference tests')
const resp = await page.request.post(`${API_BASE}/v1/messages`, {
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
Accept: 'application/json',
'anthropic-version': '2023-06-01',
},
data: {
model: 'claude-sonnet-4-6',
messages: [{ role: 'user', content: 'Reply with only: PONG' }],
max_tokens: 16,
},
timeout: 30_000,
})
expect(resp.ok(), `/v1/messages → ${resp.status()}`).toBe(true)
const json = await resp.json()
const text: string = json.content?.[0]?.text ?? ''
expect(text).toBeTruthy()
console.log(`✓ Anthropic-compat /v1/messages: "${text.trim()}"`)
})
})
-8
View File
@@ -1,8 +0,0 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
extends: ["@repo/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
-37
View File
@@ -1,37 +0,0 @@
Langfuse Enterprise License (the “Enterprise License”)
Copyright (c) 2024 Langfuse GmbH ('Langfuse')
With regard to the Langfuse Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the Langfuse Terms of Service, available
at https://langfuse.com/terms (the “Enterprise Terms”), or other
agreement governing the use of the Software, as agreed by you and Langfuse,
and otherwise have a valid Langfuse Enterprise license.
Subject to the foregoing sentence, you are free to
modify this Software and publish patches to the Software. You agree that Langfuse
and/or its licensors (as applicable) retain all right, title and interest in and
to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid Langfuse Enterprise license.
Notwithstanding the foregoing, you may copy and modify
the Software for development and testing purposes, without requiring a
subscription. You agree that Langfuse and/or its licensors (as applicable) retain
all right, title and interest in and to all such modifications. You are not
granted any other rights beyond what is expressly stated herein. Subject to the
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
For all third party components incorporated into the Langfuse Software, those
components are licensed under the original license provided by the owner of the
applicable component.
-5
View File
@@ -1,5 +0,0 @@
# Enterprise Edition
This folder includes features that are only available in the Enterprise Edition of Langfuse and on Langfuse Cloud.
See [LICENSE](../LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
-54
View File
@@ -1,54 +0,0 @@
{
"name": "@langfuse/ee",
"version": "1.0.0",
"private": true,
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"exports": {
".": {
"import": "./dist/src/index.js",
"require": "./dist/src/index.js"
},
"./sso": {
"import": "./dist/src/sso/index.js",
"require": "./dist/src/sso/index.js"
}
},
"engines": {
"node": "20"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
},
"dependencies": {
"@langfuse/shared": "workspace:*",
"@opentelemetry/api": ">=1.0.0 <1.10.0",
"axios": "^1.7.7",
"https-proxy-agent": "^7.0.6",
"next": "^14.2.21",
"next-auth": "^4.24.11",
"zod": "^3.23.8"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^20.11.29",
"@typescript-eslint/parser": "^7.12.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-prettier": "^5.1.3",
"prettier": "^3.3.3",
"ts-node": "^10.9.2",
"tsc-watch": "^6.2.0",
"typescript": "^5.4.5"
},
"pnpm": {
"overrides": {
"jsonpath-plus": "10.2.0"
}
}
}
-5
View File
@@ -1,5 +0,0 @@
import { env } from "../env";
export const isEeAvailable: boolean =
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ||
env.LANGFUSE_EE_LICENSE_KEY !== undefined;
-12
View File
@@ -1,12 +0,0 @@
import { z } from "zod";
import { env as sharedEnv, removeEmptyEnvVariables } from "@langfuse/shared";
const EnvSchema = z.object({
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z.string().optional(),
LANGFUSE_EE_LICENSE_KEY: z.string().optional(),
});
export const env = {
...sharedEnv,
...EnvSchema.parse(removeEmptyEnvVariables(process.env)),
};
View File
-14
View File
@@ -1,14 +0,0 @@
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"moduleResolution": "NodeNext",
"module": "NodeNext",
"lib": ["ES2020"],
"outDir": "./dist",
"types": ["node"],
"target": "ES2020",
"rootDir": "."
},
"include": ["."],
"exclude": ["node_modules", "dist"]
}
-11
View File
@@ -1,11 +0,0 @@
name: langfuse
error-discrimination:
strategy: status-code
auth: bearer
imports:
commons: commons.yml
errors:
- commons.Error
- commons.UnauthorizedError
- commons.AccessDeniedError
- commons.MethodNotAllowedError
-13
View File
@@ -1,13 +0,0 @@
errors:
Error:
status-code: 400
type: string
UnauthorizedError:
status-code: 401
type: string
AccessDeniedError:
status-code: 403
type: string
MethodNotAllowedError:
status-code: 405
type: string
-134
View File
@@ -1,134 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
service:
auth: true
base-path: /api/public
endpoints:
create:
docs: Add a score to the database, upserts on id
method: POST
path: /scores
request: CreateScoreRequest
types:
CreateScoreRequest:
properties:
id: optional<string>
traceId: string
name: string
value:
type: CreateScoreValue
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
observationId: optional<string>
comment: optional<string>
dataType:
type: optional<ScoreDataType>
docs: When set, must match the score value's type. If not set, will be inferred from the score value or config
configId:
type: optional<string>
docs: Reference a score config on a score. When set, the score name must equal the config name and scores must comply with the config's range and data type. For categorical scores, the value must map to a config category. Numeric scores might be constrained by the score config's max and min values
examples:
- value:
name: "novelty"
value: 0.9
traceId: "cdef-1234-5678-90ab"
- value:
name: "consistency"
value: 1.2
dataType: "NUMERIC"
traceId: "cdef-1234-5678-90ab"
- value:
name: "accuracy"
value: 0.9
dataType: "NUMERIC"
configId: "9203-4567-89ab-cdef"
traceId: "cdef-1234-5678-90ab"
- value:
name: "toxicity"
value: "not toxic"
traceId: "cdef-1234-5678-90ab"
- value:
name: "correctness"
value: "partially correct"
dataType: "CATEGORICAL"
configId: "1234-5678-90ab-cdef"
traceId: "cdef-1234-5678-90ab"
- value:
name: "hallucination"
value: 0
dataType: "BOOLEAN"
traceId: "cdef-1234-5678-90ab"
- value:
name: "helpfulness"
value: 1
dataType: "BOOLEAN"
configId: "1234-5678-90ab-cdef"
traceId: "cdef-1234-5678-90ab"
BaseScore:
properties:
id: string
traceId: string
name: string
source: ScoreSource
observationId: optional<string>
timestamp: datetime
createdAt: datetime
updatedAt: datetime
authorUserId: optional<string>
comment: optional<string>
configId:
type: optional<string>
docs: Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range
NumericScore:
extends: BaseScore
properties:
value:
type: double
docs: The numeric value of the score
BooleanScore:
extends: BaseScore
properties:
value:
type: double
docs: The numeric value of the score. Equals 1 for "True" and 0 for "False"
stringValue:
type: string
docs: The string representation of the score value. Is inferred from the numeric value and equals "True" or "False"
CategoricalScore:
extends: BaseScore
properties:
value:
type: optional<double>
docs: Only defined if a config is linked. Represents the numeric category mapping of the stringValue
stringValue:
type: string
docs: The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category
Score:
discriminant: "dataType"
union:
NUMERIC:
type: NumericScore
docs: "Score with NUMERIC data type"
CATEGORICAL:
type: CategoricalScore
docs: "Score with CATEGORICAL data type"
BOOLEAN:
type: BooleanScore
docs: "Score with BOOLEAN data type"
ScoreSource:
enum:
- ANNOTATION
- API
- EVAL
ScoreDataType:
enum:
- NUMERIC
- CATEGORICAL
- BOOLEAN
CreateScoreValue:
discriminated: false
union:
- double
- string
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
-21
View File
@@ -1,21 +0,0 @@
default-group: local
groups:
local:
generators:
# - name: fernapi/fern-typescript-browser-sdk
# version: 0.7.1
# output:
# location: npm
# url: npm.buildwithfern.com
# package-name: "@finto-fern/react-client"
# config:
# namespaceExport: Langfuse
# allowCustomFetcher: true
- name: fernapi/fern-openapi
version: 0.0.26
output:
location: local-file-system
path: ../../../web/public/generated/api-client
config:
namespaceExport: Langfuse
allowCustomFetcher: true
-29
View File
@@ -1,29 +0,0 @@
name: langfuse
docs: |
## Authentication
Authenticate with the API using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication), get API keys in the project settings:
- username: Langfuse Public Key
- password: Langfuse Secret Key
## Exports
- OpenAPI spec: https://cloud.langfuse.com/generated/api/openapi.yml
- Postman collection: https://cloud.langfuse.com/generated/postman/collection.json
error-discrimination:
strategy: status-code
auth: basic
imports:
commons: commons.yml
errors:
- commons.Error
- commons.UnauthorizedError
- commons.AccessDeniedError
- commons.MethodNotAllowedError
- commons.NotFoundError
headers:
X-Langfuse-Sdk-Name: optional<string>
X-Langfuse-Sdk-Version: optional<string>
X-Langfuse-Public-Key: optional<string>
-73
View File
@@ -1,73 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
pagination: ./utils/pagination.yml
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
create:
docs: Create a comment. Comments may be attached to different object types (trace, observation, session, prompt).
method: POST
path: /comments
request: CreateCommentRequest
response: CreateCommentResponse
get:
docs: Get all comments
method: GET
path: /comments
request:
name: GetCommentsRequest
query-parameters:
page:
type: optional<integer>
docs: Page number, starts at 1.
limit:
type: optional<integer>
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit
objectType:
type: optional<string>
docs: Filter comments by object type (trace, observation, session, prompt).
objectId:
type: optional<string>
docs: Filter comments by object id. If objectType is not provided, an error will be thrown.
authorUserId:
type: optional<string>
docs: Filter comments by author user id.
response: GetCommentsResponse
get-by-id:
docs: Get a comment by id
method: GET
path: /comments/{commentId}
path-parameters:
commentId:
type: string
docs: The unique langfuse identifier of a comment
response: commons.Comment
types:
CreateCommentRequest:
properties:
projectId:
type: string
docs: The id of the project to attach the comment to.
objectType:
type: string
docs: The type of the object to attach the comment to (trace, observation, session, prompt).
objectId:
type: string
docs: The id of the object to attach the comment to. If this does not reference a valid existing object, an error will be thrown.
content:
type: string
docs: The content of the comment. May include markdown. Currently limited to 500 characters.
authorUserId:
type: optional<string>
docs: The id of the user who created the comment.
CreateCommentResponse:
properties:
id:
type: string
docs: The id of the created object in Langfuse
GetCommentsResponse:
properties:
data: list<commons.Comment>
meta: pagination.MetaResponse
-456
View File
@@ -1,456 +0,0 @@
types:
# Objects
Trace:
properties:
id:
type: string
docs: The unique identifier of a trace
timestamp:
type: datetime
docs: The timestamp when the trace was created
name:
type: optional<string>
docs: The name of the trace
input:
type: optional<unknown>
docs: The input data of the trace. Can be any JSON.
output:
type: optional<unknown>
docs: The output data of the trace. Can be any JSON.
sessionId:
type: optional<string>
docs: The session identifier associated with the trace
release:
type: optional<string>
docs: The release version of the application when the trace was created
version:
type: optional<string>
docs: The version of the trace
userId:
type: optional<string>
docs: The user identifier associated with the trace
metadata:
type: optional<unknown>
docs: The metadata associated with the trace. Can be any JSON.
tags:
type: optional<list<string>>
docs: The tags associated with the trace. Can be an array of strings or null.
public:
type: optional<boolean>
docs: Public traces are accessible via url without login
TraceWithDetails: # GET /traces
extends: Trace
properties:
htmlPath:
type: string
docs: Path of trace in Langfuse UI
latency:
type: double
docs: Latency of trace in seconds
totalCost:
type: double
docs: Cost of trace in USD
observations:
type: list<string>
docs: List of observation ids
scores:
type: list<string>
docs: List of score ids
TraceWithFullDetails: # GET traces/[traceID]
extends: Trace
properties:
htmlPath:
type: string
docs: Path of trace in Langfuse UI
latency:
type: double
docs: Latency of trace in seconds
totalCost:
type: double
docs: Cost of trace in USD
observations:
type: list<ObservationsView>
docs: List of observations
scores:
type: list<Score>
docs: List of scores
Session:
properties:
id: string
createdAt: datetime
projectId: string
SessionWithTraces:
extends: Session
properties:
traces: list<Trace>
Observation:
properties:
id:
type: string
docs: The unique identifier of the observation
traceId:
type: optional<string>
docs: The trace ID associated with the observation
type:
type: string
docs: The type of the observation
name:
type: optional<string>
docs: The name of the observation
startTime:
type: datetime
docs: The start time of the observation
endTime:
type: optional<datetime>
docs: The end time of the observation.
completionStartTime:
type: optional<datetime>
docs: The completion start time of the observation
model:
type: optional<string>
docs: The model used for the observation
modelParameters:
type: optional<map<string, MapValue>>
docs: The parameters of the model used for the observation
input:
type: optional<unknown>
docs: The input data of the observation
version:
type: optional<string>
docs: The version of the observation
metadata:
type: optional<unknown>
docs: Additional metadata of the observation
output:
type: optional<unknown>
docs: The output data of the observation
usage:
type: optional<Usage>
docs: The usage data of the observation
level:
type: ObservationLevel
docs: The level of the observation
statusMessage:
type: optional<string>
docs: The status message of the observation
parentObservationId:
type: optional<string>
docs: The parent observation ID
promptId:
type: optional<string>
docs: The prompt ID associated with the observation
ObservationsView:
extends: Observation
properties:
promptName:
type: optional<string>
docs: The name of the prompt associated with the observation
promptVersion:
type: optional<integer>
docs: The version of the prompt associated with the observation
modelId:
type: optional<string>
docs: The unique identifier of the model
inputPrice:
type: optional<double>
docs: The price of the input in USD
outputPrice:
type: optional<double>
docs: The price of the output in USD.
totalPrice:
type: optional<double>
docs: The total price in USD.
calculatedInputCost:
type: optional<double>
docs: The calculated cost of the input in USD
calculatedOutputCost:
type: optional<double>
docs: The calculated cost of the output in USD
calculatedTotalCost:
type: optional<double>
docs: The calculated total cost in USD
latency:
type: optional<double>
docs: The latency in seconds.
timeToFirstToken:
type: optional<double>
docs: The time to the first token in seconds
Usage:
docs: Standard interface for usage and cost
properties:
input:
docs: Number of input units (e.g. tokens)
type: optional<integer>
output:
docs: Number of output units (e.g. tokens)
type: optional<integer>
total:
docs: Defaults to input+output if not set
type: optional<integer>
unit: optional<ModelUsageUnit>
inputCost:
docs: USD input cost
type: optional<double>
outputCost:
docs: USD output cost
type: optional<double>
totalCost:
docs: USD total cost, defaults to input+output
type: optional<double>
ScoreConfig:
docs: Configuration for a score
properties:
id: string
name: string
createdAt: datetime
updatedAt: datetime
projectId: string
dataType: ScoreDataType
isArchived:
type: boolean
docs: Whether the score config is archived. Defaults to false
minValue:
type: optional<double>
docs: Sets minimum value for numerical scores. If not set, the minimum value defaults to -∞
maxValue:
type: optional<double>
docs: Sets maximum value for numerical scores. If not set, the maximum value defaults to +∞
categories:
type: optional<list<ConfigCategory>>
docs: Configures custom categories for categorical scores
description: optional<string>
ConfigCategory:
properties:
value: double
label: string
BaseScore:
properties:
id: string
traceId: string
name: string
source: ScoreSource
observationId: optional<string>
timestamp: datetime
createdAt: datetime
updatedAt: datetime
authorUserId: optional<string>
comment: optional<string>
configId:
type: optional<string>
docs: Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range
queueId:
type: optional<string>
docs: Reference an annotation queue on a score. Populated if the score was initially created in an annotation queue.
NumericScore:
extends: BaseScore
properties:
value:
type: double
docs: The numeric value of the score
BooleanScore:
extends: BaseScore
properties:
value:
type: double
docs: The numeric value of the score. Equals 1 for "True" and 0 for "False"
stringValue:
type: string
docs: The string representation of the score value. Is inferred from the numeric value and equals "True" or "False"
CategoricalScore:
extends: BaseScore
properties:
value:
type: optional<double>
docs: Only defined if a config is linked. Represents the numeric category mapping of the stringValue
stringValue:
type: string
docs: The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category
Score:
discriminant: "dataType"
union:
NUMERIC:
type: NumericScore
docs: "Score with NUMERIC data type"
CATEGORICAL:
type: CategoricalScore
docs: "Score with CATEGORICAL data type"
BOOLEAN:
type: BooleanScore
docs: "Score with BOOLEAN data type"
CreateScoreValue:
discriminated: false
union:
- double
- string
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
Comment:
properties:
id: string
projectId: string
createdAt: datetime
updatedAt: datetime
objectType: CommentObjectType
objectId: string
content: string
authorUserId: optional<string>
Dataset:
properties:
id: string
name: string
description: optional<string>
metadata: optional<unknown>
projectId: string
createdAt: datetime
updatedAt: datetime
DatasetItem:
properties:
id: string
status: DatasetStatus
input: optional<unknown>
expectedOutput: optional<unknown>
metadata: optional<unknown>
sourceTraceId: optional<string>
sourceObservationId: optional<string>
datasetId: string
datasetName: string
createdAt: datetime
updatedAt: datetime
DatasetRunItem:
properties:
id: string
datasetRunId: string
datasetRunName: string
datasetItemId: string
traceId: string
observationId: optional<string>
createdAt: datetime
updatedAt: datetime
DatasetRun:
properties:
id:
type: string
docs: Unique identifier of the dataset run
name:
type: string
docs: Name of the dataset run
description:
type: optional<string>
docs: Description of the run
metadata:
type: optional<unknown>
docs: Metadata of the dataset run
datasetId:
type: string
docs: Id of the associated dataset
datasetName:
type: string
docs: Name of the associated dataset
createdAt:
type: datetime
docs: The date and time when the dataset run was created
updatedAt:
type: datetime
docs: The date and time when the dataset run was last updated
DatasetRunWithItems:
extends: DatasetRun
properties:
datasetRunItems: list<DatasetRunItem>
Model:
docs: Model definition used for transforming usage into USD cost and/or tokenization.
properties:
id: string
modelName:
docs: "Name of the model definition. If multiple with the same name exist, they are applied in the following order: (1) custom over built-in, (2) newest according to startTime where model.startTime<observation.startTime"
type: string
matchPattern:
docs: "Regex pattern which matches this model definition to generation.model. Useful in case of fine-tuned models. If you want to exact match, use `(?i)^modelname$`"
type: string
startDate:
docs: Apply only to generations which are newer than this ISO date.
type: optional<date>
unit:
docs: Unit used by this model.
type: ModelUsageUnit
inputPrice:
docs: Price (USD) per input unit
type: optional<double>
outputPrice:
docs: Price (USD) per output unit
type: optional<double>
totalPrice:
docs: Price (USD) per total unit. Cannot be set if input or output price is set.
type: optional<double>
tokenizerId:
docs: Optional. Tokenizer to be applied to observations which match to this model. See docs for more details.
type: optional<string>
tokenizerConfig:
docs: Optional. Configuration for the selected tokenizer. Needs to be JSON. See docs for more details.
type: optional<unknown>
isLangfuseManaged:
type: boolean
# Utilities
ModelUsageUnit:
docs: Unit of usage in Langfuse
enum:
- CHARACTERS
- TOKENS
- MILLISECONDS
- SECONDS
- IMAGES
- REQUESTS
ObservationLevel:
enum:
- DEBUG
- DEFAULT
- WARNING
- ERROR
MapValue:
discriminated: false
union:
- optional<string>
- optional<integer>
- optional<boolean>
- optional<list<string>>
CommentObjectType:
enum:
- TRACE
- OBSERVATION
- SESSION
- PROMPT
DatasetStatus:
enum:
- ACTIVE
- ARCHIVED
ScoreSource:
enum:
- ANNOTATION
- API
- EVAL
ScoreDataType:
enum:
- NUMERIC
- BOOLEAN
- CATEGORICAL
errors:
Error:
status-code: 400
type: unknown
UnauthorizedError:
status-code: 401
type: unknown
AccessDeniedError:
status-code: 403
type: unknown
NotFoundError:
status-code: 404
type: unknown
MethodNotAllowedError:
status-code: 405
type: unknown
@@ -1,59 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
pagination: ./utils/pagination.yml
service:
auth: true
base-path: /api/public
endpoints:
create:
method: POST
docs: Create a dataset item
path: /dataset-items
request: CreateDatasetItemRequest
response: commons.DatasetItem
get:
docs: Get a dataset item
method: GET
path: /dataset-items/{id}
path-parameters:
id:
type: string
response: commons.DatasetItem
list:
docs: Get dataset items
method: GET
path: /dataset-items
request:
name: GetDatasetItemsRequest
query-parameters:
datasetName: optional<string>
sourceTraceId: optional<string>
sourceObservationId: optional<string>
page:
type: optional<integer>
docs: page number, starts at 1
limit:
type: optional<integer>
docs: limit of items per page
response: PaginatedDatasetItems
types:
CreateDatasetItemRequest:
properties:
datasetName: string
input: optional<unknown>
expectedOutput: optional<unknown>
metadata: optional<unknown>
sourceTraceId: optional<string>
sourceObservationId: optional<string>
id:
type: optional<string>
docs: Dataset items are upserted on their id. Id needs to be unique (project-level) and cannot be reused across datasets.
status:
type: optional<commons.DatasetStatus>
docs: Defaults to ACTIVE for newly created items
PaginatedDatasetItems:
properties:
data: list<commons.DatasetItem>
meta: pagination.MetaResponse
@@ -1,28 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
create:
method: POST
docs: Create a dataset run item
path: /dataset-run-items
request: CreateDatasetRunItemRequest
response: commons.DatasetRunItem
types:
CreateDatasetRunItemRequest:
properties:
runName: string
runDescription:
type: optional<string>
docs: Description of the run. If run exists, description will be updated.
metadata:
type: optional<unknown>
docs: Metadata of the dataset run, updates run if run already exists
datasetItemId: string
observationId: optional<string>
traceId:
type: optional<string>
docs: traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId.
-74
View File
@@ -1,74 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
pagination: ./utils/pagination.yml
service:
auth: true
base-path: /api/public
endpoints:
list:
method: GET
docs: Get all datasets
path: /v2/datasets
request:
name: GetDatasetsRequest
query-parameters:
page:
type: optional<integer>
docs: page number, starts at 1
limit:
type: optional<integer>
docs: limit of items per page
response: PaginatedDatasets
get:
method: GET
docs: Get a dataset
path: /v2/datasets/{datasetName}
path-parameters:
datasetName: string
response: commons.Dataset
create:
method: POST
docs: Create a dataset
path: /v2/datasets
request: CreateDatasetRequest
response: commons.Dataset
getRun:
method: GET
docs: Get a dataset run and its items
path: /datasets/{datasetName}/runs/{runName}
path-parameters:
datasetName: string
runName: string
response: commons.DatasetRunWithItems
getRuns:
method: GET
docs: Get dataset runs
path: /datasets/{datasetName}/runs
path-parameters:
datasetName: string
request:
name: GetDatasetRunsRequest
query-parameters:
page:
type: optional<integer>
docs: page number, starts at 1
limit:
type: optional<integer>
docs: limit of items per page
response: PaginatedDatasetRuns
types:
PaginatedDatasets:
properties:
data: list<commons.Dataset>
meta: pagination.MetaResponse
CreateDatasetRequest:
properties:
name: string
description: optional<string>
metadata: optional<unknown>
PaginatedDatasetRuns:
properties:
data: list<commons.DatasetRun>
meta: pagination.MetaResponse
-28
View File
@@ -1,28 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
service:
auth: false
base-path: /api/public
endpoints:
health:
docs: Check health of API and database
method: GET
path: /health
response: HealthResponse
errors:
- ServiceUnavailableError
types:
HealthResponse:
properties:
version:
type: string
docs: Langfuse server version
status: string
examples:
- value:
version: 1.25.0
status: OK
errors:
ServiceUnavailableError:
status-code: 503
-314
View File
@@ -1,314 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
pagination: ./utils/pagination.yml
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
batch:
docs: |
Batched ingestion for Langfuse Tracing. If you want to use tracing via the API, such as to build your own Langfuse client implementation, this is the only API route you need to implement.
Notes:
- Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly.
- The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors.
method: POST
path: /ingestion
request:
name: IngestionRequest
body:
properties:
batch:
type: list<IngestionEvent>
docs: "Batch of tracing events to be ingested. Discriminated by attribute `type`."
metadata:
type: optional<unknown>
docs: Optional. Metadata field used by the Langfuse SDKs for debugging.
response:
type: IngestionResponse
status-code: 207
types:
IngestionEvent:
discriminant: "type"
union:
trace-create:
type: TraceEvent
docs: Creates a new trace. Upserts on id for updates if trace with id exists.
score-create:
type: ScoreEvent
docs: Creates a new score. Upserts on id for updates if score with id exists.
span-create:
type: CreateSpanEvent
docs: Creates a new span.
span-update:
type: UpdateSpanEvent
docs: Updates span based on id.
generation-create:
type: CreateGenerationEvent
docs: Creates a new generation.
generation-update:
type: UpdateGenerationEvent
docs: Updates a generation based on id.
event-create:
type: CreateEventEvent
docs: Creates an event.
sdk-log:
type: SDKLogEvent
docs: Langfuse SDKs only, used for debugging purposes.
# both are legacy
observation-create:
type: CreateObservationEvent
docs: Deprecated event type
observation-update:
type: UpdateObservationEvent
docs: Deprecated event type
ObservationType:
enum:
- SPAN
- GENERATION
- EVENT
IngestionUsage:
discriminated: false
union:
- commons.Usage
- OpenAIUsage
OpenAIUsage:
docs: Usage interface of OpenAI for improved compatibility.
properties:
promptTokens: optional<integer>
completionTokens: optional<integer>
totalTokens: optional<integer>
OptionalObservationBody:
properties:
traceId: optional<string>
name: optional<string>
startTime: optional<datetime>
metadata: optional<unknown>
input: optional<unknown>
output: optional<unknown>
level: optional<commons.ObservationLevel>
statusMessage: optional<string>
parentObservationId: optional<string>
version: optional<string>
CreateEventBody:
extends: OptionalObservationBody
properties:
id: optional<string>
UpdateEventBody:
extends: OptionalObservationBody
properties:
id: string
CreateSpanBody:
extends: CreateEventBody
properties:
endTime: optional<datetime>
UpdateSpanBody:
extends: UpdateEventBody
properties:
endTime: optional<datetime>
CreateGenerationBody:
extends: CreateSpanBody
properties:
completionStartTime: optional<datetime>
model: optional<string>
modelParameters: optional<map<string, commons.MapValue>>
usage: optional<IngestionUsage>
promptName: optional<string>
promptVersion: optional<integer>
UpdateGenerationBody:
extends: UpdateSpanBody
properties:
completionStartTime: optional<datetime>
model: optional<string>
modelParameters: optional<map<string, commons.MapValue>>
usage: optional<IngestionUsage>
promptName: optional<string>
promptVersion: optional<integer>
ObservationBody:
properties:
id: optional<string>
traceId: optional<string>
type: ObservationType
name: optional<string>
startTime: optional<datetime>
endTime: optional<datetime>
completionStartTime: optional<datetime>
model: optional<string>
modelParameters: optional<map<string, commons.MapValue>>
input: optional<unknown>
version: optional<string>
metadata: optional<unknown>
output: optional<unknown>
usage: optional<commons.Usage>
level: optional<commons.ObservationLevel>
statusMessage: optional<string>
parentObservationId: optional<string>
TraceBody:
properties:
id: optional<string>
timestamp: optional<datetime>
name: optional<string>
userId: optional<string>
input: optional<unknown>
output: optional<unknown>
sessionId: optional<string>
release: optional<string>
version: optional<string>
metadata: optional<unknown>
tags: optional<list<string>>
public:
type: optional<boolean>
docs: Make trace publicly accessible via url
SDKLogBody:
properties:
log: unknown
ScoreBody:
properties:
id: optional<string>
traceId: string
name: string
value:
type: commons.CreateScoreValue
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
observationId: optional<string>
comment: optional<string>
dataType:
type: optional<commons.ScoreDataType>
docs: When set, must match the score value's type. If not set, will be inferred from the score value or config
configId:
type: optional<string>
docs: Reference a score config on a score. When set, the score name must equal the config name and scores must comply with the config's range and data type. For categorical scores, the value must map to a config category. Numeric scores might be constrained by the score config's max and min values
examples:
- value:
name: "novelty"
value: 0.9
traceId: "cdef-1234-5678-90ab"
- value:
name: "consistency"
value: 1.2
dataType: "NUMERIC"
traceId: "cdef-1234-5678-90ab"
- value:
name: "accuracy"
value: 0.9
dataType: "NUMERIC"
configId: "9203-4567-89ab-cdef"
traceId: "cdef-1234-5678-90ab"
- value:
name: "toxicity"
value: "not toxic"
traceId: "cdef-1234-5678-90ab"
- value:
name: "correctness"
value: "partially correct"
dataType: "CATEGORICAL"
configId: "1234-5678-90ab-cdef"
traceId: "cdef-1234-5678-90ab"
- value:
name: "hallucination"
value: 0
dataType: "BOOLEAN"
traceId: "cdef-1234-5678-90ab"
- value:
name: "helpfulness"
value: 1
dataType: "BOOLEAN"
configId: "1234-5678-90ab-cdef"
traceId: "cdef-1234-5678-90ab"
BaseEvent:
properties:
id:
type: string
docs: UUID v4 that identifies the event
timestamp:
type: string
docs: "Datetime (ISO 8601) of event creation in client. Should be as close to actual event creation in client as possible, this timestamp will be used for ordering of events in future release. Resolution: milliseconds (required), microseconds (optimal)."
metadata:
type: optional<unknown>
docs: Optional. Metadata field used by the Langfuse SDKs for debugging.
TraceEvent:
extends: BaseEvent
properties:
body: TraceBody
CreateObservationEvent:
extends: BaseEvent
properties:
body: ObservationBody
UpdateObservationEvent:
extends: BaseEvent
properties:
body: ObservationBody
ScoreEvent:
extends: BaseEvent
properties:
body: ScoreBody
SDKLogEvent:
extends: BaseEvent
properties:
body: SDKLogBody
CreateGenerationEvent:
extends: BaseEvent
properties:
body: CreateGenerationBody
UpdateGenerationEvent:
extends: BaseEvent
properties:
body: UpdateGenerationBody
CreateSpanEvent:
extends: BaseEvent
properties:
body: CreateSpanBody
UpdateSpanEvent:
extends: BaseEvent
properties:
body: UpdateSpanBody
CreateEventEvent:
extends: BaseEvent
properties:
body: CreateEventBody
IngestionSuccess:
properties:
id: string
status: integer
IngestionError:
properties:
id: string
status: integer
message: optional<string>
error: optional<unknown>
IngestionResponse:
properties:
successes: list<IngestionSuccess>
errors: list<IngestionError>
-103
View File
@@ -1,103 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
get:
docs: Get a media record
method: GET
path: /media/{mediaId}
path-parameters:
mediaId:
type: string
docs: The unique langfuse identifier of a media record
response: GetMediaResponse
patch:
docs: Patch a media record
method: PATCH
path: /media/{mediaId}
path-parameters:
mediaId:
type: string
docs: The unique langfuse identifier of a media record
request: PatchMediaBody
getUploadUrl:
docs: Get a presigned upload URL for a media record
method: POST
path: /media
request: GetMediaUploadUrlRequest
response: GetMediaUploadUrlResponse
types:
GetMediaResponse:
properties:
mediaId:
type: string
docs: The unique langfuse identifier of a media record
contentType:
type: string
docs: The MIME type of the media record
contentLength:
type: integer
docs: The size of the media record in bytes
uploadedAt:
type: datetime
docs: The date and time when the media record was uploaded
url:
type: string
docs: The download URL of the media record
urlExpiry:
type: string
docs: The expiry date and time of the media record download URL
PatchMediaBody:
properties:
uploadedAt:
type: datetime
docs: The date and time when the media record was uploaded
uploadHttpStatus:
type: integer
docs: The HTTP status code of the upload
uploadHttpError:
type: optional<string>
docs: The HTTP error message of the upload
uploadTimeMs:
type: optional<integer>
docs: The time in milliseconds it took to upload the media record
GetMediaUploadUrlRequest:
properties:
traceId:
type: string
docs: The trace ID associated with the media record
observationId:
type: optional<string>
docs: The observation ID associated with the media record. If the media record is associated directly with a trace, this will be null.
contentType: MediaContentType
contentLength:
type: integer
docs: The size of the media record in bytes
sha256Hash:
type: string
docs: The SHA-256 hash of the media record
field:
type: string
docs: The trace / observation field the media record is associated with. This can be one of `input`, `output`, `metadata`
GetMediaUploadUrlResponse:
properties:
uploadUrl:
type: optional<string>
docs: The presigned upload URL. If the asset is already uploaded, this will be null
mediaId:
type: string
docs: The unique langfuse identifier of a media record
MediaContentType:
type: literal<"image/png","image/jpeg","image/jpg","image/webp","audio/mpeg","audio/mp3","audio/wav","text/plain","application/pdf">
docs: The MIME type of the media record
-72
View File
@@ -1,72 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
pagination: ./utils/pagination.yml
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
daily:
docs: Get daily metrics of the Langfuse project
method: GET
path: /metrics/daily
request:
name: GetDailyMetricsRequest
query-parameters:
page:
type: optional<integer>
docs: page number, starts at 1
limit:
type: optional<integer>
docs: limit of items per page
traceName:
type: optional<string>
docs: Optional filter by the name of the trace
userId:
type: optional<string>
docs: Optional filter by the userId associated with the trace
tags:
type: optional<string>
allow-multiple: true
docs: Optional filter for metrics where traces include all of these tags
fromTimestamp:
type: optional<datetime>
docs: Optional filter to only include traces and observations on or after a certain datetime (ISO 8601)
toTimestamp:
type: optional<datetime>
docs: Optional filter to only include traces and observations before a certain datetime (ISO 8601)
response: DailyMetrics
types:
DailyMetrics:
properties:
data:
type: list<DailyMetricsDetails>
docs: A list of daily metrics, only days with ingested data are included.
meta: pagination.MetaResponse
DailyMetricsDetails:
properties:
date: date
countTraces: integer
countObservations: integer
totalCost:
type: double
docs: Total model cost in USD
usage: list<UsageByModel>
UsageByModel:
docs: Daily usage of a given model. Usage corresponds to the unit set for the specific model (e.g. tokens).
properties:
model: optional<string>
inputUsage:
type: integer
docs: Total number of generation input units (e.g. tokens)
outputUsage:
type: integer
docs: Total number of generation output units (e.g. tokens)
totalUsage:
type: integer
docs: Total number of generation total units (e.g. tokens)
countTraces: integer
countObservations: integer
totalCost:
type: double
docs: Total model cost in USD
-76
View File
@@ -1,76 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
pagination: ./utils/pagination.yml
service:
auth: true
base-path: /api/public
endpoints:
create:
method: POST
docs: Create a model
path: /models
request: CreateModelRequest
response: commons.Model
list:
method: GET
docs: Get all models
path: /models
request:
name: GetModelsRequest
query-parameters:
page:
type: optional<integer>
docs: page number, starts at 1
limit:
type: optional<integer>
docs: limit of items per page
response: PaginatedModels
get:
method: GET
docs: Get a model
path: /models/{id}
path-parameters:
id: string
response: commons.Model
delete:
method: DELETE
docs: Delete a model. Cannot delete models managed by Langfuse. You can create your own definition with the same modelName to override the definition though.
path: /models/{id}
path-parameters:
id: string
types:
PaginatedModels:
properties:
data: list<commons.Model>
meta: pagination.MetaResponse
CreateModelRequest:
properties:
modelName:
docs: "Name of the model definition. If multiple with the same name exist, they are applied in the following order: (1) custom over built-in, (2) newest according to startTime where model.startTime<observation.startTime"
type: string
matchPattern:
docs: "Regex pattern which matches this model definition to generation.model. Useful in case of fine-tuned models. If you want to exact match, use `(?i)^modelname$`"
type: string
startDate:
docs: Apply only to generations which are newer than this ISO date.
type: optional<datetime>
unit:
docs: Unit used by this model.
type: commons.ModelUsageUnit
inputPrice:
docs: Price (USD) per input unit
type: optional<double>
outputPrice:
docs: Price (USD) per output unit
type: optional<double>
totalPrice:
docs: Price (USD) per total units. Cannot be set if input or output price is set.
type: optional<double>
tokenizerId:
docs: Optional. Tokenizer to be applied to observations which match to this model. See docs for more details.
type: optional<string>
tokenizerConfig:
docs: Optional. Configuration for the selected tokenizer. Needs to be JSON. See docs for more details.
type: optional<unknown>

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