Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions buf.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ deps:
- name: buf.build/grpc-ecosystem/grpc-gateway
commit: a48fcebcf8f140dd9d09359b9bb185a4
digest: b5:330af8a71b579ab96c4f3ee26929d1a68a5a9e986c7cfe0a898591fc514216bb6e723dc04c74d90fdee3f3f14f9100a54b4f079eb273e6e7213f0d5baca36ff8
- name: buf.build/opentelemetry/opentelemetry
commit: 648a3e2f02e14fe187656ea4ac3befa1
digest: b5:a0514be587ab2e8598f7102dfdaba5e985138b8c041c707e470a9c8b877410ada6e60cf2359352302f08c0504de685379f7f7a085d4699d69a2e6ed7035e78d9
- name: buf.build/redpandadata/ai-gateway
commit: 702ee5c67c994d9995e39e74bb6553f6
digest: b5:9717dd679ced8e42eab232efcc89c4ebcf9334b5aa6664b502268d5c7d25766e2c23b201822383a0a7c943d663859c7054c0656937fce7b612f7b578a59e3eb2
- name: buf.build/redpandadata/common
commit: 601698cfe71d43b1b36fd434a7f765f1
digest: b5:d566ba6746a874a5709970e7f8569584008447d655c556676aabd3430111bbbc0a303dde11d99a1955ee27ac4748bcf7c972503a66db4ef850eb55f239f851ac
Expand Down
1 change: 1 addition & 0 deletions buf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ deps:
- buf.build/redpandadata/common
- buf.build/redpandadata/core:05aa34b3829a4d5a801d9487623a7c76
- buf.build/redpandadata/otel
- buf.build/redpandadata/ai-gateway
lint:
use:
- STANDARD
Expand Down
1 change: 1 addition & 0 deletions frontend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 25 additions & 6 deletions frontend/rsbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,31 @@ export default defineConfig({
origin: ['http://localhost:3000', 'http://localhost:9090'],
credentials: true,
},
proxy: {
context: ['/api', '/redpanda.api', '/auth', '/logout'],
target: process.env.PROXY_TARGET || 'http://localhost:9090',
changeOrigin: !!process.env.PROXY_TARGET,
secure: process.env.PROXY_TARGET ? false : undefined,
},
proxy: [
// AI Gateway API - proxy to separate AI Gateway service
// Matches: /.redpanda/api/redpanda.api.aigateway.v1.*
// Proto package is: redpanda.api.aigateway.v1 (includes .api)
// AI Gateway now expects the full path with .api
...(process.env.AI_GATEWAY_URL
? [
{
context: ['/.redpanda/api/redpanda.api.aigateway.v1'],
target: process.env.AI_GATEWAY_URL,
changeOrigin: true,
secure: false,
logLevel: 'debug',
// No pathRewrite - AI Gateway expects full path with .api
},
]
: []),
// All other APIs - proxy to Console backend
{
context: ['/api', '/redpanda.api', '/auth', '/logout'],
target: process.env.PROXY_TARGET || 'http://localhost:9090',
changeOrigin: !!process.env.PROXY_TARGET,
secure: process.env.PROXY_TARGET ? false : undefined,
},
],
Comment on lines +56 to +80
Copy link
Copy Markdown
Contributor

@malinskibeniamin malinskibeniamin Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we be able to proxy both Console backend and the new Gateway API? I think currently if AI_GATEWAY_URL environment variable is set, then there is no proxy established for Console backend. Shouldn't we allow both at the same time?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think both works now 🤔 definitely works when I run it from local

},
source: {
define: {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const FEATURE_FLAGS = {
enableMcpServiceAccount: false,
enablePipelineServiceAccount: false,
enableTranscriptsInConsole: false,
enableApiKeyConfigurationAgent: false,
shadowlinkCloudUi: false,
enableNewTheme: false,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
type ServiceAccountSelectorRef,
} from 'components/ui/service-account/service-account-selector';
import { TagsFieldList } from 'components/ui/tag/tags-field-list';
import { isFeatureFlagEnabled } from 'config';
import { Loader2 } from 'lucide-react';
import { Scope } from 'protogen/redpanda/api/dataplane/v1/secret_pb';
import {
Expand All @@ -51,6 +52,7 @@ import {
import { useEffect, useMemo, useRef, useState } from 'react';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { useCreateAIAgentMutation } from 'react-query/api/ai-agent';
import { useListGatewaysQuery } from 'react-query/api/ai-gateway';
import { useListMCPServersQuery } from 'react-query/api/remote-mcp';
import { useCreateSecretMutation, useListSecretsQuery } from 'react-query/api/secret';
import { toast } from 'sonner';
Expand All @@ -72,6 +74,38 @@ export const AIAgentCreatePage = () => {
skipInvalidation: true,
});

// Feature flag: when true, use legacy API key mode (hardcoded providers)
const isLegacyApiKeyMode = isFeatureFlagEnabled('enableApiKeyConfigurationAgent');

// Gateway detection and list query (using v1 API from ai-gateway module)
// Only fetch when NOT in legacy mode
const { data: gatewaysData, isLoading: isLoadingGateways } = useListGatewaysQuery(
{},
{ enabled: !isLegacyApiKeyMode }
Comment thread
alenkacz marked this conversation as resolved.
);

const hasGatewayDeployed = useMemo(() => {
if (isLegacyApiKeyMode || isLoadingGateways) {
return false;
}
return Boolean(gatewaysData?.gateways && gatewaysData.gateways.length > 0);
}, [isLegacyApiKeyMode, gatewaysData, isLoadingGateways]);

const availableGateways = useMemo(() => {
if (isLegacyApiKeyMode || !gatewaysData?.gateways) {
return [];
}
return gatewaysData.gateways.map((gw) => {
// Extract gateway ID from name (format: "gateways/{gateway_id}")
const gatewayId = gw.name.split('/').pop() || gw.name;
return {
id: gatewayId,
displayName: gw.displayName,
description: gw.description,
};
});
}, [isLegacyApiKeyMode, gatewaysData]);

// Ref to ServiceAccountSelector to call createServiceAccount
const serviceAccountSelectorRef = useRef<ServiceAccountSelectorRef>(null);

Expand Down Expand Up @@ -107,6 +141,13 @@ export const AIAgentCreatePage = () => {
}
}, [displayName, form]);

// Auto-select first gateway when gateways are available (only if not in legacy mode)
useEffect(() => {
if (!isLegacyApiKeyMode && availableGateways.length > 0 && !form.getValues('gatewayId')) {
form.setValue('gatewayId', availableGateways[0].id);
}
}, [isLegacyApiKeyMode, availableGateways, form]);

const {
fields: tagFields,
append: appendTag,
Expand Down Expand Up @@ -294,7 +335,9 @@ export const AIAgentCreatePage = () => {
});

// Build provider configuration based on selected provider
const apiKeyRef = `\${secrets.${values.apiKeySecret}}`;
// When using gateway: api_key can be empty (proto has ignore = IGNORE_IF_ZERO_VALUE)
// When not using gateway: api_key must reference a secret
const apiKeyRef = values.apiKeySecret ? `\${secrets.${values.apiKeySecret}}` : '';
let providerConfig: AIAgent_Provider;

switch (values.provider) {
Expand Down Expand Up @@ -464,7 +507,7 @@ export const AIAgentCreatePage = () => {
</CardHeader>
<CardContent>
<LLMConfigSection
availableGateways={[]}
availableGateways={availableGateways}
availableSecrets={availableSecrets}
fieldNames={{
provider: 'provider',
Expand All @@ -475,7 +518,8 @@ export const AIAgentCreatePage = () => {
gatewayId: 'gatewayId',
}}
form={form}
hasGatewayDeployed={false}
hasGatewayDeployed={hasGatewayDeployed}
isLoadingGateways={isLoadingGateways}
mode="create"
scopes={[Scope.MCP_SERVER, Scope.AI_AGENT]}
showBaseUrl={form.watch('provider') === 'openaiCompatible'}
Expand Down
31 changes: 24 additions & 7 deletions frontend/src/components/pages/agents/create/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ export const FormSchema = z
{ message: 'Tags must have unique keys' }
),
triggerType: z.enum(['http', 'slack', 'kafka']).default('http'),
gatewayId: z
.string()
.refine(
(val) => !val || (val.length === 20 && /^[a-z0-9]+$/.test(val)),
'Gateway ID must be exactly 20 lowercase alphanumeric characters'
)
.optional()
.or(z.literal('')),
provider: z.enum(['openai', 'anthropic', 'google', 'openaiCompatible']).default('openai'),
apiKeySecret: z.string(),
model: z.string().min(1, 'Model is required'),
Expand Down Expand Up @@ -77,15 +85,24 @@ export const FormSchema = z
},
{ message: 'Subagent names must be unique' }
),
gatewayId: z
.string()
.length(20, 'Gateway ID must be exactly 20 characters')
.regex(/^[a-z0-9]+$/, 'Gateway ID must contain only lowercase letters and numbers')
.optional()
.or(z.literal('')),
})
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex validation logic with multiple conditional checks
.superRefine((data, ctx) => {
// Note: Gateway validation happens in the UI layer based on availability
// If gateways are available, gateway is required (enforced by UI)
// If gateways are NOT available, API key is required

const hasGateway = data.gatewayId && data.gatewayId.trim() !== '';

if (!hasGateway && (!data.apiKeySecret || data.apiKeySecret.trim() === '')) {
// No gateway selected: API Key is required
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'API Token is required',
path: ['apiKeySecret'],
});
}

if (data.provider === 'openaiCompatible') {
if (!data.baseUrl || data.baseUrl.trim() === '') {
ctx.addIssue({
Expand Down Expand Up @@ -131,6 +148,7 @@ export const initialValues: FormValues = {
description: '',
tags: [],
triggerType: 'http',
gatewayId: '',
provider: 'openai',
apiKeySecret: '',
model: '',
Expand All @@ -142,5 +160,4 @@ export const initialValues: FormValues = {
systemPrompt: '',
serviceAccountName: '',
subagents: [],
gatewayId: '',
};
Loading
Loading