mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
feat(models): show LangBot Models pricing
This commit is contained in:
@@ -59,9 +59,11 @@ class SpaceModelAvailability(pydantic.BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class SpaceModelSelection(pydantic.BaseModel):
|
class SpaceModelSelection(pydantic.BaseModel):
|
||||||
"""Model identity and the latest persisted probe from Space."""
|
"""Model identity, pricing, and latest persisted probe from Space."""
|
||||||
|
|
||||||
uuid: str
|
uuid: str
|
||||||
model_id: str
|
model_id: str
|
||||||
category: str | None = None
|
category: str | None = None
|
||||||
|
input_credits: float | None = None
|
||||||
|
output_credits: float | None = None
|
||||||
availability: SpaceModelAvailability = pydantic.Field(default_factory=SpaceModelAvailability)
|
availability: SpaceModelAvailability = pydantic.Field(default_factory=SpaceModelAvailability)
|
||||||
|
|||||||
@@ -899,6 +899,8 @@ class TestSpaceServiceGetModelSelection:
|
|||||||
'uuid': 'embedding-model',
|
'uuid': 'embedding-model',
|
||||||
'model_id': 'text-embedding',
|
'model_id': 'text-embedding',
|
||||||
'category': 'embedding',
|
'category': 'embedding',
|
||||||
|
'input_credits': 20,
|
||||||
|
'output_credits': 40,
|
||||||
},
|
},
|
||||||
'availability': {'up': None, 'last_probed_at': None},
|
'availability': {'up': None, 'last_probed_at': None},
|
||||||
}
|
}
|
||||||
@@ -922,6 +924,8 @@ class TestSpaceServiceGetModelSelection:
|
|||||||
result = await service.get_model_selection()
|
result = await service.get_model_selection()
|
||||||
|
|
||||||
assert result[0].category == 'embedding'
|
assert result[0].category == 'embedding'
|
||||||
|
assert result[0].input_credits == 20
|
||||||
|
assert result[0].output_credits == 40
|
||||||
assert result[0].availability.up is None
|
assert result[0].availability.up is None
|
||||||
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',
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ 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 LangBotModelMetadata from '@/app/home/components/model-availability/LangBotModelMetadata';
|
||||||
import { useLangBotModelAvailability } from '@/app/home/components/model-availability/useLangBotModelAvailability';
|
import { useLangBotModelAvailability } from '@/app/home/components/model-availability/useLangBotModelAvailability';
|
||||||
|
|
||||||
function hasUsableUuid<T extends { uuid?: string | null }>(
|
function hasUsableUuid<T extends { uuid?: string | null }>(
|
||||||
@@ -162,26 +162,39 @@ export default function DynamicFormItemComponent({
|
|||||||
DynamicFormItemType.MODEL_FALLBACK_SELECTOR,
|
DynamicFormItemType.MODEL_FALLBACK_SELECTOR,
|
||||||
].includes(config.type);
|
].includes(config.type);
|
||||||
const {
|
const {
|
||||||
availability: langbotModelAvailability,
|
metadata: langbotModelMetadata,
|
||||||
loaded: langbotModelAvailabilityLoaded,
|
loaded: langbotModelAvailabilityLoaded,
|
||||||
} = useLangBotModelAvailability(
|
} = useLangBotModelAvailability(
|
||||||
isModelSelector && !systemInfo.disable_models_service,
|
isModelSelector && !systemInfo.disable_models_service,
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderModelAvailability = (model: {
|
const renderModelOption = (model: {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
abilities?: string[];
|
||||||
provider?: { requester?: string };
|
provider?: { requester?: string };
|
||||||
}) =>
|
}) => (
|
||||||
model.provider?.requester === LANGBOT_MODELS_PROVIDER_REQUESTER ? (
|
<span className="grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-3">
|
||||||
<ModelAvailabilityIndicator
|
<span className="inline-flex min-w-0 items-center gap-1">
|
||||||
availability={
|
<span className="truncate">{model.name}</span>
|
||||||
langbotModelAvailability[model.uuid] ??
|
{model.abilities?.includes('vision') && (
|
||||||
langbotModelAvailability[model.name]
|
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||||
}
|
)}
|
||||||
show={langbotModelAvailabilityLoaded}
|
{model.abilities?.includes('func_call') && (
|
||||||
/>
|
<Wrench className="h-3 w-3 text-muted-foreground" />
|
||||||
) : null;
|
)}
|
||||||
|
</span>
|
||||||
|
{model.provider?.requester === LANGBOT_MODELS_PROVIDER_REQUESTER && (
|
||||||
|
<LangBotModelMetadata
|
||||||
|
metadata={
|
||||||
|
langbotModelMetadata[model.uuid] ?? langbotModelMetadata[model.name]
|
||||||
|
}
|
||||||
|
loaded={langbotModelAvailabilityLoaded}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
const fetchLlmModels = () => {
|
const fetchLlmModels = () => {
|
||||||
httpClient
|
httpClient
|
||||||
@@ -611,16 +624,7 @@ export default function DynamicFormItemComponent({
|
|||||||
<SelectLabel>{providerName}</SelectLabel>
|
<SelectLabel>{providerName}</SelectLabel>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<SelectItem key={model.uuid} value={model.uuid}>
|
<SelectItem key={model.uuid} value={model.uuid}>
|
||||||
<span className="inline-flex items-center gap-1">
|
{renderModelOption(model)}
|
||||||
{model.name}
|
|
||||||
{renderModelAvailability(model)}
|
|
||||||
{model.abilities?.includes('vision') && (
|
|
||||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
{model.abilities?.includes('func_call') && (
|
|
||||||
<Wrench className="h-3 w-3 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
@@ -716,16 +720,7 @@ export default function DynamicFormItemComponent({
|
|||||||
</SelectLabel>
|
</SelectLabel>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<SelectItem key={model.uuid} value={model.uuid}>
|
<SelectItem key={model.uuid} value={model.uuid}>
|
||||||
<span className="inline-flex items-center gap-1">
|
{renderModelOption(model)}
|
||||||
{model.name}
|
|
||||||
{renderModelAvailability(model)}
|
|
||||||
{model.abilities?.includes('vision') && (
|
|
||||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
{model.abilities?.includes('func_call') && (
|
|
||||||
<Wrench className="h-3 w-3 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
@@ -814,8 +809,7 @@ export default function DynamicFormItemComponent({
|
|||||||
<SelectLabel>{providerName}</SelectLabel>
|
<SelectLabel>{providerName}</SelectLabel>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<SelectItem key={model.uuid} value={model.uuid}>
|
<SelectItem key={model.uuid} value={model.uuid}>
|
||||||
{model.name}
|
{renderModelOption(model)}
|
||||||
{renderModelAvailability(model)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
@@ -907,8 +901,7 @@ export default function DynamicFormItemComponent({
|
|||||||
</SelectLabel>
|
</SelectLabel>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<SelectItem key={model.uuid} value={model.uuid}>
|
<SelectItem key={model.uuid} value={model.uuid}>
|
||||||
{model.name}
|
{renderModelOption(model)}
|
||||||
{renderModelAvailability(model)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
@@ -973,8 +966,7 @@ export default function DynamicFormItemComponent({
|
|||||||
<SelectLabel>{providerName}</SelectLabel>
|
<SelectLabel>{providerName}</SelectLabel>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<SelectItem key={model.uuid} value={model.uuid}>
|
<SelectItem key={model.uuid} value={model.uuid}>
|
||||||
{model.name}
|
{renderModelOption(model)}
|
||||||
{renderModelAvailability(model)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
@@ -1092,16 +1084,7 @@ export default function DynamicFormItemComponent({
|
|||||||
<SelectLabel>{providerName}</SelectLabel>
|
<SelectLabel>{providerName}</SelectLabel>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<SelectItem key={model.uuid} value={model.uuid}>
|
<SelectItem key={model.uuid} value={model.uuid}>
|
||||||
<span className="inline-flex items-center gap-1">
|
{renderModelOption(model)}
|
||||||
{model.name}
|
|
||||||
{renderModelAvailability(model)}
|
|
||||||
{model.abilities?.includes('vision') && (
|
|
||||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
{model.abilities?.includes('func_call') && (
|
|
||||||
<Wrench className="h-3 w-3 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
@@ -1198,16 +1181,7 @@ export default function DynamicFormItemComponent({
|
|||||||
</SelectLabel>
|
</SelectLabel>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<SelectItem key={model.uuid} value={model.uuid}>
|
<SelectItem key={model.uuid} value={model.uuid}>
|
||||||
<span className="inline-flex items-center gap-1">
|
{renderModelOption(model)}
|
||||||
{model.name}
|
|
||||||
{renderModelAvailability(model)}
|
|
||||||
{model.abilities?.includes('vision') && (
|
|
||||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
{model.abilities?.includes('func_call') && (
|
|
||||||
<Wrench className="h-3 w-3 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import type { LangBotModelAvailabilityItem } from '@/app/infra/entities/api';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
|
import { Coins } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import ModelAvailabilityIndicator from './ModelAvailabilityIndicator';
|
||||||
|
|
||||||
|
interface LangBotModelMetadataProps {
|
||||||
|
metadata?: LangBotModelAvailabilityItem;
|
||||||
|
loaded: boolean;
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCredits(value: number, locale: string): string {
|
||||||
|
if (value >= 1000) {
|
||||||
|
const thousands = value / 1000;
|
||||||
|
return `${thousands.toFixed(thousands >= 10 ? 0 : 1).replace(/\.0$/, '')}K`;
|
||||||
|
}
|
||||||
|
return new Intl.NumberFormat(locale, {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}).format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LangBotModelMetadata({
|
||||||
|
metadata,
|
||||||
|
loaded,
|
||||||
|
compact = false,
|
||||||
|
}: LangBotModelMetadataProps) {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
if (!loaded) return null;
|
||||||
|
|
||||||
|
const inputCredits = metadata?.input_credits;
|
||||||
|
const outputCredits = metadata?.output_credits;
|
||||||
|
const hasPricing = inputCredits != null && outputCredits != null;
|
||||||
|
const input =
|
||||||
|
inputCredits != null ? formatCredits(inputCredits, i18n.language) : '';
|
||||||
|
const output =
|
||||||
|
outputCredits != null ? formatCredits(outputCredits, i18n.language) : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="ml-auto inline-flex shrink-0 items-center gap-2 pl-3">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1 text-xs tabular-nums text-muted-foreground"
|
||||||
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
<Coins className="size-3" />
|
||||||
|
{hasPricing
|
||||||
|
? compact
|
||||||
|
? t('models.pricing.compact', { input, output })
|
||||||
|
: t('models.pricing.inline', { input, output })
|
||||||
|
: compact
|
||||||
|
? '—'
|
||||||
|
: t('models.pricing.unavailable')}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-64">
|
||||||
|
{hasPricing ? (
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<p>{t('models.pricing.title')}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t('models.pricing.input', {
|
||||||
|
credits: inputCredits,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t('models.pricing.output', {
|
||||||
|
credits: outputCredits,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p>{t('models.pricing.unavailable')}</p>
|
||||||
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<ModelAvailabilityIndicator
|
||||||
|
availability={metadata?.availability}
|
||||||
|
show={loaded}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import type { LangBotModelAvailability } from '@/app/infra/entities/api';
|
import type { LangBotModelAvailabilityItem } from '@/app/infra/entities/api';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
|
||||||
const CACHE_TTL_MS = 60_000;
|
const CACHE_TTL_MS = 60_000;
|
||||||
|
|
||||||
type AvailabilityMap = Record<string, LangBotModelAvailability>;
|
type ModelMetadataMap = Record<string, LangBotModelAvailabilityItem>;
|
||||||
|
|
||||||
let cachedAvailability: AvailabilityMap | null = null;
|
let cachedMetadata: ModelMetadataMap | null = null;
|
||||||
let cacheExpiresAt = 0;
|
let cacheExpiresAt = 0;
|
||||||
let pendingRequest: Promise<AvailabilityMap> | null = null;
|
let pendingRequest: Promise<ModelMetadataMap> | null = null;
|
||||||
|
|
||||||
async function loadAvailability(): Promise<AvailabilityMap> {
|
async function loadMetadata(): Promise<ModelMetadataMap> {
|
||||||
if (cachedAvailability && Date.now() < cacheExpiresAt) {
|
if (cachedMetadata && Date.now() < cacheExpiresAt) {
|
||||||
return cachedAvailability;
|
return cachedMetadata;
|
||||||
}
|
}
|
||||||
if (pendingRequest) return pendingRequest;
|
if (pendingRequest) return pendingRequest;
|
||||||
|
|
||||||
pendingRequest = httpClient
|
pendingRequest = httpClient
|
||||||
.getLangBotModelAvailability()
|
.getLangBotModelAvailability()
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const next: AvailabilityMap = {};
|
const next: ModelMetadataMap = {};
|
||||||
for (const item of response.models) {
|
for (const item of response.models) {
|
||||||
next[item.uuid] = item.availability;
|
next[item.uuid] = item;
|
||||||
next[item.model_id] = item.availability;
|
next[item.model_id] = item;
|
||||||
}
|
}
|
||||||
cachedAvailability = next;
|
cachedMetadata = next;
|
||||||
cacheExpiresAt = Date.now() + CACHE_TTL_MS;
|
cacheExpiresAt = Date.now() + CACHE_TTL_MS;
|
||||||
return next;
|
return next;
|
||||||
})
|
})
|
||||||
@@ -35,29 +35,29 @@ async function loadAvailability(): Promise<AvailabilityMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useLangBotModelAvailability(enabled = true) {
|
export function useLangBotModelAvailability(enabled = true) {
|
||||||
const [availability, setAvailability] = useState<AvailabilityMap>(
|
const [metadata, setMetadata] = useState<ModelMetadataMap>(
|
||||||
cachedAvailability ?? {},
|
cachedMetadata ?? {},
|
||||||
);
|
);
|
||||||
const [loaded, setLoaded] = useState(
|
const [loaded, setLoaded] = useState(
|
||||||
cachedAvailability !== null && Date.now() < cacheExpiresAt,
|
cachedMetadata !== null && Date.now() < cacheExpiresAt,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
let active = true;
|
let active = true;
|
||||||
loadAvailability()
|
loadMetadata()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
setAvailability(result);
|
setMetadata(result);
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Availability is supplementary; model configuration remains usable.
|
// Catalog metadata is supplementary; model configuration remains usable.
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
};
|
};
|
||||||
}, [enabled]);
|
}, [enabled]);
|
||||||
|
|
||||||
return { availability, loaded };
|
return { metadata, loaded };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export default function ModelsPanel({
|
|||||||
const canManage =
|
const canManage =
|
||||||
currentWorkspace?.permissions.includes('provider_secret.manage') ?? false;
|
currentWorkspace?.permissions.includes('provider_secret.manage') ?? false;
|
||||||
const {
|
const {
|
||||||
availability: langbotModelAvailability,
|
metadata: langbotModelMetadata,
|
||||||
loaded: langbotModelAvailabilityLoaded,
|
loaded: langbotModelAvailabilityLoaded,
|
||||||
} = useLangBotModelAvailability(active && !systemInfo.disable_models_service);
|
} = useLangBotModelAvailability(active && !systemInfo.disable_models_service);
|
||||||
|
|
||||||
@@ -559,7 +559,7 @@ 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}
|
modelMetadata={langbotModelMetadata}
|
||||||
modelAvailabilityLoaded={langbotModelAvailabilityLoaded}
|
modelAvailabilityLoaded={langbotModelAvailabilityLoaded}
|
||||||
addModelPopoverOpen={addModelPopoverOpen}
|
addModelPopoverOpen={addModelPopoverOpen}
|
||||||
editModelPopoverOpen={editModelPopoverOpen}
|
editModelPopoverOpen={editModelPopoverOpen}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import {
|
import {
|
||||||
LLMModel,
|
LLMModel,
|
||||||
EmbeddingModel,
|
EmbeddingModel,
|
||||||
LangBotModelAvailability,
|
LangBotModelAvailabilityItem,
|
||||||
ReasoningConfig,
|
ReasoningConfig,
|
||||||
} from '@/app/infra/entities/api';
|
} from '@/app/infra/entities/api';
|
||||||
import {
|
import {
|
||||||
@@ -25,14 +25,14 @@ 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';
|
import LangBotModelMetadata from '../../model-availability/LangBotModelMetadata';
|
||||||
|
|
||||||
interface ModelItemProps {
|
interface ModelItemProps {
|
||||||
model: LLMModel | EmbeddingModel;
|
model: LLMModel | EmbeddingModel;
|
||||||
canManage: boolean;
|
canManage: boolean;
|
||||||
modelType: ModelType;
|
modelType: ModelType;
|
||||||
isLangBotModels: boolean;
|
isLangBotModels: boolean;
|
||||||
availability?: LangBotModelAvailability;
|
metadata?: LangBotModelAvailabilityItem;
|
||||||
availabilityLoaded: boolean;
|
availabilityLoaded: boolean;
|
||||||
editModelPopoverOpen: string | null;
|
editModelPopoverOpen: string | null;
|
||||||
deleteConfirmOpen: string | null;
|
deleteConfirmOpen: string | null;
|
||||||
@@ -90,7 +90,7 @@ export default function ModelItem({
|
|||||||
canManage,
|
canManage,
|
||||||
modelType,
|
modelType,
|
||||||
isLangBotModels,
|
isLangBotModels,
|
||||||
availability,
|
metadata,
|
||||||
availabilityLoaded,
|
availabilityLoaded,
|
||||||
editModelPopoverOpen,
|
editModelPopoverOpen,
|
||||||
deleteConfirmOpen,
|
deleteConfirmOpen,
|
||||||
@@ -203,14 +203,8 @@ export default function ModelItem({
|
|||||||
: 'hover:bg-accent cursor-pointer'
|
: 'hover:bg-accent cursor-pointer'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex min-w-0 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')
|
||||||
@@ -237,6 +231,12 @@ export default function ModelItem({
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{isLangBotModels && (
|
||||||
|
<LangBotModelMetadata
|
||||||
|
metadata={metadata}
|
||||||
|
loaded={availabilityLoaded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{canManage && !isLangBotModels && (
|
{canManage && !isLangBotModels && (
|
||||||
<Popover
|
<Popover
|
||||||
open={isDeleteOpen}
|
open={isDeleteOpen}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
|
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
|
||||||
import {
|
import {
|
||||||
LangBotModelAvailability,
|
LangBotModelAvailabilityItem,
|
||||||
ModelProvider,
|
ModelProvider,
|
||||||
ReasoningConfig,
|
ReasoningConfig,
|
||||||
} from '@/app/infra/entities/api';
|
} from '@/app/infra/entities/api';
|
||||||
@@ -51,7 +51,7 @@ interface ProviderCardProps {
|
|||||||
isWorkspaceOwner: boolean;
|
isWorkspaceOwner: boolean;
|
||||||
ownerSpaceBound: boolean;
|
ownerSpaceBound: boolean;
|
||||||
spaceCredits: number | null;
|
spaceCredits: number | null;
|
||||||
modelAvailability: Record<string, LangBotModelAvailability>;
|
modelMetadata: Record<string, LangBotModelAvailabilityItem>;
|
||||||
modelAvailabilityLoaded: boolean;
|
modelAvailabilityLoaded: boolean;
|
||||||
// Popover states
|
// Popover states
|
||||||
addModelPopoverOpen: string | null;
|
addModelPopoverOpen: string | null;
|
||||||
@@ -121,7 +121,7 @@ export default function ProviderCard({
|
|||||||
isWorkspaceOwner,
|
isWorkspaceOwner,
|
||||||
ownerSpaceBound,
|
ownerSpaceBound,
|
||||||
spaceCredits,
|
spaceCredits,
|
||||||
modelAvailability,
|
modelMetadata,
|
||||||
modelAvailabilityLoaded,
|
modelAvailabilityLoaded,
|
||||||
addModelPopoverOpen,
|
addModelPopoverOpen,
|
||||||
editModelPopoverOpen,
|
editModelPopoverOpen,
|
||||||
@@ -432,9 +432,8 @@ export default function ProviderCard({
|
|||||||
canManage={canManage}
|
canManage={canManage}
|
||||||
modelType="llm"
|
modelType="llm"
|
||||||
isLangBotModels={isLangBotModels}
|
isLangBotModels={isLangBotModels}
|
||||||
availability={
|
metadata={
|
||||||
modelAvailability[model.uuid] ??
|
modelMetadata[model.uuid] ?? modelMetadata[model.name]
|
||||||
modelAvailability[model.name]
|
|
||||||
}
|
}
|
||||||
availabilityLoaded={modelAvailabilityLoaded}
|
availabilityLoaded={modelAvailabilityLoaded}
|
||||||
editModelPopoverOpen={editModelPopoverOpen}
|
editModelPopoverOpen={editModelPopoverOpen}
|
||||||
@@ -488,9 +487,8 @@ export default function ProviderCard({
|
|||||||
canManage={canManage}
|
canManage={canManage}
|
||||||
modelType="embedding"
|
modelType="embedding"
|
||||||
isLangBotModels={isLangBotModels}
|
isLangBotModels={isLangBotModels}
|
||||||
availability={
|
metadata={
|
||||||
modelAvailability[model.uuid] ??
|
modelMetadata[model.uuid] ?? modelMetadata[model.name]
|
||||||
modelAvailability[model.name]
|
|
||||||
}
|
}
|
||||||
availabilityLoaded={modelAvailabilityLoaded}
|
availabilityLoaded={modelAvailabilityLoaded}
|
||||||
editModelPopoverOpen={editModelPopoverOpen}
|
editModelPopoverOpen={editModelPopoverOpen}
|
||||||
@@ -542,9 +540,8 @@ export default function ProviderCard({
|
|||||||
canManage={canManage}
|
canManage={canManage}
|
||||||
modelType="rerank"
|
modelType="rerank"
|
||||||
isLangBotModels={isLangBotModels}
|
isLangBotModels={isLangBotModels}
|
||||||
availability={
|
metadata={
|
||||||
modelAvailability[model.uuid] ??
|
modelMetadata[model.uuid] ?? modelMetadata[model.name]
|
||||||
modelAvailability[model.name]
|
|
||||||
}
|
}
|
||||||
availabilityLoaded={modelAvailabilityLoaded}
|
availabilityLoaded={modelAvailabilityLoaded}
|
||||||
editModelPopoverOpen={editModelPopoverOpen}
|
editModelPopoverOpen={editModelPopoverOpen}
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ export interface LangBotModelAvailabilityItem {
|
|||||||
uuid: string;
|
uuid: string;
|
||||||
model_id: string;
|
model_id: string;
|
||||||
category: string | null;
|
category: string | null;
|
||||||
|
input_credits: number | null;
|
||||||
|
output_credits: number | null;
|
||||||
availability: LangBotModelAvailability;
|
availability: LangBotModelAvailability;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -310,6 +310,14 @@ const enUS = {
|
|||||||
notChecked: 'No check result',
|
notChecked: 'No check result',
|
||||||
lastChecked: 'Checked {{time}}',
|
lastChecked: 'Checked {{time}}',
|
||||||
},
|
},
|
||||||
|
pricing: {
|
||||||
|
compact: '{{input}} / {{output}}',
|
||||||
|
inline: '{{input}} input · {{output}} output',
|
||||||
|
title: 'Credits per 1M tokens',
|
||||||
|
input: 'Input: {{credits}} credits',
|
||||||
|
output: 'Output: {{credits}} credits',
|
||||||
|
unavailable: 'No current price',
|
||||||
|
},
|
||||||
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.',
|
||||||
|
|||||||
@@ -312,6 +312,14 @@ const esES = {
|
|||||||
notChecked: 'Sin resultado de comprobación',
|
notChecked: 'Sin resultado de comprobación',
|
||||||
lastChecked: 'Comprobado {{time}}',
|
lastChecked: 'Comprobado {{time}}',
|
||||||
},
|
},
|
||||||
|
pricing: {
|
||||||
|
compact: '{{input}} / {{output}}',
|
||||||
|
inline: 'Entrada {{input}} · salida {{output}}',
|
||||||
|
title: 'Créditos por 1 M de tokens',
|
||||||
|
input: 'Entrada: {{credits}} créditos',
|
||||||
|
output: 'Salida: {{credits}} créditos',
|
||||||
|
unavailable: 'Sin precio actual',
|
||||||
|
},
|
||||||
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.',
|
||||||
|
|||||||
@@ -315,6 +315,14 @@ const jaJP = {
|
|||||||
notChecked: 'チェック結果なし',
|
notChecked: 'チェック結果なし',
|
||||||
lastChecked: '{{time}} にチェック',
|
lastChecked: '{{time}} にチェック',
|
||||||
},
|
},
|
||||||
|
pricing: {
|
||||||
|
compact: '{{input}} / {{output}}',
|
||||||
|
inline: '入力 {{input}} · 出力 {{output}}',
|
||||||
|
title: '100万トークンあたりのクレジット',
|
||||||
|
input: '入力:{{credits}} クレジット',
|
||||||
|
output: '出力:{{credits}} クレジット',
|
||||||
|
unavailable: '現在の価格なし',
|
||||||
|
},
|
||||||
langbotModels: 'LangBot モデル',
|
langbotModels: 'LangBot モデル',
|
||||||
spaceTrialTooltip:
|
spaceTrialTooltip:
|
||||||
'無料トライアルクレジットが利用可能!LangBot アカウントでログインして、設定不要でクラウドモデルを使用できます。',
|
'無料トライアルクレジットが利用可能!LangBot アカウントでログインして、設定不要でクラウドモデルを使用できます。',
|
||||||
|
|||||||
@@ -309,6 +309,14 @@ const ruRU = {
|
|||||||
notChecked: 'Нет результата проверки',
|
notChecked: 'Нет результата проверки',
|
||||||
lastChecked: 'Проверено {{time}}',
|
lastChecked: 'Проверено {{time}}',
|
||||||
},
|
},
|
||||||
|
pricing: {
|
||||||
|
compact: '{{input}} / {{output}}',
|
||||||
|
inline: 'Ввод {{input}} · вывод {{output}}',
|
||||||
|
title: 'Кредиты за 1 млн токенов',
|
||||||
|
input: 'Ввод: {{credits}} кредитов',
|
||||||
|
output: 'Вывод: {{credits}} кредитов',
|
||||||
|
unavailable: 'Нет актуальной цены',
|
||||||
|
},
|
||||||
langbotModels: 'Модели LangBot',
|
langbotModels: 'Модели LangBot',
|
||||||
spaceTrialTooltip:
|
spaceTrialTooltip:
|
||||||
'Доступны бесплатные пробные кредиты! Войдите с аккаунтом LangBot, чтобы получить доступ к облачным моделям без настройки.',
|
'Доступны бесплатные пробные кредиты! Войдите с аккаунтом LangBot, чтобы получить доступ к облачным моделям без настройки.',
|
||||||
|
|||||||
@@ -298,6 +298,14 @@ const thTH = {
|
|||||||
notChecked: 'ไม่มีผลการตรวจสอบ',
|
notChecked: 'ไม่มีผลการตรวจสอบ',
|
||||||
lastChecked: 'ตรวจสอบเมื่อ {{time}}',
|
lastChecked: 'ตรวจสอบเมื่อ {{time}}',
|
||||||
},
|
},
|
||||||
|
pricing: {
|
||||||
|
compact: '{{input}} / {{output}}',
|
||||||
|
inline: 'อินพุต {{input}} · เอาต์พุต {{output}}',
|
||||||
|
title: 'เครดิตต่อ 1 ล้านโทเค็น',
|
||||||
|
input: 'อินพุต: {{credits}} เครดิต',
|
||||||
|
output: 'เอาต์พุต: {{credits}} เครดิต',
|
||||||
|
unavailable: 'ไม่มีราคาปัจจุบัน',
|
||||||
|
},
|
||||||
langbotModels: 'โมเดล LangBot',
|
langbotModels: 'โมเดล LangBot',
|
||||||
spaceTrialTooltip:
|
spaceTrialTooltip:
|
||||||
'มีเครดิตทดลองใช้งานฟรี! เข้าสู่ระบบด้วยบัญชี LangBot เพื่อเข้าถึงโมเดลคลาวด์โดยไม่ต้องตั้งค่า',
|
'มีเครดิตทดลองใช้งานฟรี! เข้าสู่ระบบด้วยบัญชี LangBot เพื่อเข้าถึงโมเดลคลาวด์โดยไม่ต้องตั้งค่า',
|
||||||
|
|||||||
@@ -306,6 +306,14 @@ const viVN = {
|
|||||||
notChecked: 'Chưa có kết quả kiểm tra',
|
notChecked: 'Chưa có kết quả kiểm tra',
|
||||||
lastChecked: 'Đã kiểm tra {{time}}',
|
lastChecked: 'Đã kiểm tra {{time}}',
|
||||||
},
|
},
|
||||||
|
pricing: {
|
||||||
|
compact: '{{input}} / {{output}}',
|
||||||
|
inline: 'Đầu vào {{input}} · đầu ra {{output}}',
|
||||||
|
title: 'Tín dụng trên 1 triệu token',
|
||||||
|
input: 'Đầu vào: {{credits}} tín dụng',
|
||||||
|
output: 'Đầu ra: {{credits}} tín dụng',
|
||||||
|
unavailable: 'Chưa có giá hiện tại',
|
||||||
|
},
|
||||||
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.',
|
||||||
|
|||||||
@@ -296,6 +296,14 @@ const zhHans = {
|
|||||||
notChecked: '暂无检测结果',
|
notChecked: '暂无检测结果',
|
||||||
lastChecked: '检测于 {{time}}',
|
lastChecked: '检测于 {{time}}',
|
||||||
},
|
},
|
||||||
|
pricing: {
|
||||||
|
compact: '{{input}} / {{output}}',
|
||||||
|
inline: '输入 {{input}} · 输出 {{output}}',
|
||||||
|
title: '每 1M tokens 消耗积分',
|
||||||
|
input: '输入:{{credits}} 积分',
|
||||||
|
output: '输出:{{credits}} 积分',
|
||||||
|
unavailable: '暂无价格',
|
||||||
|
},
|
||||||
langbotModels: 'LangBot 模型',
|
langbotModels: 'LangBot 模型',
|
||||||
spaceTrialTooltip:
|
spaceTrialTooltip:
|
||||||
'免费试用积分已就绪!通过 LangBot 账号登录即可零配置使用云端模型。',
|
'免费试用积分已就绪!通过 LangBot 账号登录即可零配置使用云端模型。',
|
||||||
|
|||||||
@@ -287,6 +287,14 @@ const zhHant = {
|
|||||||
notChecked: '暫無檢測結果',
|
notChecked: '暫無檢測結果',
|
||||||
lastChecked: '檢測於 {{time}}',
|
lastChecked: '檢測於 {{time}}',
|
||||||
},
|
},
|
||||||
|
pricing: {
|
||||||
|
compact: '{{input}} / {{output}}',
|
||||||
|
inline: '輸入 {{input}} · 輸出 {{output}}',
|
||||||
|
title: '每 1M tokens 消耗積分',
|
||||||
|
input: '輸入:{{credits}} 積分',
|
||||||
|
output: '輸出:{{credits}} 積分',
|
||||||
|
unavailable: '暫無價格',
|
||||||
|
},
|
||||||
langbotModels: 'LangBot 模型',
|
langbotModels: 'LangBot 模型',
|
||||||
spaceTrialTooltip:
|
spaceTrialTooltip:
|
||||||
'免費試用積分已就緒!使用 LangBot 帳號登入即可零設定使用雲端模型。',
|
'免費試用積分已就緒!使用 LangBot 帳號登入即可零設定使用雲端模型。',
|
||||||
|
|||||||
Reference in New Issue
Block a user