From 7a341c64753d7c0503c4dcba4e3b0711f2575caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Thu, 26 Mar 2026 16:43:09 +0100 Subject: [PATCH] feat: support authType on AI providers for IAM role authentication (#19016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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) --- .../ai/components/SettingsAdminAiProviderListCard.tsx | 9 +++++++-- .../settings/admin-panel/ai/types/AiProviderAuthType.ts | 1 + .../settings/admin-panel/ai/types/AiProviderItem.ts | 2 ++ .../admin-panel/SettingsAdminAiProviderDetail.tsx | 6 ++++++ .../core-modules/admin-panel/admin-panel.resolver.ts | 1 + .../ai/ai-models/services/ai-model-registry.service.ts | 5 ++--- .../ai/ai-models/types/ai-provider-auth-type.type.ts | 1 + .../ai/ai-models/types/ai-provider-config.type.ts | 2 ++ .../ai/ai-models/utils/is-provider-configured.util.ts | 4 ++++ 9 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/ai/types/AiProviderAuthType.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/ai/ai-models/types/ai-provider-auth-type.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/is-provider-configured.util.ts diff --git a/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAiProviderListCard.tsx b/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAiProviderListCard.tsx index 5218967b48..18ccc57d8d 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAiProviderListCard.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAiProviderListCard.tsx @@ -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) ? ( ) : ( diff --git a/packages/twenty-front/src/modules/settings/admin-panel/ai/types/AiProviderAuthType.ts b/packages/twenty-front/src/modules/settings/admin-panel/ai/types/AiProviderAuthType.ts new file mode 100644 index 0000000000..b43fe0eb90 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/ai/types/AiProviderAuthType.ts @@ -0,0 +1 @@ +export type AiProviderAuthType = 'key' | 'credentials' | 'role'; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/ai/types/AiProviderItem.ts b/packages/twenty-front/src/modules/settings/admin-panel/ai/types/AiProviderItem.ts index 52a6fa9b68..7c512af1f0 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/ai/types/AiProviderItem.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/ai/types/AiProviderItem.ts @@ -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 { 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) { diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts index d97708be8e..6a212b32c4 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts @@ -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 }), diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts index 87b9fb1615..a3968aad4e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts @@ -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; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/types/ai-provider-auth-type.type.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/types/ai-provider-auth-type.type.ts new file mode 100644 index 0000000000..b43fe0eb90 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/types/ai-provider-auth-type.type.ts @@ -0,0 +1 @@ +export type AiProviderAuthType = 'key' | 'credentials' | 'role'; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/types/ai-provider-config.type.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/types/ai-provider-config.type.ts index 2fb663331b..bba8af5e32 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/types/ai-provider-config.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/types/ai-provider-config.type.ts @@ -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; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/is-provider-configured.util.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/is-provider-configured.util.ts new file mode 100644 index 0000000000..fc57588628 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/is-provider-configured.util.ts @@ -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);