feat(models): show LangBot Models availability

This commit is contained in:
RockChinQ
2026-09-11 21:17:01 +08:00
parent e631da0073
commit a3509b3626
20 changed files with 375 additions and 6 deletions
@@ -220,6 +220,21 @@ class SystemRouterGroup(group.RouterGroup):
return self.http_status(503, -1, str(exc)) return self.http_status(503, -1, str(exc))
return self.success(data=model) return self.success(data=model)
@self.route(
'/model-availability',
methods=['GET'],
auth_type=group.AuthType.USER_TOKEN,
permission=Permission.RESOURCE_VIEW,
)
async def _(request_context: RequestContext) -> str:
"""Expose Space's latest persisted model probes to the WebUI."""
try:
models = await self.ap.space_service.get_model_selection()
except Exception as exc:
self.ap.logger.warning(f'Failed to load LangBot Models availability: {exc}')
return self.http_status(503, -1, 'Model availability is unavailable')
return self.success(data={'models': [model.model_dump(mode='json') for model in models]})
@self.route( @self.route(
'/tasks', '/tasks',
methods=['GET'], methods=['GET'],
+14 -3
View File
@@ -242,13 +242,14 @@ class SpaceService:
models_data = data.get('data', {}).get('models', []) models_data = data.get('data', {}).get('models', [])
return [SpaceModel.model_validate(model_dict) for model_dict in models_data] return [SpaceModel.model_validate(model_dict) for model_dict in models_data]
async def get_model_selection(self, category: str) -> typing.List[SpaceModelSelection]: async def get_model_selection(self, category: str | None = None) -> typing.List[SpaceModelSelection]:
"""Return Space models in the availability-ranked selection order.""" """Return Space models in the availability-ranked selection order."""
space_url = self._get_space_config()['url'] space_url = self._get_space_config()['url']
session = httpclient.get_session() session = httpclient.get_session()
params = {'category': category} if category else None
async with session.get( async with session.get(
f'{space_url}/api/v1/models/selection', f'{space_url}/api/v1/models/selection',
params={'category': category}, params=params,
) as response: ) as response:
if response.status != 200: if response.status != 200:
error = await httpclient.read_text_limited(response) error = await httpclient.read_text_limited(response)
@@ -266,7 +267,17 @@ class SpaceService:
models = [] models = []
for selection in data: for selection in data:
if isinstance(selection, dict) and isinstance(selection.get('model'), dict): if isinstance(selection, dict) and isinstance(selection.get('model'), dict):
models.append(selection['model']) model = dict(selection['model'])
availability = selection.get('availability')
if not isinstance(availability, dict):
# Accept the short-lived pre-release response shape.
availability = {
key: selection[key]
for key in ('up', 'last_probed_at', 'latency_ms', 'http_code')
if key in selection
}
model['availability'] = availability
models.append(model)
else: else:
models.append(selection) models.append(selection)
return [SpaceModelSelection.model_validate(model) for model in models] return [SpaceModelSelection.model_validate(model) for model in models]
+12 -1
View File
@@ -49,8 +49,19 @@ class SpaceModel(pydantic.BaseModel):
updated_at: str | None = None updated_at: str | None = None
class SpaceModelAvailability(pydantic.BaseModel):
"""Latest availability probe. ``up`` is None when no probe exists."""
up: bool | None = None
last_probed_at: str | None = None
latency_ms: int = 0
http_code: int = 0
class SpaceModelSelection(pydantic.BaseModel): class SpaceModelSelection(pydantic.BaseModel):
"""Minimal model identity returned by the ranked selection endpoint.""" """Model identity and the latest persisted probe from Space."""
uuid: str uuid: str
model_id: str model_id: str
category: str | None = None
availability: SpaceModelAvailability = pydantic.Field(default_factory=SpaceModelAvailability)
@@ -847,7 +847,15 @@ class TestSpaceServiceGetModelSelection:
data = {'models': models} data = {'models': models}
elif response_shape == 'availability-wrapper': elif response_shape == 'availability-wrapper':
data = [ data = [
{'model': model, 'latency_ms': index + 10, 'http_code': 200} {
'model': model,
'availability': {
'up': True,
'last_probed_at': '2026-09-11T12:01:18Z',
'latency_ms': index + 10,
'http_code': 200,
},
}
for index, model in enumerate(models) for index, model in enumerate(models)
] ]
else: else:
@@ -870,11 +878,56 @@ class TestSpaceServiceGetModelSelection:
result = await service.get_model_selection('chat') result = await service.get_model_selection('chat')
assert [model.uuid for model in result] == ['best-model', 'fallback-model'] assert [model.uuid for model in result] == ['best-model', 'fallback-model']
if response_shape == 'availability-wrapper':
assert result[0].availability.up is True
assert result[0].availability.last_probed_at == '2026-09-11T12:01:18Z'
assert result[0].availability.latency_ms == 10
session.get.assert_called_once_with( session.get.assert_called_once_with(
'https://space.langbot.app/api/v1/models/selection', 'https://space.langbot.app/api/v1/models/selection',
params={'category': 'chat'}, params={'category': 'chat'},
) )
async def test_selection_without_category_fetches_all_model_statuses(self):
ap = SimpleNamespace(instance_config=SimpleNamespace(data={}))
service = SpaceService(ap)
payload = {
'code': 0,
'data': {
'models': [
{
'model': {
'uuid': 'embedding-model',
'model_id': 'text-embedding',
'category': 'embedding',
},
'availability': {'up': None, 'last_probed_at': None},
}
]
},
}
mock_response = MagicMock(status=200)
with (
patch('langbot.pkg.api.http.service.space.httpclient.get_session') as get_session,
patch(
'langbot.pkg.api.http.service.space.httpclient.read_json_limited',
new=AsyncMock(return_value=payload),
),
):
session = MagicMock()
session.get.return_value.__aenter__ = AsyncMock(return_value=mock_response)
session.get.return_value.__aexit__ = AsyncMock(return_value=None)
get_session.return_value = session
result = await service.get_model_selection()
assert result[0].category == 'embedding'
assert result[0].availability.up is None
session.get.assert_called_once_with(
'https://space.langbot.app/api/v1/models/selection',
params=None,
)
async def test_recommended_model_uses_first_selection_and_refreshes_once(self): async def test_recommended_model_uses_first_selection_and_refreshes_once(self):
local_model = SimpleNamespace(uuid='local-model-uuid', name='best-chat-model') local_model = SimpleNamespace(uuid='local-model-uuid', name='best-chat-model')
persistence = SimpleNamespace( persistence = SimpleNamespace(
@@ -71,6 +71,8 @@ import { LANGBOT_MODELS_PROVIDER_REQUESTER } from '@/app/home/components/models-
import ReasoningLevelPicker, { import ReasoningLevelPicker, {
REASONING_LEVELS, REASONING_LEVELS,
} from '@/app/home/components/reasoning/ReasoningLevelPicker'; } from '@/app/home/components/reasoning/ReasoningLevelPicker';
import ModelAvailabilityIndicator from '@/app/home/components/model-availability/ModelAvailabilityIndicator';
import { useLangBotModelAvailability } from '@/app/home/components/model-availability/useLangBotModelAvailability';
function hasUsableUuid<T extends { uuid?: string | null }>( function hasUsableUuid<T extends { uuid?: string | null }>(
item: T, item: T,
@@ -153,6 +155,33 @@ export default function DynamicFormItemComponent({
const [modelsDialogOpen, setModelsDialogOpen] = useState(false); const [modelsDialogOpen, setModelsDialogOpen] = useState(false);
const [settingsSection, setSettingsSection] = const [settingsSection, setSettingsSection] =
useState<SettingsSection>('models'); useState<SettingsSection>('models');
const isModelSelector = [
DynamicFormItemType.LLM_MODEL_SELECTOR,
DynamicFormItemType.EMBEDDING_MODEL_SELECTOR,
DynamicFormItemType.RERANK_MODEL_SELECTOR,
DynamicFormItemType.MODEL_FALLBACK_SELECTOR,
].includes(config.type);
const {
availability: langbotModelAvailability,
loaded: langbotModelAvailabilityLoaded,
} = useLangBotModelAvailability(
isModelSelector && !systemInfo.disable_models_service,
);
const renderModelAvailability = (model: {
uuid: string;
name: string;
provider?: { requester?: string };
}) =>
model.provider?.requester === LANGBOT_MODELS_PROVIDER_REQUESTER ? (
<ModelAvailabilityIndicator
availability={
langbotModelAvailability[model.uuid] ??
langbotModelAvailability[model.name]
}
show={langbotModelAvailabilityLoaded}
/>
) : null;
const fetchLlmModels = () => { const fetchLlmModels = () => {
httpClient httpClient
@@ -584,6 +613,7 @@ export default function DynamicFormItemComponent({
<SelectItem key={model.uuid} value={model.uuid}> <SelectItem key={model.uuid} value={model.uuid}>
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
{model.name} {model.name}
{renderModelAvailability(model)}
{model.abilities?.includes('vision') && ( {model.abilities?.includes('vision') && (
<Eye className="h-3 w-3 text-muted-foreground" /> <Eye className="h-3 w-3 text-muted-foreground" />
)} )}
@@ -688,6 +718,7 @@ export default function DynamicFormItemComponent({
<SelectItem key={model.uuid} value={model.uuid}> <SelectItem key={model.uuid} value={model.uuid}>
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
{model.name} {model.name}
{renderModelAvailability(model)}
{model.abilities?.includes('vision') && ( {model.abilities?.includes('vision') && (
<Eye className="h-3 w-3 text-muted-foreground" /> <Eye className="h-3 w-3 text-muted-foreground" />
)} )}
@@ -784,6 +815,7 @@ export default function DynamicFormItemComponent({
{models.map((model) => ( {models.map((model) => (
<SelectItem key={model.uuid} value={model.uuid}> <SelectItem key={model.uuid} value={model.uuid}>
{model.name} {model.name}
{renderModelAvailability(model)}
</SelectItem> </SelectItem>
))} ))}
</SelectGroup> </SelectGroup>
@@ -876,6 +908,7 @@ export default function DynamicFormItemComponent({
{models.map((model) => ( {models.map((model) => (
<SelectItem key={model.uuid} value={model.uuid}> <SelectItem key={model.uuid} value={model.uuid}>
{model.name} {model.name}
{renderModelAvailability(model)}
</SelectItem> </SelectItem>
))} ))}
</SelectGroup> </SelectGroup>
@@ -941,6 +974,7 @@ export default function DynamicFormItemComponent({
{models.map((model) => ( {models.map((model) => (
<SelectItem key={model.uuid} value={model.uuid}> <SelectItem key={model.uuid} value={model.uuid}>
{model.name} {model.name}
{renderModelAvailability(model)}
</SelectItem> </SelectItem>
))} ))}
</SelectGroup> </SelectGroup>
@@ -1060,6 +1094,7 @@ export default function DynamicFormItemComponent({
<SelectItem key={model.uuid} value={model.uuid}> <SelectItem key={model.uuid} value={model.uuid}>
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
{model.name} {model.name}
{renderModelAvailability(model)}
{model.abilities?.includes('vision') && ( {model.abilities?.includes('vision') && (
<Eye className="h-3 w-3 text-muted-foreground" /> <Eye className="h-3 w-3 text-muted-foreground" />
)} )}
@@ -1165,6 +1200,7 @@ export default function DynamicFormItemComponent({
<SelectItem key={model.uuid} value={model.uuid}> <SelectItem key={model.uuid} value={model.uuid}>
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
{model.name} {model.name}
{renderModelAvailability(model)}
{model.abilities?.includes('vision') && ( {model.abilities?.includes('vision') && (
<Eye className="h-3 w-3 text-muted-foreground" /> <Eye className="h-3 w-3 text-muted-foreground" />
)} )}
@@ -0,0 +1,67 @@
import type { LangBotModelAvailability } from '@/app/infra/entities/api';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useTranslation } from 'react-i18next';
interface ModelAvailabilityIndicatorProps {
availability?: LangBotModelAvailability;
show: boolean;
}
export default function ModelAvailabilityIndicator({
availability,
show,
}: ModelAvailabilityIndicatorProps) {
const { t, i18n } = useTranslation();
if (!show) return null;
const state = availability?.up;
const label =
state === true
? t('models.availability.available')
: state === false
? t('models.availability.unavailable')
: t('models.availability.notChecked');
const dotClass =
state === true
? 'bg-emerald-500'
: state === false
? 'bg-destructive'
: 'bg-muted-foreground/50';
const checkedAt = availability?.last_probed_at
? new Intl.DateTimeFormat(i18n.language, {
dateStyle: 'medium',
timeStyle: 'short',
}).format(new Date(availability.last_probed_at))
: null;
return (
<Tooltip>
<TooltipTrigger asChild>
<span
className="inline-flex size-4 shrink-0 items-center justify-center"
aria-label={label}
onMouseDown={(event) => event.preventDefault()}
>
<span className={`size-1.5 rounded-full ${dotClass}`} />
</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-64">
<div className="space-y-0.5">
<p>{label}</p>
{checkedAt && (
<p className="text-xs text-muted-foreground">
{t('models.availability.lastChecked', { time: checkedAt })}
{availability && availability.latency_ms > 0
? ` · ${availability.latency_ms} ms`
: ''}
</p>
)}
</div>
</TooltipContent>
</Tooltip>
);
}
@@ -0,0 +1,63 @@
import { useEffect, useState } from 'react';
import type { LangBotModelAvailability } from '@/app/infra/entities/api';
import { httpClient } from '@/app/infra/http/HttpClient';
const CACHE_TTL_MS = 60_000;
type AvailabilityMap = Record<string, LangBotModelAvailability>;
let cachedAvailability: AvailabilityMap | null = null;
let cacheExpiresAt = 0;
let pendingRequest: Promise<AvailabilityMap> | null = null;
async function loadAvailability(): Promise<AvailabilityMap> {
if (cachedAvailability && Date.now() < cacheExpiresAt) {
return cachedAvailability;
}
if (pendingRequest) return pendingRequest;
pendingRequest = httpClient
.getLangBotModelAvailability()
.then((response) => {
const next: AvailabilityMap = {};
for (const item of response.models) {
next[item.uuid] = item.availability;
next[item.model_id] = item.availability;
}
cachedAvailability = next;
cacheExpiresAt = Date.now() + CACHE_TTL_MS;
return next;
})
.finally(() => {
pendingRequest = null;
});
return pendingRequest;
}
export function useLangBotModelAvailability(enabled = true) {
const [availability, setAvailability] = useState<AvailabilityMap>(
cachedAvailability ?? {},
);
const [loaded, setLoaded] = useState(
cachedAvailability !== null && Date.now() < cacheExpiresAt,
);
useEffect(() => {
if (!enabled) return;
let active = true;
loadAvailability()
.then((result) => {
if (!active) return;
setAvailability(result);
setLoaded(true);
})
.catch(() => {
// Availability is supplementary; model configuration remains usable.
});
return () => {
active = false;
};
}, [enabled]);
return { availability, loaded };
}
@@ -27,6 +27,7 @@ import { CustomApiError } from '@/app/infra/entities/common';
import { PanelBody } from '../settings-dialog/panel-layout'; import { PanelBody } from '../settings-dialog/panel-layout';
import { useCurrentWorkspace } from '@/app/infra/http'; import { useCurrentWorkspace } from '@/app/infra/http';
import type { WorkspaceSpaceBilling } from '@/app/infra/entities/workspace'; import type { WorkspaceSpaceBilling } from '@/app/infra/entities/workspace';
import { useLangBotModelAvailability } from '../model-availability/useLangBotModelAvailability';
interface ModelsPanelProps { interface ModelsPanelProps {
// True when this panel is the active section and the dialog is open. // True when this panel is the active section and the dialog is open.
@@ -89,6 +90,10 @@ export default function ModelsPanel({
const currentWorkspace = useCurrentWorkspace(); const currentWorkspace = useCurrentWorkspace();
const canManage = const canManage =
currentWorkspace?.permissions.includes('provider_secret.manage') ?? false; currentWorkspace?.permissions.includes('provider_secret.manage') ?? false;
const {
availability: langbotModelAvailability,
loaded: langbotModelAvailabilityLoaded,
} = useLangBotModelAvailability(active && !systemInfo.disable_models_service);
const [providers, setProviders] = useState<ModelProvider[]>([]); const [providers, setProviders] = useState<ModelProvider[]>([]);
const [spaceBilling, setSpaceBilling] = const [spaceBilling, setSpaceBilling] =
@@ -554,6 +559,8 @@ export default function ModelsPanel({
isWorkspaceOwner={currentWorkspace?.membership.role === 'owner'} isWorkspaceOwner={currentWorkspace?.membership.role === 'owner'}
ownerSpaceBound={spaceBilling?.owner_space_bound ?? false} ownerSpaceBound={spaceBilling?.owner_space_bound ?? false}
spaceCredits={spaceBilling?.credits ?? null} spaceCredits={spaceBilling?.credits ?? null}
modelAvailability={langbotModelAvailability}
modelAvailabilityLoaded={langbotModelAvailabilityLoaded}
addModelPopoverOpen={addModelPopoverOpen} addModelPopoverOpen={addModelPopoverOpen}
editModelPopoverOpen={editModelPopoverOpen} editModelPopoverOpen={editModelPopoverOpen}
deleteConfirmOpen={deleteConfirmOpen} deleteConfirmOpen={deleteConfirmOpen}
@@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next';
import { import {
LLMModel, LLMModel,
EmbeddingModel, EmbeddingModel,
LangBotModelAvailability,
ReasoningConfig, ReasoningConfig,
} from '@/app/infra/entities/api'; } from '@/app/infra/entities/api';
import { import {
@@ -24,12 +25,15 @@ import {
} from '../types'; } from '../types';
import ExtraArgsEditor from './ExtraArgsEditor'; import ExtraArgsEditor from './ExtraArgsEditor';
import { userInfo } from '@/app/infra/http'; import { userInfo } from '@/app/infra/http';
import ModelAvailabilityIndicator from '../../model-availability/ModelAvailabilityIndicator';
interface ModelItemProps { interface ModelItemProps {
model: LLMModel | EmbeddingModel; model: LLMModel | EmbeddingModel;
canManage: boolean; canManage: boolean;
modelType: ModelType; modelType: ModelType;
isLangBotModels: boolean; isLangBotModels: boolean;
availability?: LangBotModelAvailability;
availabilityLoaded: boolean;
editModelPopoverOpen: string | null; editModelPopoverOpen: string | null;
deleteConfirmOpen: string | null; deleteConfirmOpen: string | null;
onOpenEditModel: (modelId: string) => void; onOpenEditModel: (modelId: string) => void;
@@ -86,6 +90,8 @@ export default function ModelItem({
canManage, canManage,
modelType, modelType,
isLangBotModels, isLangBotModels,
availability,
availabilityLoaded,
editModelPopoverOpen, editModelPopoverOpen,
deleteConfirmOpen, deleteConfirmOpen,
onOpenEditModel, onOpenEditModel,
@@ -199,6 +205,12 @@ export default function ModelItem({
> >
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium">{model.name}</span> <span className="text-sm font-medium">{model.name}</span>
{isLangBotModels && (
<ModelAvailabilityIndicator
availability={availability}
show={availabilityLoaded}
/>
)}
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
{modelType === 'llm' {modelType === 'llm'
? t('models.chat') ? t('models.chat')
@@ -9,7 +9,11 @@ import {
Radar, Radar,
} from 'lucide-react'; } from 'lucide-react';
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient'; import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
import { ModelProvider, ReasoningConfig } from '@/app/infra/entities/api'; import {
LangBotModelAvailability,
ModelProvider,
ReasoningConfig,
} from '@/app/infra/entities/api';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Collapsible, Collapsible,
@@ -47,6 +51,8 @@ interface ProviderCardProps {
isWorkspaceOwner: boolean; isWorkspaceOwner: boolean;
ownerSpaceBound: boolean; ownerSpaceBound: boolean;
spaceCredits: number | null; spaceCredits: number | null;
modelAvailability: Record<string, LangBotModelAvailability>;
modelAvailabilityLoaded: boolean;
// Popover states // Popover states
addModelPopoverOpen: string | null; addModelPopoverOpen: string | null;
editModelPopoverOpen: string | null; editModelPopoverOpen: string | null;
@@ -115,6 +121,8 @@ export default function ProviderCard({
isWorkspaceOwner, isWorkspaceOwner,
ownerSpaceBound, ownerSpaceBound,
spaceCredits, spaceCredits,
modelAvailability,
modelAvailabilityLoaded,
addModelPopoverOpen, addModelPopoverOpen,
editModelPopoverOpen, editModelPopoverOpen,
deleteConfirmOpen, deleteConfirmOpen,
@@ -424,6 +432,11 @@ export default function ProviderCard({
canManage={canManage} canManage={canManage}
modelType="llm" modelType="llm"
isLangBotModels={isLangBotModels} isLangBotModels={isLangBotModels}
availability={
modelAvailability[model.uuid] ??
modelAvailability[model.name]
}
availabilityLoaded={modelAvailabilityLoaded}
editModelPopoverOpen={editModelPopoverOpen} editModelPopoverOpen={editModelPopoverOpen}
deleteConfirmOpen={deleteConfirmOpen} deleteConfirmOpen={deleteConfirmOpen}
onOpenEditModel={onOpenEditModel} onOpenEditModel={onOpenEditModel}
@@ -475,6 +488,11 @@ export default function ProviderCard({
canManage={canManage} canManage={canManage}
modelType="embedding" modelType="embedding"
isLangBotModels={isLangBotModels} isLangBotModels={isLangBotModels}
availability={
modelAvailability[model.uuid] ??
modelAvailability[model.name]
}
availabilityLoaded={modelAvailabilityLoaded}
editModelPopoverOpen={editModelPopoverOpen} editModelPopoverOpen={editModelPopoverOpen}
deleteConfirmOpen={deleteConfirmOpen} deleteConfirmOpen={deleteConfirmOpen}
onOpenEditModel={onOpenEditModel} onOpenEditModel={onOpenEditModel}
@@ -524,6 +542,11 @@ export default function ProviderCard({
canManage={canManage} canManage={canManage}
modelType="rerank" modelType="rerank"
isLangBotModels={isLangBotModels} isLangBotModels={isLangBotModels}
availability={
modelAvailability[model.uuid] ??
modelAvailability[model.name]
}
availabilityLoaded={modelAvailabilityLoaded}
editModelPopoverOpen={editModelPopoverOpen} editModelPopoverOpen={editModelPopoverOpen}
deleteConfirmOpen={deleteConfirmOpen} deleteConfirmOpen={deleteConfirmOpen}
onOpenEditModel={onOpenEditModel} onOpenEditModel={onOpenEditModel}
+18
View File
@@ -158,6 +158,24 @@ export interface RerankModel {
extra_args?: object; extra_args?: object;
} }
export interface LangBotModelAvailability {
up: boolean | null;
last_probed_at: string | null;
latency_ms: number;
http_code: number;
}
export interface LangBotModelAvailabilityItem {
uuid: string;
model_id: string;
category: string | null;
availability: LangBotModelAvailability;
}
export interface ApiRespLangBotModelAvailability {
models: LangBotModelAvailabilityItem[];
}
export interface ApiRespPipelines { export interface ApiRespPipelines {
pipelines: Pipeline[]; pipelines: Pipeline[];
} }
+5
View File
@@ -63,6 +63,7 @@ import {
BotRouteDryRunRequest, BotRouteDryRunRequest,
BotRouteDryRunResult, BotRouteDryRunResult,
BotEventRouteStatusResponse, BotEventRouteStatusResponse,
ApiRespLangBotModelAvailability,
} from '@/app/infra/entities/api'; } from '@/app/infra/entities/api';
import { Plugin } from '@/app/infra/entities/plugin'; import { Plugin } from '@/app/infra/entities/plugin';
import type { PluginLogEntry } from '@/app/infra/entities/plugin'; import type { PluginLogEntry } from '@/app/infra/entities/plugin';
@@ -1250,6 +1251,10 @@ export class BackendClient extends BaseHttpClient {
return this.get('/api/v1/system/wizard/recommended-model'); return this.get('/api/v1/system/wizard/recommended-model');
} }
public getLangBotModelAvailability(): Promise<ApiRespLangBotModelAvailability> {
return this.get('/api/v1/system/model-availability');
}
public getAsyncTasks(params?: { public getAsyncTasks(params?: {
type?: string; type?: string;
kind?: string; kind?: string;
+6
View File
@@ -304,6 +304,12 @@ const enUS = {
usesOwnerSpaceBilling: usesOwnerSpaceBilling:
"Uses the Workspace owner's LangBot Account billing and credits.", "Uses the Workspace owner's LangBot Account billing and credits.",
noModels: 'No models configured', noModels: 'No models configured',
availability: {
available: 'Available at last check',
unavailable: 'Unavailable at last check',
notChecked: 'No check result',
lastChecked: 'Checked {{time}}',
},
langbotModels: 'LangBot Models', langbotModels: 'LangBot Models',
spaceTrialTooltip: spaceTrialTooltip:
'Free trial credits available! Login with LangBot Account to access cloud models with zero configuration.', 'Free trial credits available! Login with LangBot Account to access cloud models with zero configuration.',
+6
View File
@@ -306,6 +306,12 @@ const esES = {
loginToUseModels: loginToUseModels:
'Inicia sesión con una cuenta de LangBot para usar modelos en la nube', 'Inicia sesión con una cuenta de LangBot para usar modelos en la nube',
noModels: 'No hay modelos configurados', noModels: 'No hay modelos configurados',
availability: {
available: 'Disponible en la última comprobación',
unavailable: 'No disponible en la última comprobación',
notChecked: 'Sin resultado de comprobación',
lastChecked: 'Comprobado {{time}}',
},
langbotModels: 'Modelos LangBot', langbotModels: 'Modelos LangBot',
spaceTrialTooltip: spaceTrialTooltip:
'¡Créditos de prueba gratuitos disponibles! Inicia sesión con una cuenta de LangBot para acceder a modelos en la nube sin configuración.', '¡Créditos de prueba gratuitos disponibles! Inicia sesión con una cuenta de LangBot para acceder a modelos en la nube sin configuración.',
+6
View File
@@ -309,6 +309,12 @@ const jaJP = {
usesOwnerSpaceBilling: usesOwnerSpaceBilling:
'ワークスペース所有者の LangBot アカウント課金とクレジットを使用します。', 'ワークスペース所有者の LangBot アカウント課金とクレジットを使用します。',
noModels: 'モデルがありません', noModels: 'モデルがありません',
availability: {
available: '前回のチェックで利用可能',
unavailable: '前回のチェックで利用不可',
notChecked: 'チェック結果なし',
lastChecked: '{{time}} にチェック',
},
langbotModels: 'LangBot モデル', langbotModels: 'LangBot モデル',
spaceTrialTooltip: spaceTrialTooltip:
'無料トライアルクレジットが利用可能!LangBot アカウントでログインして、設定不要でクラウドモデルを使用できます。', '無料トライアルクレジットが利用可能!LangBot アカウントでログインして、設定不要でクラウドモデルを使用できます。',
+6
View File
@@ -303,6 +303,12 @@ const ruRU = {
loginToUseModels: loginToUseModels:
'Войдите с аккаунтом LangBot, чтобы использовать облачные модели', 'Войдите с аккаунтом LangBot, чтобы использовать облачные модели',
noModels: 'Модели не настроены', noModels: 'Модели не настроены',
availability: {
available: 'Доступна при последней проверке',
unavailable: 'Недоступна при последней проверке',
notChecked: 'Нет результата проверки',
lastChecked: 'Проверено {{time}}',
},
langbotModels: 'Модели LangBot', langbotModels: 'Модели LangBot',
spaceTrialTooltip: spaceTrialTooltip:
'Доступны бесплатные пробные кредиты! Войдите с аккаунтом LangBot, чтобы получить доступ к облачным моделям без настройки.', 'Доступны бесплатные пробные кредиты! Войдите с аккаунтом LangBot, чтобы получить доступ к облачным моделям без настройки.',
+6
View File
@@ -292,6 +292,12 @@ const thTH = {
loginWithSpace: 'เข้าสู่ระบบด้วยบัญชี LangBot', loginWithSpace: 'เข้าสู่ระบบด้วยบัญชี LangBot',
loginToUseModels: 'เข้าสู่ระบบด้วยบัญชี LangBot เพื่อใช้โมเดลคลาวด์', loginToUseModels: 'เข้าสู่ระบบด้วยบัญชี LangBot เพื่อใช้โมเดลคลาวด์',
noModels: 'ยังไม่มีโมเดลที่กำหนดค่า', noModels: 'ยังไม่มีโมเดลที่กำหนดค่า',
availability: {
available: 'พร้อมใช้งานในการตรวจสอบล่าสุด',
unavailable: 'ไม่พร้อมใช้งานในการตรวจสอบล่าสุด',
notChecked: 'ไม่มีผลการตรวจสอบ',
lastChecked: 'ตรวจสอบเมื่อ {{time}}',
},
langbotModels: 'โมเดล LangBot', langbotModels: 'โมเดล LangBot',
spaceTrialTooltip: spaceTrialTooltip:
'มีเครดิตทดลองใช้งานฟรี! เข้าสู่ระบบด้วยบัญชี LangBot เพื่อเข้าถึงโมเดลคลาวด์โดยไม่ต้องตั้งค่า', 'มีเครดิตทดลองใช้งานฟรี! เข้าสู่ระบบด้วยบัญชี LangBot เพื่อเข้าถึงโมเดลคลาวด์โดยไม่ต้องตั้งค่า',
+6
View File
@@ -300,6 +300,12 @@ const viVN = {
loginToUseModels: loginToUseModels:
'Đăng nhập bằng tài khoản LangBot để sử dụng mô hình đám mây', 'Đăng nhập bằng tài khoản LangBot để sử dụng mô hình đám mây',
noModels: 'Chưa cấu hình mô hình nào', noModels: 'Chưa cấu hình mô hình nào',
availability: {
available: 'Khả dụng ở lần kiểm tra gần nhất',
unavailable: 'Không khả dụng ở lần kiểm tra gần nhất',
notChecked: 'Chưa có kết quả kiểm tra',
lastChecked: 'Đã kiểm tra {{time}}',
},
langbotModels: 'Mô hình LangBot', langbotModels: 'Mô hình LangBot',
spaceTrialTooltip: spaceTrialTooltip:
'Có tín dụng dùng thử miễn phí! Đăng nhập bằng tài khoản LangBot để truy cập mô hình đám mây không cần cấu hình.', 'Có tín dụng dùng thử miễn phí! Đăng nhập bằng tài khoản LangBot để truy cập mô hình đám mây không cần cấu hình.',
+6
View File
@@ -290,6 +290,12 @@ const zhHans = {
'工作区所有者需要绑定 LangBot 账号才能使用 LangBot 模型。', '工作区所有者需要绑定 LangBot 账号才能使用 LangBot 模型。',
usesOwnerSpaceBilling: '使用工作区所有者的 LangBot 账号计费与积分。', usesOwnerSpaceBilling: '使用工作区所有者的 LangBot 账号计费与积分。',
noModels: '暂无模型', noModels: '暂无模型',
availability: {
available: '上次检测可用',
unavailable: '上次检测不可用',
notChecked: '暂无检测结果',
lastChecked: '检测于 {{time}}',
},
langbotModels: 'LangBot 模型', langbotModels: 'LangBot 模型',
spaceTrialTooltip: spaceTrialTooltip:
'免费试用积分已就绪!通过 LangBot 账号登录即可零配置使用云端模型。', '免费试用积分已就绪!通过 LangBot 账号登录即可零配置使用云端模型。',
+6
View File
@@ -281,6 +281,12 @@ const zhHant = {
loginWithSpace: '使用 LangBot 帳號登入', loginWithSpace: '使用 LangBot 帳號登入',
loginToUseModels: '使用 LangBot 帳號登入以使用雲端模型', loginToUseModels: '使用 LangBot 帳號登入以使用雲端模型',
noModels: '暫無模型', noModels: '暫無模型',
availability: {
available: '上次檢測可用',
unavailable: '上次檢測不可用',
notChecked: '暫無檢測結果',
lastChecked: '檢測於 {{time}}',
},
langbotModels: 'LangBot 模型', langbotModels: 'LangBot 模型',
spaceTrialTooltip: spaceTrialTooltip:
'免費試用積分已就緒!使用 LangBot 帳號登入即可零設定使用雲端模型。', '免費試用積分已就緒!使用 LangBot 帳號登入即可零設定使用雲端模型。',