Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions .github/workflows/python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,26 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 16
node-version: 20.19.0
# ESLint and Prettier must be in `package.json`
- name: Install Node.js dependencies
run: cd xinference/ui/web/ui && npm ci
run: cd frontend && npm ci
- name: ESLint Check
run: cd xinference/ui/web/ui && npx eslint .
- name: Prettier Check
run: cd xinference/ui/web/ui && ./node_modules/.bin/prettier --check .
run: cd frontend && npx eslint .
- name: Build frontend (static export)
run: cd frontend && npm run build
- name: Verify static export output
run: |
test -f frontend/out/index.html
test -f frontend/out/404.html
# Dynamic routes must emit __shell__ placeholder pages.
find frontend/out -name '__shell__*' | grep -q .
- name: Serve static export from backend
run: |
pip install fastapi httpx pytest xoscar
pytest --noconftest -vv \
xinference/api/tests/test_frontend_static.py \
xinference/api/tests/test_frontend_static_real_export.py

build_test_job:
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -335,4 +347,4 @@ jobs:
xinference

fi
working-directory: .
working-directory: .
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,7 @@ asv/results
!SECURITY.md
.history/

# frontend
# frontend
**/pnpm-lock.yaml
# staged frontend static export served by the backend
xinference/ui/web/dist/
8 changes: 7 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@ include xinference/locale/*.json
include xinference/model/llm/*.json
include xinference/model/embedding/*.json
graft xinference/thirdparty
global-include xinference/ui/web/ui/build/**/*
graft xinference/ui/web/dist
graft frontend
prune frontend/.next
prune frontend/out
prune frontend/node_modules
prune frontend/.npm-cache
prune frontend/.npm-logs
14 changes: 12 additions & 2 deletions doc/source/development/contributing_environment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ is to compile the frontend.
Frontend Compilation
--------------------

Navigate to the ``inference/xinference/ui/web/ui`` directory. Then, execute the following command
Navigate to the ``inference/frontend`` directory. Then, execute the following command
to clear the cache:

::
Expand All @@ -104,11 +104,21 @@ frontend:

::

npm install
npm ci
npm run build

Still, if the first command fails to execute, you can try adding the ``--force`` option.

The build emits a static export and stages it at ``xinference/ui/web/dist``,
where the Xinference backend serves it directly — no Node.js runtime is needed
after building.

For local frontend development, start the Xinference backend separately and then run:

::

XINFERENCE_API_URL=http://127.0.0.1:9997 npm run dev

After compiling the frontend, you can ``cd`` back to the directory
where the ``setup.cfg`` and ``setup.py`` files are located,
and install Xinference via ``pip install -e .``.
2 changes: 1 addition & 1 deletion doc/source/development/xinference_internals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,4 @@ The main code is located in the `xinference/ <https://github.com/xorbitsai/infer
- `model/ <https://github.com/xorbitsai/inference/tree/main/xinference/model>`_: It provides a structure for model descriptions, creation,
and caching. See `Model`_ for more information.

- `web/ui/ <https://github.com/xorbitsai/inference/tree/main/xinference/web/ui>`_: The js code of the frontend (Web UI).
- `frontend/ <https://github.com/xorbitsai/inference/tree/main/frontend>`_: The Next.js code of the frontend (Web UI).
10 changes: 9 additions & 1 deletion doc/source/getting_started/using_xinference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,17 @@ To start a local instance of Xinference, run the following command:
Congrats! You now have Xinference running on your local machine. Once Xinference is running, there are multiple ways
we can try it: via the web UI, via cURL, via the command line, or via the Xinference's python client.

You can visit the web UI at `http://127.0.0.1:9997/ui <http://127.0.0.1:9997/ui>`_ and visit `http://127.0.0.1:9997/docs <http://127.0.0.1:9997/docs>`_
You can visit the web UI at `http://127.0.0.1:9997 <http://127.0.0.1:9997>`_ and visit `http://127.0.0.1:9997/docs <http://127.0.0.1:9997/docs>`_
to inspect the API docs.

.. note::

The web UI is a Next.js application bundled into the ``xinference`` package as a static
export and served by the backend itself, so no Node.js runtime is required at runtime.
When developing Xinference from a source checkout, build it once with
``cd frontend && npm ci && npm run build``, or work on the frontend live with
``npm run dev`` (see the "Creating a development environment" page).

You can install the Xinference command line tool and Python client using the following command:

.. code-block:: bash
Expand Down
2 changes: 2 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
.npm-cache/
.npm-logs/

# env files (can opt-in for committing if needed)
.env*
Expand Down
63 changes: 41 additions & 22 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,55 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Xinference Frontend

## Getting Started
This is the Xinference Web UI, a Next.js app. In production it is built as a
**static export**, bundled into the `xinference` Python package, and served by
the Xinference backend itself — a single process, no Node.js runtime needed.

First, run the development server:
## Local Development

Start the backend first:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
xinference-local --host 127.0.0.1 --port 9997
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
Then start the frontend dev server:

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
```bash
cd frontend
npm ci
npm run dev
```

## Learn More
Open http://127.0.0.1:3999.

To learn more about Next.js, take a look at the following resources:
In dev mode, the frontend proxies API requests to `http://127.0.0.1:9997`. To
use a different backend endpoint:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
```bash
XINFERENCE_API_URL=http://127.0.0.1:6735 npm run dev
```

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
For direct browser requests to a non-default backend, use
`NEXT_PUBLIC_API_URL` instead.

## Deploy on Vercel
## Build

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
```bash
npm run build
```

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
This produces the static export in `out/` and stages it at
`xinference/ui/web/dist` (postbuild hook), where the backend serves it. After
building, `http://<backend-host>:9997/` serves the UI directly.

Notes on the static export:

- Dynamic routes (`launch-model/[modelType]`, `running-model/[modelUid]`, ...)
are emitted as `__shell__` placeholder pages; the backend maps any real URL
to the matching shell and the client reads the actual params from the URL
(see `src/lib/route-params.ts` and `xinference/api/frontend_static.py`).
- `next.config.ts` only enables rewrites (the dev API proxy) outside export
mode; in production the UI is same-origin with the API, so no proxy is
needed.
- To build a self-contained Node server instead (no static export), use
`NEXT_OUTPUT=standalone npm run build`.
67 changes: 49 additions & 18 deletions frontend/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
// Output mode (build only):
// 'export' -> static assets in out/, bundled into the Python wheel and
// served by the Xinference backend (single-process pip path).
// This is the default for builds.
// 'standalone' -> self-contained Node server (set NEXT_OUTPUT=standalone) for
// deployments that run the frontend as a separate service.
//
// Never set an output mode in `next dev`: 'export' forces dynamicParams=false,
// so navigating to a real dynamic route (/launch-model/llm, ...) whose params
// are not in generateStaticParams throws. In production the backend SPA
// fallback maps those paths to the __shell__ page; the dev server has no such
// layer.
const isDev = process.env.NODE_ENV === 'development';
const outputMode = isDev
? undefined
: process.env.NEXT_OUTPUT === 'standalone'
? 'standalone'
: 'export';

/** @type {import('next').NextConfig} */
const nextConfig = {
// Enable standalone output for Docker deployment
output: 'standalone',
...(outputMode ? { output: outputMode } : {}),
images: { unoptimized: true },
experimental: {
optimizeCss: false,
},
Expand All @@ -21,32 +40,44 @@
eslint: {
ignoreDuringBuilds: false,
},
async redirects() {
return [
{
source: '/',
destination: '/launch-model',
permanent: true, // false = 307 (temporary redirect), true = 301 (permanent redirect)
},
];
},
// rewrites/redirects are unsupported in static export; in the single-process
// deployment the backend serves the UI same-origin so no proxy is needed.
// Keep the API proxy for `next dev` and standalone builds.
...(outputMode !== 'export'
? {
async rewrites() {
const apiUrl = (
process.env.XINFERENCE_API_URL ||
process.env.NEXT_PUBLIC_API_URL ||
'http://127.0.0.1:9997'
).replace(/\/+$/, '');
return [
{
source: '/v1/:path*',
destination: `${apiUrl}/v1/:path*`,
},
{
source: '/token',
destination: `${apiUrl}/token`,
},
];
},
}
: {}),
webpack(config: any) {

Check warning on line 67 in frontend/next.config.ts

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest, 3.10)

Unexpected any. Specify a different type
const fileLoaderRule = config.module.rules.find(
(rule: any) =>
rule.test?.test?.('.svg')
)
const fileLoaderRule = config.module.rules.find((rule: any) => rule.test?.test?.('.svg'));

Check warning on line 68 in frontend/next.config.ts

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest, 3.10)

Unexpected any. Specify a different type

if (fileLoaderRule) {
fileLoaderRule.exclude = /\.svg$/i
fileLoaderRule.exclude = /\.svg$/i;
}

config.module.rules.push({
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
})
});

return config
return config;
},
};

Expand Down
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3999",
"dev": "next dev -H 127.0.0.1 -p 3999",
"build": "next build",
"postbuild": "node scripts/stage-export.mjs",
"start": "next start",
"lint": "eslint .",
"format": "prettier . --write"
Expand Down
22 changes: 22 additions & 0 deletions frontend/scripts/stage-export.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Stage the Next.js static export inside the Python package (postbuild hook).
//
// `next build` with output 'export' emits static assets into out/; the
// Xinference backend serves them from xinference/ui/web/dist so the wheel is
// self-contained. Standalone/dev builds produce no out/ directory, in which
// case there is nothing to stage.
import { cpSync, existsSync, rmSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const frontendRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const exportDir = join(frontendRoot, 'out');
const destDir = join(frontendRoot, '..', 'xinference', 'ui', 'web', 'dist');

if (!existsSync(join(exportDir, 'index.html'))) {
console.log(`[stage-export] no static export at ${exportDir}; skipping`);
process.exit(0);
}

rmSync(destDir, { recursive: true, force: true });
cpSync(exportDir, destDir, { recursive: true });
console.log(`[stage-export] staged ${exportDir} -> ${destDir}`);
33 changes: 33 additions & 0 deletions frontend/src/app/launch-model/[modelType]/page-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client';

import { Suspense } from 'react';
import { useParams, useSearchParams } from 'next/navigation';
import LaunchModel from '@/components/pages/launch-model';
import {
getLaunchModelRouteType,
getInitialCustomType,
} from '@/components/pages/launch-model/utils';
import { decodeRouteParam } from '@/lib/route-params';

function LaunchModelPageInner() {
const params = useParams<{ modelType: string }>();
const searchParams = useSearchParams();
const modelType = decodeRouteParam(params.modelType);
const activeType = searchParams.get('activeType') ?? undefined;

return (
<LaunchModel
routeType={getLaunchModelRouteType(modelType)}
initialCustomType={getInitialCustomType(activeType)}
/>
);
}

export default function LaunchModelPageClient() {
// useSearchParams requires a Suspense boundary during prerender.
return (
<Suspense fallback={null}>
<LaunchModelPageInner />
</Suspense>
);
}
28 changes: 8 additions & 20 deletions frontend/src/app/launch-model/[modelType]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import LaunchModel from '@/components/pages/launch-model';
import { getLaunchModelRouteType, getInitialCustomType } from '@/components/pages/launch-model/utils';
import LaunchModelPageClient from './page-client';

interface LaunchModelPageProps {
params: Promise<{
modelType: string;
}>;

searchParams: Promise<{
activeType?: string;
}>;
// Server wrapper for static export: provides a placeholder param so a shell
// HTML is emitted for this dynamic route. The backend serves the shell for any
// real model type; the client component reads the actual values from the URL.
export function generateStaticParams() {
return [{ modelType: '__shell__' }];
}

export default async function LaunchModelPage({ params, searchParams }: LaunchModelPageProps) {
const { modelType } = await params;
const { activeType } = await searchParams;

return (
<LaunchModel
routeType={getLaunchModelRouteType(modelType)}
initialCustomType={getInitialCustomType(activeType)}
/>
);
export default function LaunchModelPage() {
return <LaunchModelPageClient />;
}
Loading
Loading