Skip to content

Commit 9a923f9

Browse files
feat: add generic provider discovery APIs (kaito-project#283)
Signed-off-by: Sertac Ozercan <sozercan@gmail.com> Co-authored-by: Robbie Cronin <robert.owen.cronin@gmail.com>
1 parent 4ccbe08 commit 9a923f9

47 files changed

Lines changed: 2503 additions & 421 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/e2e-dynamo-mocker.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
echo "=== Dynamo Provider Logs ==="
8181
kubectl logs -n airunway-system -l control-plane=dynamo-provider --tail=200
8282
echo "=== Dynamo Operator Logs ==="
83-
kubectl logs -n dynamo-system --all-containers --tail=200 --prefix
83+
kubectl logs -n dynamo-system --all-containers --tail=200 --prefix 2>/dev/null || echo "No Dynamo operator logs"
8484
echo "=== Events ==="
8585
kubectl get events -A --sort-by=.lastTimestamp
8686
echo "=== Pods ==="

agents.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,10 @@ Unified API for deploying ML models. Key fields:
115115

116116
### InferenceProviderConfig
117117
Cluster-scoped resource for provider registration:
118-
- `spec.capabilities.engines` - Supported inference engines
119-
- `spec.capabilities.servingModes` - Supported serving modes
120-
- `spec.capabilities.gpuSupport/cpuSupport` - Hardware support
118+
- `spec.capabilities.engines` - Authoritative provider capabilities used by controller selection, webhook compatibility checks, and gateway behavior
119+
- `spec.capabilities.engines[].servingModes` - Supported serving modes per engine
120+
- `spec.capabilities.engines[].gpuSupport/cpuSupport` - Hardware support per engine
121+
- `airunway.ai/capabilities` annotation - Optional compatibility mirror for dashboard/provider discovery clients
121122
- `spec.selectionRules` - CEL expressions for auto-selection
122123
- `status.ready` - Provider health status
123124

backend/src/hono-app.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,18 @@ describe('Hono Routes', () => {
13641364
expect(data.error.message).toBe('Authentication required');
13651365
});
13661366

1367+
test('provider detail routes require auth when AUTH_ENABLED=true', async () => {
1368+
process.env.AUTH_ENABLED = 'true';
1369+
1370+
for (const path of ['/api/settings/providers/kaito', '/api/providers/kaito']) {
1371+
const res = await app.request(path);
1372+
expect(res.status).toBe(401);
1373+
expect(res.headers.get(AIRUNWAY_AUTH_ERROR_HEADER)).toBe('true');
1374+
const data = await res.json();
1375+
expect(data.error.message).toBe('Authentication required');
1376+
}
1377+
});
1378+
13671379
test('invalid bearer token returns 401', async () => {
13681380
process.env.AUTH_ENABLED = 'true';
13691381

backend/src/hono-app.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
health,
2222
models,
2323
settings,
24+
providers,
2425
deployments,
2526
installation,
2627
oauth,
@@ -130,19 +131,24 @@ app.use('*', async (c, next) => {
130131
// Auth Middleware
131132
// ============================================================================
132133

133-
// Routes that don't require authentication
134-
// Keep this list minimal — only routes needed before login
135-
const PUBLIC_ROUTES = [
136-
'/api/cluster/status',
137-
'/api/settings', // Settings is public (read-only auth config needed by frontend)
138-
'/api/oauth', // OAuth routes must be public for initial authentication
139-
];
140-
141-
// Public routes that must match exactly (no sub-path matching)
134+
// Routes that don't require authentication. Keep this list minimal — only
135+
// routes needed before login, and avoid prefix-whitelisting provider detail
136+
// endpoints because they include install metadata and chart values.
142137
const PUBLIC_ROUTES_EXACT = [
143138
'/api/health',
144139
'/api/health/',
145140
'/api/health/version',
141+
'/api/cluster/status',
142+
'/api/settings',
143+
'/api/settings/',
144+
'/api/settings/providers',
145+
'/api/settings/providers/',
146+
'/api/providers',
147+
'/api/providers/',
148+
];
149+
150+
const PUBLIC_ROUTE_PREFIXES = [
151+
'/api/oauth', // OAuth routes must be public for initial authentication
146152
];
147153

148154
// Auth middleware for protected API routes
@@ -158,8 +164,8 @@ app.use('/api/*', async (c, next) => {
158164
return next();
159165
}
160166

161-
// Skip auth for prefix-match public routes (cluster/status, settings, oauth)
162-
if (PUBLIC_ROUTES.some(route => path === route || path.startsWith(route + '/'))) {
167+
// Skip auth for prefix-match public routes (OAuth callback/token flow).
168+
if (PUBLIC_ROUTE_PREFIXES.some(route => path === route || path.startsWith(route + '/'))) {
163169
return next();
164170
}
165171

@@ -200,6 +206,7 @@ app.route('/api/health', health);
200206
app.route('/api/cluster', health);
201207
app.route('/api/models', models);
202208
app.route('/api/settings', settings);
209+
app.route('/api/providers', providers);
203210
app.route('/api/deployments', deployments);
204211
app.route('/api/installation', installation);
205212
app.route('/api/oauth', oauth);

backend/src/lib/providers.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, test } from 'bun:test';
22
import {
33
aggregateRequiresCRDFromCapabilities,
4+
extractProviderInfo,
45
getProviderDisplayName,
56
providerRequiresRuntimeCRD,
67
} from './providers';
@@ -44,6 +45,24 @@ describe('provider metadata helpers', () => {
4445
expect(providerRequiresRuntimeCRD('custom-provider', true, 'Custom Provider')).toBe(true);
4546
});
4647

48+
test('derives requiresCRD from annotation capabilities when spec capabilities are empty', () => {
49+
const provider = extractProviderInfo({
50+
metadata: {
51+
name: 'custom-native-provider',
52+
annotations: {
53+
'airunway.ai/provider-name': 'Custom Native Provider',
54+
'airunway.ai/capabilities': JSON.stringify({
55+
engines: [{ name: 'vllm', servingModes: ['aggregated'], requiresCRD: false }],
56+
}),
57+
},
58+
},
59+
spec: { capabilities: {} },
60+
});
61+
62+
expect(provider.requiresCRD).toBe(false);
63+
expect(provider.capabilities?.engines).toEqual(['vllm']);
64+
});
65+
4766
test('defaults operator-backed providers to requiring runtime CRDs', () => {
4867
expect(providerRequiresRuntimeCRD('dynamo')).toBe(true);
4968
expect(providerRequiresRuntimeCRD('kaito')).toBe(true);

0 commit comments

Comments
 (0)