Spaces:
Running
Running
File size: 5,958 Bytes
a2c232b f250aec c7d34c1 a2c232b c7d34c1 a2c232b c7d34c1 a2c232b c7d34c1 a2c232b c7d34c1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | import { fetchOpenAIUpstream } from '../openai-image-transport';
import type { ActualCostDetails, ActualCostResolver, ResolveActualCostInput } from './types';
export const NEW_API_QUOTA_PER_UNIT = 500_000;
const LOG_TOKEN_PATH = '/api/log/token';
const LOG_MATCH_WINDOW_BEFORE_MS = 5_000;
const LOG_MATCH_WINDOW_AFTER_MS = 15_000;
const POLL_ATTEMPTS = 3;
const POLL_DELAY_MS = 600;
const FETCH_TIMEOUT_MS = 1_500;
type NewApiLogEntry = {
id?: unknown;
created_at?: unknown;
type?: unknown;
model_name?: unknown;
quota?: unknown;
request_id?: unknown;
};
type NewApiLogResponse = {
success?: unknown;
data?: unknown;
};
function unavailable(reason: string): ActualCostDetails {
return {
currency: 'usd-equivalent',
source: 'unavailable',
confidence: 'none',
upstreamProvider: 'new-api',
reason
};
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
export function quotaToUsdEquivalent(quota: number): number {
return Math.round((quota / NEW_API_QUOTA_PER_UNIT) * 1_000_000) / 1_000_000;
}
export function buildNewApiLogTokenUrl(apiBaseUrl: string): URL | undefined {
try {
const parsed = new URL(apiBaseUrl);
return new URL(LOG_TOKEN_PATH, `${parsed.protocol}//${parsed.host}`);
} catch {
return undefined;
}
}
function readNumber(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function isConsumeLog(entry: NewApiLogEntry): boolean {
const type = readNumber(entry.type);
return type === 2;
}
export function matchNewApiCostLog(input: {
logs: NewApiLogEntry[];
model: string;
startedAtMs: number;
finishedAtMs: number;
}): ActualCostDetails {
const windowStart = Math.floor((input.startedAtMs - LOG_MATCH_WINDOW_BEFORE_MS) / 1000);
const windowEnd = Math.ceil((input.finishedAtMs + LOG_MATCH_WINDOW_AFTER_MS) / 1000);
const matches = input.logs.filter((entry) => {
const createdAt = readNumber(entry.created_at);
const quota = readNumber(entry.quota);
return (
isConsumeLog(entry) &&
entry.model_name === input.model &&
createdAt !== undefined &&
createdAt >= windowStart &&
createdAt <= windowEnd &&
quota !== undefined &&
quota > 0
);
});
if (matches.length === 0) {
return unavailable('未匹配到当前请求时间窗口内的 new-api 扣费日志。');
}
if (matches.length > 1) {
return {
currency: 'usd-equivalent',
source: 'unavailable',
confidence: 'low',
upstreamProvider: 'new-api',
reason: `匹配到 ${matches.length} 条候选扣费日志,无法唯一确认。`
};
}
const match = matches[0];
const quota = readNumber(match.quota) as number;
const id = readNumber(match.id);
const requestId = typeof match.request_id === 'string' && match.request_id ? match.request_id : undefined;
return {
actualAmount: quotaToUsdEquivalent(quota),
actualQuota: quota,
currency: 'usd-equivalent',
source: 'new-api-log-token',
confidence: 'high',
upstreamProvider: 'new-api',
...(id === undefined ? {} : { matchedLogId: id }),
...(requestId ? { matchedRequestId: requestId } : {})
};
}
async function fetchLogs(url: URL, apiKey: string, upstreamProxyUrl?: string): Promise<NewApiLogEntry[] | undefined> {
const abortController = new AbortController();
const timeout = setTimeout(() => abortController.abort(), FETCH_TIMEOUT_MS);
try {
const response = await fetchOpenAIUpstream(
url,
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: 'application/json'
},
signal: abortController.signal
},
upstreamProxyUrl
);
if (!response.ok) return undefined;
const body = (await response.json()) as NewApiLogResponse;
if (body.success !== true || !Array.isArray(body.data)) return undefined;
return body.data as NewApiLogEntry[];
} finally {
clearTimeout(timeout);
}
}
export class NewApiCostResolver implements ActualCostResolver {
readonly provider = 'new-api' as const;
async resolve(input: ResolveActualCostInput): Promise<ActualCostDetails> {
if (!input.apiBaseUrl || !input.apiKey) {
return unavailable('缺少上游 API URL 或 API key,无法查询 new-api 扣费日志。');
}
const url = buildNewApiLogTokenUrl(input.apiBaseUrl);
if (!url) {
return unavailable('上游 API URL 无效,无法查询 new-api 扣费日志。');
}
try {
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) {
const logs = await fetchLogs(url, input.apiKey, input.upstreamProxyUrl);
if (logs) {
const result = matchNewApiCostLog({
logs,
model: input.model,
startedAtMs: input.startedAtMs,
finishedAtMs: input.finishedAtMs
});
if (result.source === 'new-api-log-token' || attempt === POLL_ATTEMPTS - 1) {
return result;
}
}
if (attempt < POLL_ATTEMPTS - 1) {
await sleep(POLL_DELAY_MS);
}
}
return unavailable('new-api 扣费日志接口没有返回可解析日志。');
} catch (error) {
return unavailable(error instanceof Error ? error.message : String(error));
}
}
}
|