Distribution customization

A distribution is one deployment’s own files: its name and branding, its model and routing configuration, and its feature switches. This page is for operators setting one up. It covers what configuration alone can change, how the manifest works, and how to upgrade.

A distribution can use the standard HybridInference website, apply its own branding and operating policy, or build its own public pages while sharing the console and authentication logic. Choose the smallest interface that covers your requirements; each option has a different build and upgrade cost.

The standard application provides default home, account and terms pages, dashboard, API-key management, Playground, request history and the admin console. Authentication, persistence, email, model providers and optional services still need their documented deployment configuration. The Quickstart runs a local example from inference through the Web/Admin Console.

What you can customize

The filenames below follow the example layout; manifest paths can name other files in your distribution.

What you can customize

File or interface

Rebuild requirement

Site name, public URL, support email and organization identity

distribution.yaml plus the branding.yaml selected by site.branding

No frontend rebuild; restart the backend after changing mounted files

Logo/favicon, links, API example, team/sponsors and data-policy notice

branding.yaml; optional mounted site-assets/ images served at /site-assets/*

No frontend rebuild for mounted configuration/assets; packaged asset changes need a rebuild

Complete home page /

Module client.tsx → Landing, with styles.css and module assets

Build a frontend image with the module

Appearance and supported wording of /login, /signup, /forgot-password, /reset-password, /verify-email

Module client.tsx → AuthFrame / fieldLayout / authMessages, plus styles.css and shared form hooks

Build a frontend image with the module; authentication controllers stay shared

Legal text at /terms and the confirmations sign-up asks for

Module client.tsx → TermsFrame, TermsContent and consentItems together, preserving required terms/privacy anchors

Build a frontend image with the module

Titles and descriptions of the public pages, and their document language

Module server.ts → metaMessages and locale

Build a frontend image with the module

Existing console identity and feature settings

branding.yaml, distribution.yaml and supported environment/admin settings

No frontend rebuild; Site UI cannot replace dashboard, admin, Playground, /chat, /team or /authorize layouts

Models, endpoints, aliases, weights and supported router settings

config/models.yaml, config/routing.yaml, environment and supported admin APIs

No code rebuild for configuration; file edits require backend restart, admin edits apply at runtime

Public registration and configured RAG policy

distribution.yaml → features, plus each feature’s environment/admin settings

No frontend rebuild; restart/recreate services for file/environment changes

Existing agent link/proxy integration

Frontend container environment: AGENT_PUBLIC_URL, AGENT_WEB_INTERNAL_URL, AGENT_CONTROL_PLANE_INTERNAL_URL

Recreate the frontend for runtime environment changes; /agents is not a Site UI slot

Trusted adapters, agent-access rules and quota reporting

Backend environment BACKEND_EXTENSIONS plus importable extension .py modules using documented hooks

Restart for already-mounted code; rebuild when code is packaged in the backend image

Additional routes, a different console layout, or a new authentication flow

Application source, such as apps/frontend/src/app/, in upstream or an explicitly maintained fork

Rebuild affected application images; these are outside Site UI API v1

Configuration cannot replace the home page’s hero section or the console’s theme, sidebar or layout, and adding a navigation link does not create the page it points to. A Site UI module translates only the public pages it supplies; see Document language and page titles.

Configuration-only customization

Start with the annotated config/examples/distribution.example.yaml and config/examples/branding.example.yaml. Keep deployment-owned files in your distribution repository or overlay, with credentials in the environment. Use the standard frontend image with no Site UI selection to keep the default page layouts. Reusing that image assumes its compiled backend network target matches your deployment; see When changes take effect.

Where your files live

A real deployment’s configuration lives in a distribution overlay: one directory under distributions/ holding that deployment’s manifest, config files, Compose/env inputs, and branding.

distributions/<name>/
├── distribution.yaml       # the manifest: identity + where the config files are
├── config/
│   ├── models.yaml
│   └── routing.yaml
└── deploy/
    ├── backend.env         # any *.env here; Compose reads them all
    └── docker-compose.yml  # optional overlay on deploy/docker/docker-compose.yml

The split exists so the source tree and the container image stay neutral. Model IDs, upstream base URLs, ports, site identity and branding are properties of one deployment, not of the project; keeping them in an overlay means the image can be built once and a deployment’s overlay mounted into it (deploy/docker/docker-compose.yml mounts the overlay read-only rather than baking it into the image). It also means a clone of this repository ships nobody’s hosts.

For config-path precedence and admin overrides, see Configuration.

The distribution manifest

A manifest is one versioned YAML document that names a deployment and tells the gateway where its config files are. config/examples/distribution.example.yaml is the annotated reference. Both examples below and in that file close public signup when activated; change public_signup to true or null if this deployment should accept new accounts through the public signup API:

schema_version: 1

distribution:
  id: example
  display_name: Example Router
  release: "1.0.0"

site:
  public_base_url: https://your-gateway.example
  support_email: [email protected]
  branding: branding.yaml        # copy branding.example.yaml beside this manifest

features:
  routers: [fixed]
  public_signup: false
  rag: false

paths:
  models: config/models.yaml      # relative to this file's directory
  routing: config/routing.yaml

deployment:
  target: local

schema_version must be 1. distribution.id identifies the distribution; display_name is its public name, and release records its version. deployment.target records an operator label such as local or staging; it does not select a host, build an image or deploy a service.

paths.models, paths.routing and paths.alerts select gateway configuration files. Relative paths are resolved from the manifest directory; explicit gateway environment overrides still take precedence. paths.mcp and the legal-document fields are accepted but have the limitations described below.

Point the gateway at it with two environment variables:

export DISTRIBUTION_CONFIG_PATH=distributions/<name>/distribution.yaml
export DISTRIBUTION_CONFIG_MODE=active

Public sign-up. With an active manifest, features.public_signup: false closes sign-up completely: the sign-up page disappears and POST /auth/signup answers 403 without creating an account. Neither SIGNUP_ENABLED=true nor the sign-up switch in the admin console can reopen it, and the admin API refuses that change with 400. To allow sign-up, set the field to true or remove it, restart the backend, and make sure the sign-up switch is on.

When the manifest allows sign-up, or no manifest is active, the admin console’s switch wins over SIGNUP_ENABLED (default true). GET /site-config reports the result in features.public_signup, and the console follows it. Email verification, domain-based approval and quotas still apply to every new account.

If the gateway cannot read the sign-up switch from its database within one second, it treats sign-up as closed rather than falling back to SIGNUP_ENABLED, and tries again after five seconds. Saving the switch in the admin console clears the error at once.

Public usage stats. features.public_stats: true publishes aggregate figures at /stats: approved accounts and the waiting list, tokens served per day, countries, the languages people write in and the agent clients that call the API. The page reads GET /public-stats, which serves the newest snapshot from the public_stats_snapshots table and answers 404 when the flag is off or unset. Snapshots are written by a job you schedule once a day, for example from cron or a systemd timer:

docker exec <backend-container> python -m serving.analytics.public_stats

Add --dry-run to print the snapshot instead of storing it, and --weeks N to change the 26-week window. The job reads api_logs, the hourly country rollup and account statuses, and stores aggregates only. Per-item account counts below 3 are withheld. The language section needs message text, so it is left out unless DB_STORE_FULL_CONTENT=true. A snapshot over 10 weeks of a busy gateway takes about a minute. The job keeps the 30 newest snapshots.

Activating a manifest

Setting DISTRIBUTION_CONFIG_PATH alone changes nothing. By default the gateway only does a dry run: it loads and checks the manifest and logs what it would change, but keeps using the configuration it had. The setting for this is DISTRIBUTION_CONFIG_MODE, and its default value, dark, means dry run:

[distribution dark mode] models config stays config/examples/models.openrouter.yaml
(source=default, sha256=51fd6cde50fd); manifest would use
/srv/app/distributions/example/config/models.yaml (sha256=6117799cd0bf) — DIFFERENT

Read those lines, then set DISTRIBUTION_CONFIG_MODE=active to apply the manifest. If an active manifest fails to load — a missing mount, a YAML error — the gateway logs it at CRITICAL and comes up with no models, rather than serving a different registry from the one you named. In a dry run the same failure is only logged.

Leave the mode unset or set it to exactly dark or active. Any other value, an empty one, or active without a manifest path is treated as a broken configuration: sign-up closes (403), /site-config answers 503, and the docs assistant endpoints answer 503. The warning appears once, in the startup log, so when you see those errors check DISTRIBUTION_CONFIG_MODE and DISTRIBUTION_CONFIG_PATH, fix them and restart the backend.

Check a manifest before you activate or upgrade it. Unknown keys at the top level and under features are rejected, so a misspelled feature (publicSignup, public-signup) or a feature placed at the top level fails loudly. Keep operator-specific notes in a separate file. With DISTRIBUTION_CONFIG_PATH set to the manifest you will deploy, run this with the version you are about to deploy:

uv run python - <<'PY'
import os
from pathlib import Path
from serving.config.distribution import load_distribution_config

load_distribution_config(Path(os.environ["DISTRIBUTION_CONFIG_PATH"]))
print("Manifest valid.")
PY

The paths, site, distribution and deployment sections still ignore keys they do not know, so a misspelled path key silently falls back to the default registry. After activating, check that the startup log’s Registered N routes from <path> names the file you meant.

Identity, and what the manifest must not contain

The manifest’s site: and features: sections are public: GET /site-config serves them to every visitor once the manifest is active. The site’s name and address also appear in the emails the gateway sends and in the headers it sends to OpenRouter; those come from SITE_NAME, SITE_PUBLIC_BASE_URL, SITE_DOCS_URL and SITE_SUPPORT_EMAIL when they are set, then from an active manifest, then from neutral defaults. The standard Compose file sets SITE_NAME to HybridInference unless you set it, so set it to your site’s name — or to an empty value to use the manifest’s.

Manifests must not contain secrets. Credentials stay in the environment, and schema_version: 1 deliberately does not interpolate environment variables into manifest values.

Runtime branding

Copy the complete config/examples/branding.example.yaml, set its public values, and point site.branding at it. The path is relative to the manifest’s directory. Set the site’s name in distribution.display_name; a display name alone, without a branding document, does not replace the frontend’s default name. The branding document is validated at backend startup and exposed through /site-config, so every value in it must be safe for a visitor to read.

Branding field

What the default UI uses it for

schema_version

Branding document version; currently 1

app_description, site_host

Site description and displayed host

organization.name, .url, .tagline

Organization identity, link and tagline

links.docs_url, .status_url, .github_url

Named documentation, status and repository links; an empty string hides an optional link

links.nav

Extra HTTPS header links, each with label and url, in the listed order; does not replace existing navigation

example.api_base, .api_key_env_var, .model

Public quickstart example; an empty API base hides it. The environment-variable name is an example, not an API-key value

analytics.statcounter_project_id, .statcounter_security_key

Public identifiers for optional client-side analytics; empty values disable it

signup.turnstile_site_key, .fast_track_domain, .fast_track_org

Public widget key and affiliated-organization signup presentation; not server secrets or authorization rules

storage_key_prefix

Namespace for browser storage; changing it does not migrate existing stored preferences

data_policy_notice

Displayed data-policy notice; does not configure backend data handling

assets.logo_url, .favicon_url

Branding images, served from permitted HTTPS URLs or /site-assets/*

team

/team profiles: name, affiliations, and optional badge, image, website

sponsors

Default-home sponsor images: name, alt, src, class_name, width, height

Use the template and the schema in serving/config/branding.py for permitted values. Branding rejects unknown keys; required fields cannot simply be omitted. For optional content, use the documented empty strings/lists. Sponsor class_name accepts the schema’s supported image-size classes, not arbitrary CSS. No branding field supplies a universal palette, font or page layout. Custom page components decide which runtime branding values they render.

Serve local branding images through the mounted site-assets directory and /site-assets/*, or use permitted public asset URLs. Check the URLs over HTTP. A failed or invalid /site-config response produces a configuration error instead of silently rendering a working site with different defaults.

Feature settings and their limits

  • features.rag: false in an active manifest disables the RAG UI and backend access. Allowing RAG still requires the service’s configuration; see Docs RAG Assistant.

  • AGENT_PUBLIC_URL, or the internal agent service destinations, configure the existing agent link/proxy integration. They do not install an agent service. See The public path table.

  • features.routers, paths.mcp, site.terms_document, site.privacy_document and deployment.target are accepted but have no effect yet; see Settings that currently have no effect. To publish your own terms, use a Site UI module.

A public branding hint or hidden navigation item is not a substitute for backend authorization. The registration rules above remain effective with a custom UI module.

Model/provider changes made through supported admin APIs take effect on the running gateway and are persisted in its database. They do not rewrite the YAML; stored overrides are applied on top of it again at startup. Account for both layers when investigating a configuration difference. See Runtime configuration.

When changes take effect

  • Restart the backend after changing mounted manifest, branding, model/routing or alert files. Reload the page to check presentation; the backend serves the branding snapshot loaded at startup.

  • Replace mounted site-assets files without rebuilding an image. The route allows five minutes of browser caching, so verify the served URL and account for browser/proxy caches. Changing SITE_ASSETS_DIR itself requires recreating the frontend container.

  • Recreate the affected container after changing runtime environment values. A plain container restart retains its old environment. Server-only agent destinations are runtime values.

  • Use the admin console/API for supported live edits, and check effective values and any feature-specific caching.

  • Rebuild the frontend after changing true build-only NEXT_PUBLIC_* values or the backend network target compiled from BACKEND_INTERNAL_URL. These differ from runtime branding and are not changed by a backend restart.

Use runtime branding for identity rather than introducing new build-time branding variables. The precise Compose commands are in Deployment. Rebuild the affected image when configuration or assets are baked into it instead of mounted.

The example distribution

distributions/example/ is a complete, runnable distribution kept in the repository as an example: a manifest, a one-model registry pointing at a bundled fake provider that needs no credential, Compose overlays, and smoke scripts. Walk through it with the Quickstart:

make up DISTRIBUTION=example
make smoke DISTRIBUTION=example

It carries a marker file, EXAMPLE_OVERLAY, whose presence keeps it out of automatic discovery. The Makefile picks a distribution like this:

  • Overlays are discovered from distributions/*/deploy/*.env, minus any directory containing EXAMPLE_OVERLAY.

  • Exactly one candidate: it is selected automatically, and make prints which identity it is compiling in.

  • Several candidates: make fails and asks you to name one.

  • None (the state of a fresh clone of this repository): no overlay is selected, and the stack is neutral.

DISTRIBUTION=<name> selects one by name — including the example, which is only ever selected explicitly. DISTRIBUTION=none opts out.

Custom public pages and backend code

Configuration covers the site’s identity, its branding, its models and its feature switches. Two further interfaces go beyond that, each with a build or deployment cost of its own:

  • Site UI Modules replace the public pages — the home page, the frame around the account pages, the terms — with the distribution’s own React components, compiled into its console image.

  • Backend Extensions load trusted Python code at startup to add an adapter, a Cloud Agent access rule or a quota source.

Upgrade checklist

This repository maintains the application and the interfaces it publishes: configuration, Site UI modules and backend extensions. Each distribution maintains its own configuration, module, deployment and any fork, and decides when to upgrade.

An existing source fork

  1. Inventory local changes by route and behavior. Separate branding values, public-page presentation, shared-console changes and backend logic.

  2. Where they fit, move values into runtime configuration and public-page presentation into a Site UI module. Migration is optional; you can keep the fork, with its ongoing merge and verification work. A UI module keeps authentication controllers shared.

  3. Identify remaining source changes explicitly. Console redesigns, extra routes and new authentication behavior do not become supported module features merely by moving files into the module directory.

  4. Merge the intended upstream version in an isolated branch, resolve conflicts against your required behavior, and build the exact frontend/backend pair.

  5. Verify the distribution in staging before promoting it. A successful merge or upstream CI result alone does not verify a fork’s presentation or flows.

Adopting Site UI can reduce future conflicts; it does not automatically migrate an existing fork or make arbitrary upstream source paths stable interfaces.

Before promotion

  • Pin the upstream version and, for custom UI, the module version and produced image digest. Keep the matching configuration and asset versions recoverable.

  • Read configuration/API migration notes and validate against the selected version. Unsupported module API revisions must stop the build.

  • Inspect the actual image’s /app/site-ui-manifest.json to confirm the intended standard or custom build; test packaged and mounted assets over HTTP.

  • Check public routes, configured links, narrow/wide layouts, keyboard access, form validation/loading/errors, login/logout and the registration policy.

  • Check the authenticated console, user/admin permissions and an inference request against the deployed candidate, including any local fork behavior.

  • Promote only the tested image/configuration combination and retain the prior combination for rollback.