mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat: add disable_models_service configuration to manage model service availability and update related components
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export let systemInfo: ApiRespSystemInfo = {
|
||||
enable_marketplace: true,
|
||||
cloud_service_url: '',
|
||||
allow_modify_login_info: true,
|
||||
disable_models_service: false,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -215,6 +215,7 @@ const jaJP = {
|
||||
addProvider: 'プロバイダーを追加',
|
||||
addProviderHint:
|
||||
'他のソースのモデルを使用するにはプロバイダーを追加してください',
|
||||
addProviderHintSimple: 'モデルを使用するにはプロバイダーを追加してください',
|
||||
noProviders: 'プロバイダーがありません',
|
||||
providerName: 'プロバイダー名',
|
||||
providerNameRequired: 'プロバイダー名は必須です',
|
||||
|
||||
@@ -202,6 +202,7 @@ const zhHans = {
|
||||
editProvider: '编辑供应商',
|
||||
addProvider: '添加供应商',
|
||||
addProviderHint: '添加自定义供应商以使用其他来源的模型',
|
||||
addProviderHintSimple: '添加自定义供应商以使用模型',
|
||||
noProviders: '暂无自定义供应商',
|
||||
providerName: '供应商名称',
|
||||
providerNameRequired: '供应商名称不能为空',
|
||||
|
||||
@@ -201,6 +201,7 @@ const zhHant = {
|
||||
editProvider: '編輯供應商',
|
||||
addProvider: '新增供應商',
|
||||
addProviderHint: '新增供應商以使用其他來源的模型',
|
||||
addProviderHintSimple: '新增供應商以使用模型',
|
||||
noProviders: '暫無供應商',
|
||||
providerName: '供應商名稱',
|
||||
providerNameRequired: '供應商名稱不能為空',
|
||||
|
||||
Reference in New Issue
Block a user