forked from boos/BoosAPI
feat: implement proxy billing details handling and resolveProxyLogBilling function
This commit is contained in:
@@ -37,6 +37,14 @@ interface PricingData {
|
||||
groupRatio: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface ProxyBillingPricingOverride {
|
||||
modelRatio: number;
|
||||
completionRatio: number;
|
||||
cacheRatio?: number;
|
||||
cacheCreationRatio?: number;
|
||||
groupRatio?: number;
|
||||
}
|
||||
|
||||
interface PricingCacheEntry {
|
||||
fetchedAt: number;
|
||||
ttlMs: number;
|
||||
@@ -68,6 +76,7 @@ interface EstimateProxyCostInput {
|
||||
cacheReadTokens?: number;
|
||||
cacheCreationTokens?: number;
|
||||
promptTokensIncludeCache?: boolean | null;
|
||||
billingPricingOverride?: ProxyBillingPricingOverride | null;
|
||||
}
|
||||
|
||||
interface ModelGroupPricing {
|
||||
@@ -573,6 +582,26 @@ function calculatePerCallPricing(
|
||||
return { total: 0 };
|
||||
}
|
||||
|
||||
function buildPricingOverrideModel(
|
||||
modelName: string,
|
||||
pricingOverride: ProxyBillingPricingOverride,
|
||||
): { model: PricingModel; groupRatio: Record<string, number> } {
|
||||
const groupRatio = normalizeRatio(pricingOverride.groupRatio, 1);
|
||||
return {
|
||||
model: {
|
||||
modelName,
|
||||
quotaType: 0,
|
||||
modelRatio: normalizeRatio(pricingOverride.modelRatio, 1),
|
||||
completionRatio: normalizeRatio(pricingOverride.completionRatio, 1),
|
||||
cacheRatio: normalizeRatio(pricingOverride.cacheRatio, 1),
|
||||
cacheCreationRatio: normalizeRatio(pricingOverride.cacheCreationRatio, 1),
|
||||
modelPrice: null,
|
||||
enableGroups: [DEFAULT_GROUP],
|
||||
},
|
||||
groupRatio: { [DEFAULT_GROUP]: groupRatio },
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeUsageBreakdownInput(usage: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
@@ -744,8 +773,21 @@ export async function estimateProxyCost(input: EstimateProxyCostInput): Promise<
|
||||
const promptTokens = toPositiveInt(input.promptTokens);
|
||||
const completionTokens = toPositiveInt(input.completionTokens);
|
||||
const totalTokens = toPositiveInt(input.totalTokens || (promptTokens + completionTokens));
|
||||
const usage = {
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
totalTokens,
|
||||
cacheReadTokens: input.cacheReadTokens,
|
||||
cacheCreationTokens: input.cacheCreationTokens,
|
||||
promptTokensIncludeCache: input.promptTokensIncludeCache,
|
||||
};
|
||||
|
||||
try {
|
||||
if (input.billingPricingOverride) {
|
||||
const pricingOverride = buildPricingOverrideModel(input.modelName, input.billingPricingOverride);
|
||||
return calculateModelUsageCost(pricingOverride.model, usage, pricingOverride.groupRatio);
|
||||
}
|
||||
|
||||
const pricingData = await getPricingDataCached(input);
|
||||
if (!pricingData) {
|
||||
return fallbackTokenCost(totalTokens, input.site.platform);
|
||||
@@ -756,14 +798,7 @@ export async function estimateProxyCost(input: EstimateProxyCostInput): Promise<
|
||||
return fallbackTokenCost(totalTokens, input.site.platform);
|
||||
}
|
||||
|
||||
return calculateModelUsageCost(model, {
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
totalTokens,
|
||||
cacheReadTokens: input.cacheReadTokens,
|
||||
cacheCreationTokens: input.cacheCreationTokens,
|
||||
promptTokensIncludeCache: input.promptTokensIncludeCache,
|
||||
}, pricingData.groupRatio);
|
||||
return calculateModelUsageCost(model, usage, pricingData.groupRatio);
|
||||
} catch {
|
||||
return fallbackTokenCost(totalTokens, input.site.platform);
|
||||
}
|
||||
@@ -784,22 +819,28 @@ export async function buildProxyBillingDetails(input: EstimateProxyCostInput): P
|
||||
const promptTokens = toPositiveInt(input.promptTokens);
|
||||
const completionTokens = toPositiveInt(input.completionTokens);
|
||||
const totalTokens = toPositiveInt(input.totalTokens || (promptTokens + completionTokens));
|
||||
const usage = {
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
totalTokens,
|
||||
cacheReadTokens: input.cacheReadTokens,
|
||||
cacheCreationTokens: input.cacheCreationTokens,
|
||||
promptTokensIncludeCache: input.promptTokensIncludeCache,
|
||||
};
|
||||
|
||||
try {
|
||||
if (input.billingPricingOverride) {
|
||||
const pricingOverride = buildPricingOverrideModel(input.modelName, input.billingPricingOverride);
|
||||
return calculateModelUsageBreakdown(pricingOverride.model, usage, pricingOverride.groupRatio);
|
||||
}
|
||||
|
||||
const pricingData = await getPricingDataCached(input);
|
||||
if (!pricingData) return null;
|
||||
|
||||
const model = resolveModel(input.modelName, pricingData);
|
||||
if (!model || model.quotaType === 1) return null;
|
||||
|
||||
return calculateModelUsageBreakdown(model, {
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
totalTokens,
|
||||
cacheReadTokens: input.cacheReadTokens,
|
||||
cacheCreationTokens: input.cacheCreationTokens,
|
||||
promptTokensIncludeCache: input.promptTokensIncludeCache,
|
||||
}, pricingData.groupRatio);
|
||||
return calculateModelUsageBreakdown(model, usage, pricingData.groupRatio);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ describe('proxyUsageFallbackService', () => {
|
||||
quota: 3300,
|
||||
createdAtMs: 1_700_000_123_000,
|
||||
requestTimeMs: 1450,
|
||||
billingMeta: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { fetch } from 'undici';
|
||||
import { resolvePlatformUserId } from './accountExtraConfig.js';
|
||||
import {
|
||||
buildNewApiCookieCandidates,
|
||||
fetchJsonWithShieldCookieRetry,
|
||||
} from './platforms/newApiShield.js';
|
||||
import { withExplicitProxyRequestInit } from './siteProxy.js';
|
||||
|
||||
const SELF_LOG_FETCH_TIMEOUT_MS = 8_000;
|
||||
@@ -10,7 +14,7 @@ const MATCH_MAX_CREATED_DELTA_MS = 90_000;
|
||||
const MATCH_MAX_LATENCY_DELTA_MS = 12_000;
|
||||
const QUOTA_PER_UNIT = 500_000;
|
||||
const SUPPORTED_USAGE_FALLBACK_PLATFORMS = new Set(['done-hub', 'one-hub', 'new-api', 'anyrouter']);
|
||||
const ALWAYS_LOOKUP_SELF_LOG_PLATFORMS = new Set(['done-hub', 'one-hub']);
|
||||
const ALWAYS_LOOKUP_SELF_LOG_PLATFORMS = new Set(['done-hub', 'one-hub', 'anyrouter']);
|
||||
const PLATFORM_REQUIRES_USER_HEADER = new Set(['new-api', 'anyrouter']);
|
||||
|
||||
interface ProxyUsage {
|
||||
@@ -45,6 +49,18 @@ interface ProxyUsageFallbackInput {
|
||||
interface ProxyUsageFallbackResult extends ProxyUsage {
|
||||
recoveredFromSelfLog: boolean;
|
||||
estimatedCostFromQuota: number;
|
||||
selfLogBillingMeta: SelfLogBillingMeta | null;
|
||||
}
|
||||
|
||||
export interface SelfLogBillingMeta {
|
||||
modelRatio: number;
|
||||
completionRatio: number;
|
||||
cacheRatio: number;
|
||||
cacheCreationRatio: number;
|
||||
groupRatio: number;
|
||||
cacheReadTokens: number;
|
||||
cacheCreationTokens: number;
|
||||
promptTokensIncludeCache: boolean;
|
||||
}
|
||||
|
||||
export interface SelfLogItem {
|
||||
@@ -56,6 +72,7 @@ export interface SelfLogItem {
|
||||
quota: number;
|
||||
createdAtMs: number;
|
||||
requestTimeMs: number;
|
||||
billingMeta: SelfLogBillingMeta | null;
|
||||
}
|
||||
|
||||
interface SelfLogMatchInput {
|
||||
@@ -80,6 +97,12 @@ function roundCost(value: number): number {
|
||||
return Math.round(Math.max(0, value) * 1_000_000) / 1_000_000;
|
||||
}
|
||||
|
||||
function normalizePositiveRatio(value: unknown, fallback: number): number {
|
||||
const ratio = toNumber(value, Number.NaN);
|
||||
if (Number.isFinite(ratio) && ratio >= 0) return ratio;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function toTimestampMs(value: unknown): number {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
if (value > 1_000_000_000_000) return Math.round(value);
|
||||
@@ -196,6 +219,72 @@ function mapSelfLogItem(raw: unknown): SelfLogItem | null {
|
||||
quota: toPositiveInt(row.quota),
|
||||
createdAtMs,
|
||||
requestTimeMs: toPositiveInt(row.request_time ?? row.requestTime),
|
||||
billingMeta: parseSelfLogBillingMeta(row.other),
|
||||
};
|
||||
}
|
||||
|
||||
function parseSelfLogBillingMeta(rawOther: unknown): SelfLogBillingMeta | null {
|
||||
let payload = rawOther;
|
||||
if (typeof rawOther === 'string') {
|
||||
const trimmed = rawOther.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
payload = JSON.parse(trimmed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload || typeof payload !== 'object') return null;
|
||||
const other = payload as Record<string, unknown>;
|
||||
|
||||
const modelRatio = normalizePositiveRatio(other.model_ratio ?? other.modelRatio, 1);
|
||||
const completionRatio = normalizePositiveRatio(
|
||||
other.completion_ratio ?? other.completionRatio,
|
||||
1,
|
||||
);
|
||||
const cacheRatio = normalizePositiveRatio(
|
||||
other.cache_ratio ?? other.cacheRatio,
|
||||
1,
|
||||
);
|
||||
const cacheCreationRatio = normalizePositiveRatio(
|
||||
other.cache_creation_ratio
|
||||
?? other.cacheCreationRatio
|
||||
?? other.create_cache_ratio
|
||||
?? other.createCacheRatio,
|
||||
1,
|
||||
);
|
||||
const groupRatio = normalizePositiveRatio(other.group_ratio ?? other.groupRatio, 1);
|
||||
const cacheReadTokens = toPositiveInt(
|
||||
other.cache_tokens ?? other.cacheTokens ?? other.cache_read_tokens ?? other.cacheReadTokens,
|
||||
);
|
||||
const cacheCreationTokens = toPositiveInt(
|
||||
other.cache_creation_tokens
|
||||
?? other.cacheCreationTokens
|
||||
?? other.create_cache_tokens
|
||||
?? other.createCacheTokens,
|
||||
);
|
||||
|
||||
const hasMeaningfulData = (
|
||||
cacheReadTokens > 0
|
||||
|| cacheCreationTokens > 0
|
||||
|| modelRatio !== 1
|
||||
|| completionRatio !== 1
|
||||
|| cacheRatio !== 1
|
||||
|| cacheCreationRatio !== 1
|
||||
|| groupRatio !== 1
|
||||
);
|
||||
if (!hasMeaningfulData) return null;
|
||||
|
||||
return {
|
||||
modelRatio,
|
||||
completionRatio,
|
||||
cacheRatio,
|
||||
cacheCreationRatio,
|
||||
groupRatio,
|
||||
cacheReadTokens,
|
||||
cacheCreationTokens,
|
||||
promptTokensIncludeCache: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -224,7 +313,11 @@ async function fetchSelfLogPayload(baseUrl: string, token: string, input: ProxyU
|
||||
}, SELF_LOG_FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const query = `p=0&page=1&size=${SELF_LOG_PAGE_SIZE}&order=-created_at`;
|
||||
const pageSizeParam = String(input.site.platform || '').toLowerCase() === 'anyrouter'
|
||||
? 'page_size'
|
||||
: 'size';
|
||||
const query = `p=0&page=1&${pageSizeParam}=${SELF_LOG_PAGE_SIZE}&order=-created_at`;
|
||||
const url = `${baseUrl}/api/log/self?${query}`;
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
@@ -235,7 +328,25 @@ async function fetchSelfLogPayload(baseUrl: string, token: string, input: ProxyU
|
||||
headers['New-Api-User'] = String(userId);
|
||||
}
|
||||
}
|
||||
const response = await fetch(`${baseUrl}/api/log/self?${query}`, withExplicitProxyRequestInit(input.site.proxyUrl, {
|
||||
|
||||
const shouldTryShieldCookie = platform === 'anyrouter' || token.includes('=');
|
||||
if (shouldTryShieldCookie) {
|
||||
for (const cookie of buildNewApiCookieCandidates(token)) {
|
||||
const result = await fetchJsonWithShieldCookieRetry(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...Object.fromEntries(
|
||||
Object.entries(headers).filter(([key]) => key.toLowerCase() !== 'authorization'),
|
||||
),
|
||||
Cookie: cookie,
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (result.data) return result.data;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, withExplicitProxyRequestInit(input.site.proxyUrl, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
@@ -347,6 +458,7 @@ export async function resolveProxyUsageWithSelfLogFallback(
|
||||
...normalizedUsage,
|
||||
recoveredFromSelfLog: false,
|
||||
estimatedCostFromQuota: 0,
|
||||
selfLogBillingMeta: null,
|
||||
};
|
||||
|
||||
const platform = String(input.site.platform || '').toLowerCase();
|
||||
@@ -383,6 +495,7 @@ export async function resolveProxyUsageWithSelfLogFallback(
|
||||
totalTokens: resolvedTokens.totalTokens,
|
||||
recoveredFromSelfLog: true,
|
||||
estimatedCostFromQuota: toQuotaCost(matched.quota),
|
||||
selfLogBillingMeta: matched.billingMeta,
|
||||
};
|
||||
} catch {
|
||||
return fallback;
|
||||
|
||||
Reference in New Issue
Block a user