Files
twenty/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAiModelHoverCard.tsx
T
Félix Malfait 908aefe7c1 feat: replace hardcoded AI model constants with JSON seed catalog (#18818)
## 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)
2026-03-21 16:03:58 +01:00

150 lines
3.5 KiB
TypeScript

import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import {
IconBolt,
IconCoins,
IconFileText,
IconFlag,
IconServer,
IconTag,
} from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
import { getDataResidencyDisplay } from '@/settings/admin-panel/ai/utils/getDataResidencyDisplay';
import { getModelIcon } from '@/settings/admin-panel/ai/utils/getModelIcon';
import { type ModelFamily } from '~/generated-metadata/graphql';
import { formatNumber } from '~/utils/format/formatNumber';
const StyledNameValue = styled.span`
align-items: center;
display: flex;
gap: 4px;
`;
const StyledHoverCardWrapper = styled.div`
border-radius: ${themeCssVariables.border.radius.md};
box-shadow: ${themeCssVariables.boxShadow.strong};
overflow: hidden;
`;
type SettingsAdminAiModelHoverCardProps = {
label: string;
modelFamily?: ModelFamily | null;
providerName?: string | null;
providerLabel: string;
contextWindowTokens?: number | null;
maxOutputTokens?: number | null;
inputCostPerMillionTokens?: number | null;
outputCostPerMillionTokens?: number | null;
dataResidency?: string | null;
};
const formatCost = (
inputCost?: number | null,
outputCost?: number | null,
): string => {
if (inputCost == null && outputCost == null) {
return '—';
}
const parts: string[] = [];
if (inputCost != null) {
parts.push(`$${inputCost} in`);
}
if (outputCost != null) {
parts.push(`$${outputCost} out`);
}
return parts.join(' / ');
};
export const SettingsAdminAiModelHoverCard = ({
label,
modelFamily,
providerName,
providerLabel,
contextWindowTokens,
maxOutputTokens,
inputCostPerMillionTokens,
outputCostPerMillionTokens,
dataResidency,
}: SettingsAdminAiModelHoverCardProps) => {
const ModelIcon = getModelIcon(modelFamily, providerName);
const items = [
{
Icon: IconTag,
label: t`Name`,
value: (
<StyledNameValue>
<ModelIcon size={14} />
{label}
</StyledNameValue>
),
},
{
Icon: IconServer,
label: t`Provider`,
value: providerLabel || '—',
},
...(inputCostPerMillionTokens != null || outputCostPerMillionTokens != null
? [
{
Icon: IconCoins,
label: t`Cost / 1M`,
value: formatCost(
inputCostPerMillionTokens,
outputCostPerMillionTokens,
),
},
]
: []),
...(contextWindowTokens != null
? [
{
Icon: IconFileText,
label: t`Context`,
value: `${formatNumber(contextWindowTokens, {
abbreviate: true,
decimals: 1,
})} tokens`,
},
]
: []),
...(maxOutputTokens != null
? [
{
Icon: IconBolt,
label: t`Max output`,
value: `${formatNumber(maxOutputTokens, {
abbreviate: true,
decimals: 1,
})} tokens`,
},
]
: []),
...(dataResidency
? [
{
Icon: IconFlag,
label: t`Data residency`,
value: getDataResidencyDisplay(dataResidency),
},
]
: []),
];
return (
<StyledHoverCardWrapper>
<SettingsAdminTableCard
rounded
items={items}
gridAutoColumns="120px 1fr"
/>
</StyledHoverCardWrapper>
);
};