Skip to content

Commit 29bef17

Browse files
authored
Merge pull request #1553 from mnfst/better-messages
fix(proxy): point friendly error URLs at real frontend routes
2 parents c4d03da + 68510a5 commit 29bef17

File tree

7 files changed

+68
-42
lines changed

7 files changed

+68
-42
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"manifest": patch
3+
---
4+
5+
Fix the "check your dashboard" links that Manifest embeds in `[🦚 Manifest] …` friendly error messages returned by the OpenAI-compatible proxy. Auth errors (missing / empty / invalid / expired / unrecognized key) used to emit `${baseUrl}/routing`, which 404'd — that path does not exist in the frontend router. They now point at the Workspace landing page. Agent-scoped errors used to drop the user on the agent Overview page; they now deep-link to the section that actually fixes the problem — "No API key set for X" and "Manifest is connected successfully, connect a provider" go to `/agents/:name/routing`, and "Usage limit hit" goes to `/agents/:name/limits`.

packages/backend/src/routing/proxy/__tests__/proxy-exception.filter.spec.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { ConfigService } from '@nestjs/config';
33
import { ArgumentsHost } from '@nestjs/common';
44
import { ProxyExceptionFilter } from '../proxy-exception.filter';
55

6-
function createMockHost(body: Record<string, unknown> = {}, ingestionCtx?: unknown) {
6+
function createMockHost(body: Record<string, unknown> = {}) {
77
const req: Record<string, unknown> = { body };
8-
if (ingestionCtx) req.ingestionContext = ingestionCtx;
98

109
const res = {
1110
setHeader: jest.fn(),
@@ -79,13 +78,14 @@ describe('ProxyExceptionFilter', () => {
7978
});
8079

8180
it('converts "API key expired" with dashboard URL', () => {
82-
const { host, res } = createMockHost({}, { agentName: 'my-agent' });
81+
const { host, res } = createMockHost();
8382
filter.catch(new UnauthorizedException('API key expired'), host);
8483

8584
expect(res.status).toHaveBeenCalledWith(200);
8685
const content = res.json.mock.calls[0][0].choices[0].message.content;
8786
expect(content).toContain('expired');
88-
expect(content).toContain('http://localhost:3001/agents/my-agent');
87+
expect(content).toContain('http://localhost:3001');
88+
expect(content).not.toContain('/routing');
8989
});
9090

9191
it('converts "Invalid API key" to friendly message', () => {
@@ -102,7 +102,8 @@ describe('ProxyExceptionFilter', () => {
102102
filter.catch(new UnauthorizedException('Invalid API key'), host);
103103

104104
const content = res.json.mock.calls[0][0].choices[0].message.content;
105-
expect(content).toContain('Dashboard: http://localhost:3001/routing');
105+
expect(content).toContain('Dashboard: http://localhost:3001');
106+
expect(content).not.toContain('Dashboard: http://localhost:3001/routing');
106107
});
107108
});
108109

packages/backend/src/routing/proxy/__tests__/proxy-friendly-response.spec.ts

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,37 @@ import {
88

99
describe('proxy-friendly-response', () => {
1010
describe('getDashboardUrl', () => {
11-
it('returns agent-specific URL when agentName provided', () => {
12-
const config = {
13-
get: jest.fn((key: string) => {
14-
if (key === 'app.betterAuthUrl') return 'https://app.manifest.build';
15-
return undefined;
16-
}),
17-
} as unknown as ConfigService;
18-
19-
expect(getDashboardUrl(config, 'my-agent')).toBe(
11+
const prodConfig = {
12+
get: jest.fn((key: string) => {
13+
if (key === 'app.betterAuthUrl') return 'https://app.manifest.build';
14+
return undefined;
15+
}),
16+
} as unknown as ConfigService;
17+
18+
it('returns agent Overview URL when agentName provided without section', () => {
19+
expect(getDashboardUrl(prodConfig, 'my-agent')).toBe(
2020
'https://app.manifest.build/agents/my-agent',
2121
);
2222
});
2323

24-
it('returns routing URL when no agentName', () => {
25-
const config = {
26-
get: jest.fn((key: string) => {
27-
if (key === 'app.betterAuthUrl') return 'https://app.manifest.build';
28-
return undefined;
29-
}),
30-
} as unknown as ConfigService;
24+
it('returns agent Routing URL when section is "routing"', () => {
25+
expect(getDashboardUrl(prodConfig, 'my-agent', 'routing')).toBe(
26+
'https://app.manifest.build/agents/my-agent/routing',
27+
);
28+
});
29+
30+
it('returns agent Limits URL when section is "limits"', () => {
31+
expect(getDashboardUrl(prodConfig, 'my-agent', 'limits')).toBe(
32+
'https://app.manifest.build/agents/my-agent/limits',
33+
);
34+
});
35+
36+
it('returns bare base URL (Workspace) when no agentName', () => {
37+
expect(getDashboardUrl(prodConfig)).toBe('https://app.manifest.build');
38+
});
3139

32-
expect(getDashboardUrl(config)).toBe('https://app.manifest.build/routing');
40+
it('ignores section when no agentName is supplied', () => {
41+
expect(getDashboardUrl(prodConfig, undefined, 'routing')).toBe('https://app.manifest.build');
3342
});
3443

3544
it('falls back to localhost when no betterAuthUrl configured', () => {
@@ -41,7 +50,9 @@ describe('proxy-friendly-response', () => {
4150
}),
4251
} as unknown as ConfigService;
4352

44-
expect(getDashboardUrl(config, 'demo')).toBe('http://localhost:4000/agents/demo');
53+
expect(getDashboardUrl(config, 'demo', 'routing')).toBe(
54+
'http://localhost:4000/agents/demo/routing',
55+
);
4556
});
4657

4758
it('encodes special characters in agent name', () => {
@@ -52,7 +63,9 @@ describe('proxy-friendly-response', () => {
5263
}),
5364
} as unknown as ConfigService;
5465

55-
expect(getDashboardUrl(config, 'my agent')).toBe('http://localhost:3001/agents/my%20agent');
66+
expect(getDashboardUrl(config, 'my agent', 'limits')).toBe(
67+
'http://localhost:3001/agents/my%20agent/limits',
68+
);
5669
});
5770
});
5871

packages/backend/src/routing/proxy/__tests__/proxy.service.spec.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ describe('ProxyService', () => {
313313
});
314314
});
315315

316-
it('includes dashboard URL with agent name in no-provider response', async () => {
316+
it('points no-provider response at the agent Routing page', async () => {
317317
resolveService.resolve.mockResolvedValue({
318318
tier: 'simple',
319319
model: null,
@@ -333,10 +333,10 @@ describe('ProxyService', () => {
333333

334334
const json = (await result.forward.response.json()) as Record<string, unknown>;
335335
const choices = json.choices as { message: { content: string } }[];
336-
expect(choices[0].message.content).toContain('http://localhost:3001/agents/my-agent');
336+
expect(choices[0].message.content).toContain('http://localhost:3001/agents/my-agent/routing');
337337
});
338338

339-
it('uses /routing path when agentName is not provided', async () => {
339+
it('uses bare base URL in no-provider response when agentName is missing', async () => {
340340
resolveService.resolve.mockResolvedValue({
341341
tier: 'simple',
342342
model: null,
@@ -355,8 +355,9 @@ describe('ProxyService', () => {
355355

356356
const json = (await result.forward.response.json()) as Record<string, unknown>;
357357
const choices = json.choices as { message: { content: string } }[];
358-
expect(choices[0].message.content).toContain('http://localhost:3001/routing');
359-
expect(choices[0].message.content).not.toContain('/routing/');
358+
expect(choices[0].message.content).toContain('http://localhost:3001');
359+
expect(choices[0].message.content).not.toContain('/routing');
360+
expect(choices[0].message.content).not.toContain('/agents/');
360361
});
361362

362363
it('falls back to localhost URL when betterAuthUrl is empty', async () => {
@@ -385,7 +386,7 @@ describe('ProxyService', () => {
385386

386387
const json = (await result.forward.response.json()) as Record<string, unknown>;
387388
const choices = json.choices as { message: { content: string } }[];
388-
expect(choices[0].message.content).toContain('http://localhost:4000/agents/test-agent');
389+
expect(choices[0].message.content).toContain('http://localhost:4000/agents/test-agent/routing');
389390
});
390391

391392
it('returns synthetic streaming response when no model is resolved', async () => {
@@ -438,7 +439,7 @@ describe('ProxyService', () => {
438439
choices: { message: { content: string } }[];
439440
};
440441
expect(json.choices[0].message.content).toContain('No API key set for OpenAI');
441-
expect(json.choices[0].message.content).toContain('/agents/my-agent');
442+
expect(json.choices[0].message.content).toContain('/agents/my-agent/routing');
442443
expect(result.meta.reason).toBe('no_provider_key');
443444
});
444445

@@ -1138,6 +1139,9 @@ describe('ProxyService', () => {
11381139
};
11391140
expect(json.choices[0].message.content).toContain('Usage limit hit');
11401141
expect(json.choices[0].message.content).toContain('tokens');
1142+
expect(json.choices[0].message.content).toContain(
1143+
'http://localhost:3001/agents/my-agent/limits',
1144+
);
11411145
expect(result.meta.reason).toBe('limit_exceeded');
11421146
});
11431147

packages/backend/src/routing/proxy/proxy-exception.filter.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { ExceptionFilter, Catch, ArgumentsHost, HttpException, Injectable } from
22
import { ConfigService } from '@nestjs/config';
33
import { Request, Response as ExpressResponse } from 'express';
44
import { getDashboardUrl, sendFriendlyResponse } from './proxy-friendly-response';
5-
import { IngestionContext } from '../../otlp/interfaces/ingestion-context.interface';
65

76
/** Guard-thrown messages that should become friendly chat responses. */
87
const AUTH_ERROR_MESSAGES: Record<string, string> = {
@@ -48,13 +47,10 @@ export class ProxyExceptionFilter implements ExceptionFilter {
4847
}
4948

5049
const isStream = (req.body as Record<string, unknown>)?.stream === true;
51-
const ingestionCtx = (req as Request & { ingestionContext?: IngestionContext })
52-
.ingestionContext;
53-
const agentName = ingestionCtx?.agentName;
5450

5551
const friendly = AUTH_ERROR_MESSAGES[message];
5652
if (friendly) {
57-
const dashboardUrl = getDashboardUrl(this.config, agentName);
53+
const dashboardUrl = getDashboardUrl(this.config);
5854
const content =
5955
message === 'API key expired'
6056
? `${friendly}: ${dashboardUrl}`

packages/backend/src/routing/proxy/proxy-friendly-response.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,19 @@ export interface FriendlyResult {
2121
};
2222
}
2323

24-
export function getDashboardUrl(config: ConfigService, agentName?: string): string {
24+
export type DashboardSection = 'routing' | 'limits';
25+
26+
export function getDashboardUrl(
27+
config: ConfigService,
28+
agentName?: string,
29+
section?: DashboardSection,
30+
): string {
2531
const baseUrl =
2632
config.get<string>('app.betterAuthUrl') ||
2733
`http://localhost:${config.get<number>('app.port', 3001)}`;
28-
const path = agentName ? `/agents/${encodeURIComponent(agentName)}` : '/routing';
29-
return `${baseUrl}${path}`;
34+
if (!agentName) return baseUrl;
35+
const suffix = section ? `/${section}` : '';
36+
return `${baseUrl}/agents/${encodeURIComponent(agentName)}${suffix}`;
3037
}
3138

3239
export function buildFriendlyResponse(

packages/backend/src/routing/proxy/proxy.service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export class ProxyService {
129129
resolved.auth_type,
130130
);
131131
if (apiKey === null) {
132-
const dashboardUrl = getDashboardUrl(this.config, agentName);
132+
const dashboardUrl = getDashboardUrl(this.config, agentName, 'routing');
133133
const content = `[🦚 Manifest] No API key set for ${resolved.provider} yet. Add one here: ${dashboardUrl}`;
134134
return buildFriendlyResponse(content, body.stream === true, 'no_provider_key');
135135
}
@@ -282,7 +282,7 @@ export class ProxyService {
282282
exceeded.metricType === 'cost'
283283
? `$${Number(exceeded.threshold).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
284284
: Number(exceeded.threshold).toLocaleString(undefined, { maximumFractionDigits: 0 });
285-
const dashboardUrl = getDashboardUrl(this.config, agentName);
285+
const dashboardUrl = getDashboardUrl(this.config, agentName, 'limits');
286286
return `[🦚 Manifest] Usage limit hit: ${exceeded.metricType} is at ${fmt} (limit: ${threshFmt}/${exceeded.period}). You can adjust it here: ${dashboardUrl}`;
287287
}
288288

@@ -305,7 +305,7 @@ export class ProxyService {
305305
}
306306

307307
private buildNoProviderResult(stream: boolean, agentName?: string): ProxyResult {
308-
const dashboardUrl = getDashboardUrl(this.config, agentName);
308+
const dashboardUrl = getDashboardUrl(this.config, agentName, 'routing');
309309
const content = `[🦚 Manifest] Manifest is connected successfully. To start routing requests, connect a model provider: ${dashboardUrl}`;
310310
return buildFriendlyResponse(content, stream, 'no_provider');
311311
}

0 commit comments

Comments
 (0)