Skip to content

Commit 485c74b

Browse files
authored
feat: support insight management (vllm-project#1579)
Signed-off-by: xunzhuo <xunzhuo@vllm-semantic-router.ai>
1 parent 20e3338 commit 485c74b

115 files changed

Lines changed: 8050 additions & 4231 deletions

File tree

Some content is hidden

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

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ This file is the short entrypoint for coding agents. The detailed human-readable
1616
- Full task routing, primary-skill resolution, local-rule surfacing, and validation planning still come from `make agent-report ENV=cpu|amd CHANGED_FILES="..."`.
1717
- `tools/agent/**` remains the canonical harness source; `.agents/skills/**` is only a discovery bridge.
1818

19-
If you need real AMD model deployment details instead of the minimal smoke path, also read [deploy/amd/README.md](deploy/amd/README.md) and [deploy/amd/config.yaml](deploy/amd/config.yaml).
19+
If you need real AMD model deployment details instead of the minimal smoke path, also read [deploy/amd/README.md](deploy/amd/README.md) and [deploy/recipes/balance.yaml](deploy/recipes/balance.yaml).
2020

2121
## Supported Environments
2222

config/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Inside canonical `config.yaml`:
1414
- `providers.models[]` holds concrete backend access details directly
1515
- `routing.modelCards[]` holds semantic model metadata, including optional `loras[]` catalogs for decision-level `lora_name` references
1616
- `global.router`, `global.services`, `global.stores`, `global.integrations`, and `global.model_catalog` expose router-wide overrides explicitly
17+
- embedding fallback tuning such as `global.model_catalog.embeddings.semantic.embedding_config.top_k` lives under the router-owned model catalog, not under individual signal rules
1718
- `global.router.config_source` selects the router's steady-state config source; the exhaustive reference uses `file`, while Kubernetes CRD reconciliation uses `kubernetes`
1819
- router-owned model-backed module config lives under `global.model_catalog.modules`
1920

config/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,7 @@ global:
13391339
target_dimension: 768
13401340
target_layer: 22
13411341
enable_soft_matching: true
1342+
top_k: 1
13421343
min_score_threshold: 0.45
13431344
system:
13441345
prompt_guard: models/mmbert32k-jailbreak-detector-merged

dashboard/frontend/e2e/layout-nav.spec.ts

Lines changed: 256 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,57 @@ const configResponse = {
6464
routing: {
6565
modelCards: [{ name: 'test-model' }],
6666
signals: {
67-
domains: [{ name: 'business', description: 'Business' }],
67+
domains: [
68+
{ name: 'business', description: 'Business' },
69+
{ name: 'economics', description: 'Economics' },
70+
],
71+
embeddings: [
72+
{
73+
name: 'business_analysis',
74+
threshold: 0.72,
75+
candidates: ['business'],
76+
aggregation_method: 'max',
77+
},
78+
],
79+
preferences: [
80+
{
81+
name: 'structured_delivery',
82+
threshold: 0.55,
83+
examples: ['Use a structured answer'],
84+
},
85+
],
86+
complexity: [
87+
{ name: 'general_reasoning:easy', threshold: 0.25 },
88+
{ name: 'general_reasoning:medium', threshold: 0.5 },
89+
],
6890
},
6991
decisions: [
92+
{
93+
name: 'medium_business',
94+
priority: 215,
95+
rules: {
96+
operator: 'AND',
97+
conditions: [
98+
{
99+
operator: 'OR',
100+
conditions: [
101+
{ type: 'domain', name: 'business' },
102+
{ type: 'domain', name: 'economics' },
103+
],
104+
},
105+
{
106+
operator: 'OR',
107+
conditions: [
108+
{ type: 'embedding', name: 'business_analysis' },
109+
{ type: 'complexity', name: 'general_reasoning:easy' },
110+
{ type: 'complexity', name: 'general_reasoning:medium' },
111+
{ type: 'preference', name: 'structured_delivery' },
112+
],
113+
},
114+
],
115+
},
116+
modelRefs: [{ model: 'test-model', use_reasoning: true, reasoning_effort: 'medium' }],
117+
},
70118
{
71119
name: 'business-route',
72120
priority: 1,
@@ -112,6 +160,109 @@ const statusResponse = {
112160
},
113161
};
114162

163+
const replayRecordsResponse = {
164+
object: 'router_replay.list',
165+
count: 2,
166+
total: 2,
167+
limit: 100,
168+
offset: 0,
169+
has_more: false,
170+
data: [
171+
{
172+
id: 'replay-1',
173+
timestamp: '2026-03-17T10:00:00Z',
174+
request_id: 'req-1',
175+
decision: 'business-route',
176+
original_model: 'test-model',
177+
selected_model: 'test-model',
178+
reasoning_mode: 'off',
179+
selection_method: 'static',
180+
signals: {
181+
domain: ['business'],
182+
keyword: ['finance'],
183+
},
184+
response_status: 200,
185+
from_cache: false,
186+
streaming: false,
187+
prompt_tokens: 1000,
188+
completion_tokens: 500,
189+
total_tokens: 1500,
190+
actual_cost: 0.002,
191+
baseline_cost: 0.008,
192+
cost_savings: 0.006,
193+
currency: 'USD',
194+
baseline_model: 'premium-model',
195+
},
196+
{
197+
id: 'replay-2',
198+
timestamp: '2026-03-17T10:05:00Z',
199+
request_id: 'req-2',
200+
decision: 'business-route',
201+
original_model: 'test-model',
202+
selected_model: 'test-model',
203+
reasoning_mode: 'on',
204+
selection_method: 'static',
205+
signals: {
206+
domain: ['business'],
207+
preference: ['low-latency'],
208+
},
209+
response_status: 200,
210+
from_cache: true,
211+
streaming: true,
212+
},
213+
],
214+
};
215+
216+
const replayAggregateResponse = {
217+
object: 'router_replay.aggregate',
218+
record_count: 2,
219+
summary: {
220+
total_saved: 0.006,
221+
baseline_spend: 0.008,
222+
actual_spend: 0.002,
223+
currency: 'USD',
224+
cost_record_count: 1,
225+
excluded_record_count: 1,
226+
},
227+
model_selection: [
228+
{ name: 'test-model', value: 2 },
229+
],
230+
decision_distribution: [
231+
{ name: 'business-route', value: 2 },
232+
],
233+
signal_distribution: [
234+
{ name: 'domain', value: 2 },
235+
{ name: 'keyword', value: 1 },
236+
{ name: 'preference', value: 1 },
237+
],
238+
token_volume: {
239+
input_tokens: 1000,
240+
output_tokens: 500,
241+
total_tokens: 1500,
242+
excluded_record_count: 1,
243+
},
244+
token_breakdown: {
245+
by_decision: [
246+
{
247+
name: 'business-route',
248+
input_tokens: 1000,
249+
output_tokens: 500,
250+
total_tokens: 1500,
251+
},
252+
],
253+
by_selected_model: [
254+
{
255+
name: 'test-model',
256+
input_tokens: 1000,
257+
output_tokens: 500,
258+
total_tokens: 1500,
259+
},
260+
],
261+
},
262+
available_decisions: ['business-route'],
263+
available_models: ['test-model'],
264+
};
265+
115266
async function mockCommon(
116267
page: Page,
117268
options: {
@@ -173,10 +324,46 @@ async function mockCommon(
173324
body: JSON.stringify({ canRegister: false }),
174325
});
175326
});
327+
328+
await page.route(/\/api\/router\/v1\/router_replay(?:\?.*)?$/, async route => {
329+
await route.fulfill({
330+
status: 200,
331+
contentType: 'application/json',
332+
body: JSON.stringify(replayRecordsResponse),
333+
});
334+
});
335+
336+
await page.route(/\/api\/router\/v1\/router_replay\/aggregate(?:\?.*)?$/, async route => {
337+
await route.fulfill({
338+
status: 200,
339+
contentType: 'application/json',
340+
body: JSON.stringify(replayAggregateResponse),
341+
});
342+
});
343+
344+
await page.route('**/api/router/v1/router_replay/*', async route => {
345+
const requestURL = new URL(route.request().url());
346+
const replayID = requestURL.pathname.split('/').pop();
347+
if (replayID === 'aggregate') {
348+
await route.fulfill({
349+
status: 200,
350+
contentType: 'application/json',
351+
body: JSON.stringify(replayAggregateResponse),
352+
});
353+
return;
354+
}
355+
const record = replayRecordsResponse.data.find(item => item.id === replayID);
356+
357+
await route.fulfill({
358+
status: record ? 200 : 404,
359+
contentType: 'application/json',
360+
body: JSON.stringify(record ?? { error: { message: 'not found' } }),
361+
});
362+
});
176363
}
177364

178365
test.describe('Layout top navigation', () => {
179-
test('moves ClawOS into the secondary group and merges analysis and operations', async ({ page }) => {
366+
test('keeps Insight in primary nav and moves Manager beside System on the right', async ({ page }) => {
180367
await page.setViewportSize({ width: 1440, height: 900 });
181368
await mockCommon(page);
182369

@@ -190,13 +377,18 @@ test.describe('Layout top navigation', () => {
190377
await expect(primaryGroup.getByRole('link', { name: 'Playground' })).toBeVisible();
191378
await expect(primaryGroup.getByRole('link', { name: 'Brain' })).toBeVisible();
192379
await expect(primaryGroup.getByRole('link', { name: 'DSL' })).toBeVisible();
193-
await expect(primaryGroup.getByRole('button', { name: 'Manager' })).toBeVisible();
380+
await expect(primaryGroup.getByRole('link', { name: 'Insight' })).toBeVisible();
381+
await expect(primaryGroup.getByRole('button', { name: 'Manager' })).toHaveCount(0);
194382
await expect(primaryGroup.getByRole('link', { name: 'ClawOS' })).toHaveCount(0);
195383
await expect(primaryGroup.getByRole('link', { name: 'Users' })).toHaveCount(0);
196384

197385
await expect(secondaryGroup.getByRole('link', { name: 'Users' })).toBeVisible();
198386
await expect(secondaryGroup.getByRole('link', { name: 'ClawOS' })).toBeVisible();
387+
await expect(secondaryGroup.getByRole('button', { name: 'Manager' })).toBeVisible();
199388
await expect(secondaryGroup.getByRole('button', { name: 'System' })).toBeVisible();
389+
const secondaryButtons = secondaryGroup.getByRole('button');
390+
await expect(secondaryButtons.nth(0)).toHaveText(/Manager/);
391+
await expect(secondaryButtons.nth(1)).toHaveText(/System/);
200392
await expect(globalNav.getByRole('button', { name: 'Analysis', exact: true })).toHaveCount(0);
201393
await expect(globalNav.getByRole('button', { name: 'Operations', exact: true })).toHaveCount(0);
202394

@@ -207,9 +399,10 @@ test.describe('Layout top navigation', () => {
207399
const menuItems = menu.getByRole('menuitem');
208400
await expect(menuItems.nth(0)).toHaveText('Global Config');
209401
await expect(menuItems.nth(1)).toHaveText('Evaluation');
402+
await expect(menuItems.nth(2)).toHaveText('Ratings');
210403
await expect(menu.getByRole('menuitem', { name: 'Global Config' })).toBeVisible();
211404
await expect(menu.getByRole('menuitem', { name: 'Evaluation' })).toBeVisible();
212-
await expect(menu.getByRole('menuitem', { name: 'Replay' })).toBeVisible();
405+
await expect(menu.getByRole('menuitem', { name: 'Replay' })).toHaveCount(0);
213406
await expect(menu.getByRole('menuitem', { name: 'Ratings' })).toBeVisible();
214407
await expect(menu.getByText('Operations')).toBeVisible();
215408
await expect(menu.getByRole('menuitem', { name: 'ML Setup' })).toBeVisible();
@@ -220,6 +413,65 @@ test.describe('Layout top navigation', () => {
220413
await expect(menu.getByRole('menuitem', { name: 'Tracing' })).toBeVisible();
221414
});
222415

416+
test('loads Insights charts and does not preserve the legacy replay route', async ({ page }) => {
417+
await page.setViewportSize({ width: 1440, height: 900 });
418+
await mockCommon(page);
419+
420+
await page.goto('/dashboard');
421+
await page.getByRole('link', { name: 'Insight' }).click();
422+
423+
await expect(page).toHaveURL(/\/insights$/);
424+
await expect(page.getByRole('heading', { name: 'Insights', exact: true })).toBeVisible();
425+
await expect(page.getByText('Total Saved')).toBeVisible();
426+
await expect(page.getByText('Saved %')).toBeVisible();
427+
await expect(page.getByText('Baseline Spend')).toBeVisible();
428+
await expect(page.getByText('Actual Spend')).toBeVisible();
429+
await expect(page.getByText('See what the router picked, what signals fired, and how much it saved.')).toBeVisible();
430+
await expect(page.getByText('Decisions, model picks, token usage, and savings in one view.')).toBeVisible();
431+
await expect(page.getByText('Replay-backed routing records with spend, savings, and token details per request.')).toBeVisible();
432+
await expect(page.getByRole('heading', { name: 'Model Selection', exact: true })).toBeVisible();
433+
await expect(page.getByRole('heading', { name: 'Decision Distribution', exact: true })).toBeVisible();
434+
await expect(page.getByRole('heading', { name: 'Signal Distribution', exact: true })).toBeVisible();
435+
await expect(page.getByRole('heading', { name: 'Token Volume', exact: true })).toBeVisible();
436+
await expect(page.getByRole('heading', { name: 'Tokens by Decision', exact: true })).toBeVisible();
437+
await expect(page.getByRole('heading', { name: 'Tokens by Selected Model', exact: true })).toBeVisible();
438+
await expect(page.getByText('Input Tokens').first()).toBeVisible();
439+
await expect(page.getByText('Output Tokens').first()).toBeVisible();
440+
await expect(page.getByText('Total Tokens').first()).toBeVisible();
441+
await expect(page.getByText('1 filtered record excluded from cost totals because usage or pricing data is incomplete.')).toBeVisible();
442+
await expect(page.getByText('1 filtered record excluded from token totals because usage data is incomplete.')).toBeVisible();
443+
await expect(
444+
page.getByRole('article').filter({ hasText: 'Total Saved' }).getByRole('strong'),
445+
).toHaveText('$0.0060');
446+
await expect(
447+
page.getByRole('article').filter({ hasText: 'Saved %' }).getByRole('strong'),
448+
).toHaveText('75.0%');
449+
await expect(page.getByRole('columnheader', { name: 'Actual Cost' })).toBeVisible();
450+
await expect(page.getByRole('columnheader', { name: 'Saved vs Baseline' })).toBeVisible();
451+
452+
await page.goto('/replay');
453+
await expect(page).toHaveURL(/\/dashboard$/);
454+
});
455+
456+
test('renders nested canonical decision groups in Brain topology', async ({ page }) => {
457+
await page.setViewportSize({ width: 1440, height: 900 });
458+
await mockCommon(page);
459+
460+
await page.goto('/topology');
461+
462+
const decisionNode = page
463+
.locator('[class*="decisionNode"]')
464+
.filter({ has: page.getByText('medium_business', { exact: true }) })
465+
.first();
466+
467+
await expect(decisionNode).toBeVisible();
468+
await expect(decisionNode).toContainText('AND');
469+
await expect(decisionNode).toContainText('OR: domain: business | domain: economics');
470+
await expect(decisionNode).toContainText('OR: embedding: business_analysis');
471+
await expect(decisionNode).toContainText('preference: structured_delivery');
472+
await expect(decisionNode.getByText('Referenced signals not configured')).toHaveCount(0);
473+
});
474+
223475
test('hides ML Setup for read users and redirects direct access back to the dashboard', async ({ page }) => {
224476
await page.setViewportSize({ width: 1440, height: 900 });
225477
await mockCommon(page, { user: readUser });

dashboard/frontend/e2e/setup-wizard.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ test.describe("Setup wizard routing import", () => {
174174
page,
175175
}) => {
176176
const remoteURL =
177-
"https://raw.githubusercontent.com/vllm-project/semantic-router/main/deploy/amd/config.yaml";
177+
"https://raw.githubusercontent.com/vllm-project/semantic-router/main/deploy/recipes/balance.yaml";
178178

179179
let importPayload: Record<string, unknown> | null = null;
180180
let validatePayload: Record<string, unknown> | null = null;

dashboard/frontend/src/App.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ import TopologyPage from './pages/TopologyPage'
1818
import TracingPage from './pages/TracingPage'
1919
import StatusPage from './pages/StatusPage'
2020
import LogsPage from './pages/LogsPage'
21-
import ReplayPage from './pages/ReplayPage'
2221
import EvaluationPage from './pages/EvaluationPage'
2322
import MLSetupPage from './pages/MLSetupPage'
2423
import RatingsPage from './pages/RatingsPage'
2524
import BuilderPage from './pages/BuilderPage'
2625
import DashboardPage from './pages/DashboardPage'
2726
import OpenClawPage from './pages/OpenClawPage'
2827
import UsersPage from './pages/UsersPage'
28+
import InsightsPage from './pages/InsightsPage'
2929
import { ConfigSection } from './components/ConfigNav'
3030
import { ReadonlyProvider } from './contexts/ReadonlyContext'
3131
import { SetupProvider, useSetup } from './contexts/SetupContext'
@@ -315,13 +315,13 @@ const AppRouter: React.FC = () => {
315315
}
316316
/>
317317
<Route
318-
path="/replay"
318+
path="/insights"
319319
element={
320320
<Layout
321321
configSection={configSection}
322322
onConfigSectionChange={(section) => setConfigSection(section as ConfigSection)}
323323
>
324-
<ReplayPage />
324+
<InsightsPage />
325325
</Layout>
326326
}
327327
/>

0 commit comments

Comments
 (0)