forked from boos/BoosAPI
fix: prefer self-reported proxy log client names
This commit is contained in:
@@ -110,6 +110,21 @@ describe('detectDownstreamClientContext', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prefers explicit self-reported client names before protocol-family fallbacks', () => {
|
||||||
|
expect(detectDownstreamClientContext({
|
||||||
|
downstreamPath: '/v1/responses',
|
||||||
|
headers: {
|
||||||
|
'openai-beta': 'responses-2025-03-11',
|
||||||
|
'x-openai-client-user-agent': '{"client":"openclaw"}',
|
||||||
|
},
|
||||||
|
})).toEqual({
|
||||||
|
clientKind: 'codex',
|
||||||
|
clientAppId: 'openclaw',
|
||||||
|
clientAppName: 'openclaw',
|
||||||
|
clientConfidence: 'exact',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('recognizes Claude Code requests from metadata.user_id without mutating the body', () => {
|
it('recognizes Claude Code requests from metadata.user_id without mutating the body', () => {
|
||||||
const body = {
|
const body = {
|
||||||
model: 'claude-opus-4-6',
|
model: 'claude-opus-4-6',
|
||||||
|
|||||||
@@ -142,6 +142,38 @@ function headerMatchesPrefixes(headers: NormalizedClientHeaders, key: string, pr
|
|||||||
return prefixes.some((prefix) => headerIncludes(headers, key, prefix));
|
return prefixes.some((prefix) => headerIncludes(headers, key, prefix));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeClientDisplayName(value: string): string | null {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
return trimmed.length <= 120 ? trimmed : trimmed.slice(0, 120).trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeClientAppId(value: string): string | null {
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
return normalized || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseExplicitClientSelfReportValue(value: string): string | null {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (isRecord(parsed)) {
|
||||||
|
for (const key of ['client', 'name', 'app']) {
|
||||||
|
const raw = parsed[key];
|
||||||
|
if (typeof raw !== 'string') continue;
|
||||||
|
const normalized = normalizeClientDisplayName(raw);
|
||||||
|
if (normalized) return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return normalizeClientDisplayName(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function buildBodySummary(body: unknown): DownstreamClientBodySummary {
|
function buildBodySummary(body: unknown): DownstreamClientBodySummary {
|
||||||
if (!isRecord(body)) {
|
if (!isRecord(body)) {
|
||||||
return {
|
return {
|
||||||
@@ -220,6 +252,20 @@ function detectDownstreamClientFingerprint(input: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectExplicitClientSelfReport(headers: NormalizedClientHeaders): DownstreamProtocolClientApp | null {
|
||||||
|
for (const value of headers['x-openai-client-user-agent'] || []) {
|
||||||
|
const clientAppName = parseExplicitClientSelfReportValue(value);
|
||||||
|
if (!clientAppName) continue;
|
||||||
|
return {
|
||||||
|
clientAppId: normalizeClientAppId(clientAppName) || 'self_reported_client',
|
||||||
|
clientAppName,
|
||||||
|
clientConfidence: 'exact',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function detectCodexOfficialClientApp(headers: NormalizedClientHeaders): DownstreamProtocolClientApp | null {
|
function detectCodexOfficialClientApp(headers: NormalizedClientHeaders): DownstreamProtocolClientApp | null {
|
||||||
for (const rule of codexOfficialClientAppRules) {
|
for (const rule of codexOfficialClientAppRules) {
|
||||||
const matchesOriginator = Array.isArray(rule.originatorPrefixes)
|
const matchesOriginator = Array.isArray(rule.originatorPrefixes)
|
||||||
@@ -283,8 +329,10 @@ export function detectDownstreamClientContext(input: {
|
|||||||
body?: unknown;
|
body?: unknown;
|
||||||
}): DownstreamClientContext {
|
}): DownstreamClientContext {
|
||||||
const detected = detectCliProfile(input);
|
const detected = detectCliProfile(input);
|
||||||
|
const normalizedHeaders = normalizeHeaders(input.headers);
|
||||||
|
const explicitSelfReport = detectExplicitClientSelfReport(normalizedHeaders);
|
||||||
const fingerprint = detectDownstreamClientFingerprint(input);
|
const fingerprint = detectDownstreamClientFingerprint(input);
|
||||||
const protocolClientApp = fingerprint ? null : detectProtocolClientApp({
|
const protocolClientApp = fingerprint || explicitSelfReport ? null : detectProtocolClientApp({
|
||||||
clientKind: detected.id,
|
clientKind: detected.id,
|
||||||
headers: input.headers,
|
headers: input.headers,
|
||||||
});
|
});
|
||||||
@@ -292,6 +340,6 @@ export function detectDownstreamClientContext(input: {
|
|||||||
clientKind: detected.id,
|
clientKind: detected.id,
|
||||||
...(detected.sessionId ? { sessionId: detected.sessionId } : {}),
|
...(detected.sessionId ? { sessionId: detected.sessionId } : {}),
|
||||||
...(detected.traceHint ? { traceHint: detected.traceHint } : {}),
|
...(detected.traceHint ? { traceHint: detected.traceHint } : {}),
|
||||||
...(fingerprint || protocolClientApp || {}),
|
...(explicitSelfReport || fingerprint || protocolClientApp || {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,62 @@ describe('ProxyLogs server-driven page', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders explicit client self-reports before protocol-family fallback labels', async () => {
|
||||||
|
apiMock.getProxyLogs.mockResolvedValue(buildListResponse({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
createdAt: '2026-03-09 16:00:00',
|
||||||
|
modelRequested: 'gpt-4o',
|
||||||
|
modelActual: 'gpt-4o',
|
||||||
|
status: 'success',
|
||||||
|
latencyMs: 120,
|
||||||
|
promptTokens: 10,
|
||||||
|
completionTokens: 5,
|
||||||
|
totalTokens: 15,
|
||||||
|
retryCount: 0,
|
||||||
|
estimatedCost: 1.23,
|
||||||
|
errorMessage: 'downstream: /v1/responses upstream: /v1/responses',
|
||||||
|
username: 'tester',
|
||||||
|
siteName: 'main-site',
|
||||||
|
siteUrl: 'https://main-site.example.com',
|
||||||
|
clientFamily: 'codex',
|
||||||
|
clientAppId: 'openclaw',
|
||||||
|
clientAppName: 'openclaw',
|
||||||
|
clientConfidence: 'exact',
|
||||||
|
downstreamKeyName: '移动端灰度',
|
||||||
|
downstreamKeyGroupName: '项目A',
|
||||||
|
downstreamKeyTags: ['VIP', '灰度'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
let root: ReturnType<typeof create> | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await act(async () => {
|
||||||
|
root = create(
|
||||||
|
<MemoryRouter initialEntries={['/logs']}>
|
||||||
|
<ToastProvider>
|
||||||
|
<ProxyLogs />
|
||||||
|
</ToastProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
const row = root!.root.find((node) => (
|
||||||
|
node.type === 'tr' && node.props['data-testid'] === 'proxy-log-row-101'
|
||||||
|
));
|
||||||
|
const rowText = collectText(row);
|
||||||
|
expect(rowText).toContain('openclaw');
|
||||||
|
expect(rowText).toContain('Codex');
|
||||||
|
expect(rowText).not.toContain('推测');
|
||||||
|
} finally {
|
||||||
|
root?.unmount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('re-queries the server for status, client, and search changes instead of filtering locally', async () => {
|
it('re-queries the server for status, client, and search changes instead of filtering locally', async () => {
|
||||||
let root: ReturnType<typeof create> | null = null;
|
let root: ReturnType<typeof create> | null = null;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user