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', () => {
|
||||
const body = {
|
||||
model: 'claude-opus-4-6',
|
||||
|
||||
@@ -142,6 +142,38 @@ function headerMatchesPrefixes(headers: NormalizedClientHeaders, key: string, pr
|
||||
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 {
|
||||
if (!isRecord(body)) {
|
||||
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 {
|
||||
for (const rule of codexOfficialClientAppRules) {
|
||||
const matchesOriginator = Array.isArray(rule.originatorPrefixes)
|
||||
@@ -283,8 +329,10 @@ export function detectDownstreamClientContext(input: {
|
||||
body?: unknown;
|
||||
}): DownstreamClientContext {
|
||||
const detected = detectCliProfile(input);
|
||||
const normalizedHeaders = normalizeHeaders(input.headers);
|
||||
const explicitSelfReport = detectExplicitClientSelfReport(normalizedHeaders);
|
||||
const fingerprint = detectDownstreamClientFingerprint(input);
|
||||
const protocolClientApp = fingerprint ? null : detectProtocolClientApp({
|
||||
const protocolClientApp = fingerprint || explicitSelfReport ? null : detectProtocolClientApp({
|
||||
clientKind: detected.id,
|
||||
headers: input.headers,
|
||||
});
|
||||
@@ -292,6 +340,6 @@ export function detectDownstreamClientContext(input: {
|
||||
clientKind: detected.id,
|
||||
...(detected.sessionId ? { sessionId: detected.sessionId } : {}),
|
||||
...(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 () => {
|
||||
let root: ReturnType<typeof create> | null = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user