feat(proxy): implement metrics persistence, traffic logs DB, and advanced monitoring

- Add SQLite schema for proxy metrics and traffic logs
- Implement ProxyMetricsRepo with historical aggregates
- Add TrafficLogsRepo for DB-backed traffic log queries
- Create proxy-advanced IPC handlers (metrics, traffic logs, circuit breaker, parity)
- Add ProxyMetricsService with connection snapshots and cleanup
- Add ProxyReplayService and ProxyIdeConfigService
- Implement MonitorTab and ToolsTab for advanced proxy features
- Add TrafficLogTable with sorting and filtering
- Fix i18n keys for proxy metrics
- Fix generateIdeConfig type mapping
- Update proxy routes with advanced tab integration
This commit is contained in:
Gemini Nexus Bot committed 2026-04-23 00:57:01 -03:00
1 parent c73537d44c
commit 0be7d7fea2
36 files changed
+4551 -882

No files matched your search

+115 -38
View File
@@ -50,7 +50,10 @@ export class ProxyService {
private async recordUsage(
accountId: string,
model: string,
usage: { promptTokenCount?: number; candidatesTokenCount?: number; totalTokenCount?: number } | undefined | null,
usage:
| { promptTokenCount?: number; candidatesTokenCount?: number; totalTokenCount?: number }
| undefined
| null,
requestType: string,
isEstimated?: boolean,
): Promise<void> {
@@ -79,12 +82,15 @@ export class ProxyService {
private extractUsageFromMetadata(
meta: unknown,
): { promptTokenCount?: number; candidatesTokenCount?: number; totalTokenCount?: number } | undefined {
):
| { promptTokenCount?: number; candidatesTokenCount?: number; totalTokenCount?: number }
| undefined {
if (!meta || typeof meta !== 'object') return undefined;
const m = meta as Record<string, unknown>;
return {
promptTokenCount: typeof m.promptTokenCount === 'number' ? m.promptTokenCount : undefined,
candidatesTokenCount: typeof m.candidatesTokenCount === 'number' ? m.candidatesTokenCount : undefined,
candidatesTokenCount:
typeof m.candidatesTokenCount === 'number' ? m.candidatesTokenCount : undefined,
totalTokenCount: typeof m.totalTokenCount === 'number' ? m.totalTokenCount : undefined,
};
}
@@ -332,13 +338,17 @@ export class ProxyService {
);
} else if (accumulatedText) {
const estimated = this.estimateTokens(accumulatedText);
this.recordUsage(accountId, _model, {
promptTokenCount: 0,
candidatesTokenCount: estimated,
totalTokenCount: estimated,
}, 'anthropic', true).catch((err) =>
this.logger.error('Usage recording failed', err),
);
this.recordUsage(
accountId,
_model,
{
promptTokenCount: 0,
candidatesTokenCount: estimated,
totalTokenCount: estimated,
},
'anthropic',
true,
).catch((err) => this.logger.error('Usage recording failed', err));
}
subscriber.complete();
});
@@ -504,7 +514,7 @@ export class ProxyService {
token.token.upstream_proxy_url,
extraHeaders,
);
return this.passthroughSseStream(stream, token.id, effectiveTargetModel);
return this.passthroughSseStream(stream, token.id, effectiveTargetModel);
} catch (err) {
if (err instanceof Error && this.isProjectContextError(err.message)) {
this.logger.warn(
@@ -526,7 +536,7 @@ export class ProxyService {
token.token.upstream_proxy_url,
extraHeaders,
);
return this.passthroughSseStream(stream, token.id, effectiveTargetModel);
return this.passthroughSseStream(stream, token.id, effectiveTargetModel);
} catch (fallbackErr) {
lastError = fallbackErr;
}
@@ -579,7 +589,9 @@ export class ProxyService {
if (typeof text === 'string') {
accumulatedText += text;
}
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
subscriber.next(chunk.toString());
@@ -597,13 +609,17 @@ export class ProxyService {
);
} else if (accumulatedText) {
const estimated = this.estimateTokens(accumulatedText);
this.recordUsage(accountId, model, {
promptTokenCount: 0,
candidatesTokenCount: estimated,
totalTokenCount: estimated,
}, 'gemini', true).catch((err) =>
this.logger.error('Usage recording failed', err),
);
this.recordUsage(
accountId,
model,
{
promptTokenCount: 0,
candidatesTokenCount: estimated,
totalTokenCount: estimated,
},
'gemini',
true,
).catch((err) => this.logger.error('Usage recording failed', err));
}
subscriber.complete();
});
@@ -859,14 +875,26 @@ export class ProxyService {
// Use v1internal API (same as Anthropic handler)
if (request.stream) {
try {
trafficLogger.logUpstream(requestId || 'unknown', '/v1/chat/completions', geminiBody, extraHeaders, { model: effectiveTargetModel });
trafficLogger.logUpstream(
requestId || 'unknown',
'/v1/chat/completions',
geminiBody,
extraHeaders,
{ model: effectiveTargetModel },
);
const stream = await this.geminiClient.streamGenerateInternal(
geminiBody,
token.token.access_token,
token.token.upstream_proxy_url,
extraHeaders,
);
return this.processStreamResponse(stream, request.model, requestId, token.id, effectiveTargetModel);
return this.processStreamResponse(
stream,
request.model,
requestId,
token.id,
effectiveTargetModel,
);
} catch (streamError) {
this.logger.warn(
`Stream path failed for model=${request.model}; falling back to non-stream generation: ${
@@ -874,14 +902,26 @@ export class ProxyService {
}`,
);
trafficLogger.logUpstream(requestId || 'unknown', '/v1/chat/completions', geminiBody, extraHeaders, { model: effectiveTargetModel });
trafficLogger.logUpstream(
requestId || 'unknown',
'/v1/chat/completions',
geminiBody,
extraHeaders,
{ model: effectiveTargetModel },
);
const response = await this.generateInternalWithStreamFallback(
geminiBody,
token.token.access_token,
token.token.upstream_proxy_url,
extraHeaders,
);
trafficLogger.logUpstreamResponse(requestId || 'unknown', '/v1/chat/completions', 200, response, 0);
trafficLogger.logUpstreamResponse(
requestId || 'unknown',
'/v1/chat/completions',
200,
response,
0,
);
this.recordUsage(token.id, effectiveTargetModel, response.usageMetadata, 'openai');
this.logger.log(
`Upstream response snippet after stream fallback: ${JSON.stringify(response).substring(0, 500)}`,
@@ -923,24 +963,48 @@ export class ProxyService {
fallbackBody.model = effectiveTargetModel;
this.applyInternalGenerationConstraints(fallbackBody, effectiveTargetModel, token.id);
if (request.stream) {
trafficLogger.logUpstream(requestId || 'unknown', '/v1/chat/completions', fallbackBody, extraHeaders, { model: effectiveTargetModel });
trafficLogger.logUpstream(
requestId || 'unknown',
'/v1/chat/completions',
fallbackBody,
extraHeaders,
{ model: effectiveTargetModel },
);
const stream = await this.geminiClient.streamGenerateInternal(
fallbackBody,
token.token.access_token,
token.token.upstream_proxy_url,
extraHeaders,
);
return this.processStreamResponse(stream, request.model, requestId, token.id, effectiveTargetModel);
return this.processStreamResponse(
stream,
request.model,
requestId,
token.id,
effectiveTargetModel,
);
}
trafficLogger.logUpstream(requestId || 'unknown', '/v1/chat/completions', fallbackBody, extraHeaders, { model: effectiveTargetModel });
trafficLogger.logUpstream(
requestId || 'unknown',
'/v1/chat/completions',
fallbackBody,
extraHeaders,
{ model: effectiveTargetModel },
);
const response = await this.generateInternalWithStreamFallback(
fallbackBody,
token.token.access_token,
token.token.upstream_proxy_url,
extraHeaders,
);
trafficLogger.logUpstreamResponse(requestId || 'unknown', '/v1/chat/completions', 200, response, 0);
trafficLogger.logUpstreamResponse(
requestId || 'unknown',
'/v1/chat/completions',
200,
response,
0,
);
this.recordUsage(token.id, effectiveTargetModel, response.usageMetadata, 'openai');
const claudeResponse = transformResponse(response);
return this.convertClaudeToOpenAIResponse(claudeResponse, request.model);
@@ -1135,7 +1199,14 @@ export class ProxyService {
try {
const json = JSON.parse(dataStr);
trafficLogger.logUpstreamResponse(requestId || 'unknown', '/v1/chat/completions', 200, json, 0, { rawLine: trimmed });
trafficLogger.logUpstreamResponse(
requestId || 'unknown',
'/v1/chat/completions',
200,
json,
0,
{ rawLine: trimmed },
);
// v1internal API wraps response in {"response": {"candidates": [...]}}
const responsePayload = json.response ?? json;
if (responsePayload.usageMetadata) {
@@ -1300,13 +1371,17 @@ export class ProxyService {
);
} else if (accumulatedText) {
const estimated = this.estimateTokens(accumulatedText);
this.recordUsage(accountId, effectiveTargetModel, {
promptTokenCount: 0,
candidatesTokenCount: estimated,
totalTokenCount: estimated,
}, 'openai', true).catch((err) =>
this.logger.error('Usage recording failed', err),
);
this.recordUsage(
accountId,
effectiveTargetModel,
{
promptTokenCount: 0,
candidatesTokenCount: estimated,
totalTokenCount: estimated,
},
'openai',
true,
).catch((err) => this.logger.error('Usage recording failed', err));
}
subscriber.complete();
});
@@ -1776,7 +1851,7 @@ export class ProxyService {
message: {
role: 'assistant',
// When tool_calls are present, content must be null per OpenAI spec.
content: toolCalls.length > 0 ? null : (finalContent || ''),
content: toolCalls.length > 0 ? null : finalContent || '',
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
},
finish_reason: this.mapAnthropicStopReasonToOpenAIFinishReason(
@@ -1844,7 +1919,9 @@ export class ProxyService {
if (error instanceof UpstreamRequestError) {
const status = error.status;
if (status === 401) {
this.logger.warn(`Upstream returned 401 for account ${accountId}; attempting token refresh`);
this.logger.warn(
`Upstream returned 401 for account ${accountId}; attempting token refresh`,
);
const refreshed = await this.tokenManager.forceRefreshToken(accountId);
if (refreshed) {
this.logger.log(`Token refreshed successfully for account ${accountId}; retry permitted`);