Feat : Auto restart after crash
This commit is contained in:
@@ -60,6 +60,7 @@ const FIELD = {
|
||||
MSG_ID: 13,
|
||||
MSG_TOOL_RESULTS: 18,
|
||||
MSG_IS_AGENTIC: 29,
|
||||
MSG_SERVER_BUBBLE_ID: 32,
|
||||
MSG_UNIFIED_MODE: 47,
|
||||
MSG_SUPPORTED_TOOLS: 51,
|
||||
|
||||
@@ -78,10 +79,19 @@ const FIELD = {
|
||||
CLIENT_RESULT_TOOL_CALL_ID: 35,
|
||||
CLIENT_RESULT_MODEL_CALL_ID: 48,
|
||||
CLIENT_RESULT_TOOL_INDEX: 49,
|
||||
// Aliases used by encodeClientSideToolV2Result
|
||||
CV2R_TOOL: 1,
|
||||
CV2R_MCP_RESULT: 28,
|
||||
CV2R_CALL_ID: 35,
|
||||
CV2R_MODEL_CALL_ID: 48,
|
||||
CV2R_TOOL_INDEX: 49,
|
||||
|
||||
// MCPResult (nested inside ClientSideToolV2Result.mcp_result)
|
||||
MCP_RESULT_SELECTED_TOOL: 1,
|
||||
MCP_RESULT_RESULT: 2,
|
||||
// Aliases used by encodeMcpResult
|
||||
MCPR_SELECTED_TOOL: 1,
|
||||
MCPR_RESULT: 2,
|
||||
|
||||
// ClientSideToolV2Call (nested inside ToolResult.tool_call)
|
||||
CLIENT_CALL_TOOL: 1,
|
||||
@@ -91,6 +101,14 @@ const FIELD = {
|
||||
CLIENT_CALL_RAW_ARGS: 10,
|
||||
CLIENT_CALL_TOOL_INDEX: 48,
|
||||
CLIENT_CALL_MODEL_CALL_ID: 49,
|
||||
// Aliases used by encodeClientSideToolV2Call
|
||||
CV2C_TOOL: 1,
|
||||
CV2C_MCP_PARAMS: 27,
|
||||
CV2C_CALL_ID: 3,
|
||||
CV2C_NAME: 9,
|
||||
CV2C_RAW_ARGS: 10,
|
||||
CV2C_TOOL_INDEX: 48,
|
||||
CV2C_MODEL_CALL_ID: 49,
|
||||
|
||||
// Model
|
||||
MODEL_NAME: 1,
|
||||
|
||||
@@ -49,7 +49,7 @@ export function transformToOllama(response, model) {
|
||||
const formattedCalls = toolCallsArr.map(tc => ({
|
||||
function: {
|
||||
name: tc.function.name,
|
||||
arguments: JSON.parse(tc.function.arguments || "{}")
|
||||
arguments: (() => { try { return JSON.parse(tc.function.arguments || "{}"); } catch { return {}; } })()
|
||||
}
|
||||
}));
|
||||
const ollama = JSON.stringify({
|
||||
@@ -75,6 +75,9 @@ export function transformToOllama(response, model) {
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.body) {
|
||||
return new Response("", { status: response.status, headers: { "Content-Type": "application/x-ndjson" } });
|
||||
}
|
||||
return new Response(response.body.pipeThrough(transform), {
|
||||
headers: { "Content-Type": "application/x-ndjson", "Access-Control-Allow-Origin": "*" }
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@ const isCloud = typeof caches !== "undefined" && typeof caches === "object";
|
||||
const originalFetch = globalThis.fetch;
|
||||
const proxyDispatchers = new Map();
|
||||
|
||||
// DNS cache: { [hostname]: { ip, expiry } }
|
||||
const DNS_CACHE = {};
|
||||
// DNS cache — use Map to avoid prototype pollution via malformed hostnames
|
||||
const DNS_CACHE = new Map();
|
||||
const MITM_BYPASS_HOSTS = ["cloudcode-pa.googleapis.com", "daily-cloudcode-pa.googleapis.com", "googleapis.com"];
|
||||
const MITM_BYPASS_HEADER = "x-request-source";
|
||||
const MITM_BYPASS_VALUE = "local";
|
||||
@@ -24,7 +24,7 @@ function normalizeString(value) {
|
||||
* Resolve real IP using Google DNS (bypass system DNS)
|
||||
*/
|
||||
async function resolveRealIP(hostname) {
|
||||
const cached = DNS_CACHE[hostname];
|
||||
const cached = DNS_CACHE.get(hostname);
|
||||
if (cached && Date.now() < cached.expiry) return cached.ip;
|
||||
|
||||
try {
|
||||
@@ -34,7 +34,7 @@ async function resolveRealIP(hostname) {
|
||||
resolver.setServers(GOOGLE_DNS_SERVERS);
|
||||
const resolve4 = promisify(resolver.resolve4.bind(resolver));
|
||||
const addresses = await resolve4(hostname);
|
||||
DNS_CACHE[hostname] = { ip: addresses[0], expiry: Date.now() + MEMORY_CONFIG.dnsCacheTtlMs };
|
||||
DNS_CACHE.set(hostname, { ip: addresses[0], expiry: Date.now() + MEMORY_CONFIG.dnsCacheTtlMs });
|
||||
return addresses[0];
|
||||
} catch (error) {
|
||||
console.warn(`[ProxyFetch] DNS resolve failed for ${hostname}:`, error.message);
|
||||
@@ -53,23 +53,27 @@ function shouldBypassMitmDns(url, options) {
|
||||
headers[MITM_BYPASS_HEADER.charAt(0).toUpperCase() + MITM_BYPASS_HEADER.slice(1)] === MITM_BYPASS_VALUE;
|
||||
|
||||
if (!hasLocalMarker) {
|
||||
// Debug: log when bypass is not triggered
|
||||
const hostname = new URL(url).hostname;
|
||||
if (MITM_BYPASS_HOSTS.some(host => hostname.includes(host))) {
|
||||
console.warn(`[ProxyFetch] MITM bypass NOT triggered for ${hostname} - missing header`);
|
||||
}
|
||||
try {
|
||||
const hostname = new URL(url).hostname;
|
||||
if (MITM_BYPASS_HOSTS.some(host => hostname.includes(host))) {
|
||||
console.warn(`[ProxyFetch] MITM bypass NOT triggered for ${hostname} - missing header`);
|
||||
}
|
||||
} catch { /* invalid URL — skip debug log */ }
|
||||
return false;
|
||||
}
|
||||
|
||||
const hostname = new URL(url).hostname;
|
||||
return MITM_BYPASS_HOSTS.some(host => hostname.includes(host));
|
||||
try {
|
||||
const hostname = new URL(url).hostname;
|
||||
return MITM_BYPASS_HOSTS.some(host => hostname.includes(host));
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function shouldBypassByNoProxy(targetUrl, noProxyValue) {
|
||||
const noProxy = normalizeString(noProxyValue);
|
||||
if (!noProxy) return false;
|
||||
|
||||
const hostname = new URL(targetUrl).hostname.toLowerCase();
|
||||
let hostname;
|
||||
try { hostname = new URL(targetUrl).hostname.toLowerCase(); } catch { return false; }
|
||||
const patterns = noProxy.split(",").map((p) => p.trim().toLowerCase()).filter(Boolean);
|
||||
|
||||
return patterns.some((pattern) => {
|
||||
@@ -86,7 +90,8 @@ function getEnvProxyUrl(targetUrl) {
|
||||
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
|
||||
if (shouldBypassByNoProxy(targetUrl, noProxy)) return null;
|
||||
|
||||
const protocol = new URL(targetUrl).protocol;
|
||||
let protocol;
|
||||
try { protocol = new URL(targetUrl).protocol; } catch { return null; }
|
||||
|
||||
if (protocol === "https:") {
|
||||
return process.env.HTTPS_PROXY || process.env.https_proxy ||
|
||||
@@ -152,7 +157,7 @@ async function getDispatcher(proxyUrl) {
|
||||
async function createBypassRequest(parsedUrl, realIP, options) {
|
||||
const https = await import("https");
|
||||
const net = await import("net");
|
||||
const { Readable } = require("stream");
|
||||
const { Readable } = await import("stream");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = new net.Socket();
|
||||
|
||||
@@ -44,7 +44,7 @@ async function createLogSession(sourceFormat, targetFormat, model) {
|
||||
}
|
||||
|
||||
const timestamp = formatTimestamp();
|
||||
const safeModel = model.replace(/[/:]/g, "-");
|
||||
const safeModel = (model || "unknown").replace(/[/:]/g, "-");
|
||||
const folderName = `${sourceFormat}_${targetFormat}_${safeModel}_${timestamp}`;
|
||||
const sessionPath = path.join(LOGS_DIR, folderName);
|
||||
|
||||
|
||||
@@ -52,6 +52,13 @@ export function deriveSessionId(connectionId) {
|
||||
return existing.sessionId;
|
||||
}
|
||||
|
||||
// Evict oldest entry if store exceeds max size (safety cap between cleanup cycles)
|
||||
const MAX_SESSIONS = 1000;
|
||||
if (runtimeSessionStore.size >= MAX_SESSIONS) {
|
||||
const oldest = runtimeSessionStore.keys().next().value;
|
||||
runtimeSessionStore.delete(oldest);
|
||||
}
|
||||
|
||||
const sessionId = generateBinaryStyleId();
|
||||
runtimeSessionStore.set(connectionId, { sessionId, lastUsed: Date.now() });
|
||||
return sessionId;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { parseSSELine, hasValuableContent, fixInvalidId, formatSSE } from "./str
|
||||
|
||||
export { COLORS, formatSSE };
|
||||
|
||||
const sharedDecoder = new TextDecoder();
|
||||
// sharedEncoder is stateless — safe to share across streams
|
||||
const sharedEncoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
@@ -49,6 +49,9 @@ export function createSSEStream(options = {}) {
|
||||
let buffer = "";
|
||||
let usage = null;
|
||||
|
||||
// Per-stream decoder with stream:true to correctly handle multi-byte chars split across chunks
|
||||
const decoder = new TextDecoder("utf-8", { fatal: false });
|
||||
|
||||
const state = mode === STREAM_MODE.TRANSLATE ? { ...initState(sourceFormat), provider, toolNameMap, model } : null;
|
||||
|
||||
let totalContentLength = 0;
|
||||
@@ -61,7 +64,7 @@ export function createSSEStream(options = {}) {
|
||||
if (!ttftAt) {
|
||||
ttftAt = Date.now();
|
||||
}
|
||||
const text = sharedDecoder.decode(chunk, { stream: true });
|
||||
const text = decoder.decode(chunk, { stream: true });
|
||||
buffer += text;
|
||||
reqLogger?.appendProviderChunk?.(text);
|
||||
|
||||
@@ -253,7 +256,7 @@ export function createSSEStream(options = {}) {
|
||||
flush(controller) {
|
||||
trackPendingRequest(model, provider, connectionId, false);
|
||||
try {
|
||||
const remaining = sharedDecoder.decode();
|
||||
const remaining = decoder.decode();
|
||||
if (remaining) buffer += remaining;
|
||||
|
||||
if (mode === STREAM_MODE.PASSTHROUGH) {
|
||||
|
||||
@@ -107,6 +107,9 @@ export function createDisconnectAwareStream(transformStream, streamController) {
|
||||
controller.enqueue(value);
|
||||
} catch (error) {
|
||||
streamController.handleError(error);
|
||||
// Cleanup reader/writer to avoid orphaned streams
|
||||
reader.cancel().catch(() => {});
|
||||
writer.abort().catch(() => {});
|
||||
controller.error(error);
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user