Skip to content

Commit adc1afb

Browse files
authored
Merge pull request #2159 from redpanda-data/make-ai-agent-api-keys-optional
feat(ai-agents): make API keys optional for AI providers
2 parents a7ec802 + 8176203 commit adc1afb

File tree

10 files changed

+841
-620
lines changed

10 files changed

+841
-620
lines changed

backend/pkg/protogen/redpanda/api/dataplane/v1alpha3/ai_agent.pb.go

Lines changed: 550 additions & 551 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/components/pages/agents/create/ai-agent-create-page.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { TagsFieldList } from 'components/ui/tag/tags-field-list';
3535
import { Loader2 } from 'lucide-react';
3636
import { Scope } from 'protogen/redpanda/api/dataplane/v1/secret_pb';
3737
import {
38+
AIAgent_GatewayConfigSchema,
3839
AIAgent_MCPServerSchema,
3940
type AIAgent_Provider,
4041
AIAgent_Provider_AnthropicSchema,
@@ -47,9 +48,11 @@ import {
4748
AIAgentCreateSchema,
4849
CreateAIAgentRequestSchema,
4950
} from 'protogen/redpanda/api/dataplane/v1alpha3/ai_agent_pb';
51+
import { type AIGateway, AIGateway_State } from 'protogen/redpanda/api/dataplane/v1alpha3/ai_gateway_pb';
5052
import { useEffect, useMemo, useRef, useState } from 'react';
5153
import { Controller, useFieldArray, useForm } from 'react-hook-form';
5254
import { useCreateAIAgentMutation } from 'react-query/api/ai-agent';
55+
import { useListAIGatewaysQuery } from 'react-query/api/ai-gateway';
5356
import { useListMCPServersQuery } from 'react-query/api/remote-mcp';
5457
import { useCreateSecretMutation, useListSecretsQuery } from 'react-query/api/secret';
5558
import { toast } from 'sonner';
@@ -71,6 +74,32 @@ export const AIAgentCreatePage = () => {
7174
skipInvalidation: true,
7275
});
7376

77+
// Gateway detection
78+
const { data: gatewaysData, isLoading: isLoadingGateways } = useListAIGatewaysQuery(
79+
{ pageSize: -1 },
80+
{ enabled: true }
81+
);
82+
83+
const hasGatewayDeployed = useMemo(() => {
84+
if (isLoadingGateways) {
85+
return false;
86+
}
87+
return Boolean(gatewaysData?.aiGateways && gatewaysData.aiGateways.length > 0);
88+
}, [gatewaysData, isLoadingGateways]);
89+
90+
const availableGateways = useMemo(() => {
91+
if (!gatewaysData?.aiGateways) {
92+
return [];
93+
}
94+
return gatewaysData.aiGateways
95+
.filter((gw: AIGateway) => gw.state === AIGateway_State.RUNNING)
96+
.map((gw: AIGateway) => ({
97+
id: gw.id,
98+
displayName: gw.displayName,
99+
description: gw.description,
100+
}));
101+
}, [gatewaysData]);
102+
74103
// Ref to ServiceAccountSelector to call createServiceAccount
75104
const serviceAccountSelectorRef = useRef<ServiceAccountSelectorRef>(null);
76105

@@ -342,6 +371,13 @@ export const AIAgentCreatePage = () => {
342371
});
343372
}
344373

374+
const gatewayConfig =
375+
values.gatewayId && values.gatewayId.trim() !== ''
376+
? create(AIAgent_GatewayConfigSchema, {
377+
virtualGatewayId: values.gatewayId,
378+
})
379+
: undefined;
380+
345381
await createAgent(
346382
create(CreateAIAgentRequestSchema, {
347383
aiAgent: create(AIAgentCreateSchema, {
@@ -359,6 +395,7 @@ export const AIAgentCreatePage = () => {
359395
memoryShares: selectedTier?.memory || '800M',
360396
},
361397
serviceAccount: serviceAccountConfig,
398+
gateway: gatewayConfig,
362399
}),
363400
}),
364401
{
@@ -455,15 +492,18 @@ export const AIAgentCreatePage = () => {
455492
</CardHeader>
456493
<CardContent>
457494
<LLMConfigSection
495+
availableGateways={availableGateways}
458496
availableSecrets={availableSecrets}
459497
fieldNames={{
460498
provider: 'provider',
461499
model: 'model',
462500
apiKeySecret: 'apiKeySecret',
463501
baseUrl: 'baseUrl',
464502
maxIterations: 'maxIterations',
503+
gatewayId: 'gatewayId',
465504
}}
466505
form={form}
506+
hasGatewayDeployed={hasGatewayDeployed}
467507
mode="create"
468508
scopes={[Scope.MCP_SERVER, Scope.AI_AGENT]}
469509
showBaseUrl={form.watch('provider') === 'openaiCompatible'}

frontend/src/components/pages/agents/create/schemas.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const FormSchema = z
5050
),
5151
triggerType: z.enum(['http', 'slack', 'kafka']).default('http'),
5252
provider: z.enum(['openai', 'anthropic', 'google', 'openaiCompatible']).default('openai'),
53-
apiKeySecret: z.string().min(1, 'API Token is required'),
53+
apiKeySecret: z.string(),
5454
model: z.string().min(1, 'Model is required'),
5555
baseUrl: z.string().url('Must be a valid URL').optional().or(z.literal('')),
5656
maxIterations: z
@@ -77,7 +77,14 @@ export const FormSchema = z
7777
},
7878
{ message: 'Subagent names must be unique' }
7979
),
80+
gatewayId: z
81+
.string()
82+
.length(20, 'Gateway ID must be exactly 20 characters')
83+
.regex(/^[a-z0-9]+$/, 'Gateway ID must contain only lowercase letters and numbers')
84+
.optional()
85+
.or(z.literal('')),
8086
})
87+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex validation logic with multiple conditional checks
8188
.superRefine((data, ctx) => {
8289
if (data.provider === 'openaiCompatible') {
8390
if (!data.baseUrl || data.baseUrl.trim() === '') {
@@ -105,6 +112,15 @@ export const FormSchema = z
105112
}
106113
}
107114
}
115+
116+
// API Key is required when not using a gateway
117+
if ((!data.gatewayId || data.gatewayId.trim() === '') && (!data.apiKeySecret || data.apiKeySecret.trim() === '')) {
118+
ctx.addIssue({
119+
code: z.ZodIssueCode.custom,
120+
message: 'API Token is required when not using a gateway',
121+
path: ['apiKeySecret'],
122+
});
123+
}
108124
});
109125

110126
export type FormValues = z.infer<typeof FormSchema>;
@@ -126,4 +142,5 @@ export const initialValues: FormValues = {
126142
systemPrompt: '',
127143
serviceAccountName: '',
128144
subagents: [],
145+
gatewayId: '',
129146
};

frontend/src/components/pages/agents/details/ai-agent-configuration-tab.tsx

Lines changed: 117 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { ServiceAccountSection } from 'components/ui/service-account/service-acc
4848
import { Edit, Plus, Save, Settings, ShieldCheck, Trash2 } from 'lucide-react';
4949
import { Scope } from 'protogen/redpanda/api/dataplane/v1/secret_pb';
5050
import {
51+
AIAgent_GatewayConfigSchema,
5152
AIAgent_MCPServerSchema,
5253
type AIAgent_Provider,
5354
AIAgent_Provider_AnthropicSchema,
@@ -59,8 +60,10 @@ import {
5960
AIAgentUpdateSchema,
6061
UpdateAIAgentRequestSchema,
6162
} from 'protogen/redpanda/api/dataplane/v1alpha3/ai_agent_pb';
63+
import { type AIGateway, AIGateway_State } from 'protogen/redpanda/api/dataplane/v1alpha3/ai_gateway_pb';
6264
import { useCallback, useMemo, useState } from 'react';
6365
import { useGetAIAgentQuery, useUpdateAIAgentMutation } from 'react-query/api/ai-agent';
66+
import { useListAIGatewaysQuery } from 'react-query/api/ai-gateway';
6467
import { type MCPServer, useListMCPServersQuery } from 'react-query/api/remote-mcp';
6568
import { useListSecretsQuery } from 'react-query/api/secret';
6669
import { toast } from 'sonner';
@@ -88,6 +91,7 @@ type LocalAIAgent = {
8891
systemPrompt: string;
8992
selectedMcpServers: string[];
9093
}>;
94+
gatewayId?: string;
9195
};
9296

9397
/**
@@ -272,6 +276,32 @@ export const AIAgentConfigurationTab = () => {
272276
const { data: mcpServersData } = useListMCPServersQuery();
273277
const { data: secretsData } = useListSecretsQuery();
274278

279+
// Gateway detection
280+
const { data: gatewaysData, isLoading: isLoadingGateways } = useListAIGatewaysQuery(
281+
{ pageSize: -1 },
282+
{ enabled: true }
283+
);
284+
285+
const hasGatewayDeployed = useMemo(() => {
286+
if (isLoadingGateways) {
287+
return false;
288+
}
289+
return Boolean(gatewaysData?.aiGateways && gatewaysData.aiGateways.length > 0);
290+
}, [gatewaysData, isLoadingGateways]);
291+
292+
const availableGateways = useMemo(() => {
293+
if (!gatewaysData?.aiGateways) {
294+
return [];
295+
}
296+
return gatewaysData.aiGateways
297+
.filter((gw: AIGateway) => gw.state === AIGateway_State.RUNNING)
298+
.map((gw: AIGateway) => ({
299+
id: gw.id,
300+
displayName: gw.displayName,
301+
description: gw.description,
302+
}));
303+
}, [gatewaysData]);
304+
275305
const [isEditing, setIsEditing] = useState(false);
276306
const [editedAgentData, setEditedAgentData] = useState<LocalAIAgent | null>(null);
277307
const [expandedSubagent, setExpandedSubagent] = useState<string | undefined>(undefined);
@@ -341,6 +371,7 @@ export const AIAgentConfigurationTab = () => {
341371
systemPrompt: subagent.systemPrompt,
342372
selectedMcpServers: Object.values(subagent.mcpServers || {}).map((server) => server.id),
343373
})),
374+
gatewayId: aiAgentData.aiAgent.gateway?.virtualGatewayId,
344375
};
345376
}
346377

@@ -601,6 +632,13 @@ export const AIAgentConfigurationTab = () => {
601632
const apiKeyRef = `\${secrets.${currentData.apiKeySecret}}`;
602633
const updatedProvider = createUpdatedProvider(currentData.provider.provider.case, apiKeyRef, currentData.baseUrl);
603634

635+
const gatewayConfig =
636+
currentData.gatewayId && currentData.gatewayId.trim() !== ''
637+
? create(AIAgent_GatewayConfigSchema, {
638+
virtualGatewayId: currentData.gatewayId,
639+
})
640+
: undefined;
641+
604642
await updateAIAgent(
605643
create(UpdateAIAgentRequestSchema, {
606644
id,
@@ -619,6 +657,7 @@ export const AIAgentConfigurationTab = () => {
619657
mcpServers: mcpServersMap,
620658
subagents: subagentsMap,
621659
tags: tagsMap,
660+
gateway: gatewayConfig,
622661
}),
623662
updateMask: create(FieldMaskSchema, {
624663
paths: [
@@ -630,6 +669,7 @@ export const AIAgentConfigurationTab = () => {
630669
'system_prompt',
631670
'service_account',
632671
'resources',
672+
'gateway',
633673
'mcp_servers',
634674
'subagents',
635675
'tags',
@@ -1007,10 +1047,35 @@ export const AIAgentConfigurationTab = () => {
10071047
<CardContent className="px-4 pb-4">
10081048
{isEditing ? (
10091049
<div className="space-y-4">
1050+
{/* Gateway Selection - only show if gateways deployed */}
1051+
{hasGatewayDeployed && availableGateways.length > 0 && (
1052+
<div className="space-y-2">
1053+
<Label>AI Gateway</Label>
1054+
<Text className="text-muted-foreground text-xs">Route requests through an AI Gateway</Text>
1055+
<Select
1056+
onValueChange={(value) => updateField({ gatewayId: value || undefined })}
1057+
value={displayData.gatewayId || ''}
1058+
>
1059+
<SelectTrigger>
1060+
<SelectValue placeholder="No gateway" />
1061+
</SelectTrigger>
1062+
<SelectContent>
1063+
<SelectItem value="">No gateway</SelectItem>
1064+
{availableGateways.map((gw: { id: string; displayName: string; description: string }) => (
1065+
<SelectItem key={gw.id} value={gw.id}>
1066+
{gw.displayName}
1067+
</SelectItem>
1068+
))}
1069+
</SelectContent>
1070+
</Select>
1071+
</div>
1072+
)}
1073+
10101074
{/* Provider - now editable */}
10111075
<div className="space-y-2">
10121076
<Label htmlFor="provider">Provider</Label>
10131077
<Select
1078+
disabled={!!displayData.gatewayId}
10141079
onValueChange={(value: 'openai' | 'anthropic' | 'google' | 'openaiCompatible') => {
10151080
const newProviderData = MODEL_OPTIONS_BY_PROVIDER[value];
10161081
const firstModel =
@@ -1071,12 +1136,17 @@ export const AIAgentConfigurationTab = () => {
10711136
<Label htmlFor="model">Model</Label>
10721137
{displayData.provider?.provider.case === 'openaiCompatible' ? (
10731138
<Input
1139+
disabled={!!displayData.gatewayId}
10741140
onChange={(e) => updateField({ model: e.target.value })}
10751141
placeholder="Enter model name (e.g., llama-3.1-70b)"
10761142
value={displayData.model}
10771143
/>
10781144
) : (
1079-
<Select onValueChange={(value) => updateField({ model: value })} value={displayData.model}>
1145+
<Select
1146+
disabled={!!displayData.gatewayId}
1147+
onValueChange={(value) => updateField({ model: value })}
1148+
value={displayData.model}
1149+
>
10801150
<SelectTrigger>
10811151
<SelectValue>
10821152
{Boolean(displayData.model) && detectProvider(displayData.model) ? (
@@ -1137,23 +1207,25 @@ export const AIAgentConfigurationTab = () => {
11371207
)}
11381208
</div>
11391209

1140-
{/* API Token */}
1141-
<div className="space-y-2">
1142-
<Label htmlFor="apiKeySecret">API Token</Label>
1143-
<div className="[&>div]:flex-col [&>div]:items-stretch [&>div]:gap-2">
1144-
<SecretSelector
1145-
availableSecrets={availableSecrets}
1146-
customText={AI_AGENT_SECRET_TEXT}
1147-
onChange={(value) => updateField({ apiKeySecret: value })}
1148-
placeholder="Select from secrets store or create new"
1149-
scopes={[Scope.MCP_SERVER, Scope.AI_AGENT]}
1150-
value={displayData.apiKeySecret}
1151-
/>
1210+
{/* API Token - HIDE if using gateway */}
1211+
{!displayData.gatewayId && (
1212+
<div className="space-y-2">
1213+
<Label htmlFor="apiKeySecret">API Token</Label>
1214+
<div className="[&>div]:flex-col [&>div]:items-stretch [&>div]:gap-2">
1215+
<SecretSelector
1216+
availableSecrets={availableSecrets}
1217+
customText={AI_AGENT_SECRET_TEXT}
1218+
onChange={(value) => updateField({ apiKeySecret: value })}
1219+
placeholder="Select from secrets store or create new"
1220+
scopes={[Scope.MCP_SERVER, Scope.AI_AGENT]}
1221+
value={displayData.apiKeySecret}
1222+
/>
1223+
</div>
11521224
</div>
1153-
</div>
1225+
)}
11541226

1155-
{/* Base URL - only show for openaiCompatible */}
1156-
{displayData.provider?.provider.case === 'openaiCompatible' && (
1227+
{/* Base URL - only show for openaiCompatible and when not using gateway */}
1228+
{!displayData.gatewayId && displayData.provider?.provider.case === 'openaiCompatible' && (
11571229
<div className="space-y-2">
11581230
<Label htmlFor="baseUrl">Base URL (required)</Label>
11591231
<Input
@@ -1181,6 +1253,21 @@ export const AIAgentConfigurationTab = () => {
11811253
</div>
11821254
) : (
11831255
<div className="space-y-4">
1256+
{/* Gateway - only show if gateways deployed */}
1257+
{hasGatewayDeployed && availableGateways.length > 0 && (
1258+
<div className="space-y-2">
1259+
<Label>AI Gateway</Label>
1260+
<div className="flex h-10 items-center rounded-md border border-gray-200 bg-gray-50 px-3 py-2">
1261+
<Text variant="default">
1262+
{displayData.gatewayId
1263+
? availableGateways.find((gw: { id: string; displayName: string; description: string }) => gw.id === displayData.gatewayId)?.displayName ||
1264+
displayData.gatewayId
1265+
: 'None'}
1266+
</Text>
1267+
</div>
1268+
</div>
1269+
)}
1270+
11841271
<div className="space-y-2">
11851272
<Label>Provider</Label>
11861273
<div className="flex h-10 items-center rounded-md border border-gray-200 bg-gray-50 px-3 py-2">
@@ -1198,20 +1285,25 @@ export const AIAgentConfigurationTab = () => {
11981285
<AIAgentModel model={displayData.model} />
11991286
</div>
12001287
</div>
1201-
<div className="space-y-2">
1202-
<Label>API Token</Label>
1203-
<div className="flex h-10 items-center rounded-md border border-gray-200 bg-gray-50 px-3 py-2">
1204-
<Text variant="default">{displayData.apiKeySecret || 'No secret configured'}</Text>
1205-
</div>
1206-
</div>
1207-
{agent.provider?.provider.case === 'openaiCompatible' && displayData.baseUrl && (
1288+
{/* API Token - HIDE if using gateway */}
1289+
{!displayData.gatewayId && (
12081290
<div className="space-y-2">
1209-
<Label>Base URL</Label>
1291+
<Label>API Token</Label>
12101292
<div className="flex h-10 items-center rounded-md border border-gray-200 bg-gray-50 px-3 py-2">
1211-
<Text variant="default">{displayData.baseUrl}</Text>
1293+
<Text variant="default">{displayData.apiKeySecret || 'No secret configured'}</Text>
12121294
</div>
12131295
</div>
12141296
)}
1297+
{!displayData.gatewayId &&
1298+
agent.provider?.provider.case === 'openaiCompatible' &&
1299+
displayData.baseUrl && (
1300+
<div className="space-y-2">
1301+
<Label>Base URL</Label>
1302+
<div className="flex h-10 items-center rounded-md border border-gray-200 bg-gray-50 px-3 py-2">
1303+
<Text variant="default">{displayData.baseUrl}</Text>
1304+
</div>
1305+
</div>
1306+
)}
12151307
<div className="space-y-2">
12161308
<Label>Max Iterations</Label>
12171309
<div className="flex h-10 items-center rounded-md border border-gray-200 bg-gray-50 px-3 py-2">

0 commit comments

Comments
 (0)