feat: update error messages in NewApiAdapter tests to use correct Chinese translations

feat: enhance proxy usage parser to include cache read and creation tokens

feat: add billing details to ProxyLog interface and implement related formatting functions

fix: correct mojibake issues in source files and add regression test for mojibake detection

chore: add SQL migration for billing details in proxy_logs table

test: improve site speed button tests to ensure proper state updates without DOM lookups
This commit is contained in:
cita
2026-03-06 17:10:01 +08:00
parent 2a810facb8
commit 55b1ae564e
23 changed files with 1298 additions and 344 deletions
@@ -10,6 +10,7 @@ const refreshModelsAndRebuildRoutesMock = vi.fn();
const reportProxyAllFailedMock = vi.fn();
const reportTokenExpiredMock = vi.fn();
const estimateProxyCostMock = vi.fn(async (_arg?: any) => 0);
const buildProxyBillingDetailsMock = vi.fn(async (_arg?: any) => null);
const fetchModelPricingCatalogMock = vi.fn(async (_arg?: any): Promise<any> => null);
const resolveProxyUsageWithSelfLogFallbackMock = vi.fn(async ({ usage }: any) => ({
...usage,
@@ -50,6 +51,7 @@ vi.mock('../../services/alertRules.js', () => ({
vi.mock('../../services/modelPricingService.js', () => ({
estimateProxyCost: (arg: any) => estimateProxyCostMock(arg),
buildProxyBillingDetails: (arg: any) => buildProxyBillingDetailsMock(arg),
fetchModelPricingCatalog: (arg: any) => fetchModelPricingCatalogMock(arg),
}));
@@ -92,6 +94,7 @@ describe('chat proxy stream behavior', () => {
reportProxyAllFailedMock.mockReset();
reportTokenExpiredMock.mockReset();
estimateProxyCostMock.mockClear();
buildProxyBillingDetailsMock.mockClear();
fetchModelPricingCatalogMock.mockReset();
resolveProxyUsageWithSelfLogFallbackMock.mockClear();
dbInsertMock.mockClear();
+59 -2
View File
@@ -5,7 +5,7 @@ import { fetch } from 'undici';
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { estimateProxyCost } from '../../services/modelPricingService.js';
import { buildProxyBillingDetails, estimateProxyCost } from '../../services/modelPricingService.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParser.js';
@@ -347,7 +347,14 @@ async function handleChatProxyRequest(
const streamContext = createStreamTransformContext(modelName);
const claudeContext = createClaudeDownstreamContext();
let parsedUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
let parsedUsage: ReturnType<typeof parseProxyUsage> = {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
cacheReadTokens: 0,
cacheCreationTokens: 0,
promptTokensIncludeCache: null,
};
const writeLines = (lines: string[]) => {
for (const line of lines) {
@@ -429,10 +436,25 @@ async function handleChatProxyRequest(
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
let billingDetails = await buildProxyBillingDetails({
site: selected.site,
account: selected.account,
modelName,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
if (resolvedUsage.estimatedCostFromQuota > 0 && (resolvedUsage.recoveredFromSelfLog || estimatedCost <= 0)) {
estimatedCost = resolvedUsage.estimatedCostFromQuota;
billingDetails = null;
}
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost);
@@ -450,6 +472,7 @@ async function handleChatProxyRequest(
resolvedUsage.completionTokens,
resolvedUsage.totalTokens,
estimatedCost,
billingDetails,
successfulUpstreamPath,
);
return;
@@ -593,10 +616,25 @@ async function handleChatProxyRequest(
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
let billingDetails = await buildProxyBillingDetails({
site: selected.site,
account: selected.account,
modelName,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
if (resolvedUsage.estimatedCostFromQuota > 0 && (resolvedUsage.recoveredFromSelfLog || estimatedCost <= 0)) {
estimatedCost = resolvedUsage.estimatedCostFromQuota;
billingDetails = null;
}
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost);
@@ -614,6 +652,7 @@ async function handleChatProxyRequest(
resolvedUsage.completionTokens,
resolvedUsage.totalTokens,
estimatedCost,
billingDetails,
successfulUpstreamPath,
);
return;
@@ -655,10 +694,25 @@ async function handleChatProxyRequest(
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
let billingDetails = await buildProxyBillingDetails({
site: selected.site,
account: selected.account,
modelName,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
if (resolvedUsage.estimatedCostFromQuota > 0 && (resolvedUsage.recoveredFromSelfLog || estimatedCost <= 0)) {
estimatedCost = resolvedUsage.estimatedCostFromQuota;
billingDetails = null;
}
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost);
@@ -676,6 +730,7 @@ async function handleChatProxyRequest(
resolvedUsage.completionTokens,
resolvedUsage.totalTokens,
estimatedCost,
billingDetails,
successfulUpstreamPath,
);
@@ -717,6 +772,7 @@ async function logProxy(
completionTokens = 0,
totalTokens = 0,
estimatedCost = 0,
billingDetails: unknown = null,
upstreamPath: string | null = null,
) {
try {
@@ -739,6 +795,7 @@ async function logProxy(
completionTokens,
totalTokens,
estimatedCost,
billingDetails: billingDetails ? JSON.stringify(billingDetails) : null,
errorMessage: normalizedErrorMessage,
retryCount,
createdAt,
+48 -9
View File
@@ -5,7 +5,7 @@ import { tokenRouter } from '../../services/tokenRouter.js';
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { estimateProxyCost } from '../../services/modelPricingService.js';
import { buildProxyBillingDetails, estimateProxyCost } from '../../services/modelPricingService.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { mergeProxyUsage, parseProxyUsage, pullSseDataEvents } from '../../services/proxyUsageParser.js';
@@ -106,7 +106,14 @@ export async function completionsProxyRoute(app: FastifyInstance) {
}
const decoder = new TextDecoder();
let parsedUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
let parsedUsage: ReturnType<typeof parseProxyUsage> = {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
cacheReadTokens: 0,
cacheCreationTokens: 0,
promptTokensIncludeCache: null,
};
let sseBuffer = '';
try {
while (true) {
@@ -160,15 +167,30 @@ export async function completionsProxyRoute(app: FastifyInstance) {
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
let billingDetails = await buildProxyBillingDetails({
site: selected.site,
account: selected.account,
modelName: selected.actualModel || requestedModel,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
if (resolvedUsage.estimatedCostFromQuota > 0 && (resolvedUsage.recoveredFromSelfLog || estimatedCost <= 0)) {
estimatedCost = resolvedUsage.estimatedCostFromQuota;
billingDetails = null;
}
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost);
recordDownstreamCostUsage(request, estimatedCost);
logProxy(
selected, requestedModel, 'success', 200, latency, null, retryCount,
resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost,
resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost, billingDetails,
);
return;
}
@@ -198,18 +220,33 @@ export async function completionsProxyRoute(app: FastifyInstance) {
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
let billingDetails = await buildProxyBillingDetails({
site: selected.site,
account: selected.account,
modelName: selected.actualModel || requestedModel,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
if (resolvedUsage.estimatedCostFromQuota > 0 && (resolvedUsage.recoveredFromSelfLog || estimatedCost <= 0)) {
estimatedCost = resolvedUsage.estimatedCostFromQuota;
billingDetails = null;
}
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost);
recordDownstreamCostUsage(request, estimatedCost);
logProxy(
selected, requestedModel, 'success', 200, latency, null, retryCount,
resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost,
resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost, billingDetails,
);
return reply.send(data);
return reply.send(data);
} catch (err: any) {
tokenRouter.recordFailure(selected.channel.id);
logProxy(selected, requestedModel, 'failed', 0, Date.now() - startTime, err.message, retryCount);
@@ -237,10 +274,11 @@ async function logProxy(
latencyMs: number,
errorMessage: string | null,
retryCount: number,
promptTokens = 0,
completionTokens = 0,
totalTokens = 0,
estimatedCost = 0,
promptTokens = 0,
completionTokens = 0,
totalTokens = 0,
estimatedCost = 0,
billingDetails: unknown = null,
) {
try {
const createdAt = formatUtcSqlDateTime(new Date());
@@ -261,6 +299,7 @@ async function logProxy(
completionTokens,
totalTokens,
estimatedCost,
billingDetails: billingDetails ? JSON.stringify(billingDetails) : null,
errorMessage: normalizedErrorMessage,
retryCount,
createdAt,
+24 -7
View File
@@ -5,7 +5,7 @@ import { tokenRouter } from '../../services/tokenRouter.js';
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { estimateProxyCost } from '../../services/modelPricingService.js';
import { buildProxyBillingDetails, estimateProxyCost } from '../../services/modelPricingService.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { parseProxyUsage } from '../../services/proxyUsageParser.js';
@@ -115,18 +115,33 @@ export async function embeddingsProxyRoute(app: FastifyInstance) {
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
let billingDetails = await buildProxyBillingDetails({
site: selected.site,
account: selected.account,
modelName: selected.actualModel || requestedModel,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
if (resolvedUsage.estimatedCostFromQuota > 0 && (resolvedUsage.recoveredFromSelfLog || estimatedCost <= 0)) {
estimatedCost = resolvedUsage.estimatedCostFromQuota;
billingDetails = null;
}
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost);
recordDownstreamCostUsage(request, estimatedCost);
logProxy(
selected, requestedModel, 'success', upstream.status, latency, null, retryCount,
resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost,
resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost, billingDetails,
);
return reply.code(upstream.status).send(data);
return reply.code(upstream.status).send(data);
} catch (err: any) {
tokenRouter.recordFailure(selected.channel.id);
logProxy(selected, requestedModel, 'failed', 0, Date.now() - startTime, err.message, retryCount);
@@ -152,10 +167,11 @@ async function logProxy(
latencyMs: number,
errorMessage: string | null,
retryCount: number,
promptTokens = 0,
completionTokens = 0,
totalTokens = 0,
estimatedCost = 0,
promptTokens = 0,
completionTokens = 0,
totalTokens = 0,
estimatedCost = 0,
billingDetails: unknown = null,
) {
try {
const createdAt = formatUtcSqlDateTime(new Date());
@@ -176,6 +192,7 @@ async function logProxy(
completionTokens,
totalTokens,
estimatedCost,
billingDetails: billingDetails ? JSON.stringify(billingDetails) : null,
errorMessage: normalizedErrorMessage,
retryCount,
createdAt,
+44 -9
View File
@@ -5,7 +5,7 @@ import { tokenRouter } from '../../services/tokenRouter.js';
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { estimateProxyCost } from '../../services/modelPricingService.js';
import { buildProxyBillingDetails, estimateProxyCost } from '../../services/modelPricingService.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParser.js';
@@ -571,11 +571,7 @@ function convertResponsesBodyToOpenAiBody(
return payload;
}
type UsageSummary = {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
type UsageSummary = ReturnType<typeof parseProxyUsage>;
type ResponsesToolCall = {
id: string;
@@ -1358,7 +1354,14 @@ export async function responsesProxyRoute(app: FastifyInstance) {
}
const decoder = new TextDecoder();
let parsedUsage: UsageSummary = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
let parsedUsage: UsageSummary = {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
cacheReadTokens: 0,
cacheCreationTokens: 0,
promptTokensIncludeCache: null,
};
let sseBuffer = '';
const passthroughResponsesStream = successfulUpstreamPath === '/v1/responses';
@@ -1479,15 +1482,30 @@ export async function responsesProxyRoute(app: FastifyInstance) {
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
let billingDetails = await buildProxyBillingDetails({
site: selected.site,
account: selected.account,
modelName: selected.actualModel || requestedModel,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
if (resolvedUsage.estimatedCostFromQuota > 0 && (resolvedUsage.recoveredFromSelfLog || estimatedCost <= 0)) {
estimatedCost = resolvedUsage.estimatedCostFromQuota;
billingDetails = null;
}
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost);
recordDownstreamCostUsage(request, estimatedCost);
logProxy(
selected, requestedModel, 'success', 200, latency, null, retryCount, downstreamPath,
resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost,
resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost, billingDetails,
successfulUpstreamPath,
);
return;
@@ -1530,16 +1548,31 @@ export async function responsesProxyRoute(app: FastifyInstance) {
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
let billingDetails = await buildProxyBillingDetails({
site: selected.site,
account: selected.account,
modelName: selected.actualModel || requestedModel,
promptTokens: resolvedUsage.promptTokens,
completionTokens: resolvedUsage.completionTokens,
totalTokens: resolvedUsage.totalTokens,
cacheReadTokens: parsedUsage.cacheReadTokens,
cacheCreationTokens: parsedUsage.cacheCreationTokens,
promptTokensIncludeCache: parsedUsage.promptTokensIncludeCache,
});
if (resolvedUsage.estimatedCostFromQuota > 0 && (resolvedUsage.recoveredFromSelfLog || estimatedCost <= 0)) {
estimatedCost = resolvedUsage.estimatedCostFromQuota;
billingDetails = null;
}
tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost);
recordDownstreamCostUsage(request, estimatedCost);
logProxy(
selected, requestedModel, 'success', 200, latency, null, retryCount, downstreamPath,
resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost,
resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost, billingDetails,
successfulUpstreamPath,
);
return reply.send(downstreamData);
@@ -1575,6 +1608,7 @@ async function logProxy(
completionTokens = 0,
totalTokens = 0,
estimatedCost = 0,
billingDetails: unknown = null,
upstreamPath: string | null = null,
) {
try {
@@ -1597,6 +1631,7 @@ async function logProxy(
completionTokens,
totalTokens,
estimatedCost,
billingDetails: billingDetails ? JSON.stringify(billingDetails) : null,
errorMessage: normalizedErrorMessage,
retryCount,
createdAt,