feat: add LangBot Space ChatCompletions requester and integrate with ModelsDialog and EmbeddingForm components

This commit is contained in:
Junyan Qin
2025-12-30 21:52:52 +08:00
parent 19f417174c
commit 197258ae91
7 changed files with 124 additions and 72 deletions

View File

@@ -9,7 +9,7 @@ import sqlalchemy.ext.asyncio as sqlalchemy_asyncio
import sqlalchemy
from . import database, migration
from ..entity.persistence import base, pipeline, metadata
from ..entity.persistence import base, pipeline, metadata, model as persistence_model
from ..entity import persistence
from ..core import app
from ..utils import constants, importutil
@@ -79,6 +79,7 @@ class PersistenceManager:
self.ap.logger.info(f'Successfully upgraded database to version {last_migration_number}.')
await self.write_default_pipeline()
await self.write_space_model_providers()
async def create_tables(self):
# create tables
@@ -123,7 +124,28 @@ class PersistenceManager:
await self.execute_async(sqlalchemy.insert(pipeline.LegacyPipeline).values(pipeline_data))
# =================================
async def write_space_model_providers(self):
# write space model providers
result = await self.execute_async(
sqlalchemy.select(persistence_model.ModelProvider).where(
persistence_model.ModelProvider.requester == 'space-chat-completions'
)
)
if result.first() is None:
self.ap.logger.info('Creating space model providers...')
space_chat_completions_model_provider = {
'uuid': str(uuid.uuid4()),
'name': 'LangBot Models',
'requester': 'space-chat-completions',
'base_url': 'https://api.langbot.cloud/v1',
'api_keys': [],
}
await self.execute_async(
sqlalchemy.insert(persistence_model.ModelProvider).values(space_chat_completions_model_provider)
)
# =================================
async def execute_async(self, *args, **kwargs) -> sqlalchemy.engine.cursor.CursorResult:
async with self.get_db_engine().connect() as conn:

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,17 @@
from __future__ import annotations
import typing
import openai
from . import chatcmpl
class LangBotSpaceChatCompletions(chatcmpl.OpenAIChatCompletions):
"""LangBot Space ChatCompletion API 请求器"""
client: openai.AsyncClient
default_config: dict[str, typing.Any] = {
'base_url': 'https://api.langbot.cloud/v1',
'timeout': 120,
}

View File

@@ -0,0 +1,32 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: space-chat-completions
label:
en_US: Space
zh_Hans: Space
icon: space.webp
spec:
config:
- name: base_url
label:
en_US: Base URL
zh_Hans: 基础 URL
type: string
required: true
default: https://api.langbot.cloud/v1
- name: timeout
label:
en_US: Timeout
zh_Hans: 超时时间
type: integer
required: true
default: 120
support_type:
- llm
- text-embedding
provider_category: maas
execution:
python:
path: ./spacechatcmpl.py
attr: LangBotSpaceChatCompletions

View File

@@ -53,6 +53,7 @@ interface ModelsDialogProps {
}
const LANGBOT_MODELS_PROVIDER_NAME = 'LangBot Models';
const LANGBOT_MODELS_PROVIDER_REQUESTER = 'space-chat-completions';
export default function ModelsDialog({
open,
@@ -253,10 +254,10 @@ export default function ModelsDialog({
// Separate LangBot Models provider
const langbotProvider = providers.find(
(p) => p.name === LANGBOT_MODELS_PROVIDER_NAME,
(p) => p.requester === LANGBOT_MODELS_PROVIDER_REQUESTER,
);
const otherProviders = providers.filter(
(p) => p.name !== LANGBOT_MODELS_PROVIDER_NAME,
(p) => p.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER,
);
function renderProviderCard(
@@ -501,58 +502,6 @@ export default function ModelsDialog({
if (langbotProvider) {
return renderProviderCard(langbotProvider, true);
}
return (
<Card className="mb-2">
<CardHeader className="p-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-9 h-9 rounded-lg overflow-hidden flex-shrink-0">
<img
src={langbotIcon.src}
alt="LangBot"
className="w-full h-full object-cover"
/>
</div>
<div>
<CardTitle className="text-base">
{LANGBOT_MODELS_PROVIDER_NAME}
</CardTitle>
<p className="text-xs text-muted-foreground">
{t('models.langbotModelsDescription')}
</p>
</div>
</div>
{accountType !== 'space' ? (
<Button variant="outline" size="sm" onClick={handleSpaceLogin}>
<LogIn className="h-4 w-4 mr-1" />
{t('models.loginWithSpace')}
</Button>
) : (
spaceCredits !== null && (
<div className="flex items-center gap-1 border rounded-md px-2 h-8 text-sm">
<span>
{(spaceCredits / 5000).toFixed(2)} {t('models.credits')}
</span>
<Button
variant="ghost"
size="icon"
className="h-5 w-5"
onClick={() =>
window.open(
`${systemInfo.cloud_service_url}/billing`,
'_blank',
)
}
>
<Plus className="h-3 w-3" />
</Button>
</div>
)
)}
</div>
</CardHeader>
</Card>
);
}
function handleFormClose() {

View File

@@ -373,11 +373,15 @@ export default function EmbeddingForm({
/>
</SelectTrigger>
<SelectContent>
{providers.map((p) => (
<SelectItem key={p.uuid} value={p.uuid}>
{p.name} ({p.base_url || 'default'})
</SelectItem>
))}
{providers
.filter(
(p) => p.requester !== 'space-chat-completions',
)
.map((p) => (
<SelectItem key={p.uuid} value={p.uuid}>
{p.name} ({p.base_url || 'default'})
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
@@ -413,7 +417,11 @@ export default function EmbeddingForm({
{t('models.modelManufacturer')}
</SelectLabel>
{requesterList
.filter((r) => r.category === 'manufacturer')
.filter(
(r) =>
r.category === 'manufacturer' &&
r.value !== 'space-chat-completions',
)
.map((r) => (
<SelectItem key={r.value} value={r.value}>
{r.label}
@@ -425,7 +433,11 @@ export default function EmbeddingForm({
{t('models.aggregationPlatform')}
</SelectLabel>
{requesterList
.filter((r) => r.category === 'maas')
.filter(
(r) =>
r.category === 'maas' &&
r.value !== 'space-chat-completions',
)
.map((r) => (
<SelectItem key={r.value} value={r.value}>
{r.label}
@@ -437,7 +449,11 @@ export default function EmbeddingForm({
{t('models.selfDeployed')}
</SelectLabel>
{requesterList
.filter((r) => r.category === 'self-hosted')
.filter(
(r) =>
r.category === 'self-hosted' &&
r.value !== 'space-chat-completions',
)
.map((r) => (
<SelectItem key={r.value} value={r.value}>
{r.label}

View File

@@ -385,11 +385,15 @@ export default function LLMForm({
/>
</SelectTrigger>
<SelectContent>
{providers.map((p) => (
<SelectItem key={p.uuid} value={p.uuid}>
{p.name} ({p.base_url || 'default'})
</SelectItem>
))}
{providers
.filter(
(p) => p.requester !== 'space-chat-completions',
)
.map((p) => (
<SelectItem key={p.uuid} value={p.uuid}>
{p.name} ({p.base_url || 'default'})
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
@@ -425,7 +429,11 @@ export default function LLMForm({
{t('models.modelManufacturer')}
</SelectLabel>
{requesterList
.filter((r) => r.category === 'manufacturer')
.filter(
(r) =>
r.category === 'manufacturer' &&
r.value !== 'space-chat-completions',
)
.map((r) => (
<SelectItem key={r.value} value={r.value}>
{r.label}
@@ -437,7 +445,11 @@ export default function LLMForm({
{t('models.aggregationPlatform')}
</SelectLabel>
{requesterList
.filter((r) => r.category === 'maas')
.filter(
(r) =>
r.category === 'maas' &&
r.value !== 'space-chat-completions',
)
.map((r) => (
<SelectItem key={r.value} value={r.value}>
{r.label}
@@ -449,7 +461,11 @@ export default function LLMForm({
{t('models.selfDeployed')}
</SelectLabel>
{requesterList
.filter((r) => r.category === 'self-hosted')
.filter(
(r) =>
r.category === 'self-hosted' &&
r.value !== 'space-chat-completions',
)
.map((r) => (
<SelectItem key={r.value} value={r.value}>
{r.label}