mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-11 20:37:15 +00:00
feat(models): show LangBot Models availability
This commit is contained in:
@@ -220,6 +220,21 @@ class SystemRouterGroup(group.RouterGroup):
|
||||
return self.http_status(503, -1, str(exc))
|
||||
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(
|
||||
'/tasks',
|
||||
methods=['GET'],
|
||||
|
||||
@@ -242,13 +242,14 @@ class SpaceService:
|
||||
models_data = data.get('data', {}).get('models', [])
|
||||
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."""
|
||||
space_url = self._get_space_config()['url']
|
||||
session = httpclient.get_session()
|
||||
params = {'category': category} if category else None
|
||||
async with session.get(
|
||||
f'{space_url}/api/v1/models/selection',
|
||||
params={'category': category},
|
||||
params=params,
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error = await httpclient.read_text_limited(response)
|
||||
@@ -266,7 +267,17 @@ class SpaceService:
|
||||
models = []
|
||||
for selection in data:
|
||||
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:
|
||||
models.append(selection)
|
||||
return [SpaceModelSelection.model_validate(model) for model in models]
|
||||
|
||||
@@ -49,8 +49,19 @@ class SpaceModel(pydantic.BaseModel):
|
||||
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):
|
||||
"""Minimal model identity returned by the ranked selection endpoint."""
|
||||
"""Model identity and the latest persisted probe from Space."""
|
||||
|
||||
uuid: str
|
||||
model_id: str
|
||||
category: str | None = None
|
||||
availability: SpaceModelAvailability = pydantic.Field(default_factory=SpaceModelAvailability)
|
||||
|
||||
@@ -847,7 +847,15 @@ class TestSpaceServiceGetModelSelection:
|
||||
data = {'models': models}
|
||||
elif response_shape == 'availability-wrapper':
|
||||
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)
|
||||
]
|
||||
else:
|
||||
@@ -870,11 +878,56 @@ class TestSpaceServiceGetModelSelection:
|
||||
result = await service.get_model_selection('chat')
|
||||
|
||||
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(
|
||||
'https://space.langbot.app/api/v1/models/selection',
|
||||
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):
|
||||
local_model = SimpleNamespace(uuid='local-model-uuid', name='best-chat-model')
|
||||
persistence = SimpleNamespace(
|
||||
|
||||
@@ -71,6 +71,8 @@ import { LANGBOT_MODELS_PROVIDER_REQUESTER } from '@/app/home/components/models-
|
||||
import ReasoningLevelPicker, {
|
||||
REASONING_LEVELS,
|
||||
} 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 }>(
|
||||
item: T,
|
||||
@@ -153,6 +155,33 @@ export default function DynamicFormItemComponent({
|
||||
const [modelsDialogOpen, setModelsDialogOpen] = useState(false);
|
||||
const [settingsSection, setSettingsSection] =
|
||||
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 = () => {
|
||||
httpClient
|
||||
@@ -584,6 +613,7 @@ export default function DynamicFormItemComponent({
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{model.name}
|
||||
{renderModelAvailability(model)}
|
||||
{model.abilities?.includes('vision') && (
|
||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
@@ -688,6 +718,7 @@ export default function DynamicFormItemComponent({
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{model.name}
|
||||
{renderModelAvailability(model)}
|
||||
{model.abilities?.includes('vision') && (
|
||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
@@ -784,6 +815,7 @@ export default function DynamicFormItemComponent({
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
{model.name}
|
||||
{renderModelAvailability(model)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@@ -876,6 +908,7 @@ export default function DynamicFormItemComponent({
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
{model.name}
|
||||
{renderModelAvailability(model)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@@ -941,6 +974,7 @@ export default function DynamicFormItemComponent({
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
{model.name}
|
||||
{renderModelAvailability(model)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@@ -1060,6 +1094,7 @@ export default function DynamicFormItemComponent({
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{model.name}
|
||||
{renderModelAvailability(model)}
|
||||
{model.abilities?.includes('vision') && (
|
||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
@@ -1165,6 +1200,7 @@ export default function DynamicFormItemComponent({
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{model.name}
|
||||
{renderModelAvailability(model)}
|
||||
{model.abilities?.includes('vision') && (
|
||||
<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 { useCurrentWorkspace } from '@/app/infra/http';
|
||||
import type { WorkspaceSpaceBilling } from '@/app/infra/entities/workspace';
|
||||
import { useLangBotModelAvailability } from '../model-availability/useLangBotModelAvailability';
|
||||
|
||||
interface ModelsPanelProps {
|
||||
// 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 canManage =
|
||||
currentWorkspace?.permissions.includes('provider_secret.manage') ?? false;
|
||||
const {
|
||||
availability: langbotModelAvailability,
|
||||
loaded: langbotModelAvailabilityLoaded,
|
||||
} = useLangBotModelAvailability(active && !systemInfo.disable_models_service);
|
||||
|
||||
const [providers, setProviders] = useState<ModelProvider[]>([]);
|
||||
const [spaceBilling, setSpaceBilling] =
|
||||
@@ -554,6 +559,8 @@ export default function ModelsPanel({
|
||||
isWorkspaceOwner={currentWorkspace?.membership.role === 'owner'}
|
||||
ownerSpaceBound={spaceBilling?.owner_space_bound ?? false}
|
||||
spaceCredits={spaceBilling?.credits ?? null}
|
||||
modelAvailability={langbotModelAvailability}
|
||||
modelAvailabilityLoaded={langbotModelAvailabilityLoaded}
|
||||
addModelPopoverOpen={addModelPopoverOpen}
|
||||
editModelPopoverOpen={editModelPopoverOpen}
|
||||
deleteConfirmOpen={deleteConfirmOpen}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
LLMModel,
|
||||
EmbeddingModel,
|
||||
LangBotModelAvailability,
|
||||
ReasoningConfig,
|
||||
} from '@/app/infra/entities/api';
|
||||
import {
|
||||
@@ -24,12 +25,15 @@ import {
|
||||
} from '../types';
|
||||
import ExtraArgsEditor from './ExtraArgsEditor';
|
||||
import { userInfo } from '@/app/infra/http';
|
||||
import ModelAvailabilityIndicator from '../../model-availability/ModelAvailabilityIndicator';
|
||||
|
||||
interface ModelItemProps {
|
||||
model: LLMModel | EmbeddingModel;
|
||||
canManage: boolean;
|
||||
modelType: ModelType;
|
||||
isLangBotModels: boolean;
|
||||
availability?: LangBotModelAvailability;
|
||||
availabilityLoaded: boolean;
|
||||
editModelPopoverOpen: string | null;
|
||||
deleteConfirmOpen: string | null;
|
||||
onOpenEditModel: (modelId: string) => void;
|
||||
@@ -86,6 +90,8 @@ export default function ModelItem({
|
||||
canManage,
|
||||
modelType,
|
||||
isLangBotModels,
|
||||
availability,
|
||||
availabilityLoaded,
|
||||
editModelPopoverOpen,
|
||||
deleteConfirmOpen,
|
||||
onOpenEditModel,
|
||||
@@ -199,6 +205,12 @@ export default function ModelItem({
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-medium">{model.name}</span>
|
||||
{isLangBotModels && (
|
||||
<ModelAvailabilityIndicator
|
||||
availability={availability}
|
||||
show={availabilityLoaded}
|
||||
/>
|
||||
)}
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{modelType === 'llm'
|
||||
? t('models.chat')
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
Radar,
|
||||
} from 'lucide-react';
|
||||
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 {
|
||||
Collapsible,
|
||||
@@ -47,6 +51,8 @@ interface ProviderCardProps {
|
||||
isWorkspaceOwner: boolean;
|
||||
ownerSpaceBound: boolean;
|
||||
spaceCredits: number | null;
|
||||
modelAvailability: Record<string, LangBotModelAvailability>;
|
||||
modelAvailabilityLoaded: boolean;
|
||||
// Popover states
|
||||
addModelPopoverOpen: string | null;
|
||||
editModelPopoverOpen: string | null;
|
||||
@@ -115,6 +121,8 @@ export default function ProviderCard({
|
||||
isWorkspaceOwner,
|
||||
ownerSpaceBound,
|
||||
spaceCredits,
|
||||
modelAvailability,
|
||||
modelAvailabilityLoaded,
|
||||
addModelPopoverOpen,
|
||||
editModelPopoverOpen,
|
||||
deleteConfirmOpen,
|
||||
@@ -424,6 +432,11 @@ export default function ProviderCard({
|
||||
canManage={canManage}
|
||||
modelType="llm"
|
||||
isLangBotModels={isLangBotModels}
|
||||
availability={
|
||||
modelAvailability[model.uuid] ??
|
||||
modelAvailability[model.name]
|
||||
}
|
||||
availabilityLoaded={modelAvailabilityLoaded}
|
||||
editModelPopoverOpen={editModelPopoverOpen}
|
||||
deleteConfirmOpen={deleteConfirmOpen}
|
||||
onOpenEditModel={onOpenEditModel}
|
||||
@@ -475,6 +488,11 @@ export default function ProviderCard({
|
||||
canManage={canManage}
|
||||
modelType="embedding"
|
||||
isLangBotModels={isLangBotModels}
|
||||
availability={
|
||||
modelAvailability[model.uuid] ??
|
||||
modelAvailability[model.name]
|
||||
}
|
||||
availabilityLoaded={modelAvailabilityLoaded}
|
||||
editModelPopoverOpen={editModelPopoverOpen}
|
||||
deleteConfirmOpen={deleteConfirmOpen}
|
||||
onOpenEditModel={onOpenEditModel}
|
||||
@@ -524,6 +542,11 @@ export default function ProviderCard({
|
||||
canManage={canManage}
|
||||
modelType="rerank"
|
||||
isLangBotModels={isLangBotModels}
|
||||
availability={
|
||||
modelAvailability[model.uuid] ??
|
||||
modelAvailability[model.name]
|
||||
}
|
||||
availabilityLoaded={modelAvailabilityLoaded}
|
||||
editModelPopoverOpen={editModelPopoverOpen}
|
||||
deleteConfirmOpen={deleteConfirmOpen}
|
||||
onOpenEditModel={onOpenEditModel}
|
||||
|
||||
@@ -158,6 +158,24 @@ export interface RerankModel {
|
||||
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 {
|
||||
pipelines: Pipeline[];
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
BotRouteDryRunRequest,
|
||||
BotRouteDryRunResult,
|
||||
BotEventRouteStatusResponse,
|
||||
ApiRespLangBotModelAvailability,
|
||||
} from '@/app/infra/entities/api';
|
||||
import { Plugin } 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');
|
||||
}
|
||||
|
||||
public getLangBotModelAvailability(): Promise<ApiRespLangBotModelAvailability> {
|
||||
return this.get('/api/v1/system/model-availability');
|
||||
}
|
||||
|
||||
public getAsyncTasks(params?: {
|
||||
type?: string;
|
||||
kind?: string;
|
||||
|
||||
@@ -304,6 +304,12 @@ const enUS = {
|
||||
usesOwnerSpaceBilling:
|
||||
"Uses the Workspace owner's LangBot Account billing and credits.",
|
||||
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',
|
||||
spaceTrialTooltip:
|
||||
'Free trial credits available! Login with LangBot Account to access cloud models with zero configuration.',
|
||||
|
||||
@@ -306,6 +306,12 @@ const esES = {
|
||||
loginToUseModels:
|
||||
'Inicia sesión con una cuenta de LangBot para usar modelos en la nube',
|
||||
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',
|
||||
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.',
|
||||
|
||||
@@ -309,6 +309,12 @@ const jaJP = {
|
||||
usesOwnerSpaceBilling:
|
||||
'ワークスペース所有者の LangBot アカウント課金とクレジットを使用します。',
|
||||
noModels: 'モデルがありません',
|
||||
availability: {
|
||||
available: '前回のチェックで利用可能',
|
||||
unavailable: '前回のチェックで利用不可',
|
||||
notChecked: 'チェック結果なし',
|
||||
lastChecked: '{{time}} にチェック',
|
||||
},
|
||||
langbotModels: 'LangBot モデル',
|
||||
spaceTrialTooltip:
|
||||
'無料トライアルクレジットが利用可能!LangBot アカウントでログインして、設定不要でクラウドモデルを使用できます。',
|
||||
|
||||
@@ -303,6 +303,12 @@ const ruRU = {
|
||||
loginToUseModels:
|
||||
'Войдите с аккаунтом LangBot, чтобы использовать облачные модели',
|
||||
noModels: 'Модели не настроены',
|
||||
availability: {
|
||||
available: 'Доступна при последней проверке',
|
||||
unavailable: 'Недоступна при последней проверке',
|
||||
notChecked: 'Нет результата проверки',
|
||||
lastChecked: 'Проверено {{time}}',
|
||||
},
|
||||
langbotModels: 'Модели LangBot',
|
||||
spaceTrialTooltip:
|
||||
'Доступны бесплатные пробные кредиты! Войдите с аккаунтом LangBot, чтобы получить доступ к облачным моделям без настройки.',
|
||||
|
||||
@@ -292,6 +292,12 @@ const thTH = {
|
||||
loginWithSpace: 'เข้าสู่ระบบด้วยบัญชี LangBot',
|
||||
loginToUseModels: 'เข้าสู่ระบบด้วยบัญชี LangBot เพื่อใช้โมเดลคลาวด์',
|
||||
noModels: 'ยังไม่มีโมเดลที่กำหนดค่า',
|
||||
availability: {
|
||||
available: 'พร้อมใช้งานในการตรวจสอบล่าสุด',
|
||||
unavailable: 'ไม่พร้อมใช้งานในการตรวจสอบล่าสุด',
|
||||
notChecked: 'ไม่มีผลการตรวจสอบ',
|
||||
lastChecked: 'ตรวจสอบเมื่อ {{time}}',
|
||||
},
|
||||
langbotModels: 'โมเดล LangBot',
|
||||
spaceTrialTooltip:
|
||||
'มีเครดิตทดลองใช้งานฟรี! เข้าสู่ระบบด้วยบัญชี LangBot เพื่อเข้าถึงโมเดลคลาวด์โดยไม่ต้องตั้งค่า',
|
||||
|
||||
@@ -300,6 +300,12 @@ const viVN = {
|
||||
loginToUseModels:
|
||||
'Đă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',
|
||||
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',
|
||||
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.',
|
||||
|
||||
@@ -290,6 +290,12 @@ const zhHans = {
|
||||
'工作区所有者需要绑定 LangBot 账号才能使用 LangBot 模型。',
|
||||
usesOwnerSpaceBilling: '使用工作区所有者的 LangBot 账号计费与积分。',
|
||||
noModels: '暂无模型',
|
||||
availability: {
|
||||
available: '上次检测可用',
|
||||
unavailable: '上次检测不可用',
|
||||
notChecked: '暂无检测结果',
|
||||
lastChecked: '检测于 {{time}}',
|
||||
},
|
||||
langbotModels: 'LangBot 模型',
|
||||
spaceTrialTooltip:
|
||||
'免费试用积分已就绪!通过 LangBot 账号登录即可零配置使用云端模型。',
|
||||
|
||||
@@ -281,6 +281,12 @@ const zhHant = {
|
||||
loginWithSpace: '使用 LangBot 帳號登入',
|
||||
loginToUseModels: '使用 LangBot 帳號登入以使用雲端模型',
|
||||
noModels: '暫無模型',
|
||||
availability: {
|
||||
available: '上次檢測可用',
|
||||
unavailable: '上次檢測不可用',
|
||||
notChecked: '暫無檢測結果',
|
||||
lastChecked: '檢測於 {{time}}',
|
||||
},
|
||||
langbotModels: 'LangBot 模型',
|
||||
spaceTrialTooltip:
|
||||
'免費試用積分已就緒!使用 LangBot 帳號登入即可零設定使用雲端模型。',
|
||||
|
||||
Reference in New Issue
Block a user