feat: support authType on AI providers for IAM role authentication (#19016)
## Summary - Adds an `authType` field (`'api_key' | 'access_key' | 'iam_role'`) to AI provider config - Providers like Amazon Bedrock that authenticate via IAM role (instance profile) can now be registered without explicit API keys or access keys - Backend: `isProviderConfigured()` checks `apiKey || accessKeyId || authType` - Frontend admin panel: shows green "Configured" badge and "IAM role" description for providers with `authType: "iam_role"` - Provider detail page shows "IAM role (instance profile)" in the credentials row ## Companion PR - twentyhq/twenty-infra#528 — patches `authType: "iam_role"` into dev/staging Bedrock catalogs ## Changed files - **Backend**: new `AiProviderAuthType` type, `isProviderConfigured` util, updated registry + resolver - **Frontend**: new `AiProviderAuthType` type, updated provider list card + detail page ## Test plan - [ ] Deploy with a Bedrock catalog that includes `"authType": "iam_role"` — verify Bedrock shows "Configured" in admin AI panel - [ ] Verify OpenAI/Anthropic with `apiKey` still show "Configured" - [ ] Verify a provider with no credentials and no `authType` still shows "No credentials" Made with [Cursor](https://cursor.com)
This commit is contained in:
+7
-2
@@ -33,7 +33,9 @@ const getProviderDescription = (provider: AiProviderItem): string => {
|
||||
parts.push(provider.baseUrl);
|
||||
}
|
||||
|
||||
if (provider.apiKey) {
|
||||
if (provider.authType === 'role') {
|
||||
parts.push(t`IAM role`);
|
||||
} else if (provider.apiKey) {
|
||||
parts.push(t`API key configured`);
|
||||
} else if (provider.hasAccessKey) {
|
||||
parts.push(t`IAM credentials`);
|
||||
@@ -42,6 +44,9 @@ const getProviderDescription = (provider: AiProviderItem): string => {
|
||||
return parts.join(' · ');
|
||||
};
|
||||
|
||||
const isProviderConfigured = (provider: AiProviderItem): boolean =>
|
||||
!!(provider.authType || provider.apiKey || provider.hasAccessKey);
|
||||
|
||||
export const SettingsAdminAiProviderListCard = ({
|
||||
providers,
|
||||
showAddButton = true,
|
||||
@@ -70,7 +75,7 @@ export const SettingsAdminAiProviderListCard = ({
|
||||
getItemLabel={(provider) => provider.label ?? provider.id}
|
||||
getItemDescription={getProviderDescription}
|
||||
RowRightComponent={({ item: provider }) =>
|
||||
provider.apiKey || provider.hasAccessKey ? (
|
||||
isProviderConfigured(provider) ? (
|
||||
<Status color="green" text={t`Configured`} weight="medium" />
|
||||
) : (
|
||||
<Status color="orange" text={t`No credentials`} weight="medium" />
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type AiProviderAuthType = 'key' | 'credentials' | 'role';
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type AiSdkPackage, type DataResidency } from 'twenty-shared/ai';
|
||||
|
||||
import { type AiProviderAuthType } from '@/settings/admin-panel/ai/types/AiProviderAuthType';
|
||||
import { type AiProviderSource } from '@/settings/admin-panel/ai/types/AiProviderSource';
|
||||
|
||||
// AiProviderItem = RawAiProviderConfig (from the backend's Record<string,
|
||||
@@ -11,6 +12,7 @@ export type AiProviderItem = {
|
||||
// Optional provider display/catalog name from config (not a model name; models use `models[].name` on the backend).
|
||||
name?: string;
|
||||
label?: string;
|
||||
authType?: AiProviderAuthType;
|
||||
source?: AiProviderSource;
|
||||
baseUrl?: string;
|
||||
region?: string;
|
||||
|
||||
@@ -257,6 +257,12 @@ export const SettingsAdminAiProviderDetail = () => {
|
||||
label: t`Credentials`,
|
||||
value: t`IAM credentials configured`,
|
||||
});
|
||||
} else if (provider.authType === 'role') {
|
||||
items.push({
|
||||
Icon: IconKey,
|
||||
label: t`Credentials`,
|
||||
value: t`IAM role (instance profile)`,
|
||||
});
|
||||
}
|
||||
|
||||
if (isCustomProvider && provider.dataResidency) {
|
||||
|
||||
@@ -357,6 +357,7 @@ export class AdminPanelResolver {
|
||||
npm: config.npm,
|
||||
label: config.label ?? key,
|
||||
source: isCatalog ? 'catalog' : 'custom',
|
||||
...(config.authType && { authType: config.authType }),
|
||||
...(config.name && { name: config.name }),
|
||||
...(config.baseUrl && { baseUrl: config.baseUrl }),
|
||||
...(config.region && { region: config.region }),
|
||||
|
||||
+2
-3
@@ -26,6 +26,7 @@ import { isAutoSelectModelId } from 'twenty-shared/utils';
|
||||
import { DEFAULT_MAX_OUTPUT_TOKENS } from 'src/engine/metadata-modules/ai/ai-models/types/default-max-output-tokens.const';
|
||||
import { buildCompositeModelId } from 'src/engine/metadata-modules/ai/ai-models/utils/composite-model-id.util';
|
||||
import { inferModelFamily } from 'src/engine/metadata-modules/ai/ai-models/utils/infer-model-family.util';
|
||||
import { isProviderConfigured } from 'src/engine/metadata-modules/ai/ai-models/utils/is-provider-configured.util';
|
||||
import {
|
||||
isModelAllowedByWorkspace,
|
||||
type WorkspaceModelAvailabilitySettings,
|
||||
@@ -84,9 +85,7 @@ export class AiModelRegistryService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isConfigured = !!(config.apiKey || config.accessKeyId);
|
||||
|
||||
const sdkInstance = isConfigured
|
||||
const sdkInstance = isProviderConfigured(config)
|
||||
? this.sdkProviderFactory.createProvider(providerKey, config)
|
||||
: undefined;
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export type AiProviderAuthType = 'key' | 'credentials' | 'role';
|
||||
+2
@@ -1,5 +1,6 @@
|
||||
import { type AiSdkPackage, type DataResidency } from 'twenty-shared/ai';
|
||||
|
||||
import { type AiProviderAuthType } from 'src/engine/metadata-modules/ai/ai-models/types/ai-provider-auth-type.type';
|
||||
import { type AiProviderModelConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-provider-model-config.type';
|
||||
|
||||
export type AiProviderConfig = {
|
||||
@@ -7,6 +8,7 @@ export type AiProviderConfig = {
|
||||
// Optional provider display/catalog name (e.g. models.dev label). Not a model name; per-model names live on `models[].name`.
|
||||
name?: string;
|
||||
label?: string;
|
||||
authType?: AiProviderAuthType;
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
region?: string;
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { type AiProviderConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-provider-config.type';
|
||||
|
||||
export const isProviderConfigured = (config: AiProviderConfig): boolean =>
|
||||
!!(config.apiKey || config.accessKeyId || config.authType);
|
||||
Reference in New Issue
Block a user