mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 14:22:09 +03:00
feat(proxy): implement TLS fingerprint spoofing via wreq-js
- Add TlsClient module (Chrome 124 fingerprint via wreq-js) - Integrate TLS client as opt-in layer in proxyFetch.js (ENABLE_TLS_FINGERPRINT) - Add per-request TLS tracking via AsyncLocalStorage - Add TLS fingerprint column and badge to Proxy Logger dashboard - Add TLS Fingerprint section to ProxyLogDetail modal - Add wreq-js dependency - Document ENABLE_TLS_FINGERPRINT in .env.example Adapted from upstream PR decolua/9router#137, preserving all OmniRoute-specific proxy features (AsyncLocalStorage context, proxyDispatcher, SOCKS5, Symbol-based patch state).
This commit is contained in:
@@ -57,6 +57,11 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# ALL_PROXY=socks5://127.0.0.1:7890
|
||||
# NO_PROXY=localhost,127.0.0.1
|
||||
|
||||
# TLS fingerprint spoofing (opt-in) — mimics Chrome 124 TLS handshake via wreq-js
|
||||
# Reduces risk of JA3/JA4 fingerprint-based blocking by providers (e.g., Google)
|
||||
# Requires wreq-js to be installed (included in dependencies)
|
||||
# ENABLE_TLS_FINGERPRINT=true
|
||||
|
||||
# Optional CLI runtime overrides (Docker/host integration)
|
||||
# CLI_MODE=auto
|
||||
# CLI_EXTRA_PATHS=/host-cli/bin
|
||||
|
||||
@@ -5,6 +5,14 @@ import {
|
||||
proxyConfigToUrl,
|
||||
proxyUrlForLogs,
|
||||
} from "./proxyDispatcher.js";
|
||||
import tlsClient from "./tlsClient.js";
|
||||
|
||||
function isTlsFingerprintEnabled() {
|
||||
return process.env.ENABLE_TLS_FINGERPRINT === "true";
|
||||
}
|
||||
|
||||
/** Per-request tracking of whether TLS fingerprint was used */
|
||||
const tlsFingerprintContext = new AsyncLocalStorage();
|
||||
|
||||
const isCloud = typeof caches !== "undefined" && typeof caches === "object";
|
||||
const PATCH_STATE_KEY = Symbol.for("omniroute.proxyFetch.state");
|
||||
@@ -135,6 +143,21 @@ async function patchedFetch(input, options = {}) {
|
||||
const { source, proxyUrl } = resolved;
|
||||
|
||||
if (!proxyUrl) {
|
||||
// TLS fingerprint spoofing for direct connections (no proxy configured)
|
||||
if (isTlsFingerprintEnabled() && tlsClient.available) {
|
||||
try {
|
||||
const store = tlsFingerprintContext.getStore();
|
||||
if (store) store.used = true;
|
||||
return await tlsClient.fetch(targetUrl, options);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(
|
||||
`[ProxyFetch] TLS fingerprint failed, falling back to native fetch: ${message}`
|
||||
);
|
||||
const store = tlsFingerprintContext.getStore();
|
||||
if (store) store.used = false;
|
||||
}
|
||||
}
|
||||
return originalFetch(input, options);
|
||||
}
|
||||
|
||||
@@ -153,4 +176,19 @@ if (!isCloud && !patchState.isPatched) {
|
||||
patchState.isPatched = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a function with TLS fingerprint tracking context.
|
||||
* After fn completes, returns { result, tlsFingerprintUsed }.
|
||||
*/
|
||||
export async function runWithTlsTracking(fn) {
|
||||
const store = { used: false };
|
||||
const result = await tlsFingerprintContext.run(store, fn);
|
||||
return { result, tlsFingerprintUsed: store.used };
|
||||
}
|
||||
|
||||
/** Check if TLS fingerprint is enabled and available */
|
||||
export function isTlsFingerprintActive() {
|
||||
return isTlsFingerprintEnabled() && tlsClient.available;
|
||||
}
|
||||
|
||||
export default isCloud ? originalFetch : patchedFetch;
|
||||
|
||||
94
open-sse/utils/tlsClient.js
Normal file
94
open-sse/utils/tlsClient.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import { createRequire } from "module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
let createSession;
|
||||
try {
|
||||
({ createSession } = require("wreq-js"));
|
||||
} catch {
|
||||
createSession = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get proxy URL from environment variables.
|
||||
* Priority: HTTPS_PROXY > HTTP_PROXY > ALL_PROXY
|
||||
*/
|
||||
function getProxyFromEnv() {
|
||||
return (
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy ||
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.ALL_PROXY ||
|
||||
process.env.all_proxy ||
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* TLS Client — Chrome 124 TLS fingerprint spoofing via wreq-js
|
||||
* Singleton instance used to disguise Node.js TLS handshake as Chrome browser.
|
||||
*
|
||||
* wreq-js natively supports proxy — TLS fingerprinting works through proxy.
|
||||
* Proxy URL is read from environment variables (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY).
|
||||
*/
|
||||
class TlsClient {
|
||||
constructor() {
|
||||
this.session = null;
|
||||
this.available = !!createSession;
|
||||
}
|
||||
|
||||
async getSession() {
|
||||
if (!this.available) return null;
|
||||
if (this.session) return this.session;
|
||||
|
||||
const proxy = getProxyFromEnv();
|
||||
const sessionOpts = {
|
||||
browser: "chrome_124",
|
||||
os: "macos",
|
||||
};
|
||||
if (proxy) {
|
||||
sessionOpts.proxy = proxy;
|
||||
console.log(`[TlsClient] Using proxy: ${proxy}`);
|
||||
}
|
||||
|
||||
this.session = await createSession(sessionOpts);
|
||||
console.log("[TlsClient] Session created (Chrome 124 TLS fingerprint)");
|
||||
return this.session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with Chrome 124 TLS fingerprint.
|
||||
* wreq-js Response is already fetch-compatible (headers, text(), json(), clone(), body).
|
||||
*/
|
||||
async fetch(url, options = {}) {
|
||||
const session = await this.getSession();
|
||||
if (!session) throw new Error("wreq-js not available");
|
||||
|
||||
const method = (options.method || "GET").toUpperCase();
|
||||
|
||||
const wreqOptions = {
|
||||
method,
|
||||
headers: options.headers,
|
||||
body: options.body,
|
||||
redirect: options.redirect === "manual" ? "manual" : "follow",
|
||||
};
|
||||
|
||||
// Pass signal through if available
|
||||
if (options.signal) {
|
||||
wreqOptions.signal = options.signal;
|
||||
}
|
||||
|
||||
const response = await session.fetch(url, wreqOptions);
|
||||
return response;
|
||||
}
|
||||
|
||||
async exit() {
|
||||
if (this.session) {
|
||||
await this.session.close();
|
||||
this.session = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new TlsClient();
|
||||
@@ -83,6 +83,7 @@
|
||||
"selfsigned": "^5.5.0",
|
||||
"undici": "^7.19.2",
|
||||
"uuid": "^13.0.0",
|
||||
"wreq-js": "^2.0.1",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.10"
|
||||
},
|
||||
|
||||
@@ -38,6 +38,7 @@ export function logProxyEvent(entry) {
|
||||
connectionId: entry.connectionId || null,
|
||||
comboId: entry.comboId || null,
|
||||
account: entry.account || null,
|
||||
tlsFingerprint: entry.tlsFingerprint || false,
|
||||
};
|
||||
|
||||
proxyLogs.unshift(log); // newest first
|
||||
|
||||
@@ -139,6 +139,21 @@ export default function ProxyLogDetail({ log, onClose }) {
|
||||
<div className="text-sm text-text-muted">—</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[10px] text-text-muted uppercase tracking-wider mb-1">
|
||||
TLS Fingerprint
|
||||
</div>
|
||||
{log.tlsFingerprint ? (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 rounded text-[10px] font-bold uppercase"
|
||||
style={{ backgroundColor: "rgba(6, 182, 212, 0.15)", color: "#22d3ee" }}
|
||||
>
|
||||
<span style={{ fontSize: "12px" }}>🔒</span> Chrome 124
|
||||
</span>
|
||||
) : (
|
||||
<div className="text-sm text-text-muted">Direct (native)</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="text-[10px] text-text-muted uppercase tracking-wider mb-1">
|
||||
Target URL
|
||||
|
||||
@@ -25,6 +25,7 @@ const STATUS_FILTERS = [
|
||||
const COLUMNS = [
|
||||
{ key: "status", label: "Status" },
|
||||
{ key: "proxy", label: "Proxy" },
|
||||
{ key: "tls", label: "TLS" },
|
||||
{ key: "type", label: "Type" },
|
||||
{ key: "level", label: "Level" },
|
||||
{ key: "provider", label: "Provider" },
|
||||
@@ -141,6 +142,7 @@ export default function ProxyLogger() {
|
||||
const errorCount = logs.filter((l) => l.status === "error").length;
|
||||
const timeoutCount = logs.filter((l) => l.status === "timeout").length;
|
||||
const directCount = logs.filter((l) => l.level === "direct").length;
|
||||
const tlsCount = logs.filter((l) => l.tlsFingerprint).length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -243,6 +245,11 @@ export default function ProxyLogger() {
|
||||
{directCount} direct
|
||||
</span>
|
||||
)}
|
||||
{tlsCount > 0 && (
|
||||
<span className="px-2 py-1 rounded bg-cyan-500/10 text-cyan-400 font-mono">
|
||||
🔒 {tlsCount} TLS
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sort */}
|
||||
@@ -369,6 +376,11 @@ export default function ProxyLogger() {
|
||||
Proxy
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.tls && (
|
||||
<th className="px-3 py-2.5 font-semibold text-text-muted uppercase tracking-wider text-[10px]">
|
||||
TLS
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.type && (
|
||||
<th className="px-3 py-2.5 font-semibold text-text-muted uppercase tracking-wider text-[10px]">
|
||||
Type
|
||||
@@ -443,6 +455,24 @@ export default function ProxyLogger() {
|
||||
{log.proxy ? `${log.proxy.host}:${log.proxy.port}` : "—"}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.tls && (
|
||||
<td className="px-3 py-2">
|
||||
{log.tlsFingerprint ? (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[9px] font-bold uppercase"
|
||||
style={{
|
||||
backgroundColor: "rgba(6, 182, 212, 0.15)",
|
||||
color: "#22d3ee",
|
||||
}}
|
||||
title="Chrome 124 TLS Fingerprint"
|
||||
>
|
||||
<span style={{ fontSize: "10px" }}>🔒</span> TLS
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-text-muted text-[10px]">—</span>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.type && (
|
||||
<td className="px-3 py-2">
|
||||
<span
|
||||
|
||||
@@ -16,7 +16,11 @@ import {
|
||||
getModelTargetFormat,
|
||||
PROVIDER_ID_TO_ALIAS,
|
||||
} from "@omniroute/open-sse/config/providerModels.js";
|
||||
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.js";
|
||||
import {
|
||||
runWithProxyContext,
|
||||
runWithTlsTracking,
|
||||
isTlsFingerprintActive,
|
||||
} from "@omniroute/open-sse/utils/proxyFetch.js";
|
||||
import * as log from "../utils/logger.js";
|
||||
import { updateProviderCredentials, checkAndRefreshToken } from "../services/tokenRefresh.js";
|
||||
import { getSettings, getCombos, getApiKeyMetadata } from "@/lib/localDb.js";
|
||||
@@ -192,7 +196,15 @@ export async function handleChat(request, clientRawRequest = null) {
|
||||
telemetry.endPhase();
|
||||
|
||||
// Single model request
|
||||
const response = await handleSingleModelChat(body, modelStr, clientRawRequest, request, null, apiKeyInfo, telemetry);
|
||||
const response = await handleSingleModelChat(
|
||||
body,
|
||||
modelStr,
|
||||
clientRawRequest,
|
||||
request,
|
||||
null,
|
||||
apiKeyInfo,
|
||||
telemetry
|
||||
);
|
||||
recordTelemetry(telemetry);
|
||||
return response;
|
||||
}
|
||||
@@ -244,7 +256,11 @@ async function handleSingleModelChat(
|
||||
// Pipeline: Check model availability (TTL cooldown)
|
||||
if (!isModelAvailable(provider, model)) {
|
||||
log.warn("AVAILABILITY", `${provider}/${model} is in cooldown, rejecting request`);
|
||||
return unavailableResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, `Model ${provider}/${model} is temporarily unavailable (cooldown)`, 30);
|
||||
return unavailableResponse(
|
||||
HTTP_STATUS.SERVICE_UNAVAILABLE,
|
||||
`Model ${provider}/${model} is temporarily unavailable (cooldown)`,
|
||||
30
|
||||
);
|
||||
}
|
||||
|
||||
// Pipeline: Check circuit breaker for this provider
|
||||
@@ -255,7 +271,11 @@ async function handleSingleModelChat(
|
||||
});
|
||||
if (!breaker.canExecute()) {
|
||||
log.warn("CIRCUIT", `Circuit breaker OPEN for ${provider}, rejecting request`);
|
||||
return unavailableResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, `Provider ${provider} circuit breaker is open`, 30);
|
||||
return unavailableResponse(
|
||||
HTTP_STATUS.SERVICE_UNAVAILABLE,
|
||||
`Provider ${provider} circuit breaker is open`,
|
||||
30
|
||||
);
|
||||
}
|
||||
|
||||
const userAgent = request?.headers?.get("user-agent") || "";
|
||||
@@ -270,7 +290,14 @@ async function handleSingleModelChat(
|
||||
|
||||
// All accounts unavailable — return error
|
||||
if (!credentials || credentials.allRateLimited) {
|
||||
return handleNoCredentials(credentials, excludeConnectionId, provider, model, lastError, lastStatus);
|
||||
return handleNoCredentials(
|
||||
credentials,
|
||||
excludeConnectionId,
|
||||
provider,
|
||||
model,
|
||||
lastError,
|
||||
lastStatus
|
||||
);
|
||||
}
|
||||
|
||||
const accountId = credentials.connectionId.slice(0, 8);
|
||||
@@ -283,30 +310,52 @@ async function handleSingleModelChat(
|
||||
// 3. Execute chat via core (with circuit breaker)
|
||||
if (telemetry) telemetry.startPhase("connect");
|
||||
let result;
|
||||
let tlsFingerprintUsed = false;
|
||||
try {
|
||||
result = await breaker.execute(() =>
|
||||
const chatFn = () =>
|
||||
runWithProxyContext(proxyInfo?.proxy || null, () =>
|
||||
handleChatCore({
|
||||
body: { ...body, model: `${provider}/${model}` },
|
||||
modelInfo: { provider, model },
|
||||
credentials: refreshedCredentials, log, clientRawRequest,
|
||||
connectionId: credentials.connectionId, apiKeyInfo, userAgent, comboName,
|
||||
credentials: refreshedCredentials,
|
||||
log,
|
||||
clientRawRequest,
|
||||
connectionId: credentials.connectionId,
|
||||
apiKeyInfo,
|
||||
userAgent,
|
||||
comboName,
|
||||
onCredentialsRefreshed: async (newCreds) => {
|
||||
await updateProviderCredentials(credentials.connectionId, {
|
||||
accessToken: newCreds.accessToken, refreshToken: newCreds.refreshToken,
|
||||
providerSpecificData: newCreds.providerSpecificData, testStatus: "active",
|
||||
accessToken: newCreds.accessToken,
|
||||
refreshToken: newCreds.refreshToken,
|
||||
providerSpecificData: newCreds.providerSpecificData,
|
||||
testStatus: "active",
|
||||
});
|
||||
},
|
||||
onRequestSuccess: async () => {
|
||||
await clearAccountError(credentials.connectionId, credentials);
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
// Wrap with TLS tracking when no proxy and TLS fingerprint is active
|
||||
if (!proxyInfo?.proxy && isTlsFingerprintActive()) {
|
||||
const tracked = await breaker.execute(async () => {
|
||||
return await runWithTlsTracking(chatFn);
|
||||
});
|
||||
result = tracked.result;
|
||||
tlsFingerprintUsed = tracked.tlsFingerprintUsed;
|
||||
} else {
|
||||
result = await breaker.execute(chatFn);
|
||||
}
|
||||
} catch (cbErr) {
|
||||
if (cbErr instanceof CircuitBreakerOpenError) {
|
||||
log.warn("CIRCUIT", `${provider} circuit open during retry: ${cbErr.message}`);
|
||||
return unavailableResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, `Provider ${provider} circuit breaker is open`, Math.ceil(cbErr.retryAfterMs / 1000));
|
||||
return unavailableResponse(
|
||||
HTTP_STATUS.SERVICE_UNAVAILABLE,
|
||||
`Provider ${provider} circuit breaker is open`,
|
||||
Math.ceil(cbErr.retryAfterMs / 1000)
|
||||
);
|
||||
}
|
||||
throw cbErr;
|
||||
}
|
||||
@@ -315,14 +364,27 @@ async function handleSingleModelChat(
|
||||
const proxyLatency = Date.now() - proxyStartTime;
|
||||
|
||||
// 4. Log proxy + translation events (fire-and-forget)
|
||||
safeLogEvents({ result, proxyInfo, proxyLatency, provider, model, sourceFormat, targetFormat, credentials, comboName, clientRawRequest });
|
||||
safeLogEvents({
|
||||
result,
|
||||
proxyInfo,
|
||||
proxyLatency,
|
||||
provider,
|
||||
model,
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
credentials,
|
||||
comboName,
|
||||
clientRawRequest,
|
||||
tlsFingerprintUsed,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// Pipeline: Record cost on success
|
||||
if (apiKeyInfo?.id) {
|
||||
try {
|
||||
const usage = result.usage || {};
|
||||
const estimatedCost = ((usage.prompt_tokens || 0) + (usage.completion_tokens || 0)) * 0.000001; // rough estimate
|
||||
const estimatedCost =
|
||||
((usage.prompt_tokens || 0) + (usage.completion_tokens || 0)) * 0.000001; // rough estimate
|
||||
if (estimatedCost > 0) recordCost(apiKeyInfo.id, estimatedCost);
|
||||
} catch {}
|
||||
}
|
||||
@@ -334,12 +396,18 @@ async function handleSingleModelChat(
|
||||
// Pipeline: Mark model unavailable on repeated failures (429, 503)
|
||||
if (result.status === 429 || result.status === 503) {
|
||||
setModelUnavailable(provider, model, 60000, `HTTP ${result.status}`);
|
||||
log.info("AVAILABILITY", `${provider}/${model} marked unavailable for 60s (HTTP ${result.status})`);
|
||||
log.info(
|
||||
"AVAILABILITY",
|
||||
`${provider}/${model} marked unavailable for 60s (HTTP ${result.status})`
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Fallback to next account
|
||||
const { shouldFallback } = await markAccountUnavailable(
|
||||
credentials.connectionId, result.status, result.error, provider
|
||||
credentials.connectionId,
|
||||
result.status,
|
||||
result.error,
|
||||
provider
|
||||
);
|
||||
|
||||
if (shouldFallback) {
|
||||
@@ -356,19 +424,35 @@ async function handleSingleModelChat(
|
||||
|
||||
// ──── Extracted helpers (T-28) ────
|
||||
|
||||
function handleNoCredentials(credentials, excludeConnectionId, provider, model, lastError, lastStatus) {
|
||||
function handleNoCredentials(
|
||||
credentials,
|
||||
excludeConnectionId,
|
||||
provider,
|
||||
model,
|
||||
lastError,
|
||||
lastStatus
|
||||
) {
|
||||
if (credentials?.allRateLimited) {
|
||||
const errorMsg = lastError || credentials.lastError || "Unavailable";
|
||||
const status = lastStatus || Number(credentials.lastErrorCode) || HTTP_STATUS.SERVICE_UNAVAILABLE;
|
||||
const status =
|
||||
lastStatus || Number(credentials.lastErrorCode) || HTTP_STATUS.SERVICE_UNAVAILABLE;
|
||||
log.warn("CHAT", `[${provider}/${model}] ${errorMsg} (${credentials.retryAfterHuman})`);
|
||||
return unavailableResponse(status, `[${provider}/${model}] ${errorMsg}`, credentials.retryAfter, credentials.retryAfterHuman);
|
||||
return unavailableResponse(
|
||||
status,
|
||||
`[${provider}/${model}] ${errorMsg}`,
|
||||
credentials.retryAfter,
|
||||
credentials.retryAfterHuman
|
||||
);
|
||||
}
|
||||
if (!excludeConnectionId) {
|
||||
log.error("AUTH", `No credentials for provider: ${provider}`);
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, `No credentials for provider: ${provider}`);
|
||||
}
|
||||
log.warn("CHAT", "No more accounts available", { provider });
|
||||
return errorResponse(lastStatus || HTTP_STATUS.SERVICE_UNAVAILABLE, lastError || "All accounts unavailable");
|
||||
return errorResponse(
|
||||
lastStatus || HTTP_STATUS.SERVICE_UNAVAILABLE,
|
||||
lastError || "All accounts unavailable"
|
||||
);
|
||||
}
|
||||
|
||||
async function safeResolveProxy(connectionId) {
|
||||
@@ -380,25 +464,51 @@ async function safeResolveProxy(connectionId) {
|
||||
}
|
||||
}
|
||||
|
||||
function safeLogEvents({ result, proxyInfo, proxyLatency, provider, model, sourceFormat, targetFormat, credentials, comboName, clientRawRequest }) {
|
||||
function safeLogEvents({
|
||||
result,
|
||||
proxyInfo,
|
||||
proxyLatency,
|
||||
provider,
|
||||
model,
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
credentials,
|
||||
comboName,
|
||||
clientRawRequest,
|
||||
tlsFingerprintUsed = false,
|
||||
}) {
|
||||
try {
|
||||
logProxyEvent({
|
||||
status: result.success ? "success" : result.status === 408 || result.status === 504 ? "timeout" : "error",
|
||||
proxy: proxyInfo?.proxy || null, level: proxyInfo?.level || "direct",
|
||||
levelId: proxyInfo?.levelId || null, provider, targetUrl: `${provider}/${model}`,
|
||||
latencyMs: proxyLatency, error: result.success ? null : result.error || null,
|
||||
connectionId: credentials.connectionId, comboId: comboName || null,
|
||||
status: result.success
|
||||
? "success"
|
||||
: result.status === 408 || result.status === 504
|
||||
? "timeout"
|
||||
: "error",
|
||||
proxy: proxyInfo?.proxy || null,
|
||||
level: proxyInfo?.level || "direct",
|
||||
levelId: proxyInfo?.levelId || null,
|
||||
provider,
|
||||
targetUrl: `${provider}/${model}`,
|
||||
latencyMs: proxyLatency,
|
||||
error: result.success ? null : result.error || null,
|
||||
connectionId: credentials.connectionId,
|
||||
comboId: comboName || null,
|
||||
account: credentials.connectionId?.slice(0, 8) || null,
|
||||
tlsFingerprint: tlsFingerprintUsed,
|
||||
});
|
||||
} catch {}
|
||||
try {
|
||||
logTranslationEvent({
|
||||
provider, model, sourceFormat, targetFormat,
|
||||
provider,
|
||||
model,
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
status: result.success ? "success" : "error",
|
||||
statusCode: result.success ? 200 : result.status || 500,
|
||||
latency: proxyLatency, endpoint: clientRawRequest?.endpoint || "/v1/chat/completions",
|
||||
connectionId: credentials.connectionId || null, comboName: comboName || null,
|
||||
latency: proxyLatency,
|
||||
endpoint: clientRawRequest?.endpoint || "/v1/chat/completions",
|
||||
connectionId: credentials.connectionId || null,
|
||||
comboName: comboName || null,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user