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
@@ -22,6 +22,9 @@ class SystemRouterGroup(group.RouterGroup):
'allow_modify_login_info': self.ap.instance_config.data.get('system', {}).get( 'allow_modify_login_info': self.ap.instance_config.data.get('system', {}).get(
'allow_modify_login_info', True 'allow_modify_login_info', True
), ),
'disable_models_service': self.ap.instance_config.data.get('space', {}).get(
'disable_models_service', False
),
} }
) )
@@ -45,6 +45,12 @@ class ModelManager:
await self.load_models_from_db() await self.load_models_from_db()
# Check if space models service is disabled
space_config = self.ap.instance_config.data.get('space', {})
if space_config.get('disable_models_service', False):
self.ap.logger.info('LangBot Space Models service is disabled, skipping sync.')
return
try: try:
await self.sync_new_models_from_space() await self.sync_new_models_from_space()
except Exception as e: except Exception as e:
+1
View File
@@ -78,3 +78,4 @@ space:
models_gateway_api_url: 'https://api.langbot.cloud' models_gateway_api_url: 'https://api.langbot.cloud'
# OAuth authorization page URL (user will be redirected here) # OAuth authorization page URL (user will be redirected here)
oauth_authorize_url: 'https://space.langbot.app/auth/authorize' oauth_authorize_url: 'https://space.langbot.app/auth/authorize'
disable_models_service: false
@@ -17,7 +17,7 @@ import { Switch } from '@/components/ui/switch';
import { ControllerRenderProps } from 'react-hook-form'; import { ControllerRenderProps } from 'react-hook-form';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
import { import {
LLMModel, LLMModel,
Bot, Bot,
@@ -98,7 +98,14 @@ export default function DynamicFormItemComponent({
httpClient httpClient
.getProviderLLMModels() .getProviderLLMModels()
.then((resp) => { .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) => { .catch((err) => {
toast.error('Failed to get LLM model list: ' + err.message); toast.error('Failed to get LLM model list: ' + err.message);
@@ -2,7 +2,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Plus, Boxes } from 'lucide-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 { ModelProvider } from '@/app/infra/entities/api';
import { import {
Dialog, Dialog,
@@ -13,7 +13,6 @@ import {
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { extractI18nObject } from '@/i18n/I18nProvider';
import ProviderForm from './component/provider-form/ProviderForm'; import ProviderForm from './component/provider-form/ProviderForm';
import { ProviderCard } from './components'; import { ProviderCard } from './components';
import { import {
@@ -86,17 +85,13 @@ export default function ModelsDialog({
const [isTesting, setIsTesting] = useState(false); const [isTesting, setIsTesting] = useState(false);
const [testResult, setTestResult] = useState<TestResult | null>(null); const [testResult, setTestResult] = useState<TestResult | null>(null);
const [requesterNameList, setRequesterNameList] = useState<
{ label: string; value: string }[]
>([]);
// Track if providers have been loaded initially // Track if providers have been loaded initially
const [providersLoaded, setProvidersLoaded] = useState(false); const [providersLoaded, setProvidersLoaded] = useState(false);
// Separate LangBot Models provider // Separate LangBot Models provider (hide when models service is disabled)
const langbotProvider = providers.find( const langbotProvider = systemInfo.disable_models_service
(p) => p.requester === LANGBOT_MODELS_PROVIDER_REQUESTER, ? undefined
); : providers.find((p) => p.requester === LANGBOT_MODELS_PROVIDER_REQUESTER);
const otherProviders = providers.filter( const otherProviders = providers.filter(
(p) => p.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER, (p) => p.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER,
); );
@@ -104,7 +99,6 @@ export default function ModelsDialog({
useEffect(() => { useEffect(() => {
if (open) { if (open) {
loadUserInfo(); loadUserInfo();
loadRequesterLists();
loadProviders(); loadProviders();
} }
}, [open]); }, [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() { async function loadProviders() {
try { try {
const resp = await httpClient.getModelProviders(); const resp = await httpClient.getModelProviders();
@@ -397,7 +377,6 @@ export default function ModelsDialog({
models={providerModels[provider.uuid]} models={providerModels[provider.uuid]}
accountType={accountType} accountType={accountType}
spaceCredits={spaceCredits} spaceCredits={spaceCredits}
requesterNameList={requesterNameList}
addModelPopoverOpen={addModelPopoverOpen} addModelPopoverOpen={addModelPopoverOpen}
editModelPopoverOpen={editModelPopoverOpen} editModelPopoverOpen={editModelPopoverOpen}
deleteConfirmOpen={deleteConfirmOpen} deleteConfirmOpen={deleteConfirmOpen}
@@ -462,7 +441,11 @@ export default function ModelsDialog({
<div className="flex-shrink-0 mb-3 flex justify-between items-center"> <div className="flex-shrink-0 mb-3 flex justify-between items-center">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{otherProviders.length === 0 {otherProviders.length === 0
? t('models.addProviderHint') ? t(
systemInfo.disable_models_service
? 'models.addProviderHintSimple'
: 'models.addProviderHint',
)
: t('models.providerCount', { count: otherProviders.length })} : t('models.providerCount', { count: otherProviders.length })}
</span> </span>
<Button <Button
@@ -62,7 +62,13 @@ export default function ProviderForm({
}); });
const [requesterList, setRequesterList] = useState< const [requesterList, setRequesterList] = useState<
{ label: string; value: string; category: string; defaultUrl: string }[] {
label: string;
value: string;
category: string;
defaultUrl: string;
description: string;
}[]
>([]); >([]);
useEffect(() => { useEffect(() => {
@@ -73,17 +79,20 @@ export default function ProviderForm({
}, [providerId]); }, [providerId]);
async function loadRequesters() { async function loadRequesters() {
const resp = await httpClient.getProviderRequesters('llm'); const resp = await httpClient.getProviderRequesters();
setRequesterList( setRequesterList(
resp.requesters.map((item) => ({ resp.requesters
label: extractI18nObject(item.label), .filter((item) => item.name !== 'space-chat-completions')
value: item.name, .map((item) => ({
category: item.spec.provider_category || 'manufacturer', label: extractI18nObject(item.label),
defaultUrl: value: item.name,
item.spec.config category: item.spec.provider_category || 'manufacturer',
.find((c) => c.name === 'base_url') defaultUrl:
?.default?.toString() || '', item.spec.config
})), .find((c) => c.name === 'base_url')
?.default?.toString() || '',
description: extractI18nObject(item.description),
})),
); );
} }
@@ -145,63 +154,134 @@ export default function ProviderForm({
<FormField <FormField
control={form.control} control={form.control}
name="requester" name="requester"
render={({ field }) => ( render={({ field }) => {
<FormItem> const selectedRequester = requesterList.find(
<FormLabel> (r) => r.value === field.value,
{t('models.requester')} );
<span className="text-red-500">*</span> return (
</FormLabel> <FormItem>
<Select <FormLabel>
onValueChange={(v) => { {t('models.requester')}
field.onChange(v); <span className="text-red-500">*</span>
const req = requesterList.find((r) => r.value === v); </FormLabel>
// Auto-fill default URL when creating new provider <Select
// or when base_url is empty in edit mode onValueChange={(v) => {
if (req && (!providerId || !form.getValues('base_url'))) { field.onChange(v);
form.setValue('base_url', req.defaultUrl); 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
value={field.value} if (req && (!providerId || !form.getValues('base_url'))) {
> form.setValue('base_url', req.defaultUrl);
<SelectTrigger className="bg-background"> }
<SelectValue placeholder={t('models.selectRequester')} /> }}
</SelectTrigger> value={field.value}
<SelectContent> >
<SelectGroup> <SelectTrigger className="bg-background">
<SelectLabel>{t('models.modelManufacturer')}</SelectLabel> {selectedRequester ? (
{requesterList <div className="flex items-center gap-2">
.filter((r) => r.category === 'manufacturer') <img
.map((r) => ( src={httpClient.getProviderRequesterIconURL(
<SelectItem key={r.value} value={r.value}> selectedRequester.value,
{r.label} )}
</SelectItem> alt={selectedRequester.label}
))} className="h-5 w-5 rounded"
</SelectGroup> />
<SelectGroup> <span>{selectedRequester.label}</span>
<SelectLabel>{t('models.aggregationPlatform')}</SelectLabel> </div>
{requesterList ) : (
.filter((r) => r.category === 'maas') <SelectValue placeholder={t('models.selectRequester')} />
.map((r) => ( )}
<SelectItem key={r.value} value={r.value}> </SelectTrigger>
{r.label} <SelectContent>
</SelectItem> <SelectGroup>
))} <SelectLabel>{t('models.builtin')}</SelectLabel>
</SelectGroup> {requesterList
<SelectGroup> .filter((r) => r.category === 'builtin')
<SelectLabel>{t('models.selfDeployed')}</SelectLabel> .map((r) => (
{requesterList <SelectItem key={r.value} value={r.value}>
.filter((r) => r.category === 'self-hosted') <div className="flex items-center gap-2">
.map((r) => ( <img
<SelectItem key={r.value} value={r.value}> src={httpClient.getProviderRequesterIconURL(
{r.label} r.value,
</SelectItem> )}
))} alt={r.label}
</SelectGroup> className="h-5 w-5 rounded"
</SelectContent> />
</Select> <span>{r.label}</span>
<FormMessage /> </div>
</FormItem> </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 <FormField
@@ -38,7 +38,6 @@ interface ProviderCardProps {
models?: ProviderModels; models?: ProviderModels;
accountType: 'local' | 'space'; accountType: 'local' | 'space';
spaceCredits: number | null; spaceCredits: number | null;
requesterNameList: { label: string; value: string }[];
// Popover states // Popover states
addModelPopoverOpen: string | null; addModelPopoverOpen: string | null;
editModelPopoverOpen: string | null; editModelPopoverOpen: string | null;
@@ -94,7 +93,6 @@ export default function ProviderCard({
models, models,
accountType, accountType,
spaceCredits, spaceCredits,
requesterNameList,
addModelPopoverOpen, addModelPopoverOpen,
editModelPopoverOpen, editModelPopoverOpen,
deleteConfirmOpen, deleteConfirmOpen,
@@ -128,12 +126,6 @@ export default function ProviderCard({
const totalModels = const totalModels =
(provider.llm_count || 0) + (provider.embedding_count || 0); (provider.llm_count || 0) + (provider.embedding_count || 0);
const getRequesterLabel = (requester: string) => {
return (
requesterNameList.find((r) => r.value === requester)?.label || requester
);
};
return ( return (
<Card className="mb-2"> <Card className="mb-2">
<Collapsible open={isExpanded} onOpenChange={onToggle}> <Collapsible open={isExpanded} onOpenChange={onToggle}>
@@ -159,11 +151,7 @@ export default function ProviderCard({
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CardTitle className="text-base"> <CardTitle className="text-base">{provider.name}</CardTitle>
{isLangBotModels
? provider.name
: getRequesterLabel(provider.requester)}
</CardTitle>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{t('models.modelsCount', { count: totalModels })} {t('models.modelsCount', { count: totalModels })}
</Badge> </Badge>
@@ -13,7 +13,7 @@ import {
FormMessage, FormMessage,
FormDescription, FormDescription,
} from '@/components/ui/form'; } from '@/components/ui/form';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -95,7 +95,14 @@ export default function KBForm({
const getEmbeddingModelNameList = async () => { const getEmbeddingModelNameList = async () => {
const resp = await httpClient.getProviderEmbeddingModels(); 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>) => { const onSubmit = (data: z.infer<typeof formSchema>) => {
+1
View File
@@ -242,6 +242,7 @@ export interface ApiRespSystemInfo {
cloud_service_url: string; cloud_service_url: string;
enable_marketplace: boolean; enable_marketplace: boolean;
allow_modify_login_info: boolean; allow_modify_login_info: boolean;
disable_models_service: boolean;
} }
export interface ApiRespPluginSystemStatus { export interface ApiRespPluginSystemStatus {
+1 -1
View File
@@ -57,7 +57,7 @@ export class BackendClient extends BaseHttpClient {
// ============ Provider API ============ // ============ Provider API ============
public getProviderRequesters( public getProviderRequesters(
model_type: string, model_type?: string,
): Promise<ApiRespProviderRequesters> { ): Promise<ApiRespProviderRequesters> {
return this.get('/api/v1/provider/requesters', { type: model_type }); return this.get('/api/v1/provider/requesters', { type: model_type });
} }
+1
View File
@@ -9,6 +9,7 @@ export let systemInfo: ApiRespSystemInfo = {
enable_marketplace: true, enable_marketplace: true,
cloud_service_url: '', cloud_service_url: '',
allow_modify_login_info: true, allow_modify_login_info: true,
disable_models_service: false,
}; };
/** /**
+1
View File
@@ -209,6 +209,7 @@ const enUS = {
editProvider: 'Edit Provider', editProvider: 'Edit Provider',
addProvider: 'Add Provider', addProvider: 'Add Provider',
addProviderHint: 'Add providers to use models from other sources', addProviderHint: 'Add providers to use models from other sources',
addProviderHintSimple: 'Add providers to use models',
noProviders: 'No providers yet', noProviders: 'No providers yet',
providerName: 'Provider Name', providerName: 'Provider Name',
providerNameRequired: 'Provider name is required', providerNameRequired: 'Provider name is required',
+1
View File
@@ -215,6 +215,7 @@ const jaJP = {
addProvider: 'プロバイダーを追加', addProvider: 'プロバイダーを追加',
addProviderHint: addProviderHint:
'他のソースのモデルを使用するにはプロバイダーを追加してください', '他のソースのモデルを使用するにはプロバイダーを追加してください',
addProviderHintSimple: 'モデルを使用するにはプロバイダーを追加してください',
noProviders: 'プロバイダーがありません', noProviders: 'プロバイダーがありません',
providerName: 'プロバイダー名', providerName: 'プロバイダー名',
providerNameRequired: 'プロバイダー名は必須です', providerNameRequired: 'プロバイダー名は必須です',
+1
View File
@@ -202,6 +202,7 @@ const zhHans = {
editProvider: '编辑供应商', editProvider: '编辑供应商',
addProvider: '添加供应商', addProvider: '添加供应商',
addProviderHint: '添加自定义供应商以使用其他来源的模型', addProviderHint: '添加自定义供应商以使用其他来源的模型',
addProviderHintSimple: '添加自定义供应商以使用模型',
noProviders: '暂无自定义供应商', noProviders: '暂无自定义供应商',
providerName: '供应商名称', providerName: '供应商名称',
providerNameRequired: '供应商名称不能为空', providerNameRequired: '供应商名称不能为空',
+1
View File
@@ -201,6 +201,7 @@ const zhHant = {
editProvider: '編輯供應商', editProvider: '編輯供應商',
addProvider: '新增供應商', addProvider: '新增供應商',
addProviderHint: '新增供應商以使用其他來源的模型', addProviderHint: '新增供應商以使用其他來源的模型',
addProviderHintSimple: '新增供應商以使用模型',
noProviders: '暫無供應商', noProviders: '暫無供應商',
providerName: '供應商名稱', providerName: '供應商名稱',
providerNameRequired: '供應商名稱不能為空', providerNameRequired: '供應商名稱不能為空',