feat: add proxy log client detection filters

This commit is contained in:
cita
2026-03-20 12:25:30 +08:00
parent 2de5eebf29
commit 4e60290838
34 changed files with 3511 additions and 266 deletions
+23 -43
View File
@@ -1,7 +1,6 @@
import { TextDecoder } from 'node:util';
import type { FastifyRequest, FastifyReply } from 'fastify';
import { tokenRouter } from '../../services/tokenRouter.js';
import { db, hasProxyLogDownstreamApiKeyIdColumn, schema } from '../../db/index.js';
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
@@ -49,9 +48,9 @@ import {
unwrapGeminiCliPayload,
} from '../../routes/proxy/geminiCliCompat.js';
import { dispatchRuntimeRequest } from '../../routes/proxy/runtimeExecutor.js';
import { detectCliProfile } from '../cliProfiles/registry.js';
import type { CliProfileId } from '../cliProfiles/types.js';
import { summarizeConversationFileInputsInOpenAiBody } from '../capabilities/conversationFileCapabilities.js';
import { detectDownstreamClientContext, type DownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
import { insertProxyLog } from '../../services/proxyLogStore.js';
const MAX_RETRIES = 2;
@@ -63,25 +62,6 @@ function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
type DownstreamClientContext = {
clientKind: CliProfileId;
sessionId?: string;
traceHint?: string;
};
function detectDownstreamClientContext(input: {
downstreamPath: string;
headers?: Record<string, unknown>;
body?: unknown;
}): DownstreamClientContext {
const detected = detectCliProfile(input);
return {
clientKind: detected.id,
...(detected.sessionId ? { sessionId: detected.sessionId } : {}),
...(detected.traceHint ? { traceHint: detected.traceHint } : {}),
};
}
export async function handleChatSurfaceRequest(
request: FastifyRequest,
reply: FastifyReply,
@@ -131,8 +111,6 @@ export async function handleChatSurfaceRequest(
proxyToken: getProxyAuthContext(request)?.token || null,
});
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
const logDownstreamApiKeyId = downstreamApiKeyId !== null
&& await hasProxyLogDownstreamApiKeyIdColumn();
const excludeChannelIds: number[] = [];
let retryCount = 0;
@@ -297,7 +275,7 @@ export async function handleChatSurfaceRequest(
null,
null,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
},
});
@@ -323,7 +301,7 @@ export async function handleChatSurfaceRequest(
null,
null,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
await recordOauthQuotaResetHint({
accountId: selected.account.id,
@@ -426,7 +404,7 @@ export async function handleChatSurfaceRequest(
null,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
return;
}
@@ -462,7 +440,7 @@ export async function handleChatSurfaceRequest(
null,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
if (shouldRetryProxyRequest(failure.status, failure.reason) && retryCount < MAX_RETRIES) {
@@ -500,7 +478,7 @@ export async function handleChatSurfaceRequest(
null,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
return;
}
@@ -550,7 +528,7 @@ export async function handleChatSurfaceRequest(
null,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
return;
}
@@ -604,7 +582,7 @@ export async function handleChatSurfaceRequest(
billingDetails,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
return;
}
@@ -654,7 +632,7 @@ export async function handleChatSurfaceRequest(
null,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
if (shouldRetryProxyRequest(failure.status, failure.reason) && retryCount < MAX_RETRIES) {
@@ -716,7 +694,7 @@ export async function handleChatSurfaceRequest(
billingDetails,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
return reply.send(downstreamResponse);
@@ -738,7 +716,7 @@ export async function handleChatSurfaceRequest(
null,
null,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
if (retryCount < MAX_RETRIES) {
@@ -817,8 +795,6 @@ export async function handleClaudeCountTokensSurfaceRequest(
});
const downstreamPolicy = getDownstreamRoutingPolicy(request);
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
const logDownstreamApiKeyId = downstreamApiKeyId !== null
&& await hasProxyLogDownstreamApiKeyIdColumn();
const excludeChannelIds: number[] = [];
let retryCount = 0;
@@ -942,7 +918,7 @@ export async function handleClaudeCountTokensSurfaceRequest(
null,
upstreamRequest.path,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
if (shouldRetryProxyRequest(upstream.status, typeof payload === 'string' ? payload : text) && retryCount < MAX_RETRIES) {
retryCount += 1;
@@ -969,7 +945,7 @@ export async function handleClaudeCountTokensSurfaceRequest(
null,
upstreamRequest.path,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
return reply.code(upstream.status).type(contentType).send(payload);
} catch (error: any) {
@@ -990,7 +966,7 @@ export async function handleClaudeCountTokensSurfaceRequest(
null,
null,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
if (retryCount < MAX_RETRIES) {
retryCount += 1;
@@ -1036,11 +1012,11 @@ async function logProxy(
upstreamPath,
errorMessage,
});
await db.insert(schema.proxyLogs).values({
await insertProxyLog({
routeId: selected.channel.routeId,
channelId: selected.channel.id,
accountId: selected.account.id,
...(downstreamApiKeyId !== null ? { downstreamApiKeyId } : {}),
downstreamApiKeyId,
modelRequested,
modelActual: selected.actualModel,
status,
@@ -1050,11 +1026,15 @@ async function logProxy(
completionTokens,
totalTokens,
estimatedCost,
billingDetails: billingDetails ? JSON.stringify(billingDetails) : null,
billingDetails,
clientFamily: clientContext?.clientKind || null,
clientAppId: clientContext?.clientAppId || null,
clientAppName: clientContext?.clientAppName || null,
clientConfidence: clientContext?.clientConfidence || null,
errorMessage: normalizedErrorMessage,
retryCount,
createdAt,
}).run();
});
} catch (error) {
console.warn('[proxy/chat] failed to write proxy log', error);
}
@@ -29,6 +29,8 @@ import {
wrapGeminiCliRequest,
} from '../../routes/proxy/geminiCliCompat.js';
import { dispatchRuntimeRequest } from '../../routes/proxy/runtimeExecutor.js';
import { detectDownstreamClientContext, type DownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
import { insertProxyLog } from '../../services/proxyLogStore.js';
const MAX_RETRIES = 2;
const GEMINI_MODEL_PROBES = [
@@ -203,6 +205,7 @@ async function logProxy(
retryCount: number,
downstreamPath: string,
upstreamPath: string | null,
clientContext: DownstreamClientContext | null = null,
promptTokens = 0,
completionTokens = 0,
totalTokens = 0,
@@ -210,11 +213,16 @@ async function logProxy(
try {
const createdAt = formatUtcSqlDateTime(new Date());
const normalizedErrorMessage = composeProxyLogMessage({
clientKind: clientContext?.clientKind && clientContext.clientKind !== 'generic'
? clientContext.clientKind
: null,
sessionId: clientContext?.sessionId || null,
traceHint: clientContext?.traceHint || null,
downstreamPath,
upstreamPath,
errorMessage,
});
await db.insert(schema.proxyLogs).values({
await insertProxyLog({
routeId: selected.channel.routeId,
channelId: selected.channel.id,
accountId: selected.account.id,
@@ -227,11 +235,14 @@ async function logProxy(
completionTokens,
totalTokens,
estimatedCost: 0,
billingDetails: null,
clientFamily: clientContext?.clientKind || null,
clientAppId: clientContext?.clientAppId || null,
clientAppName: clientContext?.clientAppName || null,
clientConfidence: clientContext?.clientConfidence || null,
errorMessage: normalizedErrorMessage,
retryCount,
createdAt,
}).run();
});
} catch (error) {
console.warn('[proxy/gemini] failed to write proxy log', error);
}
@@ -355,6 +366,11 @@ export async function geminiProxyRoute(app: FastifyInstance) {
const policy = getDownstreamRoutingPolicy(request);
const downstreamPath = resolveDownstreamPath(request);
const clientContext = detectDownstreamClientContext({
downstreamPath,
headers: request.headers as Record<string, unknown>,
body: request.body,
});
const excludeChannelIds: number[] = [];
let retryCount = 0;
let lastStatus = 503;
@@ -489,6 +505,7 @@ export async function geminiProxyRoute(app: FastifyInstance) {
retryCount,
downstreamPath,
upstreamPath,
clientContext,
);
if (retryCount < MAX_RETRIES) {
retryCount += 1;
@@ -523,6 +540,7 @@ export async function geminiProxyRoute(app: FastifyInstance) {
retryCount,
downstreamPath,
upstreamPath,
clientContext,
);
reply.raw.end();
return;
@@ -572,6 +590,7 @@ export async function geminiProxyRoute(app: FastifyInstance) {
retryCount,
downstreamPath,
upstreamPath,
clientContext,
parsedUsage.promptTokens,
parsedUsage.completionTokens,
parsedUsage.totalTokens,
@@ -607,6 +626,7 @@ export async function geminiProxyRoute(app: FastifyInstance) {
retryCount,
downstreamPath,
upstreamPath,
clientContext,
parsedUsage.promptTokens,
parsedUsage.completionTokens,
parsedUsage.totalTokens,
@@ -629,6 +649,7 @@ export async function geminiProxyRoute(app: FastifyInstance) {
retryCount,
downstreamPath,
upstreamPath,
clientContext,
);
return reply.code(upstream.status).type(contentType || 'application/json').send(text);
}
@@ -743,6 +764,7 @@ export async function geminiProxyRoute(app: FastifyInstance) {
retryCount,
downstreamPath,
null,
clientContext,
);
if (retryCount < MAX_RETRIES) {
retryCount += 1;
@@ -780,6 +802,7 @@ export async function geminiProxyRoute(app: FastifyInstance) {
retryCount,
downstreamPath,
upstreamPath,
clientContext,
parsedUsage.promptTokens,
parsedUsage.completionTokens,
parsedUsage.totalTokens,
@@ -809,6 +832,7 @@ export async function geminiProxyRoute(app: FastifyInstance) {
retryCount,
downstreamPath,
upstreamPath || null,
clientContext,
);
if (retryCount < MAX_RETRIES) {
retryCount += 1;
@@ -1,6 +1,5 @@
import { TextDecoder } from 'node:util';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { db, hasProxyLogDownstreamApiKeyIdColumn, schema } from '../../db/index.js';
import { tokenRouter } from '../../services/tokenRouter.js';
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
@@ -44,12 +43,12 @@ import {
unwrapGeminiCliPayload,
} from '../../routes/proxy/geminiCliCompat.js';
import { isCodexResponsesSurface } from '../cliProfiles/codexProfile.js';
import { detectCliProfile } from '../cliProfiles/registry.js';
import type { CliProfileId } from '../cliProfiles/types.js';
import {
summarizeConversationFileInputsInOpenAiBody,
summarizeConversationFileInputsInResponsesBody,
} from '../capabilities/conversationFileCapabilities.js';
import { detectDownstreamClientContext, type DownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
import { insertProxyLog } from '../../services/proxyLogStore.js';
const MAX_RETRIES = 2;
@@ -135,25 +134,6 @@ function carriesResponsesFileUrlInput(value: unknown): boolean {
type UsageSummary = ReturnType<typeof parseProxyUsage>;
type DownstreamClientContext = {
clientKind: CliProfileId;
sessionId?: string;
traceHint?: string;
};
function detectDownstreamClientContext(input: {
downstreamPath: string;
headers?: Record<string, unknown>;
body?: unknown;
}): DownstreamClientContext {
const detected = detectCliProfile(input);
return {
clientKind: detected.id,
...(detected.sessionId ? { sessionId: detected.sessionId } : {}),
...(detected.traceHint ? { traceHint: detected.traceHint } : {}),
};
}
function deriveCodexExplicitSessionId(body: Record<string, unknown>): string | null {
const promptCacheKey = typeof body.prompt_cache_key === 'string'
? body.prompt_cache_key.trim()
@@ -196,8 +176,6 @@ export async function handleOpenAiResponsesSurfaceRequest(
if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return;
const downstreamPolicy = getDownstreamRoutingPolicy(request);
const downstreamApiKeyId = getProxyAuthContext(request)?.keyId ?? null;
const logDownstreamApiKeyId = downstreamApiKeyId !== null
&& await hasProxyLogDownstreamApiKeyIdColumn();
const excludeChannelIds: number[] = [];
let retryCount = 0;
@@ -397,7 +375,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
null,
null,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
},
});
@@ -423,7 +401,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
null,
null,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
await recordOauthQuotaResetHint({
accountId: selected.account.id,
@@ -505,7 +483,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
usageForLog.promptTokens, usageForLog.completionTokens, usageForLog.totalTokens, estimatedCost, billingDetails,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
} catch (error) {
console.error('[responses] post-stream success logging failed:', error);
@@ -578,7 +556,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
null,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
return;
}
@@ -617,7 +595,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
null,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
if (shouldRetryProxyRequest(failure.status, failure.reason) && retryCount < MAX_RETRIES) {
@@ -652,7 +630,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
null,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
return;
}
@@ -708,7 +686,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
null,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
return;
}
@@ -772,7 +750,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
null,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
if (shouldRetryProxyRequest(failure.status, failure.reason) && retryCount < MAX_RETRIES) {
retryCount += 1;
@@ -826,7 +804,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost, billingDetails,
successfulUpstreamPath,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
return reply.send(downstreamData);
} catch (err: any) {
@@ -847,7 +825,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
null,
null,
clientContext,
logDownstreamApiKeyId ? downstreamApiKeyId : null,
downstreamApiKeyId,
);
if (retryCount < MAX_RETRIES) {
retryCount += 1;
@@ -894,11 +872,11 @@ async function logProxy(
upstreamPath,
errorMessage,
});
await db.insert(schema.proxyLogs).values({
await insertProxyLog({
routeId: selected.channel.routeId,
channelId: selected.channel.id,
accountId: selected.account.id,
...(downstreamApiKeyId !== null ? { downstreamApiKeyId } : {}),
downstreamApiKeyId,
modelRequested,
modelActual: selected.actualModel,
status,
@@ -908,11 +886,15 @@ async function logProxy(
completionTokens,
totalTokens,
estimatedCost,
billingDetails: billingDetails ? JSON.stringify(billingDetails) : null,
billingDetails,
clientFamily: clientContext?.clientKind || null,
clientAppId: clientContext?.clientAppId || null,
clientAppName: clientContext?.clientAppName || null,
clientConfidence: clientContext?.clientConfidence || null,
errorMessage: normalizedErrorMessage,
retryCount,
createdAt,
}).run();
});
} catch (error) {
console.warn('[proxy/responses] failed to write proxy log', error);
}