1
0
forked from boos/BoosAPI

Merge pull request #223 from cita-777/codex/proxy-log-self-report-client-name

fix: prefer self-reported proxy log client names
This commit is contained in:
Cita
2026-03-21 11:52:48 +08:00
committed by GitHub
3 changed files with 121 additions and 2 deletions
@@ -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;