Skip to content

Commit 8176203

Browse files
alenkaczclaude
andcommitted
feat(ai-agents): make API keys optional for OpenAI, Anthropic, Google, and OpenAI-compatible providers
Make API keys optional in AI agent proto definitions to support deployments using AI Gateway for authentication. When using an AI Gateway, the gateway handles authentication, making API keys unnecessary for agent configuration. Changes: - Remove REQUIRED field behavior from api_key fields in Provider messages - Add IGNORE_IF_ZERO_VALUE validation to allow empty API keys - Update frontend forms to conditionally require API key based on gateway usage - Add gateway configuration support in agent creation and details pages - Regenerate proto files and OpenAPI schemas Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent a7ec802 commit 8176203

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)