908aefe7c1
## Summary - Replaces per-provider TypeScript constant files (`openai-models.const.ts`, `anthropic-models.const.ts`, etc.) with a single `ai-providers.json` catalog as the source of truth - Adds runtime model discovery via AI SDK for self-hosted providers, with `models.dev` enrichment for pricing/capabilities - Introduces composite model IDs (`provider/modelId`) for canonical, conflict-free identification - Simplifies provider configuration: API keys are injected from environment variables (e.g., `OPENAI_API_KEY`) - Adds admin panel UI for provider management (add/remove/test), model discovery, recommended model configuration, and default fast/smart model selection per workspace - Removes deprecated config variables (`AI_DISABLED_MODEL_IDS`, `AUTO_ENABLE_NEW_AI_MODELS`, etc.) - Adds database migration for composite model ID format ## Test plan - [ ] Server typecheck passes - [ ] Frontend typecheck passes - [ ] Server unit tests pass - [ ] Frontend unit tests pass - [ ] CI pipeline green - [ ] Admin panel AI tab loads correctly - [ ] Provider discovery works for configured providers - [ ] Model recommendation toggles persist - [ ] Default fast/smart model selection works Made with [Cursor](https://cursor.com)
241 lines
7.6 KiB
TypeScript
241 lines
7.6 KiB
TypeScript
import { styled } from '@linaria/react';
|
|
import { t } from '@lingui/core/macro';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { useState } from 'react';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { HorizontalSeparator } from 'twenty-ui/display';
|
|
import { ProgressBar } from 'twenty-ui/feedback';
|
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
|
|
|
import { ContextUsageProgressRing } from '@/ai/components/internal/ContextUsageProgressRing';
|
|
import { agentChatHasMessageComponentSelector } from '@/ai/states/agentChatHasMessageComponentSelector';
|
|
import {
|
|
agentChatUsageState,
|
|
type AgentChatLastMessageUsage,
|
|
} from '@/ai/states/agentChatUsageState';
|
|
import { SettingsBillingLabelValueItem } from '@/billing/components/internal/SettingsBillingLabelValueItem';
|
|
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
|
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
|
import { formatNumber } from '~/utils/format/formatNumber';
|
|
|
|
const StyledContainer = styled.div`
|
|
position: relative;
|
|
`;
|
|
|
|
const StyledTrigger = styled.div<{ hasUsage: boolean }>`
|
|
align-items: center;
|
|
cursor: ${({ hasUsage }) => (hasUsage ? 'pointer' : 'default')};
|
|
display: flex;
|
|
height: 24px;
|
|
justify-content: center;
|
|
min-width: 24px;
|
|
transition: background calc(${themeCssVariables.animation.duration.fast} * 1s)
|
|
ease;
|
|
|
|
&:hover {
|
|
background: ${({ hasUsage }) =>
|
|
hasUsage
|
|
? themeCssVariables.background.transparent.light
|
|
: 'transparent'};
|
|
}
|
|
`;
|
|
|
|
const StyledHoverCard = styled.div`
|
|
background: ${themeCssVariables.background.primary};
|
|
border: 1px solid ${themeCssVariables.border.color.medium};
|
|
border-radius: ${themeCssVariables.border.radius.md};
|
|
bottom: calc(100% + 8px);
|
|
box-shadow: ${themeCssVariables.boxShadow.strong};
|
|
left: 0;
|
|
min-width: 280px;
|
|
position: absolute;
|
|
z-index: ${themeCssVariables.lastLayerZIndex};
|
|
`;
|
|
|
|
const StyledSection = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
padding: ${themeCssVariables.spacing[3]};
|
|
`;
|
|
|
|
const StyledRow = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
`;
|
|
|
|
const StyledContextWindowValue = styled.span`
|
|
color: ${themeCssVariables.font.color.secondary};
|
|
font-size: ${themeCssVariables.font.size.sm};
|
|
font-weight: ${themeCssVariables.font.weight.medium};
|
|
`;
|
|
|
|
const StyledSectionTitle = styled.span`
|
|
color: ${themeCssVariables.font.color.primary};
|
|
font-size: ${themeCssVariables.font.size.xs};
|
|
font-weight: ${themeCssVariables.font.weight.semiBold};
|
|
padding-bottom: ${themeCssVariables.spacing[2]};
|
|
`;
|
|
|
|
const formatCredits = (credits: number): string => {
|
|
if (Number.isInteger(credits)) {
|
|
return credits.toLocaleString();
|
|
}
|
|
|
|
return credits.toLocaleString(undefined, {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 1,
|
|
});
|
|
};
|
|
|
|
const getCachedLabel = (lastMessage: AgentChatLastMessageUsage): string => {
|
|
if (lastMessage.cachedInputTokens <= 0 || lastMessage.inputTokens <= 0) {
|
|
return '';
|
|
}
|
|
|
|
const cachedPercent = Math.round(
|
|
(lastMessage.cachedInputTokens / lastMessage.inputTokens) * 100,
|
|
);
|
|
|
|
return ` (${t`${cachedPercent}% cached`})`;
|
|
};
|
|
|
|
export const AIChatContextUsageButton = () => {
|
|
const { t } = useLingui();
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const agentChatUsage = useAtomStateValue(agentChatUsageState);
|
|
|
|
const hasMessages = useAtomComponentSelectorValue(
|
|
agentChatHasMessageComponentSelector,
|
|
);
|
|
|
|
if (!hasMessages) {
|
|
return null;
|
|
}
|
|
|
|
if (!agentChatUsage) {
|
|
return (
|
|
<StyledContainer>
|
|
<StyledTrigger hasUsage={false}>
|
|
<ContextUsageProgressRing percentage={0} />
|
|
</StyledTrigger>
|
|
</StyledContainer>
|
|
);
|
|
}
|
|
|
|
const percentage = Math.min(
|
|
(agentChatUsage.conversationSize / agentChatUsage.contextWindowTokens) *
|
|
100,
|
|
100,
|
|
);
|
|
const formattedPercentage = percentage.toFixed(1);
|
|
const totalCredits =
|
|
agentChatUsage.inputCredits + agentChatUsage.outputCredits;
|
|
const lastMessage = agentChatUsage.lastMessage;
|
|
|
|
return (
|
|
<StyledContainer
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
>
|
|
<StyledTrigger hasUsage={true}>
|
|
<ContextUsageProgressRing percentage={percentage} />
|
|
</StyledTrigger>
|
|
|
|
{isHovered && (
|
|
<StyledHoverCard>
|
|
<StyledSection>
|
|
<StyledSectionTitle>{t`Context window`}</StyledSectionTitle>
|
|
<StyledRow>
|
|
<StyledContextWindowValue>
|
|
{formattedPercentage}%
|
|
</StyledContextWindowValue>
|
|
<StyledContextWindowValue>
|
|
{formatNumber(agentChatUsage.conversationSize, {
|
|
abbreviate: true,
|
|
decimals: 1,
|
|
})}{' '}
|
|
/{' '}
|
|
{formatNumber(agentChatUsage.contextWindowTokens, {
|
|
abbreviate: true,
|
|
decimals: 1,
|
|
})}{' '}
|
|
{t`tokens`}
|
|
</StyledContextWindowValue>
|
|
</StyledRow>
|
|
<ProgressBar
|
|
value={percentage}
|
|
barColor={
|
|
percentage > 80
|
|
? themeCssVariables.color.red
|
|
: percentage > 60
|
|
? themeCssVariables.color.orange
|
|
: themeCssVariables.color.blue
|
|
}
|
|
backgroundColor={themeCssVariables.background.tertiary}
|
|
withBorderRadius
|
|
/>
|
|
</StyledSection>
|
|
|
|
{isDefined(lastMessage) && (
|
|
<>
|
|
<HorizontalSeparator
|
|
noMargin
|
|
color={themeCssVariables.background.tertiary}
|
|
/>
|
|
<StyledSection>
|
|
<StyledSectionTitle>{t`Last message`}</StyledSectionTitle>
|
|
<SettingsBillingLabelValueItem
|
|
label={t`Input tokens`}
|
|
value={`${formatNumber(lastMessage.inputTokens, {
|
|
abbreviate: true,
|
|
decimals: 1,
|
|
})}${getCachedLabel(lastMessage)}`}
|
|
/>
|
|
<SettingsBillingLabelValueItem
|
|
label={t`Output tokens`}
|
|
value={formatNumber(lastMessage.outputTokens, {
|
|
abbreviate: true,
|
|
decimals: 1,
|
|
})}
|
|
/>
|
|
<SettingsBillingLabelValueItem
|
|
label={t`Cost`}
|
|
value={`${formatCredits(lastMessage.inputCredits + lastMessage.outputCredits)} ${t`credits`}`}
|
|
/>
|
|
</StyledSection>
|
|
</>
|
|
)}
|
|
|
|
<HorizontalSeparator
|
|
noMargin
|
|
color={themeCssVariables.background.tertiary}
|
|
/>
|
|
<StyledSection>
|
|
<StyledSectionTitle>{t`Conversation`}</StyledSectionTitle>
|
|
<SettingsBillingLabelValueItem
|
|
label={t`Input tokens`}
|
|
value={formatNumber(agentChatUsage.inputTokens, {
|
|
abbreviate: true,
|
|
decimals: 1,
|
|
})}
|
|
/>
|
|
<SettingsBillingLabelValueItem
|
|
label={t`Output tokens`}
|
|
value={formatNumber(agentChatUsage.outputTokens, {
|
|
abbreviate: true,
|
|
decimals: 1,
|
|
})}
|
|
/>
|
|
<SettingsBillingLabelValueItem
|
|
label={t`Total cost`}
|
|
value={`${formatCredits(totalCredits)} ${t`credits`}`}
|
|
/>
|
|
</StyledSection>
|
|
</StyledHoverCard>
|
|
)}
|
|
</StyledContainer>
|
|
);
|
|
};
|