mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
fix(translator): ESM-safe registry + tool-id pairing + responses max_tokens; add real-creds tests
- translator/index.js: replace require() with static side-effect imports (ESM-safe), lazy-init registry maps to survive circular import order - openai-responses->openai: map max_output_tokens -> max_tokens (avoid leaking field upstream) - gemini/antigravity -> openai: derive deterministic tool_call id from name so functionCall/functionResponse pair correctly (fixes provider tool-pairing 400s) - add offline unit tests (finish-reason, usage, session-manager, ollama malformed args, const guard) - add real-creds integration tests (provider-cases + all-formats matrix: 6 inbound formats x 4 scenarios) Includes co-located provider registry refactor (pricing/capabilities/media providers) and sessionManager updates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -79,4 +79,126 @@ export function generateBinaryStyleId() {
|
||||
*/
|
||||
export function clearSessionStore() {
|
||||
runtimeSessionStore.clear();
|
||||
assistantSessionStore.clear();
|
||||
}
|
||||
|
||||
// Conversation-stable session store: Key = hash(scope+assistant text), Value = { sessionId, lastUsed }
|
||||
const assistantSessionStore = new Map();
|
||||
const ASSISTANT_MIN_LEN = 50;
|
||||
const ASSISTANT_CAP_LEN = 200;
|
||||
const MAX_ASSISTANT_SESSIONS = 5000;
|
||||
|
||||
// Client headers/body fields that carry an upstream session id (priority order)
|
||||
const SESSION_HEADER_KEYS = ["x-session-id", "session_id", "x-amp-thread-id", "x-client-request-id"];
|
||||
const CLAUDE_CODE_SESSION_RE = /_session_([a-f0-9-]+)$/;
|
||||
|
||||
function sha16(text) {
|
||||
return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
|
||||
}
|
||||
|
||||
// Normalize a session id candidate (trim, length cap)
|
||||
function normalizeSessionId(value) {
|
||||
if (typeof value !== "string") return null;
|
||||
const v = value.trim();
|
||||
if (!v || v.length > 256) return null;
|
||||
return v;
|
||||
}
|
||||
|
||||
// Extract Claude Code session id from metadata.user_id (_session_{uuid} | JSON {session_id})
|
||||
function extractClaudeCodeSession(userId) {
|
||||
if (typeof userId !== "string" || !userId) return null;
|
||||
const m = userId.match(CLAUDE_CODE_SESSION_RE);
|
||||
if (m) return m[1];
|
||||
if (userId[0] === "{") {
|
||||
try { return normalizeSessionId(JSON.parse(userId)?.session_id); } catch { /* noop */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Lowercase-key lookup for raw client headers
|
||||
function headerValue(headers, key) {
|
||||
if (!headers || typeof headers !== "object") return null;
|
||||
return normalizeSessionId(headers[key] ?? headers[key.toLowerCase()]);
|
||||
}
|
||||
|
||||
// Read client-provided session id from headers/body (no generation)
|
||||
function extractClientSessionId(headers, body) {
|
||||
const claude = extractClaudeCodeSession(body?.metadata?.user_id);
|
||||
if (claude) return `claude:${claude}`;
|
||||
for (const key of SESSION_HEADER_KEYS) {
|
||||
const v = headerValue(headers, key);
|
||||
if (v) return v;
|
||||
}
|
||||
const fromBody =
|
||||
normalizeSessionId(body?.prompt_cache_key) ||
|
||||
normalizeSessionId(body?.session_id) ||
|
||||
normalizeSessionId(body?.conversation_id) ||
|
||||
normalizeSessionId(body?.metadata?.user_id);
|
||||
return fromBody || null;
|
||||
}
|
||||
|
||||
// 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;
|
||||
if (!items) return "";
|
||||
let text = "";
|
||||
for (const item of items) {
|
||||
if (item?.role !== "assistant") continue;
|
||||
if (typeof item.content === "string") text += item.content;
|
||||
else if (Array.isArray(item.content)) {
|
||||
for (const c of item.content) text += c?.text || c?.output || "";
|
||||
}
|
||||
if (text.length >= ASSISTANT_CAP_LEN) break;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// Stable session id keyed on accumulated assistant text (avoids collision on identical first user prompt)
|
||||
function assistantTextSessionId(scope, body) {
|
||||
const text = accumulateAssistantText(body);
|
||||
if (text.length < ASSISTANT_MIN_LEN) return null;
|
||||
const hash = sha16(`${scope}:${text.slice(0, ASSISTANT_CAP_LEN)}`);
|
||||
const existing = assistantSessionStore.get(hash);
|
||||
if (existing) {
|
||||
existing.lastUsed = Date.now();
|
||||
return existing.sessionId;
|
||||
}
|
||||
if (assistantSessionStore.size >= MAX_ASSISTANT_SESSIONS) {
|
||||
assistantSessionStore.delete(assistantSessionStore.keys().next().value);
|
||||
}
|
||||
const sessionId = generateBinaryStyleId();
|
||||
assistantSessionStore.set(hash, { sessionId, lastUsed: Date.now() });
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a conversation-stable session id (generalizes Codex resolveCacheSessionId).
|
||||
* Priority: client session → accumulated-assistant-text hash → workspaceId → per-connection.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {object} [opts.headers] - Raw client request headers (lowercase keys)
|
||||
* @param {object} [opts.body] - Parsed request 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
|
||||
*/
|
||||
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;
|
||||
const ws = normalizeSessionId(workspaceId);
|
||||
if (ws) return ws;
|
||||
return deriveSessionId(connectionId);
|
||||
}
|
||||
|
||||
// Cleanup expired assistant-session entries
|
||||
const assistantCleanup = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of assistantSessionStore) {
|
||||
if (now - entry.lastUsed > MEMORY_CONFIG.sessionTtlMs) assistantSessionStore.delete(key);
|
||||
}
|
||||
}, MEMORY_CONFIG.sessionCleanupIntervalMs);
|
||||
if (assistantCleanup.unref) assistantCleanup.unref();
|
||||
|
||||
Reference in New Issue
Block a user