merge origin/master into gitea/new_feature

Bring local branch up to v0.5.35 while keeping xAI image/edit, SuperGrok
quota tracking, per-provider timeouts, and pinned model-test actions.
This commit is contained in:
2026-07-17 15:31:47 +07:00
224 changed files with 21037 additions and 1643 deletions

View File

@@ -0,0 +1,125 @@
import { MEMORY_CONFIG } from "../config/runtimeConfig.js";
const sessionStartStore = new Map();
const MAX_SESSION_STARTS = 5000;
function clone(value) {
return value == null ? value : JSON.parse(JSON.stringify(value));
}
function sessionKey(connectionId, conversationId) {
return `${connectionId || ""}:${conversationId || ""}`;
}
function ensureUserMessageModelId(message, modelId) {
if (message?.userInputMessage && !message.userInputMessage.modelId && modelId) {
message.userInputMessage.modelId = modelId;
}
return message;
}
function ensureHistoryModelIds(history, modelId) {
for (const item of history || []) {
ensureUserMessageModelId(item, modelId);
}
return history;
}
function prefixUserMessage(message, contentPrefix, modelId) {
const out = clone(message) || { userInputMessage: { content: "" } };
if (!out.userInputMessage) out.userInputMessage = { content: "" };
ensureUserMessageModelId(out, modelId);
if (contentPrefix) {
const content = out.userInputMessage.content || "";
out.userInputMessage.content = content
? `${contentPrefix}\n\n${content}`
: contentPrefix;
}
return out;
}
function findFirstUserIndex(history) {
return history.findIndex((item) => item?.userInputMessage);
}
function rememberSessionStart(key, entry) {
if (sessionStartStore.size >= MAX_SESSION_STARTS) {
sessionStartStore.delete(sessionStartStore.keys().next().value);
}
sessionStartStore.set(key, { ...entry, lastUsed: Date.now() });
}
/**
* Preserve Kiro cacheability by freezing the first user message (`msg0`) for a
* session, replaying that exact message as the first history user on later
* turns, and injecting volatile current-time context only into the current turn.
*/
export function applyKiroSessionReplay({
conversationId,
connectionId,
modelId,
systemPrompt = "",
contentPrefix = "",
currentContentPrefix = "",
history = [],
currentMessage,
} = {}) {
const key = sessionKey(connectionId, conversationId);
const existing = conversationId ? sessionStartStore.get(key) : null;
const baseHistory = clone(history) || [];
const baseCurrent = clone(currentMessage) || { userInputMessage: { content: "" } };
if (existing && existing.modelId === modelId && existing.systemPrompt === systemPrompt) {
existing.lastUsed = Date.now();
const firstUserIndex = findFirstUserIndex(baseHistory);
const sessionStart = ensureUserMessageModelId(clone(existing.sessionStart), modelId);
if (firstUserIndex >= 0) {
baseHistory[firstUserIndex] = sessionStart;
} else {
baseHistory.unshift(sessionStart);
}
return {
history: ensureHistoryModelIds(baseHistory, modelId),
currentMessage: prefixUserMessage(baseCurrent, currentContentPrefix, modelId),
replayed: true,
};
}
const firstUserIndex = findFirstUserIndex(baseHistory);
let sessionStart;
let nextCurrent = ensureUserMessageModelId(baseCurrent, modelId);
if (firstUserIndex >= 0) {
sessionStart = prefixUserMessage(baseHistory[firstUserIndex], contentPrefix, modelId);
baseHistory[firstUserIndex] = clone(sessionStart);
nextCurrent = prefixUserMessage(baseCurrent, currentContentPrefix, modelId);
} else {
sessionStart = prefixUserMessage(baseCurrent, contentPrefix, modelId);
nextCurrent = clone(sessionStart);
}
if (conversationId) {
rememberSessionStart(key, {
sessionStart: clone(sessionStart),
modelId,
systemPrompt,
});
}
return {
history: ensureHistoryModelIds(baseHistory, modelId),
currentMessage: nextCurrent,
replayed: false,
};
}
export function clearKiroSessionReplayStore() {
sessionStartStore.clear();
}
const cleanup = setInterval(() => {
const now = Date.now();
for (const [key, entry] of sessionStartStore) {
if (now - entry.lastUsed > MEMORY_CONFIG.sessionTtlMs) sessionStartStore.delete(key);
}
}, MEMORY_CONFIG.sessionCleanupIntervalMs);
if (cleanup.unref) cleanup.unref();

View File

@@ -13,6 +13,7 @@ import { MEMORY_CONFIG } from "../config/runtimeConfig.js";
// Runtime storage: Key = connectionId, Value = { sessionId, lastUsed }
const runtimeSessionStore = new Map();
const continuationStore = new Map();
// Periodically evict entries that haven't been used within TTL
const cleanupInterval = setInterval(() => {
@@ -80,6 +81,7 @@ export function generateBinaryStyleId() {
export function clearSessionStore() {
runtimeSessionStore.clear();
assistantSessionStore.clear();
continuationStore.clear();
}
// Conversation-stable session store: Key = hash(scope+assistant text), Value = { sessionId, lastUsed }
@@ -87,9 +89,10 @@ const assistantSessionStore = new Map();
const ASSISTANT_MIN_LEN = 50;
const ASSISTANT_CAP_LEN = 50;
const MAX_ASSISTANT_SESSIONS = 5000;
const MAX_CONTINUATION_SESSIONS = 5000;
// Client headers/body fields that carry an upstream session id (priority order)
const SESSION_HEADER_KEYS = ["x-session-id", "session-id", "session_id", "x-amp-thread-id", "x-client-request-id"];
const SESSION_HEADER_KEYS = ["x-session-id", "session-id", "session_id", "x-amp-thread-id"];
const CLAUDE_CODE_SESSION_RE = /_session_([a-f0-9-]+)$/;
function sha16(text) {
@@ -131,7 +134,7 @@ function extractAntigravitySession(body) {
return m ? normalizeSessionId(m[1]) : null;
}
function extractClientSessionId(headers, body) {
function extractClientSessionId(headers, body, scope = "") {
const claude = extractClaudeCodeSession(body?.metadata?.user_id);
if (claude) return `claude:${claude}`;
const antigravity = extractAntigravitySession(body);
@@ -140,18 +143,25 @@ function extractClientSessionId(headers, body) {
const v = headerValue(headers, key);
if (v) return v;
}
const requestId = scope === "kiro" ? null : headerValue(headers, "x-client-request-id");
if (requestId) return requestId;
const fromBody =
normalizeSessionId(body?.prompt_cache_key) ||
normalizeSessionId(body?.session_id) ||
normalizeSessionId(body?.conversation_id) ||
normalizeSessionId(body?.metadata?.user_id);
(scope === "kiro" ? null : normalizeSessionId(body?.metadata?.user_id));
return fromBody || null;
}
function requestMessages(body) {
if (Array.isArray(body?.messages)) return body.messages;
if (Array.isArray(body?.input)) return body.input;
return [];
}
// Accumulate assistant text from OpenAI/Responses-style input/messages (cap-limited)
function accumulateAssistantText(body) {
const items = Array.isArray(body?.input) ? body.input
: Array.isArray(body?.messages) ? body.messages : null;
const items = requestMessages(body);
if (!items) return "";
let text = "";
for (const item of items) {
@@ -193,16 +203,39 @@ function assistantTextSessionId(scope, body) {
* @param {string} [opts.connectionId] - Connection identifier (fallback scope)
* @param {string} [opts.workspaceId] - Provider workspace id (account-wide fallback)
* @param {string} [opts.scope] - Provider scope to isolate cache keys across providers
* @returns {string} A stable session id
* @returns {{sessionId: string, ephemeral: boolean}} A session id plus whether it is one-shot
*/
export function resolveSessionId({ headers, body, connectionId, workspaceId, scope = "" } = {}) {
const client = extractClientSessionId(headers, body);
if (client) return client;
const fromAssistant = assistantTextSessionId(`${scope}:${connectionId || ""}`, body);
if (fromAssistant) return fromAssistant;
export function resolveSessionIdentity({ headers, body, connectionId, workspaceId, scope = "" } = {}) {
const client = extractClientSessionId(headers, body, scope);
if (client) return { sessionId: client, ephemeral: false };
const fromAssistant = scope === "kiro" ? null : assistantTextSessionId(`${scope}:${connectionId || ""}`, body);
if (fromAssistant) return { sessionId: fromAssistant, ephemeral: false };
const ws = normalizeSessionId(workspaceId);
if (ws) return ws;
return deriveSessionId(connectionId);
if (ws) return { sessionId: ws, ephemeral: false };
if (scope === "kiro") return { sessionId: generateBinaryStyleId(), ephemeral: true };
return { sessionId: deriveSessionId(connectionId), ephemeral: false };
}
export function resolveSessionId(opts = {}) {
return resolveSessionIdentity(opts).sessionId;
}
export function resolveContinuationId({ sessionId, connectionId, scope = "", ephemeral = false } = {}) {
if (ephemeral) return crypto.randomUUID();
const key = `${scope}:${connectionId || ""}:${sessionId || ""}`;
const existing = continuationStore.get(key);
if (existing) {
existing.lastUsed = Date.now();
continuationStore.delete(key);
continuationStore.set(key, existing);
return existing.continuationId;
}
const continuationId = crypto.randomUUID();
if (continuationStore.size >= MAX_CONTINUATION_SESSIONS) {
continuationStore.delete(continuationStore.keys().next().value);
}
continuationStore.set(key, { continuationId, lastUsed: Date.now() });
return continuationId;
}
// Capture session id from request body + credentials (envelope still intact here)
@@ -227,5 +260,8 @@ const assistantCleanup = setInterval(() => {
for (const [key, entry] of assistantSessionStore) {
if (now - entry.lastUsed > MEMORY_CONFIG.sessionTtlMs) assistantSessionStore.delete(key);
}
for (const [key, entry] of continuationStore) {
if (now - entry.lastUsed > MEMORY_CONFIG.sessionTtlMs) continuationStore.delete(key);
}
}, MEMORY_CONFIG.sessionCleanupIntervalMs);
if (assistantCleanup.unref) assistantCleanup.unref();

View File

@@ -15,16 +15,19 @@ function getTimeString() {
* @param {string} options.provider - Provider name
* @param {string} options.model - Model name
*/
export function createStreamController({ onDisconnect, onError, log, provider, model } = {}) {
export function createStreamController({ onDisconnect, onError, log, provider, model, reqTag = "" } = {}) {
const abortController = new AbortController();
const startTime = Date.now();
let disconnected = false;
let abortTimeout = null;
const logStream = (status) => {
// Only abnormal terminations are logged; normal completion is covered by "📊 done".
// isError uses errorLine (always shown, ignores LOG_LEVEL) so failures survive quiet levels.
const logStream = (symbol, status, isError = false) => {
const duration = Date.now() - startTime;
const p = provider?.toUpperCase() || "UNKNOWN";
console.log(`[${getTimeString()}] 🌊 [STREAM] ${p} | ${model || "unknown"} | ${duration}ms | ${status}`);
const emit = isError ? log?.errorLine : log?.line;
if (emit) emit(reqTag, symbol, `${status} · ${provider}/${model} · ${duration}ms`);
else console.log(`[${getTimeString()}] ${symbol} ${provider}/${model} · ${status} · ${duration}ms`);
};
return {
@@ -38,7 +41,7 @@ export function createStreamController({ onDisconnect, onError, log, provider, m
if (disconnected) return;
disconnected = true;
logStream(`disconnect: ${reason}`);
logStream("⚡", `DISCONNECT: ${reason}`);
dbg("CTRL", `${provider}/${model} | disconnect=${reason} | dur=${Date.now() - startTime}ms`);
// Delay abort to allow cleanup
@@ -49,13 +52,11 @@ export function createStreamController({ onDisconnect, onError, log, provider, m
onDisconnect?.({ reason, duration: Date.now() - startTime });
},
// Call when stream completes normally
// Call when stream completes normally (no line here — "📊 done" is authoritative)
handleComplete: () => {
if (disconnected) return;
disconnected = true;
logStream("complete");
if (abortTimeout) {
clearTimeout(abortTimeout);
abortTimeout = null;
@@ -73,11 +74,11 @@ export function createStreamController({ onDisconnect, onError, log, provider, m
}
if (error.name === "AbortError") {
logStream("aborted");
logStream("⚡", "ABORTED");
return;
}
logStream(`error: ${error.message}`);
logStream("✗", `ERROR: ${error.message}${error.stack ? `\n ${error.stack}` : ""}`, true);
onError?.(error);
},

View File

@@ -4,6 +4,9 @@
import { FORMATS } from "../translator/formats.js";
// Legacy per-chunk usage console line; off by default (superseded by "📊 done")
const DEBUG_USAGE = process.env.LOG_USAGE_VERBOSE === "1";
// ANSI color codes
export const COLORS = {
reset: "\x1b[0m",
@@ -401,6 +404,10 @@ export function estimateUsage(body, contentLength, targetFormat = FORMATS.OPENAI
export function logUsage(provider, usage, model = null, connectionId = null, apiKey = null) {
if (!usage || typeof usage !== "object") return;
// Console output moved to the unified "📊 done" line (streamingHandler). Kept as
// a no-op hook so callers stay unchanged; usage persistence happens via saveUsageStats.
if (!DEBUG_USAGE) return;
const p = provider?.toUpperCase() || "UNKNOWN";
// Support both formats: