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)
150 lines
3.5 KiB
TypeScript
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>
|
|
);
|
|
};
|