mirror of
https://github.com/moeru-ai/airi.git
synced 2026-07-09 15:58:27 +00:00
refactor(server): split capability alias admin routes
This commit is contained in:
parent
281614d396
commit
c7de38b121
9 changed files with 286 additions and 79 deletions
|
|
@ -56,6 +56,7 @@ import { registerTotalUsersGauge } from './otel/gauges/total-users'
|
|||
import { registerTtsPoolGauge } from './otel/gauges/tts-pool'
|
||||
import { createAdminRoutes } from './routes/admin'
|
||||
import { createAdminUiRoutes } from './routes/admin-ui'
|
||||
import { createAdminCapabilityAliasRoutes } from './routes/admin/capability-aliases'
|
||||
import { createAdminRouterConfigRoutes } from './routes/admin/config/router'
|
||||
import { createAdminFluxGrantsRoutes } from './routes/admin/flux-grants'
|
||||
import { createAdminProviderCatalogRoutes } from './routes/admin/provider-catalog'
|
||||
|
|
@ -420,6 +421,14 @@ export async function buildApp(deps: AppDeps) {
|
|||
service: deps.voicePackService,
|
||||
}))
|
||||
|
||||
/**
|
||||
* Admin product capability alias curation routes.
|
||||
*/
|
||||
.route('/api/admin/capability-aliases', createAdminCapabilityAliasRoutes({
|
||||
configKV: deps.configKV,
|
||||
service: deps.providerCatalogService,
|
||||
}))
|
||||
|
||||
/**
|
||||
* Admin provider catalog curation routes.
|
||||
*/
|
||||
|
|
|
|||
112
apps/server/src/routes/admin/capability-aliases/index.ts
Normal file
112
apps/server/src/routes/admin/capability-aliases/index.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import type { Context } from 'hono'
|
||||
import type { GenericSchema, InferOutput } from 'valibot'
|
||||
|
||||
import type { ConfigKVService } from '../../../services/adapters/config-kv'
|
||||
import type { ProviderCatalogService } from '../../../services/domain/provider-catalog'
|
||||
import type { HonoEnv } from '../../../types/hono'
|
||||
|
||||
import { Hono } from 'hono'
|
||||
import { boolean, integer, maxLength, minValue, number, object, optional, picklist, pipe, safeParse, string } from 'valibot'
|
||||
|
||||
import { adminGuard } from '../../../middlewares/admin-guard'
|
||||
import { authGuard } from '../../../middlewares/auth'
|
||||
import { createBadRequestError, createNotFoundError } from '../../../utils/error'
|
||||
|
||||
const SurfaceSchema = picklist(['llm', 'asr'])
|
||||
|
||||
const AliasUpdateBodySchema = object({
|
||||
displayName: optional(pipe(string(), maxLength(120))),
|
||||
enabled: optional(boolean()),
|
||||
displayOrder: optional(pipe(number(), integer(), minValue(0))),
|
||||
fallbackEnabled: optional(boolean()),
|
||||
loadBalancingEnabled: optional(boolean()),
|
||||
})
|
||||
|
||||
const AliasRouteUpdateBodySchema = object({
|
||||
enabled: optional(boolean()),
|
||||
pool: optional(picklist(['primary', 'fallback'])),
|
||||
weight: optional(pipe(number(), integer(), minValue(1))),
|
||||
displayOrder: optional(pipe(number(), integer(), minValue(0))),
|
||||
})
|
||||
|
||||
export interface AdminCapabilityAliasRoutesDeps {
|
||||
configKV: ConfigKVService
|
||||
service: ProviderCatalogService
|
||||
}
|
||||
|
||||
function parseIssues(issues: Array<{ path?: Array<{ key: unknown }>, message: string }>) {
|
||||
return issues.map(i => ({
|
||||
path: i.path?.map(p => p.key).join('.'),
|
||||
message: i.message,
|
||||
}))
|
||||
}
|
||||
|
||||
async function readJson(c: Context<HonoEnv>): Promise<unknown> {
|
||||
const raw = await c.req.json().catch(() => null)
|
||||
if (raw == null)
|
||||
throw createBadRequestError('Request body must be JSON', 'INVALID_BODY')
|
||||
return raw
|
||||
}
|
||||
|
||||
async function readBody<S extends GenericSchema>(c: Context<HonoEnv>, schema: S): Promise<InferOutput<S>> {
|
||||
const parsed = safeParse(schema, await readJson(c))
|
||||
if (!parsed.success)
|
||||
throw createBadRequestError('Invalid request body', 'INVALID_BODY', parseIssues(parsed.issues))
|
||||
return parsed.output
|
||||
}
|
||||
|
||||
async function syncAliasesFromConfig(deps: AdminCapabilityAliasRoutesDeps, surface: 'llm' | 'asr') {
|
||||
const config = await deps.configKV.getOrThrow('LLM_ROUTER_CONFIG')
|
||||
if (surface === 'llm') {
|
||||
const defaultModel = await deps.configKV.getOrThrow('DEFAULT_CHAT_MODEL')
|
||||
const modelIds = [
|
||||
defaultModel,
|
||||
...Object.keys(config.llm.models).sort().filter(modelId => modelId !== defaultModel),
|
||||
]
|
||||
return await deps.service.syncAliasesFromRouterConfig({ surface, modelIds })
|
||||
}
|
||||
|
||||
return await deps.service.syncAliasesFromRouterConfig({
|
||||
surface,
|
||||
modelIds: Object.keys(config.asr?.models ?? {}).sort(),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin routes for product capability aliases.
|
||||
*
|
||||
* Mounted at `/api/admin/capability-aliases`. These routes curate product
|
||||
* choices that clients can request, such as LLM `auto` or ASR `auto`. Real
|
||||
* provider inventory stays in the provider catalog.
|
||||
*/
|
||||
export function createAdminCapabilityAliasRoutes(deps: AdminCapabilityAliasRoutesDeps) {
|
||||
return new Hono<HonoEnv>()
|
||||
.use('*', authGuard)
|
||||
.use('*', adminGuard)
|
||||
.get('/', async (c) => {
|
||||
const rawSurface = c.req.query('surface')
|
||||
const parsed = rawSurface ? safeParse(SurfaceSchema, rawSurface) : null
|
||||
if (parsed && !parsed.success)
|
||||
throw createBadRequestError('Invalid surface', 'INVALID_QUERY', parseIssues(parsed.issues))
|
||||
|
||||
return c.json(await deps.service.listAliases(parsed?.success ? parsed.output : undefined))
|
||||
})
|
||||
.post('/sync', async (c) => {
|
||||
const body = await readBody(c, object({ surface: SurfaceSchema }))
|
||||
return c.json({ aliases: await syncAliasesFromConfig(deps, body.surface) })
|
||||
})
|
||||
.patch('/:id', async (c) => {
|
||||
const body = await readBody(c, AliasUpdateBodySchema)
|
||||
const updated = await deps.service.updateAlias(c.req.param('id'), body)
|
||||
if (!updated)
|
||||
throw createNotFoundError('Capability alias not found')
|
||||
return c.json(updated)
|
||||
})
|
||||
.patch('/routes/:id', async (c) => {
|
||||
const body = await readBody(c, AliasRouteUpdateBodySchema)
|
||||
const updated = await deps.service.updateAliasRoute(c.req.param('id'), body)
|
||||
if (!updated)
|
||||
throw createNotFoundError('Capability alias route not found')
|
||||
return c.json(updated)
|
||||
})
|
||||
}
|
||||
146
apps/server/src/routes/admin/capability-aliases/route.test.ts
Normal file
146
apps/server/src/routes/admin/capability-aliases/route.test.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import type { ConfigKVService } from '../../../services/adapters/config-kv'
|
||||
import type { ProviderCatalogService } from '../../../services/domain/provider-catalog'
|
||||
import type { HonoEnv } from '../../../types/hono'
|
||||
|
||||
import { Hono } from 'hono'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createAdminCapabilityAliasRoutes } from '.'
|
||||
import { ApiError } from '../../../utils/error'
|
||||
|
||||
interface MockUser {
|
||||
id: string
|
||||
email: string
|
||||
role?: string | null
|
||||
}
|
||||
|
||||
const ADMIN: MockUser = { id: 'admin-1', email: 'admin@example.com', role: 'admin' }
|
||||
|
||||
function createConfigKV(): ConfigKVService {
|
||||
return {
|
||||
getOrThrow: vi.fn(async (key: string) => {
|
||||
if (key === 'DEFAULT_CHAT_MODEL')
|
||||
return 'chat-default'
|
||||
if (key === 'LLM_ROUTER_CONFIG') {
|
||||
return {
|
||||
llm: {
|
||||
models: {
|
||||
'chat-default': { upstreams: [] },
|
||||
'chat-fallback': { upstreams: [] },
|
||||
},
|
||||
},
|
||||
tts: { models: {} },
|
||||
asr: { models: { 'aliyun/asr-primary': { provider: 'aliyun-nls', upstreams: [] } } },
|
||||
}
|
||||
}
|
||||
throw new ApiError(503, 'CONFIG_NOT_SET', 'Service configuration is incomplete')
|
||||
}),
|
||||
getOptional: vi.fn(async () => null),
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
} as unknown as ConfigKVService
|
||||
}
|
||||
|
||||
function createService(): ProviderCatalogService {
|
||||
return {
|
||||
syncAliasesFromRouterConfig: vi.fn(async () => []),
|
||||
listAliases: vi.fn(async () => []),
|
||||
resolveEnabledAlias: vi.fn(),
|
||||
updateAlias: vi.fn(async (_id, input) => ({ id: 'alias-1', ...input })),
|
||||
updateAliasRoute: vi.fn(async (_id, input) => ({ id: 'route-1', ...input })),
|
||||
} as unknown as ProviderCatalogService
|
||||
}
|
||||
|
||||
function createTestApp(input: {
|
||||
user: MockUser | null
|
||||
configKV?: ConfigKVService
|
||||
service?: ProviderCatalogService
|
||||
}) {
|
||||
return new Hono<HonoEnv>()
|
||||
.use('*', async (c, next) => {
|
||||
c.set('user', input.user as HonoEnv['Variables']['user'])
|
||||
await next()
|
||||
})
|
||||
.route('/api/admin/capability-aliases', createAdminCapabilityAliasRoutes({
|
||||
configKV: input.configKV ?? createConfigKV(),
|
||||
service: input.service ?? createService(),
|
||||
}))
|
||||
.onError((err, c) => {
|
||||
if (err instanceof ApiError)
|
||||
return c.json({ error: err.errorCode, details: err.details }, err.statusCode)
|
||||
return c.json({ error: 'internal', message: (err as Error).message }, 500)
|
||||
})
|
||||
}
|
||||
|
||||
function jsonRequest(app: Hono<HonoEnv>, method: string, path: string, body?: unknown) {
|
||||
return app.request(path, {
|
||||
method,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: body == null ? undefined : JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
describe('admin capability alias routes', () => {
|
||||
it('returns 401 when unauthenticated', async () => {
|
||||
const service = createService()
|
||||
const app = createTestApp({ user: null, service })
|
||||
const res = await jsonRequest(app, 'GET', '/api/admin/capability-aliases')
|
||||
|
||||
expect(res.status).toBe(401)
|
||||
expect(service.listAliases).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('syncs LLM aliases from router config with the default model first', async () => {
|
||||
const service = createService()
|
||||
const app = createTestApp({ user: ADMIN, service })
|
||||
|
||||
const res = await jsonRequest(app, 'POST', '/api/admin/capability-aliases/sync', {
|
||||
surface: 'llm',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(service.syncAliasesFromRouterConfig).toHaveBeenCalledWith({
|
||||
surface: 'llm',
|
||||
modelIds: ['chat-default', 'chat-fallback'],
|
||||
})
|
||||
})
|
||||
|
||||
it('syncs ASR aliases from router config', async () => {
|
||||
const service = createService()
|
||||
const app = createTestApp({ user: ADMIN, service })
|
||||
|
||||
const res = await jsonRequest(app, 'POST', '/api/admin/capability-aliases/sync', {
|
||||
surface: 'asr',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(service.syncAliasesFromRouterConfig).toHaveBeenCalledWith({
|
||||
surface: 'asr',
|
||||
modelIds: ['aliyun/asr-primary'],
|
||||
})
|
||||
})
|
||||
|
||||
it('maps missing aliases to 404 on update', async () => {
|
||||
const service = createService()
|
||||
vi.mocked(service.updateAlias).mockResolvedValueOnce(null)
|
||||
const app = createTestApp({ user: ADMIN, service })
|
||||
|
||||
const res = await jsonRequest(app, 'PATCH', '/api/admin/capability-aliases/missing', {
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
|
||||
it('updates alias routes separately from aliases', async () => {
|
||||
const service = createService()
|
||||
const app = createTestApp({ user: ADMIN, service })
|
||||
|
||||
const res = await jsonRequest(app, 'PATCH', '/api/admin/capability-aliases/routes/route-1', {
|
||||
pool: 'fallback',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(service.updateAliasRoute).toHaveBeenCalledWith('route-1', { pool: 'fallback' })
|
||||
})
|
||||
})
|
||||
|
|
@ -9,7 +9,7 @@ import type { HonoEnv } from '../../../types/hono'
|
|||
import { Buffer } from 'node:buffer'
|
||||
|
||||
import { Hono } from 'hono'
|
||||
import { any, array, boolean, integer, maxLength, minValue, nullable, number, object, optional, picklist, pipe, record, safeParse, string } from 'valibot'
|
||||
import { any, array, boolean, integer, maxLength, minValue, nullable, number, object, optional, pipe, record, safeParse, string } from 'valibot'
|
||||
|
||||
import { adminGuard } from '../../../middlewares/admin-guard'
|
||||
import { authGuard } from '../../../middlewares/auth'
|
||||
|
|
@ -18,23 +18,6 @@ import { createBadGatewayError, createBadRequestError, createNotFoundError } fro
|
|||
|
||||
const DEFAULT_PREVIEW_TEXT = 'Hello, this is an AIRI voice preview.'
|
||||
|
||||
const SurfaceSchema = picklist(['llm', 'asr'])
|
||||
|
||||
const AliasUpdateBodySchema = object({
|
||||
displayName: optional(pipe(string(), maxLength(120))),
|
||||
enabled: optional(boolean()),
|
||||
displayOrder: optional(pipe(number(), integer(), minValue(0))),
|
||||
fallbackEnabled: optional(boolean()),
|
||||
loadBalancingEnabled: optional(boolean()),
|
||||
})
|
||||
|
||||
const AliasRouteUpdateBodySchema = object({
|
||||
enabled: optional(boolean()),
|
||||
pool: optional(picklist(['primary', 'fallback'])),
|
||||
weight: optional(pipe(number(), integer(), minValue(1))),
|
||||
displayOrder: optional(pipe(number(), integer(), minValue(0))),
|
||||
})
|
||||
|
||||
const TtsModelUpdateBodySchema = object({
|
||||
displayName: optional(pipe(string(), maxLength(120))),
|
||||
enabled: optional(boolean()),
|
||||
|
|
@ -91,23 +74,6 @@ async function readBody<S extends GenericSchema>(c: Context<HonoEnv>, schema: S)
|
|||
return parsed.output
|
||||
}
|
||||
|
||||
async function syncAliasesFromConfig(deps: AdminProviderCatalogRoutesDeps, surface: 'llm' | 'asr') {
|
||||
const config = await deps.configKV.getOrThrow('LLM_ROUTER_CONFIG')
|
||||
if (surface === 'llm') {
|
||||
const defaultModel = await deps.configKV.getOrThrow('DEFAULT_CHAT_MODEL')
|
||||
const modelIds = [
|
||||
defaultModel,
|
||||
...Object.keys(config.llm.models).sort().filter(modelId => modelId !== defaultModel),
|
||||
]
|
||||
return await deps.service.syncAliasesFromRouterConfig({ surface, modelIds })
|
||||
}
|
||||
|
||||
return await deps.service.syncAliasesFromRouterConfig({
|
||||
surface,
|
||||
modelIds: Object.keys(config.asr?.models ?? {}).sort(),
|
||||
})
|
||||
}
|
||||
|
||||
async function syncTtsModelsFromConfig(deps: AdminProviderCatalogRoutesDeps) {
|
||||
const config = await deps.configKV.getOrThrow('LLM_ROUTER_CONFIG')
|
||||
return await deps.service.syncTtsModelsFromRouterConfig({
|
||||
|
|
@ -124,40 +90,14 @@ async function syncTtsModelsFromConfig(deps: AdminProviderCatalogRoutesDeps) {
|
|||
* Admin routes for provider catalog curation.
|
||||
*
|
||||
* Mounted at `/api/admin/provider-catalog`. These routes curate only the
|
||||
* catalog state: enabled flags, display order, capability aliases, and TTS
|
||||
* voice metadata. Real upstream URLs, credentials, and provider fallback
|
||||
* config stay owned by `LLM_ROUTER_CONFIG`.
|
||||
* catalog state: enabled flags, display order, and TTS voice metadata. Real
|
||||
* upstream URLs, credentials, and provider fallback config stay owned by
|
||||
* `LLM_ROUTER_CONFIG`.
|
||||
*/
|
||||
export function createAdminProviderCatalogRoutes(deps: AdminProviderCatalogRoutesDeps) {
|
||||
return new Hono<HonoEnv>()
|
||||
.use('*', authGuard)
|
||||
.use('*', adminGuard)
|
||||
.get('/aliases', async (c) => {
|
||||
const rawSurface = c.req.query('surface')
|
||||
const parsed = rawSurface ? safeParse(SurfaceSchema, rawSurface) : null
|
||||
if (parsed && !parsed.success)
|
||||
throw createBadRequestError('Invalid surface', 'INVALID_QUERY', parseIssues(parsed.issues))
|
||||
|
||||
return c.json(await deps.service.listAliases(parsed?.success ? parsed.output : undefined))
|
||||
})
|
||||
.post('/aliases/sync', async (c) => {
|
||||
const body = await readBody(c, object({ surface: SurfaceSchema }))
|
||||
return c.json({ aliases: await syncAliasesFromConfig(deps, body.surface) })
|
||||
})
|
||||
.patch('/aliases/:id', async (c) => {
|
||||
const body = await readBody(c, AliasUpdateBodySchema)
|
||||
const updated = await deps.service.updateAlias(c.req.param('id'), body)
|
||||
if (!updated)
|
||||
throw createNotFoundError('Capability alias not found')
|
||||
return c.json(updated)
|
||||
})
|
||||
.patch('/alias-routes/:id', async (c) => {
|
||||
const body = await readBody(c, AliasRouteUpdateBodySchema)
|
||||
const updated = await deps.service.updateAliasRoute(c.req.param('id'), body)
|
||||
if (!updated)
|
||||
throw createNotFoundError('Capability alias route not found')
|
||||
return c.json(updated)
|
||||
})
|
||||
.get('/tts/models', async (c) => {
|
||||
return c.json(await deps.service.listTtsModels())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -140,10 +140,10 @@ describe('admin provider catalog routes', () => {
|
|||
it('returns 401 when unauthenticated', async () => {
|
||||
const service = createService()
|
||||
const app = createTestApp({ user: null, service })
|
||||
const res = await jsonRequest(app, 'GET', '/api/admin/provider-catalog/aliases')
|
||||
const res = await jsonRequest(app, 'GET', '/api/admin/provider-catalog/tts/models')
|
||||
|
||||
expect(res.status).toBe(401)
|
||||
expect(service.listAliases).not.toHaveBeenCalled()
|
||||
expect(service.listTtsModels).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('syncs TTS voices from the provider into the provider catalog', async () => {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const navItems = [
|
|||
{ to: '/users', icon: 'i-lucide-users', label: 'Users' },
|
||||
{ to: '/flux', icon: 'i-lucide-coins', label: 'Flux' },
|
||||
{ to: '/llm-router', icon: 'i-lucide-route', label: 'LLM Router' },
|
||||
{ to: '/providers', icon: 'i-lucide-network', label: 'Providers' },
|
||||
{ to: '/capability-aliases', icon: 'i-lucide-route', label: 'Capability Aliases' },
|
||||
{ to: '/tts', icon: 'i-lucide-audio-lines', label: 'TTS' },
|
||||
{ to: '/voice-packs', icon: 'i-lucide-volume-2', label: 'Voice Packs' },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ 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 ProviderCatalogPage from './pages/ProviderCatalogPage.vue'
|
||||
import TtsCatalogPage from './pages/TtsCatalogPage.vue'
|
||||
import UsersPage from './pages/UsersPage.vue'
|
||||
import VoicePackFormPage from './pages/VoicePackFormPage.vue'
|
||||
|
|
@ -28,7 +28,7 @@ const router = createRouter({
|
|||
{ path: '/users', component: UsersPage },
|
||||
{ path: '/flux', component: FluxPage },
|
||||
{ path: '/llm-router', component: LlmRouterPage },
|
||||
{ path: '/providers', component: ProviderCatalogPage },
|
||||
{ path: '/capability-aliases', component: CapabilityAliasesPage },
|
||||
{ path: '/tts', component: TtsCatalogPage },
|
||||
{ path: '/voice-packs', component: VoicePacksPage },
|
||||
{ path: '/voice-packs/new', name: 'voice-pack-new', component: VoicePackFormPage },
|
||||
|
|
|
|||
|
|
@ -501,20 +501,20 @@ export const adminApi = {
|
|||
}),
|
||||
capabilityAliases: (surface?: CapabilityAliasSurface) => {
|
||||
const suffix = surface ? `?surface=${encodeURIComponent(surface)}` : ''
|
||||
return adminFetch<CapabilityAlias[]>(`/provider-catalog/aliases${suffix}`)
|
||||
return adminFetch<CapabilityAlias[]>(`/capability-aliases${suffix}`)
|
||||
},
|
||||
syncCapabilityAliases: (surface: CapabilityAliasSurface) =>
|
||||
adminFetch<{ aliases: CapabilityAlias[] }>('/provider-catalog/aliases/sync', {
|
||||
adminFetch<{ aliases: CapabilityAlias[] }>('/capability-aliases/sync', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ surface }),
|
||||
}),
|
||||
updateCapabilityAlias: (id: string, body: Partial<Pick<CapabilityAlias, 'displayName' | 'enabled' | 'displayOrder' | 'fallbackEnabled' | 'loadBalancingEnabled'>>) =>
|
||||
adminFetch<CapabilityAlias>(`/provider-catalog/aliases/${encodeURIComponent(id)}`, {
|
||||
adminFetch<CapabilityAlias>(`/capability-aliases/${encodeURIComponent(id)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
updateCapabilityAliasRoute: (id: string, body: Partial<Pick<CapabilityAliasRoute, 'enabled' | 'pool' | 'weight' | 'displayOrder'>>) =>
|
||||
adminFetch<CapabilityAliasRoute>(`/provider-catalog/alias-routes/${encodeURIComponent(id)}`, {
|
||||
adminFetch<CapabilityAliasRoute>(`/capability-aliases/routes/${encodeURIComponent(id)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ async function loadAliases() {
|
|||
aliases.value = await adminApi.capabilityAliases(surface.value)
|
||||
}
|
||||
catch (error) {
|
||||
toast.error(errorMessageFromUnknown(error, 'Failed to load provider catalog'))
|
||||
toast.error(errorMessageFromUnknown(error, 'Failed to load capability aliases'))
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
|
|
@ -37,11 +37,11 @@ async function syncAliases() {
|
|||
syncing.value = true
|
||||
try {
|
||||
await adminApi.syncCapabilityAliases(surface.value)
|
||||
toast.success('Provider aliases synced')
|
||||
toast.success('Capability aliases synced')
|
||||
await loadAliases()
|
||||
}
|
||||
catch (error) {
|
||||
toast.error(errorMessageFromUnknown(error, 'Failed to sync provider aliases'))
|
||||
toast.error(errorMessageFromUnknown(error, 'Failed to sync capability aliases'))
|
||||
}
|
||||
finally {
|
||||
syncing.value = false
|
||||
|
|
@ -83,10 +83,10 @@ function formatDate(value: string): string {
|
|||
<div :class="['flex', 'flex-col', 'gap-3', 'border-b', 'border-neutral-200', 'px-5', 'py-4', 'md:flex-row', 'md:items-center', 'md:justify-between']">
|
||||
<div>
|
||||
<h2 :class="['text-sm', 'font-semibold']">
|
||||
Provider Catalog
|
||||
Capability Aliases
|
||||
</h2>
|
||||
<p :class="['mt-1', 'text-sm', 'text-neutral-500']">
|
||||
Product aliases for official LLM and ASR capabilities.
|
||||
Product choices for LLM and ASR requests.
|
||||
</p>
|
||||
</div>
|
||||
<div :class="['flex', 'flex-wrap', 'items-center', 'gap-2']">
|
||||
|
|
@ -120,7 +120,7 @@ function formatDate(value: string): string {
|
|||
|
||||
<div v-if="loading && aliases.length === 0" :class="['empty-state']">
|
||||
<span :class="['i-lucide-loader-2', 'animate-spin', 'text-2xl']" />
|
||||
Loading provider catalog
|
||||
Loading capability aliases
|
||||
</div>
|
||||
|
||||
<div v-else-if="aliases.length > 0" :class="['divide-y', 'divide-neutral-200']">
|
||||
Loading…
Add table
Add a link
Reference in a new issue