diff --git a/.github/workflows/deploy-cloudflare-admin-ui.yml b/.github/workflows/deploy-cloudflare-admin-ui.yml
deleted file mode 100644
index cc1bd49c2..000000000
--- a/.github/workflows/deploy-cloudflare-admin-ui.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-name: Cloudflare Pages (Admin UI)
-
-on:
- push:
- branches:
- - 'main'
-
- workflow_dispatch:
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- deploy:
- name: Deploy - ui-admin
- runs-on: ubuntu-latest
- permissions:
- contents: read
- deployments: write
-
- environment:
- name: Admin Production
- url: https://admin.airi.build/
-
- steps:
- - uses: actions/checkout@v6
- # Turborepo
- - name: Cache turbo build setup
- uses: actions/cache@v5
- with:
- path: .turbo
- key: ${{ runner.os }}-turbo-admin-${{ github.sha }}
- restore-keys: |
- ${{ runner.os }}-turbo-admin-
- ${{ runner.os }}-turbo-
- - uses: pnpm/action-setup@v4
- - uses: actions/setup-node@v6
- with:
- node-version: lts/*
- cache: pnpm
- - run: pnpm i -g wrangler@4
- - run: pnpm install --frozen-lockfile
- - run: pnpm run build:packages
- - name: Build ui-admin
- run: pnpm -F @proj-airi/ui-admin run build
- env:
- VITE_SERVER_URL: 'https://api.airi.build'
-
- - uses: cloudflare/wrangler-action@v3.14.1
- with:
- apiToken: ${{ secrets.MOERU_CLOUDFLARE_API_TOKEN }}
- accountId: ${{ secrets.MOERU_CLOUDFLARE_ACCOUNT_ID }}
- command: pages deploy ./apps/ui-admin/dist --project-name=airi-server-admin --branch=main
- gitHubToken: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/deploy-cloudflare-workers-dev-server.yml b/.github/workflows/deploy-cloudflare-workers-dev-server.yml
index c409a73f0..9a928a762 100644
--- a/.github/workflows/deploy-cloudflare-workers-dev-server.yml
+++ b/.github/workflows/deploy-cloudflare-workers-dev-server.yml
@@ -130,50 +130,3 @@ jobs:
- name: Print preview URL
run: |
echo "Preview URL: https://server-dev.airi-server-auth.pages.dev/ui/"
-
- deploy-admin-ui:
- name: Deploy - ui-admin (server-dev)
- runs-on: ubuntu-latest
- permissions:
- contents: read
- deployments: write
-
- environment:
- name: AdminServerDev
- url: https://server-dev.airi-server-admin.pages.dev/
-
- steps:
- - uses: actions/checkout@v6
- # Turborepo
- - name: Cache turbo build setup
- uses: actions/cache@v5
- with:
- path: .turbo
- key: ${{ runner.os }}-turbo-admin-${{ github.sha }}
- restore-keys: |
- ${{ runner.os }}-turbo-admin-
- ${{ runner.os }}-turbo-
- - uses: pnpm/action-setup@v4
- - uses: actions/setup-node@v6
- with:
- node-version: lts/*
- cache: pnpm
- - run: pnpm i -g wrangler@4
- - run: pnpm install --frozen-lockfile
- - run: pnpm run build:packages
- - name: Build ui-admin
- run: pnpm -F @proj-airi/ui-admin run build
- env:
- VITE_SERVER_URL: 'https://airi-server-dev.up.railway.app'
-
- - name: Wrangler Pages Deploy
- uses: cloudflare/wrangler-action@v3.14.1
- with:
- apiToken: ${{ secrets.MOERU_CLOUDFLARE_API_TOKEN }}
- accountId: ${{ secrets.MOERU_CLOUDFLARE_ACCOUNT_ID }}
- command: pages deploy ./apps/ui-admin/dist --project-name=airi-server-admin --branch=server-dev
- gitHubToken: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Print preview URL
- run: |
- echo "Preview URL: https://server-dev.airi-server-admin.pages.dev/"
diff --git a/apps/server/README.md b/apps/server/README.md
index 1e86ba0d0..f60f07a3a 100644
--- a/apps/server/README.md
+++ b/apps/server/README.md
@@ -38,7 +38,7 @@ Set this when previewing or deploying auth UI to a different Cloudflare URL.
## `ADMIN_UI_URL`
-`apps/ui-admin` is deployed separately from the server image. The API server still owns the historical `/admin/*` entrypoints and redirects them to **`ADMIN_UI_URL`**.
+The admin UI is deployed from the standalone `proj-airi` repository. The API server still owns the historical `/admin/*` entrypoints and redirects them to **`ADMIN_UI_URL`**.
Default:
diff --git a/apps/server/src/libs/env.ts b/apps/server/src/libs/env.ts
index bf11d7627..3de03c850 100644
--- a/apps/server/src/libs/env.ts
+++ b/apps/server/src/libs/env.ts
@@ -86,8 +86,8 @@ const EnvSchema = object({
AUTH_UI_URL: optional(string(), 'https://accounts.airi.build/ui'),
// Standalone admin UI base URL. The server keeps `/admin/*` as the historical
- // entrypoint and redirects those requests here after ui-admin moved out of
- // the server image.
+ // entrypoint and redirects those requests here after the admin UI moved to
+ // the standalone proj-airi repository.
ADMIN_UI_URL: optional(string(), 'https://admin.airi.build'),
// Canonical user-facing web app origin. Used as the Stripe redirect base
diff --git a/apps/server/src/utils/server-admin-ui.ts b/apps/server/src/utils/server-admin-ui.ts
index c027696e0..bd6b0e2cf 100644
--- a/apps/server/src/utils/server-admin-ui.ts
+++ b/apps/server/src/utils/server-admin-ui.ts
@@ -9,7 +9,7 @@ export const SERVER_DEV_ADMIN_UI_URL = 'https://server-dev.airi-server-admin.pag
*
* Use when:
* - Redirecting server-owned admin UI entrypoints to the standalone
- * `apps/ui-admin` deployment.
+ * admin UI deployment from the `proj-airi` repository.
* - Preserving dashboard route paths and query parameters.
*
* Expects:
diff --git a/apps/ui-admin/README.md b/apps/ui-admin/README.md
deleted file mode 100644
index cd7c93c43..000000000
--- a/apps/ui-admin/README.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# AIRI Admin Dashboard
-
-Admin dashboard for operating the hosted AIRI server. It is a standalone Vue/Vite app deployed to Cloudflare Pages; the API server redirects its historical `/admin/*` entrypoints to this app.
-
-## Use When
-
-- Reviewing server metrics, users, flux balances, LLM router config, and curated Voice Packs.
-- Building operator-only workflows that depend on the server admin API under `/api/admin`.
-
-## Do Not Use When
-
-- Building end-user settings or character-card flows. Those belong in the stage apps and shared stage packages.
-- Adding unauthenticated server UI. This app expects the server admin guard and Better Auth session cookies.
-
-## Commands
-
-```sh
-pnpm -F @proj-airi/ui-admin dev
-pnpm -F @proj-airi/ui-admin typecheck
-pnpm -F @proj-airi/ui-admin build
-```
-
-## Build Output
-
-`pnpm -F @proj-airi/ui-admin build` writes to `apps/ui-admin/dist`. Server builds do not package this output; deploy the directory through the admin Cloudflare Pages workflow.
diff --git a/apps/ui-admin/index.html b/apps/ui-admin/index.html
deleted file mode 100644
index c45c50df5..000000000
--- a/apps/ui-admin/index.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
- AIRI Admin
-
-
-
-
-
-
-
- This dashboard requires JavaScript.
-
-
diff --git a/apps/ui-admin/package.json b/apps/ui-admin/package.json
deleted file mode 100644
index 289a24fb2..000000000
--- a/apps/ui-admin/package.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "name": "@proj-airi/ui-admin",
- "type": "module",
- "private": true,
- "description": "Admin dashboard for Project AIRI",
- "scripts": {
- "build": "vite build",
- "dev": "vite --host",
- "preview": "vite preview",
- "typecheck": "vue-tsc --noEmit"
- },
- "dependencies": {
- "@proj-airi/font-chillroundm": "workspace:^",
- "@proj-airi/stage-shared": "workspace:^",
- "@proj-airi/ui": "workspace:^",
- "@vueuse/core": "^14.2.1",
- "nprogress": "catalog:",
- "pinia": "catalog:",
- "vue": "catalog:",
- "vue-router": "catalog:",
- "vue-sonner": "catalog:"
- },
- "devDependencies": {
- "@iconify-json/lucide": "^1.2.102",
- "@types/nprogress": "^0.2.3",
- "@unocss/reset": "^66.6.8",
- "@vitejs/plugin-vue": "^6.0.6",
- "@vue-macros/volar": "^3.1.2",
- "unocss": "catalog:",
- "vite": "catalog:",
- "vue-macros": "catalog:",
- "vue-tsc": "catalog:"
- }
-}
diff --git a/apps/ui-admin/public/_headers b/apps/ui-admin/public/_headers
deleted file mode 100644
index 7f40d0894..000000000
--- a/apps/ui-admin/public/_headers
+++ /dev/null
@@ -1,4 +0,0 @@
-/assets/*
- cache-control: max-age=31536000
- cache-control: immutable
-
diff --git a/apps/ui-admin/public/_redirects b/apps/ui-admin/public/_redirects
deleted file mode 100644
index 7797f7c6a..000000000
--- a/apps/ui-admin/public/_redirects
+++ /dev/null
@@ -1 +0,0 @@
-/* /index.html 200
diff --git a/apps/ui-admin/src/App.test.ts b/apps/ui-admin/src/App.test.ts
deleted file mode 100644
index a5bbd5743..000000000
--- a/apps/ui-admin/src/App.test.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-// @vitest-environment jsdom
-
-import type { App as VueApp } from 'vue'
-
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { createApp, nextTick } from 'vue'
-import { createRouter, createWebHistory } from 'vue-router'
-
-import App from './App.vue'
-
-import { AdminApiError } from './modules/api'
-
-const mocks = vi.hoisted(() => ({
- me: vi.fn(),
-}))
-
-vi.mock('./modules/api', async (importOriginal) => {
- const actual = await importOriginal()
- return {
- ...actual,
- adminApi: {
- me: mocks.me,
- },
- }
-})
-
-describe('admin app shell', () => {
- let app: VueApp
- let host: HTMLElement
-
- beforeEach(() => {
- mocks.me.mockRejectedValue(new AdminApiError('unauthorized', 401, null))
- window.history.replaceState(null, '', '/llm-router?api_server_url=https%3A%2F%2Fapi.airi.build')
- document.body.innerHTML = '
'
- host = document.querySelector('#app')!
- })
-
- afterEach(() => {
- app.unmount()
- vi.clearAllMocks()
- })
-
- it('shows a sign-in page with backend switching instead of immediately redirecting on 401', async () => {
- const router = createRouter({
- history: createWebHistory('/'),
- routes: [
- { path: '/llm-router', component: { template: '
' } },
- ],
- })
- app = createApp(App)
- app.use(router)
- app.mount(host)
- await router.isReady()
- await flushPromises()
-
- expect(router.currentRoute.value.path).toBe('/llm-router')
- expect(host.textContent).toContain('Sign in to AIRI Admin')
- expect(host.textContent).toContain('Production - api.airi.build')
- const href = host.querySelector('a')?.getAttribute('href')
- expect(href).toContain('https://api.airi.build/auth/sign-in?redirect=')
- expect(decodeURIComponent(href ?? '')).toContain('api_server_url=https%3A%2F%2Fapi.airi.build')
- })
-})
-
-async function flushPromises() {
- await nextTick()
- await Promise.resolve()
- await nextTick()
-}
diff --git a/apps/ui-admin/src/App.vue b/apps/ui-admin/src/App.vue
deleted file mode 100644
index 4ecaa30cf..000000000
--- a/apps/ui-admin/src/App.vue
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
-
-
-
-
- Loading admin session
-
-
-
-
-
-
-
-
-
-
- {{ needsSignIn ? 'Sign in to AIRI Admin' : 'Admin access required' }}
-
-
- {{ needsSignIn ? 'Choose the backend environment, then continue to its auth page.' : accessError }}
-
-
-
- Sign in
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ currentTitle }}
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/ui-admin/src/components/admin-list/AdminListPanel.vue b/apps/ui-admin/src/components/admin-list/AdminListPanel.vue
deleted file mode 100644
index d57fa19be..000000000
--- a/apps/ui-admin/src/components/admin-list/AdminListPanel.vue
+++ /dev/null
@@ -1,187 +0,0 @@
-
-
-
-
-
-
-
- {{ title }}
-
-
- {{ description }}
-
-
-
-
-
-
-
- {{ filter.label }}
-
-
- {{ option.label }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ column.label }}
-
-
-
- {{ column.label }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ emptyLabel }}
-
-
-
-
-
- Loading page {{ loadingPage ?? currentPage }}
-
-
-
-
-
- Page {{ currentPage }} of {{ totalPages }} · {{ pageStart }}-{{ pageEnd }} of {{ totalItems.toLocaleString() }} items
-
-
-
-
-
- Previous
-
-
- Next
-
-
-
-
-
-
diff --git a/apps/ui-admin/src/components/admin-shell/ApiEnvironmentSelect.vue b/apps/ui-admin/src/components/admin-shell/ApiEnvironmentSelect.vue
deleted file mode 100644
index 1ecca4c69..000000000
--- a/apps/ui-admin/src/components/admin-shell/ApiEnvironmentSelect.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-
-
-
- API environment
-
-
- {{ environment.label }} - {{ environment.description }}
-
-
-
-
diff --git a/apps/ui-admin/src/components/llm-router/RouterAdvancedJsonPanel.vue b/apps/ui-admin/src/components/llm-router/RouterAdvancedJsonPanel.vue
deleted file mode 100644
index f76b1f88b..000000000
--- a/apps/ui-admin/src/components/llm-router/RouterAdvancedJsonPanel.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
-
- Advanced JSON
-
-
- Escape hatch for auditing or unsupported future fields.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ error }}
-
-
-
-
-
-
-
-
-
diff --git a/apps/ui-admin/src/components/llm-router/RouterDefaultsEditor.vue b/apps/ui-admin/src/components/llm-router/RouterDefaultsEditor.vue
deleted file mode 100644
index c2e22bdd8..000000000
--- a/apps/ui-admin/src/components/llm-router/RouterDefaultsEditor.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
-
- Defaults
-
-
- Writes default model aliases alongside provider slices when filled.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Recommended voices
-
-
- JSON object written to DEFAULT_TTS_VOICES.
-
-
-
-
-
-
-
-
diff --git a/apps/ui-admin/src/components/llm-router/RouterModeControl.vue b/apps/ui-admin/src/components/llm-router/RouterModeControl.vue
deleted file mode 100644
index af336e76b..000000000
--- a/apps/ui-admin/src/components/llm-router/RouterModeControl.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
-
-
- Write Mode
-
-
- Merge keeps untouched router models. Reset replaces the router model tree with this request.
-
-
-
-
- {{ mode === 'reset' ? 'Reset' : 'Merge' }}
-
-
-
-
-
-
-
-
-
- Existing LLM/TTS router models not included in this request will be dropped by the server.
-
-
-
diff --git a/apps/ui-admin/src/components/llm-router/RouterPreviewPanel.vue b/apps/ui-admin/src/components/llm-router/RouterPreviewPanel.vue
deleted file mode 100644
index 714c98e5f..000000000
--- a/apps/ui-admin/src/components/llm-router/RouterPreviewPanel.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
- {{ title }}
-
-
-
-
-
-
- Slices
-
-
- {{ applied.length }}
-
-
-
-
- Invalidated keys
-
-
- {{ invalidatedKeys.length }}
-
-
-
-
-
-
- Applied
-
-
-
- {{ item.kind }}
- {{ item.modelName }}
-
-
-
-
-
-
- Keys
-
-
-
- {{ key }}
-
-
-
-
-
{{ formatJson(result.preview) }}
-
-
-
-
- No data yet
-
-
-
diff --git a/apps/ui-admin/src/components/llm-router/RouterSliceEditor.vue b/apps/ui-admin/src/components/llm-router/RouterSliceEditor.vue
deleted file mode 100644
index c1ee487c0..000000000
--- a/apps/ui-admin/src/components/llm-router/RouterSliceEditor.vue
+++ /dev/null
@@ -1,177 +0,0 @@
-
-
-
-
-
-
-
- {{ index }}. {{ title }}
-
-
- {{ slice.kind }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Enable streaming upstream
- Writes UNSPEECH_UPSTREAM.streaming for WebSocket TTS.
-
-
-
-
-
-
-
diff --git a/apps/ui-admin/src/components/voice-packs/DatalistField.vue b/apps/ui-admin/src/components/voice-packs/DatalistField.vue
deleted file mode 100644
index ec5fe9e31..000000000
--- a/apps/ui-admin/src/components/voice-packs/DatalistField.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
- {{ label }}
- *
-
-
- {{ description }}
-
-
-
-
-
-
-
diff --git a/apps/ui-admin/src/main.ts b/apps/ui-admin/src/main.ts
deleted file mode 100644
index 4a7817a0a..000000000
--- a/apps/ui-admin/src/main.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import NProgress from 'nprogress'
-
-import { createPinia } from 'pinia'
-import { createApp } from 'vue'
-import { createRouter, createWebHistory } from 'vue-router'
-import { Toaster } from 'vue-sonner'
-
-import App from './App.vue'
-import CapabilityAliasesPage from './pages/CapabilityAliasesPage.vue'
-import FluxPage from './pages/FluxPage.vue'
-import LlmRouterPage from './pages/LlmRouterPage.vue'
-import OverviewPage from './pages/OverviewPage.vue'
-import TtsCatalogPage from './pages/TtsCatalogPage.vue'
-import UsersPage from './pages/UsersPage.vue'
-import VoicePackFormPage from './pages/VoicePackFormPage.vue'
-import VoicePacksPage from './pages/VoicePacksPage.vue'
-
-import '@proj-airi/font-chillroundm/index.css'
-import '@unocss/reset/tailwind.css'
-import 'vue-sonner/style.css'
-import './styles/main.css'
-import 'uno.css'
-
-const router = createRouter({
- history: createWebHistory('/'),
- routes: [
- { path: '/', component: OverviewPage },
- { path: '/users', component: UsersPage },
- { path: '/flux', component: FluxPage },
- { path: '/llm-router', component: LlmRouterPage },
- { path: '/capability-aliases', component: CapabilityAliasesPage },
- { path: '/tts', component: TtsCatalogPage },
- { path: '/voice-packs', component: VoicePacksPage },
- { path: '/voice-packs/new', name: 'voice-pack-new', component: VoicePackFormPage },
- { path: '/voice-packs/:id/edit', name: 'voice-pack-edit', component: VoicePackFormPage },
- ],
-})
-
-router.beforeEach((to, from) => {
- if (to.path !== from.path)
- NProgress.start()
-})
-
-router.afterEach(() => {
- NProgress.done()
-})
-
-createApp(App)
- .use(createPinia())
- .use(router)
- .component('Toaster', Toaster)
- .mount('#app')
diff --git a/apps/ui-admin/src/modules/api.test.ts b/apps/ui-admin/src/modules/api.test.ts
deleted file mode 100644
index 25aa69ce8..000000000
--- a/apps/ui-admin/src/modules/api.test.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { describe, expect, it } from 'vitest'
-
-import { buildAdminSignInUrl } from './api'
-
-describe('ui-admin API URL helpers', () => {
- it('builds sign-in URLs with the absolute admin return URL', () => {
- expect(buildAdminSignInUrl(
- 'http://127.0.0.1:3000',
- 'http://127.0.0.1:5178/llm-router?api_server_url=http%3A%2F%2F127.0.0.1%3A3000',
- )).toBe(
- 'http://127.0.0.1:3000/auth/sign-in?redirect=http%3A%2F%2F127.0.0.1%3A5178%2Fllm-router%3Fapi_server_url%3Dhttp%253A%252F%252F127.0.0.1%253A3000',
- )
- })
-
- it('builds local dev sign-in URLs on the local API origin', () => {
- expect(buildAdminSignInUrl(
- 'http://localhost:3000',
- 'http://localhost:5178/',
- )).toBe(
- 'http://localhost:3000/auth/sign-in?redirect=http%3A%2F%2Flocalhost%3A5178%2F',
- )
- })
-})
diff --git a/apps/ui-admin/src/modules/api.ts b/apps/ui-admin/src/modules/api.ts
deleted file mode 100644
index b30e9e802..000000000
--- a/apps/ui-admin/src/modules/api.ts
+++ /dev/null
@@ -1,550 +0,0 @@
-import { defaultApiServerUrl, getServerAdminBootstrapContext } from './server-admin-context'
-
-export interface AdminUser {
- id: string
- name: string
- email: string
- emailVerified: boolean
- image: string | null
- createdAt: string
- updatedAt: string
- flux: number
- stripeCustomerId: string | null
-}
-
-export interface AdminMe {
- role: 'admin'
- user: Pick
-}
-
-export interface AdminMetrics {
- totalUsers: number
- verifiedUsers: number
- activeSessions: number
- currentFlux: number
- issuedFlux: number
- llmRequests24h: number
- llmFlux24h: number
- adminSeats: number
- grafanaEmbedUrl: string | null
-}
-
-export interface FluxTransaction {
- id: string
- type: string
- amount: number
- balanceBefore: number
- balanceAfter: number
- description: string
- metadata: unknown
- createdAt: string
-}
-
-export interface AdminUsersPage {
- users: AdminUser[]
- hasMore: boolean
- nextOffset: number | null
- total: number
-}
-
-export interface AdminRouterOpenRouterSlice {
- kind: 'openrouter'
- modelName: string
- overrideModel: string
- plaintextKey?: string
- baseURL?: string
- keyEntryId?: string
- existingKeyEntryId?: string
- headerTemplate?: string
-}
-
-export interface AdminRouterBedrockSlice {
- kind: 'bedrock'
- modelName: string
- overrideModel: string
- plaintextKey?: string
- baseURL?: string
- keyEntryId?: string
- existingKeyEntryId?: string
- headerTemplate?: string
-}
-
-export interface AdminRouterOpenAICompatibleSlice {
- kind: 'openai-compatible'
- modelName: string
- overrideModel: string
- plaintextKey?: string
- baseURL?: string
- keyEntryId?: string
- existingKeyEntryId?: string
- headerTemplate?: string
-}
-
-export interface AdminRouterAzureSlice {
- kind: 'azure'
- modelName: string
- region: string
- defaultVoice?: string
- plaintextKey?: string
- keyEntryId?: string
- existingKeyEntryId?: string
-}
-
-export interface AdminRouterDashscopeSlice {
- kind: 'dashscope-cosyvoice'
- modelName: string
- region: 'intl' | 'cn'
- upstreamModel: string
- plaintextKey?: string
- keyEntryId?: string
- existingKeyEntryId?: string
-}
-
-export interface AdminRouterStepfunSlice {
- kind: 'stepfun'
- modelName: string
- upstreamModel?: 'stepaudio-2.5-tts' | 'step-tts-2' | 'step-tts-mini'
- defaultVoice?: string
- instruction?: string
- plaintextKey?: string
- keyEntryId?: string
- existingKeyEntryId?: string
-}
-
-export interface AdminRouterUnspeechSlice {
- kind: 'unspeech'
- restBaseURL: string
- streaming?: {
- upstreamURL: string
- plaintextKey?: string
- keyEntryId?: string
- existingKeyEntryId?: string
- models?: Array<{ id: string, name?: string, description?: string }>
- defaultModel?: string
- }
-}
-
-export interface AdminRouterAliyunNlsAsrSlice {
- kind: 'aliyun-nls-asr'
- modelName: string
- accessKeyId: string
- appKey: string
- region?: 'cn-shanghai' | 'cn-shanghai-internal' | 'cn-beijing' | 'cn-beijing-internal' | 'cn-shenzhen' | 'cn-shenzhen-internal'
- plaintextKey?: string
- keyEntryId?: string
- existingKeyEntryId?: string
-}
-
-export type AdminRouterConfigSlice
- = | AdminRouterOpenRouterSlice
- | AdminRouterBedrockSlice
- | AdminRouterOpenAICompatibleSlice
- | AdminRouterAzureSlice
- | AdminRouterDashscopeSlice
- | AdminRouterStepfunSlice
- | AdminRouterAliyunNlsAsrSlice
- | AdminRouterUnspeechSlice
-
-export interface AdminRouterConfigRequest {
- mode?: 'merge' | 'reset'
- dryRun?: boolean
- slices?: AdminRouterConfigSlice[]
- defaults?: {
- chatModel?: string
- ttsModel?: string
- ttsVoices?: Record>
- }
-}
-
-export interface AdminRouterConfigResult {
- applied: Array>
- invalidatedKeys: string[]
- preview: Record
-}
-
-export interface AdminRouterConfigCurrent {
- request: AdminRouterConfigRequest
- preview: Record
- loadedAt: string
- missingKeys: string[]
-}
-
-export interface VoicePackParams {
- pitch?: number
- volume?: number
- rate?: number
-}
-
-export interface VoicePack {
- id: string
- name: string
- description: string | null
- provider: string
- model: string
- voiceId: string
- upstreamVoiceId: string
- ttsModelId: string
- params: VoicePackParams
- costMultiplier: number
- enabled: boolean
- createdAt: string
- updatedAt: string
-}
-
-export interface VoicePackPayload {
- name: string
- description?: string
- provider: string
- model: string
- voiceId: string
- upstreamVoiceId: string
- ttsModelId: string
- params?: VoicePackParams
- costMultiplier: number
- enabled?: boolean
-}
-
-export interface SpeechModel {
- id: string
- name: string
-}
-
-export interface SpeechModelsResult {
- models: SpeechModel[]
- default: string | null
-}
-
-export interface SpeechVoice {
- id: string
- name: string
- description?: string
- labels?: Record
- tags?: string[]
- languages?: { code: string, title: string }[]
- preview_audio_url?: string
-}
-
-export interface SpeechVoicesResult {
- voices: SpeechVoice[]
- recommended: Record
-}
-
-export interface SpeechTestPayload {
- model: string
- input: string
- voice: string
- speed?: number
- extra_body?: {
- voice_pack?: Record
- }
-}
-
-export type CapabilityAliasSurface = 'llm' | 'asr'
-export type CapabilityAliasRoutePool = 'primary' | 'fallback'
-
-export interface CapabilityAliasRoute {
- id: string
- aliasId: string
- routerModelId: string
- pool: CapabilityAliasRoutePool
- enabled: boolean
- weight: number
- displayOrder: number
- createdAt: string
- updatedAt: string
-}
-
-export interface CapabilityAlias {
- id: string
- surface: CapabilityAliasSurface
- aliasId: string
- displayName: string
- enabled: boolean
- displayOrder: number
- fallbackEnabled: boolean
- loadBalancingEnabled: boolean
- routes: CapabilityAliasRoute[]
- createdAt: string
- updatedAt: string
-}
-
-export interface ProviderCatalogTtsModel {
- id: string
- routerModelId: string
- provider: string
- displayName: string
- enabled: boolean
- displayOrder: number
- lastSyncedAt: string | null
- createdAt: string
- updatedAt: string
-}
-
-export interface ProviderCatalogTtsVoice {
- id: string
- ttsModelId: string
- providerVoiceId: string
- displayName: string
- enabled: boolean
- displayOrder: number
- languages: Array<{ code: string, title?: string }>
- labels: Record
- previewAudioUrl: string | null
- source: 'provider-sync' | 'manual'
- lastSyncedAt: string | null
- createdAt: string
- updatedAt: string
-}
-
-export class AdminApiError extends Error {
- constructor(
- message: string,
- public readonly status: number,
- public readonly payload: unknown,
- ) {
- super(message)
- this.name = 'AdminApiError'
- }
-}
-
-export function apiServerUrl(): string {
- return getServerAdminBootstrapContext()?.apiServerUrl ?? defaultApiServerUrl()
-}
-
-/**
- * Builds an API-owned sign-in URL that returns to the exact admin page.
- *
- * Use when:
- * - The standalone admin app needs to bounce through the API auth route.
- * - The admin app may be hosted on a different origin than the auth UI.
- *
- * Expects:
- * - `currentUrl` is the browser's absolute admin URL.
- *
- * Returns:
- * - An API `/auth/sign-in` URL carrying an absolute trusted return target.
- */
-export function buildAdminSignInUrl(apiServerUrl: string, currentUrl: string): string {
- const url = new URL('/auth/sign-in', apiServerUrl)
- url.searchParams.set('redirect', currentUrl)
- return url.toString()
-}
-
-export function signInUrl(): string {
- return buildAdminSignInUrl(apiServerUrl(), window.location.href)
-}
-
-async function adminFetch(path: string, init: RequestInit = {}): Promise {
- const endpoint = new URL(`/api/admin${path}`, apiServerUrl())
- return fetchJson(endpoint, init)
-}
-
-async function publicFetch(path: string, init: RequestInit = {}): Promise {
- const endpoint = new URL(`/api/v1${path}`, apiServerUrl())
- return fetchJson(endpoint, init)
-}
-
-async function fetchJson(endpoint: URL, init: RequestInit = {}): Promise {
- const headers = new Headers(init.headers)
-
- if (init.body && !headers.has('Content-Type'))
- headers.set('Content-Type', 'application/json')
-
- const response = await fetch(endpoint.toString(), {
- ...init,
- headers,
- credentials: 'include',
- })
-
- let payload: unknown = null
- try {
- payload = await response.json()
- }
- catch {
- const contentType = response.headers.get('Content-Type')
- throw new AdminApiError(
- `Expected JSON from ${endpoint.pathname}, got ${contentType ?? 'an empty response'}. Check api_server_url.`,
- response.status,
- null,
- )
- }
-
- if (!response.ok) {
- const message = extractErrorMessage(payload) ?? `Admin API request failed (${response.status})`
- throw new AdminApiError(message, response.status, payload)
- }
-
- return payload as T
-}
-
-async function publicFetchBlob(path: string, init: RequestInit = {}): Promise {
- const endpoint = new URL(`/api/v1${path}`, apiServerUrl())
- const headers = new Headers(init.headers)
-
- if (init.body && !headers.has('Content-Type'))
- headers.set('Content-Type', 'application/json')
-
- const response = await fetch(endpoint.toString(), {
- ...init,
- headers,
- credentials: 'include',
- })
-
- if (!response.ok) {
- let payload: unknown = null
- try {
- payload = await response.json()
- }
- catch {
- payload = await response.text().catch(() => null)
- }
- const message = extractErrorMessage(payload) ?? `Audio API request failed (${response.status})`
- throw new AdminApiError(message, response.status, payload)
- }
-
- return await response.blob()
-}
-
-function extractErrorMessage(payload: unknown): string | null {
- if (!payload || typeof payload !== 'object')
- return null
- const maybe = payload as { message?: unknown, error?: unknown }
- if (typeof maybe.message === 'string')
- return maybe.message
- if (typeof maybe.error === 'string')
- return maybe.error
- return null
-}
-
-export const adminApi = {
- me: () => adminFetch('/me'),
- metrics: () => adminFetch('/metrics'),
- users: (params: { query?: string, limit?: number, offset?: number, sortDirection?: string, sortKey?: string, status?: string }) => {
- const query = new URLSearchParams()
- if (params.query)
- query.set('query', params.query)
- if (params.limit != null)
- query.set('limit', String(params.limit))
- if (params.offset != null)
- query.set('offset', String(params.offset))
- if (params.sortDirection)
- query.set('sortDirection', params.sortDirection)
- if (params.sortKey)
- query.set('sortKey', params.sortKey)
- if (params.status)
- query.set('status', params.status)
- const suffix = query.toString() ? `?${query.toString()}` : ''
- return adminFetch(`/users${suffix}`)
- },
- user: (id: string) => adminFetch<{ user: AdminUser, recentFluxTransactions: FluxTransaction[] }>(`/users/${encodeURIComponent(id)}`),
- grantUserFlux: (id: string, body: { amount: number, description: string, idempotencyKey?: string }) =>
- adminFetch<{ balanceBefore: number, balanceAfter: number, fluxTransactionId: string, idempotent: boolean }>(`/users/${encodeURIComponent(id)}/flux/grant`, {
- method: 'POST',
- body: JSON.stringify(body),
- }),
- setUserFlux: (id: string, body: { balance: number, description: string }) =>
- adminFetch<{ balanceBefore: number, balanceAfter: number, fluxTransactionId: string | null, changed: boolean }>(`/users/${encodeURIComponent(id)}/flux`, {
- method: 'PATCH',
- body: JSON.stringify(body),
- }),
- fluxGrantPreview: (body: { amount: number, description: string, emails: string[], idempotencyKey?: string }) =>
- adminFetch('/flux-grants?dryRun=true', {
- method: 'POST',
- body: JSON.stringify(body),
- }),
- fluxGrant: (body: { amount: number, description: string, emails: string[], idempotencyKey?: string }) =>
- adminFetch('/flux-grants', {
- method: 'POST',
- body: JSON.stringify(body),
- }),
- applyRouterConfig: (body: AdminRouterConfigRequest, dryRun: boolean) =>
- adminFetch('/config/router', {
- method: 'POST',
- body: JSON.stringify({ ...body, dryRun }),
- }),
- routerConfig: () => adminFetch('/config/router'),
- speechModels: async (): Promise => {
- const data = await publicFetch<{ default?: unknown, models?: SpeechModel[] }>('/audio/models')
- return {
- models: Array.isArray(data.models) ? data.models : [],
- default: typeof data.default === 'string' ? data.default : null,
- }
- },
- speechVoices: async (model: string): Promise => {
- const query = new URLSearchParams()
- query.set('model', model)
- const data = await publicFetch>(`/audio/voices?${query.toString()}`)
- return {
- voices: Array.isArray(data.voices) ? data.voices : [],
- recommended: data.recommended && typeof data.recommended === 'object' ? data.recommended : {},
- }
- },
- testSpeech: (body: SpeechTestPayload) =>
- publicFetchBlob('/audio/speech', {
- method: 'POST',
- body: JSON.stringify(body),
- }),
- voicePacks: () => adminFetch('/voice-packs'),
- createVoicePack: (body: VoicePackPayload) =>
- adminFetch('/voice-packs', {
- method: 'POST',
- body: JSON.stringify(body),
- }),
- updateVoicePack: (id: string, body: Partial) =>
- adminFetch(`/voice-packs/${encodeURIComponent(id)}`, {
- method: 'PATCH',
- body: JSON.stringify(body),
- }),
- disableVoicePack: (id: string) =>
- adminFetch(`/voice-packs/${encodeURIComponent(id)}/disable`, {
- method: 'POST',
- }),
- capabilityAliases: (surface?: CapabilityAliasSurface) => {
- const suffix = surface ? `?surface=${encodeURIComponent(surface)}` : ''
- return adminFetch(`/capability-aliases${suffix}`)
- },
- syncCapabilityAliases: (surface: CapabilityAliasSurface) =>
- adminFetch<{ aliases: CapabilityAlias[] }>('/capability-aliases/sync', {
- method: 'POST',
- body: JSON.stringify({ surface }),
- }),
- updateCapabilityAlias: (id: string, body: Partial>) =>
- adminFetch(`/capability-aliases/${encodeURIComponent(id)}`, {
- method: 'PATCH',
- body: JSON.stringify(body),
- }),
- updateCapabilityAliasRoute: (id: string, body: Partial>) =>
- adminFetch(`/capability-aliases/routes/${encodeURIComponent(id)}`, {
- method: 'PATCH',
- body: JSON.stringify(body),
- }),
- providerCatalogTtsModels: () => adminFetch('/provider-catalog/tts/models'),
- syncProviderCatalogTtsModels: () =>
- adminFetch<{ models: ProviderCatalogTtsModel[] }>('/provider-catalog/tts/models/sync', {
- method: 'POST',
- }),
- updateProviderCatalogTtsModel: (id: string, body: Partial>) =>
- adminFetch(`/provider-catalog/tts/models/${encodeURIComponent(id)}`, {
- method: 'PATCH',
- body: JSON.stringify(body),
- }),
- providerCatalogTtsVoices: (model: string) => {
- const query = new URLSearchParams({ model })
- return adminFetch(`/provider-catalog/tts/voices?${query.toString()}`)
- },
- syncProviderCatalogTtsVoices: (routerModelId: string) =>
- adminFetch<{ voices: ProviderCatalogTtsVoice[], syncedCount: number }>('/provider-catalog/tts/voices/sync', {
- method: 'POST',
- body: JSON.stringify({ routerModelId }),
- }),
- updateProviderCatalogTtsVoice: (id: string, body: Partial>) =>
- adminFetch(`/provider-catalog/tts/voices/${encodeURIComponent(id)}`, {
- method: 'PATCH',
- body: JSON.stringify(body),
- }),
- generateProviderCatalogTtsVoicePreview: (id: string, body: { text?: string, responseFormat?: string } = {}) =>
- adminFetch<{ voice: ProviderCatalogTtsVoice, contentType: string, byteLength: number }>(`/provider-catalog/tts/voices/${encodeURIComponent(id)}/preview`, {
- method: 'POST',
- body: JSON.stringify(body),
- }),
-}
diff --git a/apps/ui-admin/src/modules/router-config-form.test.ts b/apps/ui-admin/src/modules/router-config-form.test.ts
deleted file mode 100644
index c9b2e3fd3..000000000
--- a/apps/ui-admin/src/modules/router-config-form.test.ts
+++ /dev/null
@@ -1,254 +0,0 @@
-import { describe, expect, it } from 'vitest'
-
-import {
- buildRouterConfigRequest,
- createRouterConfigFormState,
- createRouterSliceDraft,
- defaultTtsVoicesJson,
- formatRouterConfigRequestJson,
- formStateFromRequestJson,
- validateRouterConfigForm,
-} from './router-config-form'
-
-describe('router config form builder', () => {
- it('compiles the default OpenRouter chat setup', () => {
- const form = createRouterConfigFormState()
- const openrouter = form.slices[0]
- if (openrouter.kind !== 'openrouter')
- throw new Error('expected default slice to be OpenRouter')
- openrouter.plaintextKey = 'sk-openrouter'
-
- expect(buildRouterConfigRequest(form)).toEqual({
- errors: [],
- request: {
- mode: 'merge',
- slices: [{
- kind: 'openrouter',
- modelName: 'chat-default',
- overrideModel: 'openai/gpt-4o-mini',
- plaintextKey: 'sk-openrouter',
- baseURL: 'https://openrouter.ai/api/v1',
- }],
- defaults: {
- chatModel: 'chat-default',
- },
- },
- })
- })
-
- it('compiles Bedrock and OpenAI-compatible LLM slices', () => {
- const bedrock = createRouterSliceDraft('bedrock', 'bedrock-test')
- bedrock.plaintextKey = 'bedrock-token'
- const compatible = createRouterSliceDraft('openai-compatible', 'compatible-test')
- compatible.plaintextKey = 'sk-compatible'
- compatible.baseURL = 'https://llm.example.com/v1'
-
- expect(buildRouterConfigRequest({
- mode: 'merge',
- slices: [bedrock, compatible],
- defaults: { chatModel: '', ttsModel: '', ttsVoicesJson: '' },
- }).request?.slices).toEqual([
- {
- kind: 'bedrock',
- modelName: 'chat-bedrock',
- overrideModel: 'us.anthropic.claude-3-5-sonnet-20241022-v2:0',
- plaintextKey: 'bedrock-token',
- baseURL: 'https://bedrock-mantle.us-east-1.api.aws/v1',
- keyEntryId: 'bedrock-prod-1',
- },
- {
- kind: 'openai-compatible',
- modelName: 'chat-compatible',
- overrideModel: 'gpt-4o-mini',
- plaintextKey: 'sk-compatible',
- baseURL: 'https://llm.example.com/v1',
- keyEntryId: 'openai-compatible-prod-1',
- },
- ])
- })
-
- it('compiles Azure speech defaults without OpenRouter-only fields', () => {
- const azure = createRouterSliceDraft('azure', 'azure-test')
- azure.plaintextKey = 'azure-key'
- azure.defaultVoice = 'zh-CN-XiaoxiaoNeural'
- azure.keyEntryId = 'azure-eastasia-1'
- const form = {
- mode: 'merge' as const,
- slices: [azure],
- defaults: {
- chatModel: '',
- ttsModel: 'microsoft/v1',
- ttsVoicesJson: '',
- },
- }
-
- expect(buildRouterConfigRequest(form).request).toEqual({
- mode: 'merge',
- slices: [{
- kind: 'azure',
- modelName: 'microsoft/v1',
- region: 'eastasia',
- defaultVoice: 'zh-CN-XiaoxiaoNeural',
- plaintextKey: 'azure-key',
- keyEntryId: 'azure-eastasia-1',
- }],
- defaults: {
- ttsModel: 'microsoft/v1',
- },
- })
- })
-
- it('preserves DashScope region and upstream model', () => {
- const dashscope = createRouterSliceDraft('dashscope-cosyvoice', 'dashscope-test')
- dashscope.region = 'cn'
- dashscope.upstreamModel = 'cosyvoice-v1'
- dashscope.plaintextKey = 'dashscope-key'
-
- expect(buildRouterConfigRequest({
- mode: 'merge',
- slices: [dashscope],
- defaults: { chatModel: '', ttsModel: '', ttsVoicesJson: '' },
- }).request?.slices).toEqual([{
- kind: 'dashscope-cosyvoice',
- modelName: 'alibaba/cosyvoice-v2',
- region: 'cn',
- upstreamModel: 'cosyvoice-v1',
- plaintextKey: 'dashscope-key',
- }])
- })
-
- it('compiles StepFun optional instruction and voice fields', () => {
- const stepfun = createRouterSliceDraft('stepfun', 'stepfun-test')
- stepfun.plaintextKey = 'stepfun-key'
- stepfun.defaultVoice = 'cixingnansheng'
- stepfun.instruction = 'Speak warmly'
-
- expect(buildRouterConfigRequest({
- mode: 'merge',
- slices: [stepfun],
- defaults: { chatModel: '', ttsModel: '', ttsVoicesJson: '' },
- }).request?.slices).toEqual([{
- kind: 'stepfun',
- modelName: 'stepfun/stepaudio-2.5-tts',
- upstreamModel: 'stepaudio-2.5-tts',
- defaultVoice: 'cixingnansheng',
- instruction: 'Speak warmly',
- plaintextKey: 'stepfun-key',
- }])
- })
-
- it('compiles UnSpeech REST-only and streaming requests', () => {
- const restOnly = createRouterSliceDraft('unspeech', 'unspeech-rest')
- const streaming = createRouterSliceDraft('unspeech', 'unspeech-stream')
- streaming.streamingEnabled = true
- streaming.streamingPlaintextKey = 'volcengine-key'
- streaming.streamingDefaultModel = 'volcengine/seed-tts-2.0'
-
- expect(buildRouterConfigRequest({
- mode: 'merge',
- slices: [restOnly],
- defaults: { chatModel: '', ttsModel: '', ttsVoicesJson: '' },
- }).request?.slices).toEqual([{
- kind: 'unspeech',
- restBaseURL: 'http://airi-unspeech.railway.internal:5933',
- }])
-
- expect(buildRouterConfigRequest({
- mode: 'merge',
- slices: [streaming],
- defaults: { chatModel: '', ttsModel: '', ttsVoicesJson: '' },
- }).request?.slices).toEqual([{
- kind: 'unspeech',
- restBaseURL: 'http://airi-unspeech.railway.internal:5933',
- streaming: {
- upstreamURL: 'ws://airi-unspeech.railway.internal:5933/v1/audio/speech/stream',
- plaintextKey: 'volcengine-key',
- models: [{
- id: 'volcengine/seed-tts-2.0',
- name: 'Seed TTS 2.0',
- }],
- defaultModel: 'volcengine/seed-tts-2.0',
- },
- }])
- })
-
- it('validates missing keys, URL schemes, empty aliases, and duplicate UnSpeech slices', () => {
- const openrouter = createRouterSliceDraft('openrouter', 'bad-openrouter')
- openrouter.modelName = ''
- openrouter.baseURL = 'ftp://openrouter.local'
- const unspeechA = createRouterSliceDraft('unspeech', 'unspeech-a')
- const unspeechB = createRouterSliceDraft('unspeech', 'unspeech-b')
- unspeechA.streamingEnabled = true
- unspeechA.streamingUpstreamURL = 'https://not-websocket.local'
-
- expect(validateRouterConfigForm({
- mode: 'merge',
- slices: [openrouter, unspeechA, unspeechB],
- defaults: { chatModel: '', ttsModel: '', ttsVoicesJson: '' },
- })).toEqual(expect.arrayContaining([
- 'Only one UnSpeech slice can be submitted at a time.',
- 'Slice 1 (OpenRouter): model alias is required.',
- 'Slice 1 (OpenRouter): provider key is required unless an existing key is loaded.',
- 'Slice 1 (OpenRouter): base URL must start with http:// or https://.',
- 'Slice 2 (UnSpeech): streaming URL must start with ws:// or wss://.',
- 'Slice 2 (UnSpeech): streaming provider key is required unless an existing key is loaded.',
- ]))
- })
-
- it('preserves loaded existing key entries when plaintext fields are blank', () => {
- const imported = formStateFromRequestJson(JSON.stringify({
- mode: 'merge',
- slices: [{
- kind: 'openrouter',
- modelName: 'chat-default',
- overrideModel: 'openai/gpt-4o-mini',
- baseURL: 'https://openrouter.ai/api/v1',
- keyEntryId: 'openrouter-prod-1',
- existingKeyEntryId: 'openrouter-prod-1',
- }],
- defaults: {
- chatModel: 'chat-default',
- },
- }))
-
- expect(buildRouterConfigRequest(imported)).toEqual({
- errors: [],
- request: {
- mode: 'merge',
- slices: [{
- kind: 'openrouter',
- modelName: 'chat-default',
- overrideModel: 'openai/gpt-4o-mini',
- baseURL: 'https://openrouter.ai/api/v1',
- keyEntryId: 'openrouter-prod-1',
- existingKeyEntryId: 'openrouter-prod-1',
- }],
- defaults: {
- chatModel: 'chat-default',
- },
- },
- })
- })
-
- it('round-trips supported advanced JSON through form import and export', () => {
- const form = createRouterConfigFormState()
- const openrouter = form.slices[0]
- if (openrouter.kind !== 'openrouter')
- throw new Error('expected default slice to be OpenRouter')
- openrouter.plaintextKey = 'sk-openrouter'
- form.defaults.ttsVoicesJson = defaultTtsVoicesJson()
- const request = buildRouterConfigRequest(form).request
- if (!request)
- throw new Error('expected valid request')
-
- const imported = formStateFromRequestJson(formatRouterConfigRequestJson(request))
- expect(buildRouterConfigRequest(imported).request).toEqual(request)
- })
-
- it('rejects unsupported advanced JSON slice kinds', () => {
- expect(() => formStateFromRequestJson(JSON.stringify({
- mode: 'merge',
- slices: [{ kind: 'future-provider' }],
- }))).toThrow('Unsupported router slice kind "future-provider".')
- })
-})
diff --git a/apps/ui-admin/src/modules/router-config-form.ts b/apps/ui-admin/src/modules/router-config-form.ts
deleted file mode 100644
index 49eef8631..000000000
--- a/apps/ui-admin/src/modules/router-config-form.ts
+++ /dev/null
@@ -1,780 +0,0 @@
-import type {
- AdminRouterAliyunNlsAsrSlice,
- AdminRouterAzureSlice,
- AdminRouterBedrockSlice,
- AdminRouterConfigRequest,
- AdminRouterConfigSlice,
- AdminRouterDashscopeSlice,
- AdminRouterOpenAICompatibleSlice,
- AdminRouterOpenRouterSlice,
- AdminRouterStepfunSlice,
- AdminRouterUnspeechSlice,
-} from './api'
-
-import { errorMessageFromUnknown } from '@proj-airi/stage-shared'
-
-export type RouterConfigMode = 'merge' | 'reset'
-export type RouterSliceKind = AdminRouterConfigSlice['kind']
-export type DashscopeRegion = AdminRouterDashscopeSlice['region']
-export type StepfunModel = NonNullable
-export type AliyunNlsRegion = NonNullable
-
-export interface RouterDefaultsDraft {
- chatModel: string
- ttsModel: string
- ttsVoicesJson: string
-}
-
-interface SliceDraftBase {
- id: string
- kind: RouterSliceKind
-}
-
-export interface OpenRouterSliceDraft extends SliceDraftBase {
- kind: 'openrouter'
- modelName: string
- overrideModel: string
- plaintextKey: string
- baseURL: string
- keyEntryId: string
- existingKeyEntryId: string
- headerTemplate: string
-}
-
-export interface BedrockSliceDraft extends SliceDraftBase {
- kind: 'bedrock'
- modelName: string
- overrideModel: string
- plaintextKey: string
- baseURL: string
- keyEntryId: string
- existingKeyEntryId: string
- headerTemplate: string
-}
-
-export interface OpenAICompatibleSliceDraft extends SliceDraftBase {
- kind: 'openai-compatible'
- modelName: string
- overrideModel: string
- plaintextKey: string
- baseURL: string
- keyEntryId: string
- existingKeyEntryId: string
- headerTemplate: string
-}
-
-type LlmSliceDraft = OpenRouterSliceDraft | BedrockSliceDraft | OpenAICompatibleSliceDraft
-
-export interface AzureSliceDraft extends SliceDraftBase {
- kind: 'azure'
- modelName: string
- region: string
- defaultVoice: string
- plaintextKey: string
- keyEntryId: string
- existingKeyEntryId: string
-}
-
-export interface DashscopeSliceDraft extends SliceDraftBase {
- kind: 'dashscope-cosyvoice'
- modelName: string
- region: DashscopeRegion
- upstreamModel: string
- plaintextKey: string
- keyEntryId: string
- existingKeyEntryId: string
-}
-
-export interface StepfunSliceDraft extends SliceDraftBase {
- kind: 'stepfun'
- modelName: string
- upstreamModel: StepfunModel
- defaultVoice: string
- instruction: string
- plaintextKey: string
- keyEntryId: string
- existingKeyEntryId: string
-}
-
-export interface UnspeechSliceDraft extends SliceDraftBase {
- kind: 'unspeech'
- restBaseURL: string
- streamingEnabled: boolean
- streamingUpstreamURL: string
- streamingPlaintextKey: string
- streamingKeyEntryId: string
- streamingExistingKeyEntryId: string
- streamingModelsJson: string
- streamingDefaultModel: string
-}
-
-export interface AliyunNlsAsrSliceDraft extends SliceDraftBase {
- kind: 'aliyun-nls-asr'
- modelName: string
- accessKeyId: string
- appKey: string
- region: AliyunNlsRegion
- plaintextKey: string
- keyEntryId: string
- existingKeyEntryId: string
-}
-
-export type RouterSliceDraft
- = | OpenRouterSliceDraft
- | BedrockSliceDraft
- | OpenAICompatibleSliceDraft
- | AzureSliceDraft
- | DashscopeSliceDraft
- | StepfunSliceDraft
- | AliyunNlsAsrSliceDraft
- | UnspeechSliceDraft
-
-export interface RouterConfigFormState {
- mode: RouterConfigMode
- slices: RouterSliceDraft[]
- defaults: RouterDefaultsDraft
-}
-
-export interface RouterConfigBuildResult {
- request: AdminRouterConfigRequest | null
- errors: string[]
-}
-
-const DEFAULT_TTS_VOICES_JSON = '{\n "alibaba/cosyvoice-v2": {\n "zh-CN": "longxiaochun_v2"\n }\n}'
-const DEFAULT_STREAMING_MODELS_JSON = '[\n {\n "id": "volcengine/seed-tts-2.0",\n "name": "Seed TTS 2.0"\n }\n]'
-
-let draftId = 0
-
-export const ROUTER_SLICE_KIND_OPTIONS: Array<{ label: string, value: RouterSliceKind, description: string }> = [
- { label: 'OpenRouter', value: 'openrouter', description: 'LLM chat model alias' },
- { label: 'Bedrock', value: 'bedrock', description: 'Amazon Bedrock OpenAI-compatible chat alias' },
- { label: 'OpenAI Compatible', value: 'openai-compatible', description: 'Custom OpenAI-compatible chat alias' },
- { label: 'Azure Speech', value: 'azure', description: 'Microsoft TTS model alias' },
- { label: 'DashScope CosyVoice', value: 'dashscope-cosyvoice', description: 'Alibaba TTS model alias' },
- { label: 'StepFun TTS', value: 'stepfun', description: 'StepAudio / Step TTS model alias' },
- { label: 'Aliyun NLS ASR', value: 'aliyun-nls-asr', description: 'Alibaba realtime ASR model alias' },
- { label: 'UnSpeech', value: 'unspeech', description: 'REST and optional streaming TTS upstream' },
-]
-
-export const DASHSCOPE_REGION_OPTIONS: Array<{ label: string, value: DashscopeRegion, description: string }> = [
- { label: 'International', value: 'intl', description: 'dashscope-intl.aliyuncs.com' },
- { label: 'China', value: 'cn', description: 'dashscope.aliyuncs.com' },
-]
-
-export const STEPFUN_MODEL_OPTIONS: Array<{ label: string, value: StepfunModel }> = [
- { label: 'StepAudio 2.5 TTS', value: 'stepaudio-2.5-tts' },
- { label: 'Step TTS 2', value: 'step-tts-2' },
- { label: 'Step TTS Mini', value: 'step-tts-mini' },
-]
-
-export const ALIYUN_NLS_REGION_OPTIONS: Array<{ label: string, value: AliyunNlsRegion }> = [
- { label: 'Shanghai', value: 'cn-shanghai' },
- { label: 'Shanghai Internal', value: 'cn-shanghai-internal' },
- { label: 'Beijing', value: 'cn-beijing' },
- { label: 'Beijing Internal', value: 'cn-beijing-internal' },
- { label: 'Shenzhen', value: 'cn-shenzhen' },
- { label: 'Shenzhen Internal', value: 'cn-shenzhen-internal' },
-]
-
-/**
- * Creates the default LLM Router form state.
- *
- * Use when:
- * - The admin page needs a fresh editable request with the common OpenRouter
- * chat-default path prefilled.
- *
- * Returns:
- * - Mutable form state that can be compiled by {@link buildRouterConfigRequest}.
- */
-export function createRouterConfigFormState(): RouterConfigFormState {
- return {
- mode: 'merge',
- slices: [createRouterSliceDraft('openrouter')],
- defaults: {
- chatModel: 'chat-default',
- ttsModel: '',
- ttsVoicesJson: '',
- },
- }
-}
-
-/**
- * Creates an editable provider slice draft.
- *
- * Use when:
- * - The admin adds a provider card or imports a supported JSON slice.
- *
- * Returns:
- * - A draft with provider-specific operational defaults.
- */
-export function createRouterSliceDraft(kind: 'openrouter', id?: string): OpenRouterSliceDraft
-export function createRouterSliceDraft(kind: 'bedrock', id?: string): BedrockSliceDraft
-export function createRouterSliceDraft(kind: 'openai-compatible', id?: string): OpenAICompatibleSliceDraft
-export function createRouterSliceDraft(kind: 'azure', id?: string): AzureSliceDraft
-export function createRouterSliceDraft(kind: 'dashscope-cosyvoice', id?: string): DashscopeSliceDraft
-export function createRouterSliceDraft(kind: 'stepfun', id?: string): StepfunSliceDraft
-export function createRouterSliceDraft(kind: 'aliyun-nls-asr', id?: string): AliyunNlsAsrSliceDraft
-export function createRouterSliceDraft(kind: 'unspeech', id?: string): UnspeechSliceDraft
-export function createRouterSliceDraft(kind: RouterSliceKind, id?: string): RouterSliceDraft
-export function createRouterSliceDraft(kind: RouterSliceKind, id?: string): RouterSliceDraft {
- const sliceId = id ?? `${kind}-${++draftId}`
- switch (kind) {
- case 'openrouter':
- return {
- id: sliceId,
- kind,
- modelName: 'chat-default',
- overrideModel: 'openai/gpt-4o-mini',
- plaintextKey: '',
- baseURL: 'https://openrouter.ai/api/v1',
- keyEntryId: '',
- existingKeyEntryId: '',
- headerTemplate: '',
- }
- case 'bedrock':
- return {
- id: sliceId,
- kind,
- modelName: 'chat-bedrock',
- overrideModel: 'us.anthropic.claude-3-5-sonnet-20241022-v2:0',
- plaintextKey: '',
- baseURL: 'https://bedrock-mantle.us-east-1.api.aws/v1',
- keyEntryId: 'bedrock-prod-1',
- existingKeyEntryId: '',
- headerTemplate: '',
- }
- case 'openai-compatible':
- return {
- id: sliceId,
- kind,
- modelName: 'chat-compatible',
- overrideModel: 'gpt-4o-mini',
- plaintextKey: '',
- baseURL: 'https://api.example.com/v1',
- keyEntryId: 'openai-compatible-prod-1',
- existingKeyEntryId: '',
- headerTemplate: '',
- }
- case 'azure':
- return {
- id: sliceId,
- kind,
- modelName: 'microsoft/v1',
- region: 'eastasia',
- defaultVoice: '',
- plaintextKey: '',
- keyEntryId: '',
- existingKeyEntryId: '',
- }
- case 'dashscope-cosyvoice':
- return {
- id: sliceId,
- kind,
- modelName: 'alibaba/cosyvoice-v2',
- region: 'intl',
- upstreamModel: 'cosyvoice-v2',
- plaintextKey: '',
- keyEntryId: '',
- existingKeyEntryId: '',
- }
- case 'stepfun':
- return {
- id: sliceId,
- kind,
- modelName: 'stepfun/stepaudio-2.5-tts',
- upstreamModel: 'stepaudio-2.5-tts',
- defaultVoice: '',
- instruction: '',
- plaintextKey: '',
- keyEntryId: '',
- existingKeyEntryId: '',
- }
- case 'aliyun-nls-asr':
- return {
- id: sliceId,
- kind,
- modelName: 'auto',
- accessKeyId: '',
- appKey: '',
- region: 'cn-shanghai',
- plaintextKey: '',
- keyEntryId: '',
- existingKeyEntryId: '',
- }
- case 'unspeech':
- return {
- id: sliceId,
- kind,
- restBaseURL: 'http://airi-unspeech.railway.internal:5933',
- streamingEnabled: false,
- streamingUpstreamURL: 'ws://airi-unspeech.railway.internal:5933/v1/audio/speech/stream',
- streamingPlaintextKey: '',
- streamingKeyEntryId: '',
- streamingExistingKeyEntryId: '',
- streamingModelsJson: DEFAULT_STREAMING_MODELS_JSON,
- streamingDefaultModel: '',
- }
- }
-}
-
-export function defaultTtsVoicesJson(): string {
- return DEFAULT_TTS_VOICES_JSON
-}
-
-/**
- * Builds the admin router config request from form state.
- *
- * Use when:
- * - Previewing or applying the visible form payload.
- *
- * Expects:
- * - Plaintext keys are present in draft state only; callers should submit the
- * returned request directly and avoid rendering it in normal summaries.
- *
- * Returns:
- * - A request when client-side validation passes, otherwise actionable errors.
- */
-export function buildRouterConfigRequest(form: RouterConfigFormState): RouterConfigBuildResult {
- const errors = validateRouterConfigForm(form)
- if (errors.length > 0)
- return { request: null, errors }
-
- return { request: routerConfigRequestFromFormDraft(form), errors: [] }
-}
-
-/**
- * Projects form state into request JSON without validating it first.
- *
- * Use when:
- * - The UI needs to export the current draft for inspection before all
- * required fields are complete.
- *
- * Returns:
- * - The request shape the validated builder would submit once errors are fixed.
- */
-export function routerConfigRequestFromFormDraft(form: RouterConfigFormState): AdminRouterConfigRequest {
- const slices = form.slices.map(sliceToRequest)
- const defaults = defaultsToRequest(form.defaults)
- return {
- mode: form.mode,
- slices,
- ...(Object.keys(defaults).length > 0 ? { defaults } : {}),
- }
-}
-
-export function validateRouterConfigForm(form: RouterConfigFormState): string[] {
- const errors: string[] = []
- const unspeechCount = form.slices.filter(slice => slice.kind === 'unspeech').length
- if (unspeechCount > 1)
- errors.push('Only one UnSpeech slice can be submitted at a time.')
-
- for (const [index, slice] of form.slices.entries())
- errors.push(...validateSlice(slice, index + 1))
-
- validateDefaults(form.defaults, errors)
-
- const hasDefaults = hasText(form.defaults.chatModel) || hasText(form.defaults.ttsModel) || hasText(form.defaults.ttsVoicesJson)
- if (form.slices.length === 0 && !hasDefaults)
- errors.push('Add at least one provider slice or default alias before previewing.')
-
- return errors
-}
-
-/**
- * Imports supported router config request JSON into editable form state.
- *
- * Before:
- * - `{ "mode": "merge", "slices": [{ "kind": "openrouter", ... }] }`
- *
- * After:
- * - Form state with one OpenRouter draft and matching defaults.
- */
-export function formStateFromRequestJson(json: string): RouterConfigFormState {
- const parsed = JSON.parse(json) as unknown
- if (!isRecord(parsed))
- throw new Error('Advanced JSON must be an object.')
-
- const mode = parsed.mode === 'reset' ? 'reset' : 'merge'
- const slicesValue = parsed.slices
- const slices = Array.isArray(slicesValue)
- ? slicesValue.map((slice, index) => draftFromRequestSlice(slice, index + 1))
- : []
-
- return {
- mode,
- slices,
- defaults: defaultsFromUnknown(parsed.defaults),
- }
-}
-
-export function formatRouterConfigRequestJson(request: AdminRouterConfigRequest): string {
- return JSON.stringify(request, null, 2)
-}
-
-function validateSlice(slice: RouterSliceDraft, ordinal: number): string[] {
- const label = `Slice ${ordinal} (${kindLabel(slice.kind)})`
- switch (slice.kind) {
- case 'openrouter':
- case 'bedrock':
- case 'openai-compatible':
- return [
- required(slice.modelName, `${label}: model alias is required.`),
- noPipe(slice.modelName, `${label}: model alias must not contain "|".`),
- required(slice.overrideModel, `${label}: upstream model is required.`),
- requiredKey(slice.plaintextKey, slice.existingKeyEntryId, `${label}: provider key is required unless an existing key is loaded.`),
- httpUrl(slice.baseURL, `${label}: base URL must start with http:// or https://.`),
- ].filter(isPresent)
- case 'azure':
- return [
- required(slice.modelName, `${label}: model alias is required.`),
- noPipe(slice.modelName, `${label}: model alias must not contain "|".`),
- required(slice.region, `${label}: Azure region is required.`),
- requiredKey(slice.plaintextKey, slice.existingKeyEntryId, `${label}: provider key is required unless an existing key is loaded.`),
- ].filter(isPresent)
- case 'dashscope-cosyvoice':
- return [
- required(slice.modelName, `${label}: model alias is required.`),
- noPipe(slice.modelName, `${label}: model alias must not contain "|".`),
- required(slice.upstreamModel, `${label}: upstream model is required.`),
- requiredKey(slice.plaintextKey, slice.existingKeyEntryId, `${label}: provider key is required unless an existing key is loaded.`),
- ].filter(isPresent)
- case 'stepfun':
- return [
- required(slice.modelName, `${label}: model alias is required.`),
- noPipe(slice.modelName, `${label}: model alias must not contain "|".`),
- requiredKey(slice.plaintextKey, slice.existingKeyEntryId, `${label}: provider key is required unless an existing key is loaded.`),
- ].filter(isPresent)
- case 'aliyun-nls-asr':
- return [
- required(slice.modelName, `${label}: model alias is required.`),
- noPipe(slice.modelName, `${label}: model alias must not contain "|".`),
- required(slice.accessKeyId, `${label}: access key id is required.`),
- required(slice.appKey, `${label}: app key is required.`),
- requiredKey(slice.plaintextKey, slice.existingKeyEntryId, `${label}: access key secret is required unless an existing key is loaded.`),
- ].filter(isPresent)
- case 'unspeech':
- return [
- required(slice.restBaseURL, `${label}: REST base URL is required.`),
- httpUrl(slice.restBaseURL, `${label}: REST base URL must start with http:// or https://.`),
- ...(slice.streamingEnabled
- ? [
- required(slice.streamingUpstreamURL, `${label}: streaming WebSocket URL is required.`),
- wsUrl(slice.streamingUpstreamURL, `${label}: streaming URL must start with ws:// or wss://.`),
- requiredKey(slice.streamingPlaintextKey, slice.streamingExistingKeyEntryId, `${label}: streaming provider key is required unless an existing key is loaded.`),
- validateStreamingModels(slice.streamingModelsJson, label),
- ].filter(isPresent)
- : []),
- ].filter(isPresent)
- }
-}
-
-function validateDefaults(defaults: RouterDefaultsDraft, errors: string[]) {
- if (!hasText(defaults.ttsVoicesJson))
- return
- try {
- parseTtsVoices(defaults.ttsVoicesJson)
- }
- catch (error) {
- errors.push(errorMessageFromUnknown(error, 'Default TTS voices must be valid JSON.'))
- }
-}
-
-function validateStreamingModels(json: string, label: string): string | undefined {
- if (!hasText(json))
- return undefined
- try {
- parseStreamingModels(json)
- return undefined
- }
- catch (error) {
- return error instanceof Error ? `${label}: ${error.message}` : `${label}: streaming models must be valid JSON.`
- }
-}
-
-function sliceToRequest(slice: RouterSliceDraft): AdminRouterConfigSlice {
- switch (slice.kind) {
- case 'openrouter':
- case 'bedrock':
- case 'openai-compatible': {
- const request: AdminRouterOpenRouterSlice | AdminRouterBedrockSlice | AdminRouterOpenAICompatibleSlice = {
- kind: slice.kind,
- modelName: trim(slice.modelName),
- overrideModel: trim(slice.overrideModel),
- baseURL: trim(slice.baseURL),
- }
- assignOptional(request, 'plaintextKey', slice.plaintextKey)
- assignOptional(request, 'keyEntryId', slice.keyEntryId)
- assignOptional(request, 'existingKeyEntryId', slice.existingKeyEntryId)
- assignOptional(request, 'headerTemplate', slice.headerTemplate)
- return request
- }
- case 'azure': {
- const request: AdminRouterAzureSlice = {
- kind: slice.kind,
- modelName: trim(slice.modelName),
- region: trim(slice.region),
- }
- assignOptional(request, 'plaintextKey', slice.plaintextKey)
- assignOptional(request, 'defaultVoice', slice.defaultVoice)
- assignOptional(request, 'keyEntryId', slice.keyEntryId)
- assignOptional(request, 'existingKeyEntryId', slice.existingKeyEntryId)
- return request
- }
- case 'dashscope-cosyvoice': {
- const request: AdminRouterDashscopeSlice = {
- kind: slice.kind,
- modelName: trim(slice.modelName),
- region: slice.region,
- upstreamModel: trim(slice.upstreamModel),
- }
- assignOptional(request, 'plaintextKey', slice.plaintextKey)
- assignOptional(request, 'keyEntryId', slice.keyEntryId)
- assignOptional(request, 'existingKeyEntryId', slice.existingKeyEntryId)
- return request
- }
- case 'stepfun': {
- const request: AdminRouterStepfunSlice = {
- kind: slice.kind,
- modelName: trim(slice.modelName),
- upstreamModel: slice.upstreamModel,
- }
- assignOptional(request, 'plaintextKey', slice.plaintextKey)
- assignOptional(request, 'defaultVoice', slice.defaultVoice)
- assignOptional(request, 'instruction', slice.instruction)
- assignOptional(request, 'keyEntryId', slice.keyEntryId)
- assignOptional(request, 'existingKeyEntryId', slice.existingKeyEntryId)
- return request
- }
- case 'aliyun-nls-asr': {
- const request: AdminRouterAliyunNlsAsrSlice = {
- kind: slice.kind,
- modelName: trim(slice.modelName),
- accessKeyId: trim(slice.accessKeyId),
- appKey: trim(slice.appKey),
- region: slice.region,
- }
- assignOptional(request, 'plaintextKey', slice.plaintextKey)
- assignOptional(request, 'keyEntryId', slice.keyEntryId)
- assignOptional(request, 'existingKeyEntryId', slice.existingKeyEntryId)
- return request
- }
- case 'unspeech': {
- const request: AdminRouterUnspeechSlice = {
- kind: slice.kind,
- restBaseURL: trim(slice.restBaseURL),
- }
- if (slice.streamingEnabled) {
- request.streaming = {
- upstreamURL: trim(slice.streamingUpstreamURL),
- }
- assignOptional(request.streaming, 'plaintextKey', slice.streamingPlaintextKey)
- assignOptional(request.streaming, 'keyEntryId', slice.streamingKeyEntryId)
- assignOptional(request.streaming, 'existingKeyEntryId', slice.streamingExistingKeyEntryId)
- assignOptional(request.streaming, 'defaultModel', slice.streamingDefaultModel)
- const models = parseStreamingModels(slice.streamingModelsJson)
- if (models.length > 0)
- request.streaming.models = models
- }
- return request
- }
- }
-}
-
-function defaultsToRequest(defaults: RouterDefaultsDraft): NonNullable {
- const out: NonNullable = {}
- assignOptional(out, 'chatModel', defaults.chatModel)
- assignOptional(out, 'ttsModel', defaults.ttsModel)
- if (hasText(defaults.ttsVoicesJson))
- out.ttsVoices = parseTtsVoices(defaults.ttsVoicesJson)
- return out
-}
-
-function draftFromRequestSlice(value: unknown, ordinal: number): RouterSliceDraft {
- if (!isRecord(value) || typeof value.kind !== 'string')
- throw new Error(`slices[${ordinal - 1}] must include a supported kind.`)
-
- switch (value.kind) {
- case 'openrouter':
- case 'bedrock':
- case 'openai-compatible': {
- const draft = createRouterSliceDraft(value.kind, `imported-${value.kind}-${ordinal}`) as LlmSliceDraft
- draft.modelName = stringValue(value.modelName)
- draft.overrideModel = stringValue(value.overrideModel)
- draft.plaintextKey = stringValue(value.plaintextKey)
- draft.baseURL = stringValue(value.baseURL) || draft.baseURL
- draft.keyEntryId = stringValue(value.keyEntryId)
- draft.existingKeyEntryId = stringValue(value.existingKeyEntryId)
- draft.headerTemplate = stringValue(value.headerTemplate)
- return draft
- }
- case 'azure': {
- const draft = createRouterSliceDraft('azure', `imported-azure-${ordinal}`) as AzureSliceDraft
- draft.modelName = stringValue(value.modelName)
- draft.region = stringValue(value.region)
- draft.defaultVoice = stringValue(value.defaultVoice)
- draft.plaintextKey = stringValue(value.plaintextKey)
- draft.keyEntryId = stringValue(value.keyEntryId)
- draft.existingKeyEntryId = stringValue(value.existingKeyEntryId)
- return draft
- }
- case 'dashscope-cosyvoice': {
- const draft = createRouterSliceDraft('dashscope-cosyvoice', `imported-dashscope-${ordinal}`) as DashscopeSliceDraft
- draft.modelName = stringValue(value.modelName)
- draft.region = value.region === 'cn' ? 'cn' : 'intl'
- draft.upstreamModel = stringValue(value.upstreamModel)
- draft.plaintextKey = stringValue(value.plaintextKey)
- draft.keyEntryId = stringValue(value.keyEntryId)
- draft.existingKeyEntryId = stringValue(value.existingKeyEntryId)
- return draft
- }
- case 'stepfun': {
- const draft = createRouterSliceDraft('stepfun', `imported-stepfun-${ordinal}`) as StepfunSliceDraft
- draft.modelName = stringValue(value.modelName)
- draft.upstreamModel = isStepfunModel(value.upstreamModel) ? value.upstreamModel : draft.upstreamModel
- draft.defaultVoice = stringValue(value.defaultVoice)
- draft.instruction = stringValue(value.instruction)
- draft.plaintextKey = stringValue(value.plaintextKey)
- draft.keyEntryId = stringValue(value.keyEntryId)
- draft.existingKeyEntryId = stringValue(value.existingKeyEntryId)
- return draft
- }
- case 'aliyun-nls-asr': {
- const draft = createRouterSliceDraft('aliyun-nls-asr', `imported-aliyun-nls-asr-${ordinal}`) as AliyunNlsAsrSliceDraft
- draft.modelName = stringValue(value.modelName)
- draft.accessKeyId = stringValue(value.accessKeyId)
- draft.appKey = stringValue(value.appKey)
- draft.region = isAliyunNlsRegion(value.region) ? value.region : draft.region
- draft.plaintextKey = stringValue(value.plaintextKey)
- draft.keyEntryId = stringValue(value.keyEntryId)
- draft.existingKeyEntryId = stringValue(value.existingKeyEntryId)
- return draft
- }
- case 'unspeech': {
- const draft = createRouterSliceDraft('unspeech', `imported-unspeech-${ordinal}`) as UnspeechSliceDraft
- draft.restBaseURL = stringValue(value.restBaseURL)
- const streaming = isRecord(value.streaming) ? value.streaming : null
- draft.streamingEnabled = streaming != null
- if (streaming) {
- draft.streamingUpstreamURL = stringValue(streaming.upstreamURL)
- draft.streamingPlaintextKey = stringValue(streaming.plaintextKey)
- draft.streamingKeyEntryId = stringValue(streaming.keyEntryId)
- draft.streamingExistingKeyEntryId = stringValue(streaming.existingKeyEntryId)
- draft.streamingDefaultModel = stringValue(streaming.defaultModel)
- if (Array.isArray(streaming.models))
- draft.streamingModelsJson = JSON.stringify(streaming.models, null, 2)
- }
- return draft
- }
- default:
- throw new Error(`Unsupported router slice kind "${value.kind}".`)
- }
-}
-
-function defaultsFromUnknown(value: unknown): RouterDefaultsDraft {
- const defaults = isRecord(value) ? value : {}
- return {
- chatModel: stringValue(defaults.chatModel),
- ttsModel: stringValue(defaults.ttsModel),
- ttsVoicesJson: isRecord(defaults.ttsVoices) ? JSON.stringify(defaults.ttsVoices, null, 2) : '',
- }
-}
-
-function parseTtsVoices(json: string): Record> {
- const parsed = JSON.parse(json) as unknown
- if (!isRecord(parsed))
- throw new Error('Default TTS voices must be a JSON object.')
-
- const out: Record> = {}
- for (const [model, locales] of Object.entries(parsed)) {
- if (!model.trim() || !isRecord(locales))
- throw new Error('Default TTS voices must map model IDs to locale objects.')
- out[model] = {}
- for (const [locale, voice] of Object.entries(locales)) {
- if (!locale.trim() || typeof voice !== 'string' || !voice.trim())
- throw new Error('Default TTS voices locales must map to voice IDs.')
- out[model][locale] = voice
- }
- }
- return out
-}
-
-function parseStreamingModels(json: string): NonNullable['models']> {
- if (!hasText(json))
- return []
- const parsed = JSON.parse(json) as unknown
- if (!Array.isArray(parsed))
- throw new Error('streaming models must be a JSON array.')
-
- return parsed.map((item, index) => {
- if (!isRecord(item) || typeof item.id !== 'string' || !item.id.trim())
- throw new Error(`streaming models[${index}].id is required.`)
-
- return {
- id: item.id,
- ...(typeof item.name === 'string' && item.name.trim() ? { name: item.name } : {}),
- ...(typeof item.description === 'string' && item.description.trim() ? { description: item.description } : {}),
- }
- })
-}
-
-function kindLabel(kind: RouterSliceKind): string {
- return ROUTER_SLICE_KIND_OPTIONS.find(option => option.value === kind)?.label ?? kind
-}
-
-function required(value: string, message: string): string | undefined {
- return hasText(value) ? undefined : message
-}
-
-function requiredKey(value: string, existingKeyEntryId: string, message: string): string | undefined {
- return hasText(value) || hasText(existingKeyEntryId) ? undefined : message
-}
-
-function noPipe(value: string, message: string): string | undefined {
- return value.includes('|') ? message : undefined
-}
-
-function httpUrl(value: string, message: string): string | undefined {
- return /^https?:\/\/\S+$/u.test(value.trim()) ? undefined : message
-}
-
-function wsUrl(value: string, message: string): string | undefined {
- return /^wss?:\/\/\S+$/u.test(value.trim()) ? undefined : message
-}
-
-function assignOptional(target: T, key: string, value: string) {
- if (hasText(value))
- Object.assign(target, { [key]: trim(value) })
-}
-
-function trim(value: string): string {
- return value.trim()
-}
-
-function hasText(value: string): boolean {
- return value.trim().length > 0
-}
-
-function isPresent(value: string | undefined): value is string {
- return value != null
-}
-
-function isRecord(value: unknown): value is Record {
- return value != null && typeof value === 'object' && !Array.isArray(value)
-}
-
-function stringValue(value: unknown): string {
- return typeof value === 'string' ? value : ''
-}
-
-function isStepfunModel(value: unknown): value is StepfunModel {
- return value === 'stepaudio-2.5-tts' || value === 'step-tts-2' || value === 'step-tts-mini'
-}
-
-function isAliyunNlsRegion(value: unknown): value is AliyunNlsRegion {
- return value === 'cn-shanghai'
- || value === 'cn-shanghai-internal'
- || value === 'cn-beijing'
- || value === 'cn-beijing-internal'
- || value === 'cn-shenzhen'
- || value === 'cn-shenzhen-internal'
-}
diff --git a/apps/ui-admin/src/modules/server-admin-context.test.ts b/apps/ui-admin/src/modules/server-admin-context.test.ts
deleted file mode 100644
index cfe651640..000000000
--- a/apps/ui-admin/src/modules/server-admin-context.test.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-// @vitest-environment jsdom
-
-import { describe, expect, it } from 'vitest'
-
-import { ADMIN_API_ENVIRONMENTS, apiEnvironmentValueFor, buildApiServerSwitchUrl, defaultStandaloneApiServerUrl, getServerAdminBootstrapContext, resolveStandaloneServerAdminContext } from './server-admin-context'
-
-describe('ui-admin bootstrap context', () => {
- it('uses the trusted API server origin carried by standalone server redirects', () => {
- expect(resolveStandaloneServerAdminContext(
- 'https://admin.airi.build/users?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app%2Fapi%2Fadmin',
- )).toEqual({
- apiServerUrl: 'https://airi-server-dev.up.railway.app',
- currentUrl: 'https://admin.airi.build/users?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app%2Fapi%2Fadmin',
- })
- })
-
- it('ignores untrusted API server origins from crafted standalone admin URLs', () => {
- expect(resolveStandaloneServerAdminContext(
- 'https://admin.airi.build/users?api_server_url=https%3A%2F%2Fevil.example',
- )).toBeNull()
- })
-
- it('allows localhost API origins for local development', () => {
- expect(resolveStandaloneServerAdminContext(
- 'http://localhost:5173/admin/users?api_server_url=http%3A%2F%2F127.0.0.1%3A3000',
- )?.apiServerUrl).toBe('http://127.0.0.1:3000')
- })
-
- it('normalizes known production API hosts to HTTPS when the query param is typed with HTTP', () => {
- expect(resolveStandaloneServerAdminContext(
- 'http://localhost:5178/llm-router?api_server_url=http%3A%2F%2Fapi.airi.build',
- )?.apiServerUrl).toBe('https://api.airi.build')
- })
-
- it('defaults local standalone dev UI origins to the local API port', () => {
- expect(defaultStandaloneApiServerUrl('http://localhost:5178')).toBe('http://localhost:3000')
- expect(defaultStandaloneApiServerUrl('http://127.0.0.1:5178')).toBe('http://127.0.0.1:3000')
- })
-
- it('keeps non-local standalone origins unchanged without redirect context', () => {
- expect(defaultStandaloneApiServerUrl('https://admin-preview.example')).toBe('https://admin-preview.example')
- })
-
- it('defaults known standalone admin deployments to their API origins', () => {
- expect(defaultStandaloneApiServerUrl('https://admin.airi.build')).toBe('https://api.airi.build')
- expect(defaultStandaloneApiServerUrl('https://server-dev.airi-server-admin.pages.dev')).toBe('https://airi-server-dev.up.railway.app')
- })
-
- it('exposes production, testing, and local API environment choices', () => {
- expect(ADMIN_API_ENVIRONMENTS.map(environment => environment.value)).toEqual([
- 'https://api.airi.build',
- 'https://airi-server-dev.up.railway.app',
- 'http://localhost:3000',
- ])
- })
-
- it('maps custom localhost API origins to the local environment option', () => {
- expect(apiEnvironmentValueFor('http://127.0.0.1:8787')).toBe('http://localhost:3000')
- })
-
- it('builds a switched admin URL without dropping the current page state', () => {
- expect(buildApiServerSwitchUrl(
- 'https://admin.airi.build/voice-packs/new?draft=1#advanced',
- 'https://airi-server-dev.up.railway.app/api/admin',
- )).toBe('https://admin.airi.build/voice-packs/new?draft=1&api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app#advanced')
- })
-
- it('falls back to the standalone query context when the static placeholder script is still present', () => {
- document.body.innerHTML = ''
- window.history.replaceState(
- null,
- '',
- '/admin/users?api_server_url=https%3A%2F%2Fairi-server-dev.up.railway.app',
- )
-
- expect(getServerAdminBootstrapContext()?.apiServerUrl).toBe('https://airi-server-dev.up.railway.app')
- })
-})
diff --git a/apps/ui-admin/src/modules/server-admin-context.ts b/apps/ui-admin/src/modules/server-admin-context.ts
deleted file mode 100644
index 1c27924a8..000000000
--- a/apps/ui-admin/src/modules/server-admin-context.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-export interface ServerAdminBootstrapContext {
- apiServerUrl: string
- currentUrl: string
-}
-
-export interface AdminApiEnvironment {
- label: string
- value: string
- description: string
-}
-
-const SCRIPT_ID = 'airi-server-admin-context'
-const API_SERVER_URL_QUERY_PARAM = 'api_server_url'
-
-export const ADMIN_API_ENVIRONMENTS = [
- {
- label: 'Production',
- value: 'https://api.airi.build',
- description: 'api.airi.build',
- },
- {
- label: 'Testing',
- value: 'https://airi-server-dev.up.railway.app',
- description: 'airi-server-dev.up.railway.app',
- },
- {
- label: 'Local',
- value: 'http://localhost:3000',
- description: 'localhost:3000',
- },
-] as const satisfies readonly AdminApiEnvironment[]
-
-const TRUSTED_STANDALONE_API_SERVER_ORIGINS = [
- 'https://api.airi.build',
- 'https://airi-server-dev.up.railway.app',
-]
-
-const TRUSTED_HTTPS_API_SERVER_HOSTS = new Map(
- TRUSTED_STANDALONE_API_SERVER_ORIGINS.map((origin) => {
- const url = new URL(origin)
- return [url.hostname, origin]
- }),
-)
-
-const DEFAULT_API_SERVER_ORIGINS_BY_ADMIN_UI_ORIGIN = new Map([
- ['https://admin.airi.build', 'https://api.airi.build'],
- ['https://server-dev.airi-server-admin.pages.dev', 'https://airi-server-dev.up.railway.app'],
-])
-
-const TRUSTED_LOCAL_API_SERVER_ORIGIN_PATTERNS = [
- /^http:\/\/localhost(:\d+)?$/,
- /^http:\/\/127\.0\.0\.1(:\d+)?$/,
- /^https:\/\/localhost(:\d+)?$/,
- /^https:\/\/127\.0\.0\.1(:\d+)?$/,
-]
-
-let cachedContext: ServerAdminBootstrapContext | null | undefined
-
-export function getServerAdminBootstrapContext(): ServerAdminBootstrapContext | null {
- if (cachedContext !== undefined)
- return cachedContext
-
- const element = document.getElementById(SCRIPT_ID)
- if (!element) {
- cachedContext = resolveStandaloneServerAdminContext(window.location.href)
- return cachedContext
- }
-
- try {
- const parsed = JSON.parse(element.textContent ?? '') as Partial
- cachedContext = {
- apiServerUrl: parsed.apiServerUrl ?? defaultApiServerUrl(),
- currentUrl: parsed.currentUrl ?? window.location.href,
- }
- return cachedContext
- }
- catch {
- cachedContext = resolveStandaloneServerAdminContext(window.location.href)
- return cachedContext
- }
-}
-
-export function defaultApiServerUrl(): string {
- return import.meta.env.VITE_SERVER_URL || defaultStandaloneApiServerUrl(window.location.origin)
-}
-
-/**
- * Resolves the API origin for a standalone admin UI without redirect context.
- *
- * Before:
- * - "http://localhost:5178"
- *
- * After:
- * - "http://localhost:3000"
- */
-export function defaultStandaloneApiServerUrl(currentOrigin: string): string {
- const origin = new URL(currentOrigin).origin
- const knownApiServerOrigin = DEFAULT_API_SERVER_ORIGINS_BY_ADMIN_UI_ORIGIN.get(origin)
- if (knownApiServerOrigin)
- return knownApiServerOrigin
-
- if (TRUSTED_LOCAL_API_SERVER_ORIGIN_PATTERNS.some(pattern => pattern.test(origin))) {
- const url = new URL(origin)
- url.port = '3000'
- return url.origin
- }
-
- return origin
-}
-
-/**
- * Resolves API-server context carried by server redirects into static admin UI.
- *
- * Use when:
- * - The standalone admin UI serves more than one AIRI environment from the same
- * Pages deployment.
- *
- * Expects:
- * - The server-owned `/admin/*` redirect sets `api_server_url`.
- * - Only known AIRI API origins and localhost development origins are accepted.
- *
- * Returns:
- * - A bootstrap context using the trusted API origin, or null when no trusted
- * override is present.
- */
-export function resolveStandaloneServerAdminContext(currentUrl: string): ServerAdminBootstrapContext | null {
- const url = new URL(currentUrl)
- const apiServerUrl = normalizeTrustedApiServerUrl(
- url.searchParams.get(API_SERVER_URL_QUERY_PARAM),
- )
-
- if (!apiServerUrl)
- return null
-
- return {
- apiServerUrl,
- currentUrl,
- }
-}
-
-export function apiEnvironmentValueFor(apiServerUrl: string): string {
- const origin = new URL(apiServerUrl).origin
- const known = ADMIN_API_ENVIRONMENTS.find(environment => environment.value === origin)
- if (known)
- return known.value
-
- if (TRUSTED_LOCAL_API_SERVER_ORIGIN_PATTERNS.some(pattern => pattern.test(origin)))
- return ADMIN_API_ENVIRONMENTS.find(environment => environment.label === 'Local')?.value ?? origin
-
- return origin
-}
-
-export function buildApiServerSwitchUrl(currentUrl: string, apiServerUrl: string): string {
- const url = new URL(currentUrl)
- const origin = new URL(apiServerUrl).origin
- url.searchParams.set(API_SERVER_URL_QUERY_PARAM, origin)
- return url.toString()
-}
-
-function normalizeTrustedApiServerUrl(value: string | null): string | null {
- if (!value)
- return null
-
- try {
- const url = new URL(value)
- const normalizedHttpsOrigin = TRUSTED_HTTPS_API_SERVER_HOSTS.get(url.hostname)
- if (normalizedHttpsOrigin)
- return normalizedHttpsOrigin
-
- const origin = url.origin
-
- if (TRUSTED_STANDALONE_API_SERVER_ORIGINS.includes(origin))
- return origin
-
- if (TRUSTED_LOCAL_API_SERVER_ORIGIN_PATTERNS.some(pattern => pattern.test(origin)))
- return origin
-
- return null
- }
- catch {
- return null
- }
-}
diff --git a/apps/ui-admin/src/pages/CapabilityAliasesPage.vue b/apps/ui-admin/src/pages/CapabilityAliasesPage.vue
deleted file mode 100644
index 93f19c9ac..000000000
--- a/apps/ui-admin/src/pages/CapabilityAliasesPage.vue
+++ /dev/null
@@ -1,219 +0,0 @@
-
-
-
-
-
-
-
-
- Capability Aliases
-
-
- Product choices for LLM and ASR requests.
-
-
-
-
-
- LLM
-
-
- ASR
-
-
-
-
- {{ enabledCount }} enabled
-
-
-
- {{ routeCount }} routes
-
-
-
-
-
-
-
- Loading capability aliases
-
-
-
-
-
-
- No aliases configured
-
-
-
-
-
diff --git a/apps/ui-admin/src/pages/FluxPage.vue b/apps/ui-admin/src/pages/FluxPage.vue
deleted file mode 100644
index d7837265a..000000000
--- a/apps/ui-admin/src/pages/FluxPage.vue
+++ /dev/null
@@ -1,166 +0,0 @@
-
-
-
-
-
-
-
-
- Bulk Flux Grant
-
-
- Issues promo Flux to existing users by email. Preview first for recipient validation.
-
-
-
-
- Admin guarded
-
-
-
-
-
-
-
-
-
- Recipients
-
-
- {{ formatNumber(emails.length) }}
-
-
- Synchronous grants are capped by the server.
-
-
-
-
-
- Preview
-
- {{ formatJson(preview) }}
-
-
- No preview yet
-
-
-
-
-
- Last Result
-
- {{ formatJson(result) }}
-
-
- No grant result
-
-
-
-
-
diff --git a/apps/ui-admin/src/pages/LlmRouterPage.test.ts b/apps/ui-admin/src/pages/LlmRouterPage.test.ts
deleted file mode 100644
index 2a0e2c5bc..000000000
--- a/apps/ui-admin/src/pages/LlmRouterPage.test.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-// @vitest-environment jsdom
-
-import type { App } from 'vue'
-
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { createApp, nextTick } from 'vue'
-
-import LlmRouterPage from './LlmRouterPage.vue'
-
-const mocks = vi.hoisted(() => ({
- applyRouterConfig: vi.fn(),
- routerConfig: vi.fn(),
- toastSuccess: vi.fn(),
- toastError: vi.fn(),
-}))
-
-vi.mock('../modules/api', () => ({
- adminApi: {
- applyRouterConfig: mocks.applyRouterConfig,
- routerConfig: mocks.routerConfig,
- },
-}))
-
-vi.mock('vue-sonner', () => ({
- toast: {
- success: mocks.toastSuccess,
- error: mocks.toastError,
- },
-}))
-
-describe('llm router page', () => {
- let app: App
- let host: HTMLElement
-
- beforeEach(() => {
- mocks.routerConfig.mockResolvedValue({
- request: {
- mode: 'merge',
- slices: [],
- defaults: {},
- },
- preview: {},
- loadedAt: '2026-06-10T00:00:00.000Z',
- missingKeys: ['LLM_ROUTER_CONFIG'],
- })
- mocks.applyRouterConfig.mockResolvedValue({
- applied: [{ kind: 'openrouter', target: 'llm-router', surface: 'llm', modelName: 'chat-default', keyEntryId: 'k1' }],
- invalidatedKeys: [],
- preview: {
- LLM_ROUTER_CONFIG: {
- llm: {
- models: {
- 'chat-default': {
- upstreams: [{ keys: [{ id: 'k1', ciphertext: '' }] }],
- },
- },
- },
- },
- },
- })
- document.body.innerHTML = '
'
- host = document.querySelector('#app')!
- app = createApp(LlmRouterPage)
- app.mount(host)
- })
-
- afterEach(() => {
- app.unmount()
- vi.clearAllMocks()
- })
-
- it('starts as a form-first router config editor', () => {
- expect(host.textContent).toContain('Router Config Form')
- expect(host.textContent).toContain('Provider configuration')
- expect(host.textContent).toContain('LLM')
- expect(host.textContent).toContain('TTS')
- expect(host.textContent).toContain('Streaming TTS')
- expect(host.textContent).toContain('ASR')
- expect(host.textContent).not.toContain('LLM and TTS providers')
- expect(host.textContent).toContain('OpenRouter')
- expect(host.textContent).not.toContain('Router Config Request')
- })
-
- it('loads current router config into the form and preserves existing keys', async () => {
- app.unmount()
- mocks.routerConfig.mockResolvedValueOnce({
- request: {
- mode: 'merge',
- slices: [{
- kind: 'openrouter',
- modelName: 'chat-live',
- overrideModel: 'openai/gpt-4.1-mini',
- baseURL: 'https://openrouter.ai/api/v1',
- keyEntryId: 'openrouter-live',
- existingKeyEntryId: 'openrouter-live',
- }],
- defaults: {
- chatModel: 'chat-live',
- },
- },
- preview: {
- LLM_ROUTER_CONFIG: { llm: { models: { 'chat-live': {} } } },
- },
- loadedAt: '2026-06-10T00:00:00.000Z',
- missingKeys: [],
- })
- document.body.innerHTML = '
'
- host = document.querySelector('#app')!
- app = createApp(LlmRouterPage)
- app.mount(host)
- await flushPromises()
-
- expect(host.textContent).toContain('Loaded current config')
- expect(host.textContent).toContain('Loaded key entry openrouter-live')
- expect(previewButton().disabled).toBe(false)
- })
-
- it('shows an actionable error when the current config response is not JSON data', async () => {
- app.unmount()
- mocks.routerConfig.mockResolvedValueOnce(null)
- document.body.innerHTML = '
'
- host = document.querySelector('#app')!
- app = createApp(LlmRouterPage)
- app.mount(host)
- await flushPromises()
-
- expect(host.textContent).toContain('Current router config response is not a valid JSON object.')
- })
-
- it('separates LLM, TTS, Streaming TTS, and ASR provider slices by tab', async () => {
- buttonByText('TTS').click()
- await nextTick()
-
- expect(host.textContent).toContain('No TTS provider slices')
- expect(sectionHeadings(host)).not.toContain('1. OpenRouter')
-
- buttonByText('Add').click()
- await nextTick()
-
- expect(host.textContent).toContain('Azure Speech')
-
- buttonByText('Streaming TTS').click()
- await nextTick()
-
- expect(host.textContent).toContain('No Streaming TTS provider slices')
- expect(sectionHeadings(host)).not.toContain('1. Azure Speech')
-
- buttonByText('Add').click()
- await nextTick()
-
- expect(host.textContent).toContain('UnSpeech')
-
- buttonByText('ASR').click()
- await nextTick()
-
- expect(host.textContent).toContain('No ASR provider slices')
- expect(sectionHeadings(host)).not.toContain('1. UnSpeech')
-
- buttonByText('Add').click()
- await nextTick()
-
- expect(host.textContent).toContain('Aliyun NLS ASR')
- })
-
- it('keeps preview disabled until required fields are complete', async () => {
- expect(previewButton().disabled).toBe(true)
-
- const keyInput = host.querySelector('input[type="password"]')
- expect(keyInput).not.toBeNull()
- keyInput!.value = 'sk-openrouter'
- keyInput!.dispatchEvent(new Event('input', { bubbles: true }))
- await nextTick()
-
- expect(previewButton().disabled).toBe(false)
- })
-
- it('previews the built form payload without hand-written JSON', async () => {
- const keyInput = host.querySelector('input[type="password"]')
- keyInput!.value = 'sk-openrouter'
- keyInput!.dispatchEvent(new Event('input', { bubbles: true }))
- await nextTick()
-
- previewButton().click()
- await nextTick()
- await flushPromises()
- await nextTick()
-
- expect(mocks.applyRouterConfig).toHaveBeenCalledWith({
- mode: 'merge',
- slices: [{
- kind: 'openrouter',
- modelName: 'chat-default',
- overrideModel: 'openai/gpt-4o-mini',
- plaintextKey: 'sk-openrouter',
- baseURL: 'https://openrouter.ai/api/v1',
- }],
- defaults: {
- chatModel: 'chat-default',
- },
- }, true)
- expect(mocks.toastSuccess).toHaveBeenCalledWith('Router config preview generated')
- })
-
- it('exports the current form to advanced JSON', async () => {
- const exportButton = buttonByText('Export Form')
- exportButton.click()
- await nextTick()
-
- const advancedTextarea = Array.from(host.querySelectorAll('textarea')).at(-1)
- expect(advancedTextarea?.value).toContain('"kind": "openrouter"')
- expect(advancedTextarea?.value).toContain('"chatModel": "chat-default"')
- })
-})
-
-function previewButton(): HTMLButtonElement {
- return buttonByText('Preview')
-}
-
-async function flushPromises() {
- await nextTick()
- await Promise.resolve()
- await nextTick()
-}
-
-function buttonByText(text: string): HTMLButtonElement {
- const button = Array.from(document.querySelectorAll('button'))
- .find(item => item.textContent?.includes(text))
- if (!(button instanceof HTMLButtonElement))
- throw new Error(`Button "${text}" not found`)
- return button
-}
-
-function sectionHeadings(host: HTMLElement): string[] {
- return Array.from(host.querySelectorAll('h3'))
- .map(heading => heading.textContent?.trim() ?? '')
-}
diff --git a/apps/ui-admin/src/pages/LlmRouterPage.vue b/apps/ui-admin/src/pages/LlmRouterPage.vue
deleted file mode 100644
index b8ad5a96a..000000000
--- a/apps/ui-admin/src/pages/LlmRouterPage.vue
+++ /dev/null
@@ -1,510 +0,0 @@
-
-
-
-
-
-
-
-
- Router Config Form
-
-
- Builds LLM_ROUTER_CONFIG, UNSPEECH_UPSTREAM, and default model aliases without hand-written JSON.
-
-
-
-
- {{ hasValidationErrors ? `${validationErrors.length} issues` : 'Ready' }}
-
-
-
-
-
-
-
-
- Current Config
-
-
- {{ currentError ?? currentStatusLabel }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Provider configuration
-
-
- LLM, TTS, Streaming TTS, and ASR providers are edited separately, then applied as one router config request.
-
-
-
-
-
-
-
-
- {{ tab.label }}
-
- {{ tab.count }}
-
-
-
-
-
-
-
-
-
-
-
- {{ activeProviderLabel() }}
-
- config writes chat model aliases under LLM_ROUTER_CONFIG.
-
-
- config writes speech model aliases under LLM_ROUTER_CONFIG.
-
-
- config writes UNSPEECH_UPSTREAM settings for realtime speech synthesis.
-
-
- config writes realtime transcription model aliases under LLM_ROUTER_CONFIG.asr.
-
-
-
-
-
-
-
- No {{ activeProviderLabel() }} provider slices
-
-
-
- UnSpeech is limited to one Streaming TTS slice per request. ASR writes to LLM_ROUTER_CONFIG.asr.
-
-
-
-
-
-
-
-
-
-
- {{ pendingSummary.slices }}
- slices
-
-
- {{ pendingSummary.llmSlices }}
- LLM
-
-
- {{ pendingSummary.ttsSlices }}
- TTS
-
-
- {{ pendingSummary.streamingTtsSlices }}
- Streaming TTS
-
-
- {{ pendingSummary.asrSlices }}
- ASR
-
-
- {{ pendingSummary.defaults }}
- defaults
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/ui-admin/src/pages/OverviewPage.vue b/apps/ui-admin/src/pages/OverviewPage.vue
deleted file mode 100644
index f50248ae5..000000000
--- a/apps/ui-admin/src/pages/OverviewPage.vue
+++ /dev/null
@@ -1,146 +0,0 @@
-
-
-
-
-
-
-
-
- {{ card.label }}
-
-
-
-
-
-
- {{ card.value }}
-
-
-
- {{ card.trend }}
-
-
-
-
-
-
-
-
- Operational Overview
-
-
- Reserved surface for Grafana panels and live gateway indicators.
-
-
-
-
- Grafana Embed
-
-
-
-
-
-
-
-
-
-
-
-
- Grafana placeholder
-
-
- Add an embed URL later without changing the page shell.
-
-
-
-
- Ready
-
-
-
-
-
-
-
- Admin Seats
-
-
- {{ formatNumber(metrics?.adminSeats) }}
-
-
-
-
- Router Config
-
-
- Open Router Config
-
-
-
-
-
-
-
-
diff --git a/apps/ui-admin/src/pages/TtsCatalogPage.vue b/apps/ui-admin/src/pages/TtsCatalogPage.vue
deleted file mode 100644
index 48bdad06a..000000000
--- a/apps/ui-admin/src/pages/TtsCatalogPage.vue
+++ /dev/null
@@ -1,309 +0,0 @@
-
-
-
-
-
-
-
-
- TTS Models
-
-
- Official speech models visible to clients.
-
-
-
-
-
-
-
-
- {{ enabledModels }} enabled
-
-
-
- {{ models.length }} total
-
-
-
-
-
- Loading TTS models
-
-
-
-
-
-
-
- {{ model.displayName }}
-
-
- {{ model.routerModelId }}
-
-
-
- {{ model.enabled ? 'Enabled' : 'Disabled' }}
-
-
-
- updateModel(model, { displayName: (event.target as HTMLInputElement).value })">
- updateModel(model, { displayOrder: Number((event.target as HTMLInputElement).value) })">
-
-
- updateModel(model, { enabled: (event.target as HTMLInputElement).checked })">
- Visible to clients
-
-
-
-
-
-
- No TTS models synced
-
-
-
-
-
-
-
-
- TTS Voices
-
-
- {{ selectedModelRow ? selectedModelRow.routerModelId : 'Select a model to manage voices.' }}
-
-
-
-
-
- {{ model.displayName }}
-
-
-
- {{ enabledVoices }} enabled
-
-
- {{ disabledVoices }} disabled
-
-
-
-
-
-
-
- Loading TTS voices
-
-
-
-
-
-
- No voices synced for this model
-
-
-
-
-
diff --git a/apps/ui-admin/src/pages/UsersPage.vue b/apps/ui-admin/src/pages/UsersPage.vue
deleted file mode 100644
index 3eb1bcb5b..000000000
--- a/apps/ui-admin/src/pages/UsersPage.vue
+++ /dev/null
@@ -1,332 +0,0 @@
-
-
-
-
-
-
-
-
-
- {{ user.name }}
-
-
- {{ user.email }}
-
-
-
-
-
- {{ user.emailVerified ? 'Verified' : 'Unverified' }}
-
-
-
- {{ formatNumber(user.flux) }}
-
- {{ formatDate(user.createdAt) }}
-
-
-
-
-
-
-
-
- Select a user
-
-
-
-
-
- {{ selected.name }}
-
-
- {{ selected.email }}
-
-
-
- {{ selectedStatus }}
-
-
-
-
-
-
- Flux Balance
-
-
- {{ formatNumber(selected.flux) }}
-
-
-
-
- Joined
-
-
- {{ formatDate(selected.createdAt) }}
-
-
-
-
-
-
-
-
-
-
-
-
- Recent Flux Ledger
-
-
- No Flux transactions
-
-
-
-
- {{ tx.description }}
-
- {{ tx.type === 'debit' ? '-' : '+' }}{{ formatNumber(tx.amount) }}
-
-
-
- {{ tx.type }} · {{ formatDate(tx.createdAt) }} · {{ formatNumber(tx.balanceBefore) }} -> {{ formatNumber(tx.balanceAfter) }}
-
-
-
-
-
-
-
diff --git a/apps/ui-admin/src/pages/VoicePackFormPage.test.ts b/apps/ui-admin/src/pages/VoicePackFormPage.test.ts
deleted file mode 100644
index 3b7ceea41..000000000
--- a/apps/ui-admin/src/pages/VoicePackFormPage.test.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-// @vitest-environment jsdom
-
-import type { App } from 'vue'
-
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { createApp, nextTick } from 'vue'
-
-import VoicePackFormPage from './VoicePackFormPage.vue'
-
-const mocks = vi.hoisted(() => ({
- createVoicePack: vi.fn(),
- disableVoicePack: vi.fn(),
- replace: vi.fn(),
- route: {
- name: 'voice-pack-new',
- params: {},
- },
- speechModels: vi.fn(),
- speechVoices: vi.fn(),
- testSpeech: vi.fn(),
- toastError: vi.fn(),
- toastSuccess: vi.fn(),
- updateVoicePack: vi.fn(),
- voicePacks: vi.fn(),
-}))
-
-vi.mock('../modules/api', () => ({
- adminApi: {
- createVoicePack: mocks.createVoicePack,
- disableVoicePack: mocks.disableVoicePack,
- speechModels: mocks.speechModels,
- speechVoices: mocks.speechVoices,
- testSpeech: mocks.testSpeech,
- updateVoicePack: mocks.updateVoicePack,
- voicePacks: mocks.voicePacks,
- },
-}))
-
-vi.mock('vue-router', () => ({
- useRoute: () => mocks.route,
- useRouter: () => ({
- replace: mocks.replace,
- }),
-}))
-
-vi.mock('vue-sonner', () => ({
- toast: {
- error: mocks.toastError,
- success: mocks.toastSuccess,
- },
-}))
-
-describe('voice pack form page', () => {
- let app: App
- let host: HTMLElement
-
- beforeEach(() => {
- mocks.route.name = 'voice-pack-new'
- mocks.route.params = {}
- mocks.voicePacks.mockResolvedValue([])
- mocks.speechModels.mockResolvedValue({
- models: [
- { id: 'alibaba/cosyvoice-v1', name: 'alibaba/cosyvoice-v1' },
- { id: 'stepfun/stepaudio-2.5-tts', name: 'stepfun/stepaudio-2.5-tts' },
- ],
- default: null,
- })
- mocks.speechVoices.mockResolvedValue({
- voices: [{ id: 'longxiaochun', name: 'Long Xiaochun' }],
- recommended: { 'zh-CN': 'longxiaochun' },
- })
- document.body.innerHTML = '
'
- host = document.querySelector('#app')!
- app = createApp(VoicePackFormPage)
- })
-
- afterEach(() => {
- app.unmount()
- vi.clearAllMocks()
- })
-
- it('uses the configured speech catalog model when creating a new Voice Pack', async () => {
- app.mount(host)
- await flushPromises()
-
- expect(mocks.speechModels).toHaveBeenCalledTimes(1)
- expect(mocks.speechVoices).toHaveBeenCalledWith('alibaba/cosyvoice-v1')
- expect(mocks.speechVoices).not.toHaveBeenCalledWith('volcengine/seed-tts-2.0')
- })
-
- it('prefers the server speech catalog default when it is available', async () => {
- mocks.speechModels.mockResolvedValueOnce({
- models: [
- { id: 'alibaba/cosyvoice-v1', name: 'alibaba/cosyvoice-v1' },
- { id: 'stepfun/stepaudio-2.5-tts', name: 'stepfun/stepaudio-2.5-tts' },
- ],
- default: 'stepfun/stepaudio-2.5-tts',
- })
-
- app.mount(host)
- await flushPromises()
-
- expect(mocks.speechVoices).toHaveBeenCalledWith('stepfun/stepaudio-2.5-tts')
- })
-})
-
-async function flushPromises() {
- await nextTick()
- await Promise.resolve()
- await nextTick()
-}
diff --git a/apps/ui-admin/src/pages/VoicePackFormPage.vue b/apps/ui-admin/src/pages/VoicePackFormPage.vue
deleted file mode 100644
index 5e6399bc6..000000000
--- a/apps/ui-admin/src/pages/VoicePackFormPage.vue
+++ /dev/null
@@ -1,621 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- Voice Packs
-
-
-
- {{ pageTitle }}
-
-
- Select from configured speech models and voices, or type custom values when the catalog is incomplete.
-
-
-
-
-
- {{ form.status === 'enabled' ? 'Enabled' : 'Disabled' }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ paramsError }}
-
-
- {{ formError }}
-
-
-
-
-
-
-
-
- Test Audio
-
-
-
-
-
-
-
-
-
-
-
- Catalog Status
-
-
-
-
Models
-
- {{ models.length }}
-
-
-
-
Voices
-
- {{ voices.length }}
-
-
-
-
Recommended
-
- {{ firstRecommendedVoiceId(recommendedVoices) || 'none' }}
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/ui-admin/src/pages/VoicePacksPage.vue b/apps/ui-admin/src/pages/VoicePacksPage.vue
deleted file mode 100644
index 7250fa764..000000000
--- a/apps/ui-admin/src/pages/VoicePacksPage.vue
+++ /dev/null
@@ -1,145 +0,0 @@
-
-
-
-
-
-
-
- Voice Packs
-
-
- Curated speech presets exposed to users for character-card binding.
-
-
-
-
-
- {{ enabledCount }} enabled
-
-
-
- {{ disabledCount }} disabled
-
-
-
-
-
-
-
-
-
- Loading Voice Packs
-
-
-
-
-
- Name
- Routing
- Voice Alias
- Upstream Voice
- Cost
- Status
- Updated
-
-
-
-
-
-
- {{ pack.name }}
-
-
- {{ pack.description || 'No description' }}
-
-
-
-
- {{ pack.ttsModelId }}
-
-
- {{ pack.provider }} / {{ pack.model }}
-
-
-
- {{ pack.voiceId }}
-
-
- {{ pack.upstreamVoiceId }}
-
- {{ formatMultiplier(pack.costMultiplier) }}
-
-
-
- {{ pack.enabled ? 'Enabled' : 'Disabled' }}
-
-
- {{ formatDate(pack.updatedAt) }}
-
-
-
-
-
-
- No Voice Packs configured
-
-
-
-
-
-
diff --git a/apps/ui-admin/src/styles/main.css b/apps/ui-admin/src/styles/main.css
deleted file mode 100644
index 52957e0f3..000000000
--- a/apps/ui-admin/src/styles/main.css
+++ /dev/null
@@ -1,485 +0,0 @@
-@import '@proj-airi/ui/main.css';
-
-:root {
- --admin-accent: #059669;
- color-scheme: light;
-}
-
-html,
-body,
-#app {
- min-height: 100%;
- margin: 0;
-}
-
-body {
- background: #f7f8fa;
- color: #18181b;
- font-feature-settings: "cv02", "cv03", "cv04", "cv11";
-}
-
-html.dark {
- color-scheme: dark;
-}
-
-html.dark body {
- background: #09090b;
- color: #f4f4f5;
-}
-
-button,
-input,
-textarea,
-select {
- font: inherit;
-}
-
-#nprogress {
- pointer-events: none;
-}
-
-#nprogress .bar {
- background: var(--admin-accent);
- height: 2px;
- left: 0;
- position: fixed;
- top: 0;
- width: 100%;
- z-index: 50;
-}
-
-.admin-shell {
- display: grid;
- grid-template-columns: 248px minmax(0, 1fr);
- min-height: 100vh;
-}
-
-.admin-sidebar {
- background: #f4f5f7;
- border-right: 1px solid #e5e7eb;
- display: flex;
- flex-direction: column;
- gap: 8px;
- padding: 18px 14px;
-}
-
-html.dark .admin-sidebar {
- background: #111113;
- border-color: #27272a;
-}
-
-.admin-main {
- min-width: 0;
-}
-
-html.dark .admin-main {
- background: #09090b;
-}
-
-.admin-topbar {
- align-items: center;
- background: rgba(255, 255, 255, 0.86);
- border-bottom: 1px solid #e5e7eb;
- display: flex;
- height: 56px;
- justify-content: space-between;
- padding: 0 28px;
- position: sticky;
- top: 0;
- z-index: 10;
-}
-
-html.dark .admin-topbar {
- background: rgba(9, 9, 11, 0.88);
- border-color: #27272a;
-}
-
-.admin-content {
- padding: 22px;
-}
-
-.quick-action {
- align-items: center;
- background: #059669;
- border: 0;
- border-radius: 7px;
- color: white;
- display: flex;
- font-size: 13px;
- font-weight: 600;
- gap: 8px;
- height: 34px;
- justify-content: flex-start;
- margin-top: 12px;
- padding: 0 10px;
-}
-
-.nav-item {
- align-items: center;
- border-radius: 7px;
- color: #27272a;
- display: flex;
- font-size: 13px;
- gap: 9px;
- height: 34px;
- padding: 0 9px;
- text-decoration: none;
-}
-
-html.dark .nav-item {
- color: #d4d4d8;
-}
-
-.nav-item:hover {
- background: #e9ecef;
-}
-
-html.dark .nav-item:hover {
- background: #27272a;
-}
-
-.nav-item-active {
- background: #ffffff;
- box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
- font-weight: 600;
-}
-
-html.dark .nav-item-active {
- background: #18181b;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
- color: #f4f4f5;
-}
-
-.panel {
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
-}
-
-html.dark .panel {
- background: #18181b;
- border-color: #27272a;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
-}
-
-.metric-card {
- background: linear-gradient(180deg, #ffffff 0%, #f8fbfa 100%);
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- min-height: 132px;
- padding: 20px;
-}
-
-html.dark .metric-card {
- background: linear-gradient(180deg, #18181b 0%, #111113 100%);
- border-color: #27272a;
-}
-
-.field {
- background: #fff;
- border: 1px solid #dfe3e8;
- border-radius: 7px;
- color: #171717;
- min-height: 36px;
- outline: none;
- padding: 0 10px;
- width: 100%;
-}
-
-html.dark .field {
- background: #09090b;
- border-color: #27272a;
- color: #f4f4f5;
-}
-
-.field:focus,
-.textarea:focus {
- border-color: #10b981;
- box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.14);
-}
-
-.textarea {
- background: #fff;
- border: 1px solid #dfe3e8;
- border-radius: 7px;
- color: #171717;
- min-height: 112px;
- outline: none;
- padding: 10px;
- resize: vertical;
- width: 100%;
-}
-
-html.dark .textarea {
- background: #09090b;
- border-color: #27272a;
- color: #f4f4f5;
-}
-
-.btn {
- align-items: center;
- border: 1px solid #dfe3e8;
- border-radius: 7px;
- display: inline-flex;
- font-size: 13px;
- font-weight: 600;
- gap: 8px;
- height: 36px;
- justify-content: center;
- padding: 0 12px;
-}
-
-.btn-primary {
- background: #059669;
- border-color: #059669;
- color: white;
-}
-
-.btn-secondary {
- background: white;
- color: #27272a;
-}
-
-html.dark .btn-secondary {
- background: #18181b;
- border-color: #3f3f46;
- color: #f4f4f5;
-}
-
-.btn-danger {
- background: #fff;
- border-color: #fecaca;
- color: #b91c1c;
-}
-
-html.dark .btn-danger {
- background: #18181b;
- border-color: #7f1d1d;
- color: #fecaca;
-}
-
-.table {
- border-collapse: collapse;
- width: 100%;
-}
-
-.table th,
-.table td {
- border-bottom: 1px solid #edf0f2;
- padding: 12px 14px;
- text-align: left;
- vertical-align: middle;
-}
-
-html.dark .table th,
-html.dark .table td {
- border-color: #27272a;
-}
-
-.table th {
- background: #f8fafc;
- color: #52525b;
- font-size: 12px;
- font-weight: 600;
-}
-
-html.dark .table th {
- background: #111113;
- color: #a1a1aa;
-}
-
-.table td {
- color: #27272a;
- font-size: 13px;
-}
-
-html.dark .table td {
- color: #e4e4e7;
-}
-
-.badge {
- align-items: center;
- border: 1px solid #e5e7eb;
- border-radius: 999px;
- display: inline-flex;
- font-size: 12px;
- font-weight: 600;
- gap: 6px;
- height: 24px;
- padding: 0 8px;
-}
-
-html.dark .badge {
- border-color: #3f3f46;
- color: #e4e4e7;
-}
-
-.badge-green {
- background: #ecfdf5;
- border-color: #bbf7d0;
- color: #047857;
-}
-
-html.dark .badge-green {
- background: rgba(6, 78, 59, 0.5);
- border-color: rgba(52, 211, 153, 0.28);
- color: #6ee7b7;
-}
-
-.badge-amber {
- background: #fffbeb;
- border-color: #fde68a;
- color: #b45309;
-}
-
-html.dark .badge-amber {
- background: rgba(120, 53, 15, 0.45);
- border-color: rgba(251, 191, 36, 0.3);
- color: #fcd34d;
-}
-
-.empty-state {
- align-items: center;
- color: #71717a;
- display: flex;
- flex-direction: column;
- font-size: 14px;
- gap: 10px;
- justify-content: center;
- min-height: 180px;
-}
-
-html.dark .empty-state {
- color: #a1a1aa;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- color-scheme: dark;
- }
-
- body {
- background: #09090b;
- color: #f4f4f5;
- }
-
- .admin-sidebar {
- background: #111113;
- border-color: #27272a;
- }
-
- .admin-main {
- background: #09090b;
- }
-
- .admin-topbar {
- background: rgba(9, 9, 11, 0.88);
- border-color: #27272a;
- }
-
- .nav-item {
- color: #d4d4d8;
- }
-
- .nav-item:hover {
- background: #27272a;
- }
-
- .nav-item-active {
- background: #18181b;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
- color: #f4f4f5;
- }
-
- .panel {
- background: #18181b;
- border-color: #27272a;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
- }
-
- .metric-card {
- background: linear-gradient(180deg, #18181b 0%, #111113 100%);
- border-color: #27272a;
- }
-
- .field,
- .textarea {
- background: #09090b;
- border-color: #27272a;
- color: #f4f4f5;
- }
-
- .btn-secondary {
- background: #18181b;
- border-color: #3f3f46;
- color: #f4f4f5;
- }
-
- .btn-danger {
- background: #18181b;
- border-color: #7f1d1d;
- color: #fecaca;
- }
-
- .table th,
- .table td {
- border-color: #27272a;
- }
-
- .table th {
- background: #111113;
- color: #a1a1aa;
- }
-
- .table td {
- color: #e4e4e7;
- }
-
- .badge {
- border-color: #3f3f46;
- color: #e4e4e7;
- }
-
- .badge-green {
- background: rgba(6, 78, 59, 0.5);
- border-color: rgba(52, 211, 153, 0.28);
- color: #6ee7b7;
- }
-
- .badge-amber {
- background: rgba(120, 53, 15, 0.45);
- border-color: rgba(251, 191, 36, 0.3);
- color: #fcd34d;
- }
-
- .empty-state {
- color: #a1a1aa;
- }
-}
-
-@media (max-width: 900px) {
- .admin-shell {
- grid-template-columns: 1fr;
- }
-
- .admin-sidebar {
- border-bottom: 1px solid #e5e7eb;
- border-right: 0;
- min-height: auto;
- }
-
- html.dark .admin-sidebar {
- border-bottom-color: #27272a;
- }
-
- .admin-content {
- padding: 16px;
- }
-}
-
-@media (max-width: 900px) and (prefers-color-scheme: dark) {
- .admin-sidebar {
- border-bottom-color: #27272a;
- }
-}
diff --git a/apps/ui-admin/tsconfig.json b/apps/ui-admin/tsconfig.json
deleted file mode 100644
index e2616a33f..000000000
--- a/apps/ui-admin/tsconfig.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "extends": "../../tsconfig.json",
- "compilerOptions": {
- "jsx": "preserve",
- "lib": [
- "DOM",
- "ESNext",
- "DOM.Iterable",
- "DOM.AsyncIterable"
- ],
- "useDefineForClassFields": true,
- "paths": {
- "@proj-airi/stage-shared/*": [
- "../../packages/stage-shared/src/*"
- ]
- },
- "resolveJsonModule": true,
- "types": [
- "vite/client",
- "vue-macros/macros-global"
- ],
- "strict": true,
- "skipLibCheck": true
- },
- "vueCompilerOptions": {
- "plugins": [
- "@vue-macros/volar/define-models",
- "@vue-macros/volar/define-slots"
- ]
- }
-}
diff --git a/apps/ui-admin/uno.config.ts b/apps/ui-admin/uno.config.ts
deleted file mode 100644
index df19debe1..000000000
--- a/apps/ui-admin/uno.config.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { mergeConfigs, presetWebFonts } from 'unocss'
-
-import { presetWebFontsFonts, sharedUnoConfig } from '../../uno.config'
-
-export default mergeConfigs([
- sharedUnoConfig(),
- {
- presets: [
- presetWebFonts({
- fonts: {
- ...presetWebFontsFonts('fontsource'),
- },
- timeouts: {
- warning: 5000,
- failure: 10000,
- },
- }),
- ],
- },
-])
diff --git a/apps/ui-admin/vite.config.ts b/apps/ui-admin/vite.config.ts
deleted file mode 100644
index 413e2e75a..000000000
--- a/apps/ui-admin/vite.config.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { join, resolve } from 'node:path'
-
-import Vue from '@vitejs/plugin-vue'
-import Unocss from 'unocss/vite'
-import VueMacros from 'vue-macros/vite'
-
-import { defineConfig } from 'vite'
-
-export default defineConfig({
- resolve: {
- alias: {
- '@proj-airi/stage-shared': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-shared', 'src')),
- },
- },
- server: {
- fs: {
- strict: false,
- },
- },
- build: {
- emptyOutDir: true,
- outDir: resolve(join(import.meta.dirname, 'dist')),
- sourcemap: true,
- },
- plugins: [
- VueMacros({
- plugins: {
- vue: Vue({
- include: [/\.vue$/, /\.md$/],
- }),
- vueJsx: false,
- },
- betterDefine: false,
- }),
- Unocss(),
- ],
-})
diff --git a/apps/ui-admin/vitest.config.ts b/apps/ui-admin/vitest.config.ts
deleted file mode 100644
index 24a6c26d6..000000000
--- a/apps/ui-admin/vitest.config.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { join } from 'node:path'
-import { cwd } from 'node:process'
-
-import Vue from '@vitejs/plugin-vue'
-import VueMacros from 'vue-macros/vite'
-
-import { loadEnv } from 'vite'
-import { defineConfig } from 'vitest/config'
-
-export default defineConfig(({ mode }) => {
- return {
- plugins: [
- VueMacros({
- plugins: {
- vue: Vue({
- include: [/\.vue$/, /\.md$/],
- }),
- vueJsx: false,
- },
- betterDefine: false,
- }),
- ],
- test: {
- include: ['src/**/*.test.ts'],
- env: loadEnv(mode, join(cwd(), 'apps', 'ui-admin'), ''),
- },
- }
-})
diff --git a/apps/ui-admin/wrangler.toml b/apps/ui-admin/wrangler.toml
deleted file mode 100644
index a55ebf004..000000000
--- a/apps/ui-admin/wrangler.toml
+++ /dev/null
@@ -1,7 +0,0 @@
-name = "airi-server-admin"
-compatibility_date = "2025-04-01"
-preview_urls = true
-
-[assets]
-directory = "./dist"
-not_found_handling = "single-page-application"
diff --git a/apps/ui-server-auth/README.md b/apps/ui-server-auth/README.md
index 01381e89e..286d704f2 100644
--- a/apps/ui-server-auth/README.md
+++ b/apps/ui-server-auth/README.md
@@ -11,7 +11,7 @@ Auth UI for the hosted AIRI server. It is a Vue/Vite app deployed separately fro
## Do Not Use When
- Building the main stage app sign-in callback pages that consume OIDC tokens.
-- Adding admin-only operational pages. Those belong in `apps/ui-admin`.
+- Adding admin-only operational pages. Those belong in the standalone `proj-airi` admin repository.
## Commands
diff --git a/package.json b/package.json
index 85ca0172b..790b8dafe 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,6 @@
"dev:web": "pnpm -rF @proj-airi/stage-web run dev",
"dev:web:https": "pnpm -rF @proj-airi/stage-web run dev:https",
"dev:server-auth": "pnpm -rF @proj-airi/ui-server-auth run dev",
- "dev:admin": "pnpm -rF @proj-airi/ui-admin run dev",
"dev:pocket:ios": "pnpm -rF @proj-airi/stage-pocket run dev:ios",
"dev:pocket:android": "pnpm -rF @proj-airi/stage-pocket run dev:android",
"dev:server": "pnpm -rF @proj-airi/server-runtime run dev",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ba5321493..a6bc902a5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2257,7 +2257,7 @@ importers:
version: 3.0.2(electron@41.2.1)
'@electron-toolkit/tsconfig':
specifier: 'catalog:'
- version: 2.0.0(@types/node@24.12.2)
+ version: 2.0.0(@types/node@25.6.0)
'@electron-toolkit/utils':
specifier: 'catalog:'
version: 4.0.0(electron@41.2.1)
@@ -2296,7 +2296,7 @@ importers:
version: 3.1.0
'@intlify/unplugin-vue-i18n':
specifier: 'catalog:'
- version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
+ version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
'@modelcontextprotocol/sdk':
specifier: 'catalog:'
version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)
@@ -2332,10 +2332,10 @@ importers:
version: link:../../packages/ui-transitions
'@proj-airi/unplugin-fetch':
specifier: 'catalog:'
- version: 0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
+ version: 0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@proj-airi/unplugin-live2d-sdk':
specifier: 'catalog:'
- version: 0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
+ version: 0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
'@types/audioworklet':
specifier: 'catalog:'
version: 0.0.97
@@ -2362,7 +2362,7 @@ importers:
version: 2.10.3
'@vitejs/plugin-vue':
specifier: 'catalog:'
- version: 6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
+ version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
'@vue-macros/volar':
specifier: 'catalog:'
version: 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
@@ -2395,7 +2395,7 @@ importers:
version: 6.8.3
electron-vite:
specifier: 'catalog:'
- version: 5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
+ version: 5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
get-port-please:
specifier: 'catalog:'
version: 3.2.0
@@ -2416,31 +2416,31 @@ importers:
version: 2.2.6
unocss-preset-scrollbar:
specifier: 'catalog:'
- version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
+ version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
unplugin-info:
specifier: 'catalog:'
- version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
+ version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
unplugin-yaml:
specifier: 'catalog:'
- version: 4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
+ version: 4.1.0(@nuxt/kit@3.20.2(magicast@0.5.2))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vite:
specifier: 'catalog:'
- version: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
+ version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-bundle-visualizer:
specifier: 'catalog:'
version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@4.60.1)
vite-plugin-mkcert:
specifier: 'catalog:'
- version: 2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
+ version: 2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vite-plugin-vue-devtools:
specifier: 'catalog:'
- version: 8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
+ version: 8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
vite-plugin-vue-layouts:
specifier: 'catalog:'
- version: 0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
+ version: 0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
vue-macros:
specifier: 'catalog:'
- version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
+ version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
vue-tsc:
specifier: 'catalog:'
version: 3.2.6(typescript@5.9.3)
@@ -2836,64 +2836,6 @@ importers:
specifier: 'catalog:'
version: 3.2.6(typescript@5.9.3)
- apps/ui-admin:
- dependencies:
- '@proj-airi/font-chillroundm':
- specifier: workspace:^
- version: link:../../packages/font-chillroundm
- '@proj-airi/stage-shared':
- specifier: workspace:^
- version: link:../../packages/stage-shared
- '@proj-airi/ui':
- specifier: workspace:^
- version: link:../../packages/ui
- '@vueuse/core':
- specifier: ^14.2.1
- version: 14.2.1(vue@3.5.32(typescript@5.9.3))
- nprogress:
- specifier: 'catalog:'
- version: 0.2.0
- pinia:
- specifier: 'catalog:'
- version: 3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3))
- vue:
- specifier: 'catalog:'
- version: 3.5.32(typescript@5.9.3)
- vue-router:
- specifier: 'catalog:'
- version: 5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
- vue-sonner:
- specifier: 'catalog:'
- version: 2.0.9(vue@3.5.32(typescript@5.9.3))
- devDependencies:
- '@iconify-json/lucide':
- specifier: ^1.2.102
- version: 1.2.102
- '@types/nprogress':
- specifier: ^0.2.3
- version: 0.2.3
- '@unocss/reset':
- specifier: ^66.6.8
- version: 66.6.8
- '@vitejs/plugin-vue':
- specifier: ^6.0.6
- version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/volar':
- specifier: ^3.1.2
- version: 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
- unocss:
- specifier: 'catalog:'
- version: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
- vite:
- specifier: 'catalog:'
- version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- vue-macros:
- specifier: 'catalog:'
- version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
- vue-tsc:
- specifier: 'catalog:'
- version: 3.2.6(typescript@5.9.3)
-
apps/ui-server-auth:
dependencies:
'@date-fns/utc':
@@ -20735,9 +20677,9 @@ snapshots:
dependencies:
electron: 41.2.1
- '@electron-toolkit/tsconfig@2.0.0(@types/node@24.12.2)':
+ '@electron-toolkit/tsconfig@2.0.0(@types/node@25.6.0)':
dependencies:
- '@types/node': 24.12.2
+ '@types/node': 25.6.0
'@electron-toolkit/utils@4.0.0(electron@41.2.1)':
dependencies:
@@ -21647,31 +21589,6 @@ snapshots:
- supports-color
- typescript
- '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
- '@intlify/bundle-utils': 11.0.7(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))
- '@intlify/shared': 11.3.2
- '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
- '@rollup/pluginutils': 5.3.0(rollup@4.60.1)
- '@typescript-eslint/scope-manager': 8.58.1
- '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3)
- debug: 4.4.3(supports-color@10.2.2)
- fast-glob: 3.3.3
- pathe: 2.0.3
- picocolors: 1.1.1
- unplugin: 2.3.11
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- vue: 3.5.32(typescript@5.9.3)
- optionalDependencies:
- vue-i18n: 11.3.2(vue@3.5.32(typescript@5.9.3))
- transitivePeerDependencies:
- - '@vue/compiler-dom'
- - eslint
- - rollup
- - supports-color
- - typescript
-
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
@@ -23674,35 +23591,11 @@ snapshots:
transitivePeerDependencies:
- magicast
- '@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
- dependencies:
- ofetch: 1.5.1
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
-
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
ofetch: 1.5.1
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- '@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
- dependencies:
- ofetch: 1.5.1
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- yauzl: 3.3.0
- transitivePeerDependencies:
- - '@types/node'
- - '@vitejs/devtools'
- - esbuild
- - jiti
- - less
- - sass
- - sass-embedded
- - stylus
- - sugarss
- - terser
- - tsx
- - yaml
-
'@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
dependencies:
ofetch: 1.5.1
@@ -25249,12 +25142,6 @@ snapshots:
transitivePeerDependencies:
- typescript
- '@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
- dependencies:
- '@rolldown/pluginutils': 1.0.0-rc.13
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- vue: 3.5.32(typescript@5.9.3)
-
'@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.13
@@ -25532,15 +25419,6 @@ snapshots:
transitivePeerDependencies:
- vue
- '@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
- dependencies:
- sirv: 3.0.2
- vue: 3.5.32(typescript@5.9.3)
- optionalDependencies:
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- transitivePeerDependencies:
- - typescript
-
'@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
sirv: 3.0.2
@@ -27618,7 +27496,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- electron-vite@5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
+ electron-vite@5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
@@ -27626,7 +27504,7 @@ snapshots:
esbuild: 0.25.12
magic-string: 0.30.21
picocolors: 1.1.1
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
+ vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- supports-color
@@ -33617,6 +33495,11 @@ snapshots:
'@unocss/preset-mini': 66.6.8
unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
+ unocss-preset-scrollbar@4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))):
+ dependencies:
+ '@unocss/preset-mini': 66.6.8
+ unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
+
unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@unocss/cli': 66.6.8
@@ -33683,14 +33566,6 @@ snapshots:
unplugin: 2.3.11
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
- optionalDependencies:
- esbuild: 0.27.2
- rolldown: 1.0.0-rc.16
- rollup: 4.60.1
- unplugin: 2.3.11
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
-
unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies:
esbuild: 0.27.2
@@ -33712,19 +33587,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
- dependencies:
- ci-info: 4.4.0
- git-url-parse: 16.1.0
- simple-git: 3.36.0
- unplugin: 2.3.11
- optionalDependencies:
- esbuild: 0.27.2
- rollup: 4.60.1
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- transitivePeerDependencies:
- - supports-color
-
unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
ci-info: 4.4.0
@@ -33818,17 +33680,6 @@ snapshots:
rollup: 2.80.0
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- unplugin-yaml@4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
- dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.60.1)
- unplugin: 3.0.0
- yaml: 2.8.3
- optionalDependencies:
- esbuild: 0.27.2
- rolldown: 1.0.0-rc.16
- rollup: 4.60.1
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
-
unplugin@2.3.11:
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -34056,22 +33907,12 @@ snapshots:
- rollup
- supports-color
- vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
- dependencies:
- birpc: 2.9.0
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- vite-hot-client: 2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
-
vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
birpc: 2.9.0
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-hot-client: 2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
- vite-hot-client@2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
- dependencies:
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
-
vite-hot-client@2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
@@ -34117,21 +33958,6 @@ snapshots:
- tsx
- yaml
- vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
- dependencies:
- ansis: 4.2.0
- debug: 4.4.3(supports-color@10.2.2)
- error-stack-parser-es: 1.0.5
- ohash: 2.0.11
- open: 10.2.0
- perfect-debounce: 2.1.0
- sirv: 3.0.2
- unplugin-utils: 0.3.1
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- vite-dev-rpc: 1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
- transitivePeerDependencies:
- - supports-color
-
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
ansis: 4.2.0
@@ -34163,13 +33989,6 @@ snapshots:
- typescript
- ws
- vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
- dependencies:
- debug: 4.4.3(supports-color@10.2.2)
- supports-color: 10.2.2
- undici: 8.1.0
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
-
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
@@ -34188,20 +34007,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
- dependencies:
- '@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
- '@vue/devtools-kit': 8.1.1
- '@vue/devtools-shared': 8.1.1
- sirv: 3.0.2
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- vite-plugin-inspect: 11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
- vite-plugin-vue-inspector: 5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
- transitivePeerDependencies:
- - '@nuxt/kit'
- - supports-color
- - vue
-
vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
dependencies:
'@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
@@ -34216,21 +34021,6 @@ snapshots:
- supports-color
- vue
- vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
- dependencies:
- '@babel/core': 7.29.0
- '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.0)
- '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
- '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
- '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.29.0)
- '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0)
- '@vue/compiler-dom': 3.5.32
- kolorist: 1.8.0
- magic-string: 0.30.21
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- transitivePeerDependencies:
- - supports-color
-
vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@babel/core': 7.29.0
@@ -34246,16 +34036,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
- dependencies:
- debug: 4.4.3(supports-color@10.2.2)
- fast-glob: 3.3.3
- vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
- vue: 3.5.32(typescript@5.9.3)
- vue-router: 5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
- transitivePeerDependencies:
- - supports-color
-
vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
@@ -34523,54 +34303,6 @@ snapshots:
- vue-tsc
- webpack
- vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
- dependencies:
- '@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/boolean-prop': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/chain-call': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/common': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/config': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/define-emit': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/define-models': 3.1.2(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/define-prop': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/define-props': 3.1.2(@vue-macros/reactivity-transform@3.1.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/define-props-refs': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/define-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/define-slots': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/define-stylex': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/devtools': 3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
- '@vue-macros/export-expose': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/export-props': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/export-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/hoist-static': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/jsx-directive': 3.1.2(typescript@5.9.3)
- '@vue-macros/named-template': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/reactivity-transform': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/script-lang': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/setup-block': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/setup-component': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/setup-sfc': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/short-bind': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/short-emits': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/short-vmodel': 3.1.2(vue@3.5.32(typescript@5.9.3))
- '@vue-macros/volar': 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
- unplugin: 2.3.11
- unplugin-combine: 2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
- unplugin-vue-define-options: 3.1.2(vue@3.5.32(typescript@5.9.3))
- vue: 3.5.32(typescript@5.9.3)
- transitivePeerDependencies:
- - '@emnapi/core'
- - '@emnapi/runtime'
- - '@rspack/core'
- - '@vueuse/core'
- - esbuild
- - rolldown
- - rollup
- - typescript
- - vite
- - vue-tsc
- - webpack
-
vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
dependencies:
'@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))