feat: add disable_models_service configuration to manage model service availability and update related components

This commit is contained in:
Junyan Qin
2026-01-01 15:40:39 +08:00
parent 75c2a063cc
commit 61f08f3218
15 changed files with 194 additions and 113 deletions

View File

@@ -17,7 +17,7 @@ import { Switch } from '@/components/ui/switch';
import { ControllerRenderProps } from 'react-hook-form';
import { Button } from '@/components/ui/button';
import { useEffect, useState } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
import {
LLMModel,
Bot,
@@ -98,7 +98,14 @@ export default function DynamicFormItemComponent({
httpClient
.getProviderLLMModels()
.then((resp) => {
setLlmModels(resp.models);
let models = resp.models;
// Filter out space-chat-completions models when models service is disabled
if (systemInfo.disable_models_service) {
models = models.filter(
(m) => m.provider?.requester !== 'space-chat-completions',
);
}
setLlmModels(models);
})
.catch((err) => {
toast.error('Failed to get LLM model list: ' + err.message);

View File

@@ -2,7 +2,7 @@
import { useState, useEffect } from 'react';
import { Plus, Boxes } from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
import { ModelProvider } from '@/app/infra/entities/api';
import {
Dialog,
@@ -13,7 +13,6 @@ import {
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { extractI18nObject } from '@/i18n/I18nProvider';
import ProviderForm from './component/provider-form/ProviderForm';
import { ProviderCard } from './components';
import {
@@ -86,17 +85,13 @@ export default function ModelsDialog({
const [isTesting, setIsTesting] = useState(false);
const [testResult, setTestResult] = useState<TestResult | null>(null);
const [requesterNameList, setRequesterNameList] = useState<
{ label: string; value: string }[]
>([]);
// Track if providers have been loaded initially
const [providersLoaded, setProvidersLoaded] = useState(false);
// Separate LangBot Models provider
const langbotProvider = providers.find(
(p) => p.requester === LANGBOT_MODELS_PROVIDER_REQUESTER,
);
// Separate LangBot Models provider (hide when models service is disabled)
const langbotProvider = systemInfo.disable_models_service
? undefined
: providers.find((p) => p.requester === LANGBOT_MODELS_PROVIDER_REQUESTER);
const otherProviders = providers.filter(
(p) => p.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER,
);
@@ -104,7 +99,6 @@ export default function ModelsDialog({
useEffect(() => {
if (open) {
loadUserInfo();
loadRequesterLists();
loadProviders();
}
}, [open]);
@@ -134,20 +128,6 @@ export default function ModelsDialog({
}
}
async function loadRequesterLists() {
try {
const llmRequesters = await httpClient.getProviderRequesters('llm');
setRequesterNameList(
llmRequesters.requesters.map((item) => ({
label: extractI18nObject(item.label),
value: item.name,
})),
);
} catch (err) {
console.error('Failed to load requester lists', err);
}
}
async function loadProviders() {
try {
const resp = await httpClient.getModelProviders();
@@ -397,7 +377,6 @@ export default function ModelsDialog({
models={providerModels[provider.uuid]}
accountType={accountType}
spaceCredits={spaceCredits}
requesterNameList={requesterNameList}
addModelPopoverOpen={addModelPopoverOpen}
editModelPopoverOpen={editModelPopoverOpen}
deleteConfirmOpen={deleteConfirmOpen}
@@ -462,7 +441,11 @@ export default function ModelsDialog({
<div className="flex-shrink-0 mb-3 flex justify-between items-center">
<span className="text-sm text-muted-foreground">
{otherProviders.length === 0
? t('models.addProviderHint')
? t(
systemInfo.disable_models_service
? 'models.addProviderHintSimple'
: 'models.addProviderHint',
)
: t('models.providerCount', { count: otherProviders.length })}
</span>
<Button

View File

@@ -62,7 +62,13 @@ export default function ProviderForm({
});
const [requesterList, setRequesterList] = useState<
{ label: string; value: string; category: string; defaultUrl: string }[]
{
label: string;
value: string;
category: string;
defaultUrl: string;
description: string;
}[]
>([]);
useEffect(() => {
@@ -73,17 +79,20 @@ export default function ProviderForm({
}, [providerId]);
async function loadRequesters() {
const resp = await httpClient.getProviderRequesters('llm');
const resp = await httpClient.getProviderRequesters();
setRequesterList(
resp.requesters.map((item) => ({
label: extractI18nObject(item.label),
value: item.name,
category: item.spec.provider_category || 'manufacturer',
defaultUrl:
item.spec.config
.find((c) => c.name === 'base_url')
?.default?.toString() || '',
})),
resp.requesters
.filter((item) => item.name !== 'space-chat-completions')
.map((item) => ({
label: extractI18nObject(item.label),
value: item.name,
category: item.spec.provider_category || 'manufacturer',
defaultUrl:
item.spec.config
.find((c) => c.name === 'base_url')
?.default?.toString() || '',
description: extractI18nObject(item.description),
})),
);
}
@@ -145,63 +154,134 @@ export default function ProviderForm({
<FormField
control={form.control}
name="requester"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('models.requester')}
<span className="text-red-500">*</span>
</FormLabel>
<Select
onValueChange={(v) => {
field.onChange(v);
const req = requesterList.find((r) => r.value === v);
// Auto-fill default URL when creating new provider
// or when base_url is empty in edit mode
if (req && (!providerId || !form.getValues('base_url'))) {
form.setValue('base_url', req.defaultUrl);
}
}}
value={field.value}
>
<SelectTrigger className="bg-background">
<SelectValue placeholder={t('models.selectRequester')} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t('models.modelManufacturer')}</SelectLabel>
{requesterList
.filter((r) => r.category === 'manufacturer')
.map((r) => (
<SelectItem key={r.value} value={r.value}>
{r.label}
</SelectItem>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>{t('models.aggregationPlatform')}</SelectLabel>
{requesterList
.filter((r) => r.category === 'maas')
.map((r) => (
<SelectItem key={r.value} value={r.value}>
{r.label}
</SelectItem>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>{t('models.selfDeployed')}</SelectLabel>
{requesterList
.filter((r) => r.category === 'self-hosted')
.map((r) => (
<SelectItem key={r.value} value={r.value}>
{r.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
render={({ field }) => {
const selectedRequester = requesterList.find(
(r) => r.value === field.value,
);
return (
<FormItem>
<FormLabel>
{t('models.requester')}
<span className="text-red-500">*</span>
</FormLabel>
<Select
onValueChange={(v) => {
field.onChange(v);
const req = requesterList.find((r) => r.value === v);
// Auto-fill default URL when creating new provider
// or when base_url is empty in edit mode
if (req && (!providerId || !form.getValues('base_url'))) {
form.setValue('base_url', req.defaultUrl);
}
}}
value={field.value}
>
<SelectTrigger className="bg-background">
{selectedRequester ? (
<div className="flex items-center gap-2">
<img
src={httpClient.getProviderRequesterIconURL(
selectedRequester.value,
)}
alt={selectedRequester.label}
className="h-5 w-5 rounded"
/>
<span>{selectedRequester.label}</span>
</div>
) : (
<SelectValue placeholder={t('models.selectRequester')} />
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t('models.builtin')}</SelectLabel>
{requesterList
.filter((r) => r.category === 'builtin')
.map((r) => (
<SelectItem key={r.value} value={r.value}>
<div className="flex items-center gap-2">
<img
src={httpClient.getProviderRequesterIconURL(
r.value,
)}
alt={r.label}
className="h-5 w-5 rounded"
/>
<span>{r.label}</span>
</div>
</SelectItem>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>{t('models.modelManufacturer')}</SelectLabel>
{requesterList
.filter((r) => r.category === 'manufacturer')
.map((r) => (
<SelectItem key={r.value} value={r.value}>
<div className="flex items-center gap-2">
<img
src={httpClient.getProviderRequesterIconURL(
r.value,
)}
alt={r.label}
className="h-5 w-5 rounded"
/>
<span>{r.label}</span>
</div>
</SelectItem>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>
{t('models.aggregationPlatform')}
</SelectLabel>
{requesterList
.filter((r) => r.category === 'maas')
.map((r) => (
<SelectItem key={r.value} value={r.value}>
<div className="flex items-center gap-2">
<img
src={httpClient.getProviderRequesterIconURL(
r.value,
)}
alt={r.label}
className="h-5 w-5 rounded"
/>
<span>{r.label}</span>
</div>
</SelectItem>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>{t('models.selfDeployed')}</SelectLabel>
{requesterList
.filter((r) => r.category === 'self-hosted')
.map((r) => (
<SelectItem key={r.value} value={r.value}>
<div className="flex items-center gap-2">
<img
src={httpClient.getProviderRequesterIconURL(
r.value,
)}
alt={r.label}
className="h-5 w-5 rounded"
/>
<span>{r.label}</span>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
{selectedRequester?.description && (
<p className="text-sm text-muted-foreground">
{selectedRequester.description}
</p>
)}
</FormItem>
);
}}
/>
<FormField

View File

@@ -38,7 +38,6 @@ interface ProviderCardProps {
models?: ProviderModels;
accountType: 'local' | 'space';
spaceCredits: number | null;
requesterNameList: { label: string; value: string }[];
// Popover states
addModelPopoverOpen: string | null;
editModelPopoverOpen: string | null;
@@ -94,7 +93,6 @@ export default function ProviderCard({
models,
accountType,
spaceCredits,
requesterNameList,
addModelPopoverOpen,
editModelPopoverOpen,
deleteConfirmOpen,
@@ -128,12 +126,6 @@ export default function ProviderCard({
const totalModels =
(provider.llm_count || 0) + (provider.embedding_count || 0);
const getRequesterLabel = (requester: string) => {
return (
requesterNameList.find((r) => r.value === requester)?.label || requester
);
};
return (
<Card className="mb-2">
<Collapsible open={isExpanded} onOpenChange={onToggle}>
@@ -159,11 +151,7 @@ export default function ProviderCard({
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<CardTitle className="text-base">
{isLangBotModels
? provider.name
: getRequesterLabel(provider.requester)}
</CardTitle>
<CardTitle className="text-base">{provider.name}</CardTitle>
<Badge variant="outline" className="text-xs">
{t('models.modelsCount', { count: totalModels })}
</Badge>

View File

@@ -13,7 +13,7 @@ import {
FormMessage,
FormDescription,
} from '@/components/ui/form';
import { httpClient } from '@/app/infra/http/HttpClient';
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
import {
Select,
SelectContent,
@@ -95,7 +95,14 @@ export default function KBForm({
const getEmbeddingModelNameList = async () => {
const resp = await httpClient.getProviderEmbeddingModels();
setEmbeddingModels(resp.models);
let models = resp.models;
// Filter out space-chat-completions models when models service is disabled
if (systemInfo.disable_models_service) {
models = models.filter(
(m) => m.provider?.requester !== 'space-chat-completions',
);
}
setEmbeddingModels(models);
};
const onSubmit = (data: z.infer<typeof formSchema>) => {

View File

@@ -242,6 +242,7 @@ export interface ApiRespSystemInfo {
cloud_service_url: string;
enable_marketplace: boolean;
allow_modify_login_info: boolean;
disable_models_service: boolean;
}
export interface ApiRespPluginSystemStatus {

View File

@@ -57,7 +57,7 @@ export class BackendClient extends BaseHttpClient {
// ============ Provider API ============
public getProviderRequesters(
model_type: string,
model_type?: string,
): Promise<ApiRespProviderRequesters> {
return this.get('/api/v1/provider/requesters', { type: model_type });
}

View File

@@ -9,6 +9,7 @@ export let systemInfo: ApiRespSystemInfo = {
enable_marketplace: true,
cloud_service_url: '',
allow_modify_login_info: true,
disable_models_service: false,
};
/**

View File

@@ -209,6 +209,7 @@ const enUS = {
editProvider: 'Edit Provider',
addProvider: 'Add Provider',
addProviderHint: 'Add providers to use models from other sources',
addProviderHintSimple: 'Add providers to use models',
noProviders: 'No providers yet',
providerName: 'Provider Name',
providerNameRequired: 'Provider name is required',

View File

@@ -215,6 +215,7 @@ const jaJP = {
addProvider: 'プロバイダーを追加',
addProviderHint:
'他のソースのモデルを使用するにはプロバイダーを追加してください',
addProviderHintSimple: 'モデルを使用するにはプロバイダーを追加してください',
noProviders: 'プロバイダーがありません',
providerName: 'プロバイダー名',
providerNameRequired: 'プロバイダー名は必須です',

View File

@@ -202,6 +202,7 @@ const zhHans = {
editProvider: '编辑供应商',
addProvider: '添加供应商',
addProviderHint: '添加自定义供应商以使用其他来源的模型',
addProviderHintSimple: '添加自定义供应商以使用模型',
noProviders: '暂无自定义供应商',
providerName: '供应商名称',
providerNameRequired: '供应商名称不能为空',

View File

@@ -201,6 +201,7 @@ const zhHant = {
editProvider: '編輯供應商',
addProvider: '新增供應商',
addProviderHint: '新增供應商以使用其他來源的模型',
addProviderHintSimple: '新增供應商以使用模型',
noProviders: '暫無供應商',
providerName: '供應商名稱',
providerNameRequired: '供應商名稱不能為空',