"use strict"; const https = require("https"); const http = require("http"); const crypto = require("crypto"); // ============================================================ // 1. Configuration // ============================================================ const CONFIG = require("./config.json"); const BASE_HOST = "ima.qq.com"; const BASE_URL = "https://ima.qq.com"; // Use env var if available, otherwise fall back to config.json let COOKIE = (() => { const envCookie = process.env.IMA_COOKIE; if (envCookie) return envCookie; const cfgCookie = CONFIG.auth.cookie; // Reject non-ASCII placeholder cookies (e.g. Chinese placeholder text) if (cfgCookie && /^[-]+$/.test(cfgCookie)) return cfgCookie; return ''; })(); let IMA_TOKEN = ""; let BKN = ""; // 2. HTTP Client // ============================================================ const IMA_HEADERS = { "from_browser_ima": "1", "x-ima-cookie": COOKIE, "x-ima-bkn": BKN, referer: BASE_URL, origin: BASE_URL, "User-Agent": "okhttp/4.12.0", "Content-Type": "application/json; charset=utf-8", "Accept-Encoding": "gzip", // fix: match real App request headers }; function rebuildCookieState() { const m = COOKIE.match(/IMA-TOKEN=([^;]+)/); IMA_TOKEN = m ? m[1] : ""; let h = 5381; for (let i = 0; i < IMA_TOKEN.length; i++) h += (h << 5) + IMA_TOKEN.charCodeAt(i); BKN = String(h & 0x7fffffff); IMA_HEADERS["x-ima-cookie"] = COOKIE; IMA_HEADERS["x-ima-bkn"] = BKN; } rebuildCookieState(); // token refresh using IMA_REFRESH_TOKEN env var (mimics ima_runner.py logic) const REFRESH_TOKEN = process.env.IMA_REFRESH_TOKEN || ""; const REGISTRATION_ID = process.env.IMA_REGISTRATION_ID || ""; let tokenExpiresAt = 0; let refreshTimer = null; async function refreshImaToken() { if (!REFRESH_TOKEN || !REGISTRATION_ID) return false; const uidMatch = COOKIE.match(/IMA-UID=([^;]+)/); if (!uidMatch) return false; const uid = uidMatch[1]; try { const result = await imaPost("/auth_login/refresh", { user_id: uid, refresh_token: REFRESH_TOKEN, token_type: 14, registration_id: REGISTRATION_ID, }, {}, 15000); if (result && result.data && result.data.cookie) { COOKIE = result.data.cookie; rebuildCookieState(); if (result.data.expires_in) tokenExpiresAt = Date.now() + result.data.expires_in * 1000; console.log(`[REFRESH] Token refreshed, valid ${result.data.expires_in || "?"}s, BKN=${BKN}`); return true; } } catch (e) { console.log(`[REFRESH] Failed: ${e.message}`); } return false; } function scheduleAutoRefresh() { if (!REFRESH_TOKEN || !REGISTRATION_ID) return; const interval = 600000; // check every 10 min refreshTimer = setInterval(async () => { if (Date.now() > tokenExpiresAt - 300000) await refreshImaToken(); }, interval); } // If env var IMA_API_KEYS is set, it replaces config.json keys entirely const API_KEYS = new Set(); if (process.env.IMA_API_KEYS) { process.env.IMA_API_KEYS.split(",").forEach(k => API_KEYS.add(k.trim())); } else { (CONFIG.api_keys || []).forEach(k => API_KEYS.add(k)); } function maskKey(k) { if (!k) return "(empty)"; if (k.length <= 12) return k; return k.slice(0, 6) + "..." + k.slice(-4); } // ============================================================ function imaPost(path, body, extraH = {}, timeout = 30000) { return new Promise((resolve, reject) => { const payload = JSON.stringify(body); const req = https.request({ hostname: BASE_HOST, port: 443, path, method: "POST", headers: { ...IMA_HEADERS, ...extraH, "Content-Length": Buffer.byteLength(payload) }, timeout, }, (res) => { // fix: handle gzip compressed responses const zlib = require("zlib"); const encoding = res.headers["content-encoding"] || ""; let stream = res; if (encoding === "gzip") stream = res.pipe(zlib.createGunzip()); else if (encoding === "deflate") stream = res.pipe(zlib.createInflate()); const chunks = []; stream.on("data", (c) => chunks.push(c)); stream.on("end", () => { const raw = Buffer.concat(chunks).toString("utf-8"); try { resolve({ status: res.statusCode, data: JSON.parse(raw) }); } catch { resolve({ status: res.statusCode, data: raw }); } }); stream.on("error", reject); }); req.on("error", reject); req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); }); req.write(payload); req.end(); }); } function imaSse(path, body) { return new Promise((resolve, reject) => { const payload = JSON.stringify(body); const req = https.request({ hostname: BASE_HOST, port: 443, path, method: "POST", headers: { ...IMA_HEADERS, Accept: "text/event-stream", "Content-Length": Buffer.byteLength(payload) }, timeout: 180000, }, (res) => { if (res.statusCode !== 200) { let e = ""; res.on("data", c => e += c); res.on("end", () => reject(new Error(`HTTP ${res.statusCode}: ${e}`))); return; } resolve(parseSSE(res)); }); req.on("error", reject); req.on("timeout", () => { req.destroy(); reject(new Error("SSE timeout")); }); req.write(payload); req.end(); }); } function parseSSEBlock(part) { let event = ""; const dataLines = []; for (const line of part.split("\n")) { if (line.startsWith("event:")) event = line.slice(6).trim(); else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, "")); else if (line.startsWith("data")) dataLines.push(""); } return { event, data: dataLines.join("\n") }; } async function* parseSSE(readable) { let buf = ""; for await (const chunk of readable) { buf += chunk.toString("utf-8"); const parts = buf.split(/\r?\n\r?\n/); buf = parts.pop() || ""; for (const part of parts) { if (!part.trim()) continue; const evt = parseSSEBlock(part); if (evt.event || evt.data) yield evt; } } if (buf.trim()) { const evt = parseSSEBlock(buf); if (evt.event || evt.data) yield evt; } } const CONTROL_EVENTS = new Set(["COMPLETED", "CLOSE", "INNER_EXCEPTION", "ERROR", "FAILED"]); const DEBUG_SSE = process.env.IMA_DEBUG === "1"; function extractEventText(d) { if (d == null) return ""; if (typeof d === "string") return d; if (typeof d !== "object") return String(d); for (const k of ["Text", "text", "Content", "content", "Delta", "delta", "Msg", "msg", "reply", "Reply", "answer", "Answer"]) { const v = d[k]; if (typeof v === "string" && v) return v; } return ""; } function eventText(evt) { const raw = evt && evt.data; if (!raw) return ""; let d; try { d = JSON.parse(raw); } catch { try { d = tryRepairJson(raw); } catch { d = null; } } if (d == null) return ""; return extractEventText(d); } // ============================================================ // 3. IMA API // ============================================================ async function imaInitSession(question) { const { data } = await imaPost("/cgi-bin/session_logic/init_session", { env_info: { interact_type: 2, robot_type: 10000 }, name: (question || "New Chat").slice(0, 50), msgs_limit: 20, // IMA limit 20 (code=51 if exceeded) }); if (data.code === 0) return data.session_id; throw new Error(`InitSession failed: code=${data.code} msg=${data.msg}`); } function imaQaStream(sessionId, question, modelType, modelId) { return imaSse("/cgi-bin/assistant/qa", { session_id: sessionId, robot_type: 10000, question, question_type: 2, command_info: { question_info: {} }, client_id: crypto.randomUUID(), model_info: { model_type: modelType, model_id: modelId }, }); } async function collectResponseText(events) { let text = ""; const seen = []; for await (const evt of events) { if (CONTROL_EVENTS.has(evt.event)) break; text += eventText(evt); if (DEBUG_SSE) seen.push(evt.event || "(none)"); } if (DEBUG_SSE && !text) console.error(`[SSE-EMPTY] events=${JSON.stringify(seen)}`); return text; } // ============================================================ // 4. Session Cache // ============================================================ const sessions = new Map(); const SESSION_TTL = 30 * 60 * 1000; function getCachedSession(convId) { const now = Date.now(); for (const [k, v] of sessions) { if (now - v.ts > SESSION_TTL) sessions.delete(k); } if (convId && sessions.has(convId)) { sessions.get(convId).ts = now; return sessions.get(convId).id; } return null; } async function ensureSession(convId, question, forceNew = false) { if (!forceNew) { const c = getCachedSession(convId); if (c) return c; } const id = await imaInitSession(question); if (convId) sessions.set(convId, { id, ts: Date.now() }); return id; } // fix: auto-rebuild IMA session when msgs_limit reached async function imaQaStreamWithRetry(convId, question, modelType, modelId) { let sessionId = await ensureSession(convId, question); const events = await imaQaStream(sessionId, question, modelType, modelId); // detect session limit errors (INNER_EXCEPTION with session limit) return (async function*() { let hitLimit = false; for await (const evt of events) { if (evt.event === "INNER_EXCEPTION" || evt.event === "ERROR" || evt.event === "FAILED") { // IMA session full returns INNER_EXCEPTION, rebuild session and retry once try { const newId = await ensureSession(convId, question, true); const retryEvents = await imaQaStream(newId, question, modelType, modelId); for await (const e2 of retryEvents) yield e2; } catch (_) { yield evt; // retry also failed, pass through original event } return; } yield evt; } })(); } // ============================================================ // 5. Models // ============================================================ const MODELS = CONFIG.models; const DEFAULT_MODEL = CONFIG.default_model; function resolveModel(requested) { if (!requested) return MODELS[DEFAULT_MODEL]; if (MODELS[requested]) return MODELS[requested]; const lower = requested.toLowerCase(); for (const [k, v] of Object.entries(MODELS)) { if (k.toLowerCase() === lower || String(v.type) === lower) return v; } return MODELS[DEFAULT_MODEL]; } // ============================================================ // 6. Auth & Tools // ============================================================ function checkAuth(req) { const bearer = (req.headers["authorization"] || "").replace(/^Bearer\s+/i, ""); if (bearer && API_KEYS.has(bearer)) return true; return API_KEYS.has(req.headers["x-api-key"] || ""); } function cors(res) { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, x-conversation-id, x-session-id, x-request-id, x-stainless-*"); } function json(res, code, obj) { cors(res); res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" }); res.end(JSON.stringify(obj)); if (code >= 400) { console.error(`[RESP] ${code} ${JSON.stringify(obj).slice(0, 200)}`); } } function extractContent(msg) { if (typeof msg.content === "string") return msg.content; if (Array.isArray(msg.content)) return msg.content.map(c => { if (c.type === "text") return c.text; if (c.type === "tool_result") { // Anthropic tool_result content string content_block[] const tc = typeof c.content === "string" ? c.content : Array.isArray(c.content) ? c.content.map(cc => cc.text || "").join("") : ""; return `Tool result:\n${tc}`; } if (c.type === "tool_use") return `Tool call: ${c.name}(${JSON.stringify(c.input)})`; return ""; }).filter(Boolean).join("\n"); return String(msg.content || ""); } // ============================================================ // 7. Function Calling - Prompt Injection Engine // ============================================================ function escapeRawCtrlInStrings(s) { let out = "", inStr = false, esc = false; for (let i = 0; i < s.length; i++) { const c = s[i]; if (esc) { out += c; esc = false; continue; } if (c === "\\") { out += c; esc = true; continue; } if (c === '"') { inStr = !inStr; out += c; continue; } if (inStr) { if (c === "\n") { out += "\\n"; continue; } if (c === "\r") { out += "\\r"; continue; } if (c === "\t") { out += "\\t"; continue; } } out += c; } return out; } // fix common LLM JSON errors function tryRepairJson(raw) { try { return JSON.parse(raw); } catch (e) { /* continue */ } let fixed = raw; // 0. escape bare control chars in strings fixed = escapeRawCtrlInStrings(fixed); try { return JSON.parse(fixed); } catch (e) { /* continue */ } // 1. remove trailing commas fixed = fixed.replace(/,(\s*[}\]])/g, '$1'); try { return JSON.parse(fixed); } catch (e) { /* continue */ } // 2. add missing closing brackets let depth = 0; for (const c of fixed) { if (c === '{' || c === '[') depth++; if (c === '}' || c === ']') depth--; } if (depth > 0) { const lastChar = fixed.trim().slice(-1); const closer = lastChar === '}' ? ']' : '}'; fixed += closer.repeat(Math.min(depth, 5)); try { return JSON.parse(fixed); } catch (e) { /* continue */ } } // 3. extract name + arguments (best-effort recovery) const nameMatch = fixed.match(/"name"\s*:\s*"([^"]+)"/); if (nameMatch) { const argsStart = fixed.indexOf('{', fixed.indexOf('"arguments"')); if (argsStart >= 0) { let d = 0, end = -1; for (let i = argsStart; i < fixed.length; i++) { if (fixed[i] === '{' || fixed[i] === '[') d++; if (fixed[i] === '}' || fixed[i] === ']') { d--; if (d === 0) { end = i + 1; break; } } } try { return { name: nameMatch[1], arguments: JSON.parse(fixed.slice(argsStart, end)) }; } catch (e) { /* fall through */ } } return { name: nameMatch[1], arguments: {} }; } return null; } // compress JSON Schema: remove metadata, single-line compact output function compactSchema(schema) { if (!schema || typeof schema !== "object") return "{}"; // deep copy then recursive cleanup function clean(obj) { if (Array.isArray(obj)) return obj.map(clean); if (obj && typeof obj === "object") { const out = {}; for (const [k, v] of Object.entries(obj)) { if (k.startsWith("$")) continue; // remove $schema, $defs, $ref, $dynamicAnchor, etc. out[k] = clean(v); } return out; } return obj; } return JSON.stringify(clean(schema)); // single line, no indentation } function buildToolsPrompt(tools) { if (!tools || tools.length === 0) return ""; const funcDefs = tools .filter(t => t.type === "function" && t.function) .map(t => { const fn = t.function; const params = compactSchema(fn.parameters); return ` ${fn.description || "No description"} ${params} `; }) .join("\n"); return `## CRITICAL -- YOU MUST USE FUNCTION CALLING You have access to these functions. When the user asks you to do something that a function can handle, you MUST call the function. NEVER say "I cannot" or "I don't have the ability". NEVER tell the user to save a file, run a command, or do any step themselves - DO IT by calling the function (e.g. use Write to create files, Bash to run commands). ALWAYS use a function instead. ${funcDefs} ## HOW TO CALL A FUNCTION Output EXACTLY this format, then STOP: {"name": "", "arguments": {}} Rules: - arguments MUST be a valid JSON object matching the function's parameters - For file content, put the FULL content in the string value (newlines allowed) - Do NOT add any text before or after the block - NEVER output file content or code for the user to copy - write it via Write`; } // - "" // (1) ("YOU MUST call / ") // (2) ("") -- " // ", // (3) intent != completion; if unfinished, MUST call function now // " X", function buildToolsPromptSoft(tools) { if (!tools || tools.length === 0) return ""; const funcDefs = tools .filter(t => t.type === "function" && t.function) .map(t => { const fn = t.function; return ` ${fn.description || "No description"} ${compactSchema(fn.parameters)} `; }) .join("\n"); return `## Available functions ${funcDefs} ## CRITICAL - keep going until the task is fully done The user's request may need MULTIPLE steps. After each function result, decide: the task is NOT finished yet -> call the next function NOW; the task IS fully finished -> give the final answer in plain text. NEVER reply with only your intention (e.g. "Let me read the file", "I'll now start the server"I will now do it"). Stating intent is NOT an action and NOT an answer. If you intend to do something, you MUST emit the function call for it in THIS SAME reply. ## To call a function Output EXACTLY: {"name":"...","arguments":{...}} then stop. (For file content, put the FULL content in the string value; newlines allowed.) ## To finish Only when every step is done, reply to the user in plain text WITHOUT any block.`; } // StreamFilter: real-time filter of blocks // prevent Claude Code client from stopping on XML tags class StreamFilter { constructor() { this._buf = ''; // pending buffer this._emitted = ''; // emitted clean text this._funcBlocks = []; // captured function_call JSON this._inFunc = false; // whether inside this._tagLen = 16; // ''.length this._leftover = ''; // leftover text parseCalls cannot parse (stream fallback) } // feed new text chunk, return clean text for client feed(chunk) { this._buf += chunk; const out = []; const startTag = ''; const endTag = ''; while (this._buf.length > 0) { if (!this._inFunc) { const idx = this._buf.indexOf(startTag); if (idx === -1) { // no start tag found - check tail for partial match const partialLen = this._partialMatch(startTag); if (partialLen > 0) { // keep potential partial tag in buffer const safe = this._buf.slice(0, -partialLen); if (safe) out.push(safe); this._buf = this._buf.slice(-partialLen); break; } // completely safe, output all out.push(this._buf); this._buf = ''; break; } // found start tag - output text before it if (idx > 0) out.push(this._buf.slice(0, idx)); this._buf = this._buf.slice(idx + startTag.length); this._inFunc = true; } if (this._inFunc) { const idx = this._buf.indexOf(endTag); if (idx === -1) { // no end tag yet - suppress all break; } // found end tag - capture function call JSON const json = this._buf.slice(0, idx).trim(); if (json) this._funcBlocks.push(json); this._buf = this._buf.slice(idx + endTag.length); this._inFunc = false; // continue loop, may have more text or function_calls } } const text = out.join(''); if (text) this._emitted += text; return text; } // check if buffer tail partially matches tag _partialMatch(tag) { for (let i = tag.length - 1; i > 0; i--) { if (this._buf.endsWith(tag.slice(0, i))) return i; } return 0; } // called on stream end - return residual buffer content flush() { if (this._inFunc) { // fix: stream ended before arrived // old logic discarded buffer, causing no reply after tool call // capture as function block, let parseCalls() repair it const pending = this._buf.trim(); if (pending) this._funcBlocks.push(pending); this._buf = ''; this._inFunc = false; return ''; } if (this._buf) { this._emitted += this._buf; const r = this._buf; this._buf = ''; return r; } return ''; } // parse all captured function_call blocks parseCalls() { const calls = []; for (const raw of this._funcBlocks) { const parsed = tryRepairJson(raw); if (parsed && parsed.name) { calls.push({ id: 'toolu_' + crypto.randomUUID().slice(0, 12), type: 'function', function: { name: parsed.name || '', arguments: JSON.stringify(parsed.arguments || parsed.parameters || {}), }, }); } else if (raw.length > 0) { // JSON repair failed - accumulate to _leftover for fallback // (non-stream uses cleanText; stream reads leftover separately) const note = '\n[Function call (malformed)]:\n' + raw + '\n'; this._emitted += note; this._leftover += note; } } return calls; } get cleanText() { return this._emitted; } get hasFunc() { return this._funcBlocks.length > 0; } get leftover() { return this._leftover; } } function parseFunctionCalls(text) { if (!text) return { found: false, text: "" }; // also match ```json {...} ``` format const jsonBlockRegex = /```(?:json)?\s*\n?\s*(\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})\s*\n?\s*```/g; const regex = /\s*\n?\s*(\{[\s\S]*?\})\s*\n?\s*<\/function_call>/g; const calls = []; let cleanText = text; // try both formats: and ```json const patterns = [ regex, /```(?:json)?\s*\n?\s*\{[^{}]*"name"\s*:\s*"[^"]+"[^{}]*"arguments"\s*:\s*\{[\s\S]*?\}\s*\}\s*\n?\s*```/g, ]; for (const pattern of patterns) { let match; while ((match = pattern.exec(text)) !== null) { const parsed = tryRepairJson(match[1].trim()); if (parsed && parsed.name) { calls.push({ id: "toolu_" + crypto.randomUUID().slice(0, 12), type: "function", function: { name: parsed.name || "", arguments: JSON.stringify(parsed.arguments || parsed.parameters || {}), }, }); } } if (calls.length > 0) break; } if (calls.length > 0) { cleanText = text.replace(regex, "").replace(/```(?:json)?\s*\n?\s*\{[\s\S]*?\}\s*\n?\s*```/g, "").trim(); return { found: true, calls, text: cleanText }; } // debug: check if model output resembles function call format if (text.includes('function') || text.includes('tool') || text.includes('bash') || text.includes('read_file')) { } return { found: false, text }; } // ============================================================ // 8. OpenAI /v1/chat/completions // ============================================================ async function openaiChat(req, res, body) { const modelKey = body.model || DEFAULT_MODEL; const model = resolveModel(modelKey); const stream = !!body.stream; const messages = body.messages || []; const tools = body.tools || null; const toolChoice = body.tool_choice || null; // --- build question --- const hasToolResults = messages.some(m => m.role === "tool"); const hasToolCalls = messages.some(m => m.role === "assistant" && m.tool_calls); let question; // determine tool set (shared by both branches) let effectiveTools = tools; if ((!effectiveTools || effectiveTools.length === 0) && toolChoice !== "none") { effectiveTools = [ {type: "function", function: {name: "Bash", description: "Execute bash command", parameters: {type: "object", properties: {command: {type: "string"}}, required: ["command"]}}}, {type: "function", function: {name: "Read", description: "Read a file", parameters: {type: "object", properties: {file_path: {type: "string"}}, required: ["file_path"]}}}, {type: "function", function: {name: "Write", description: "Write to a file", parameters: {type: "object", properties: {file_path: {type: "string"}, content: {type: "string"}}, required: ["file_path", "content"]}}}, {type: "function", function: {name: "Glob", description: "Find files by pattern", parameters: {type: "object", properties: {pattern: {type: "string"}}, required: ["pattern"]}}}, {type: "function", function: {name: "Grep", description: "Search file contents", parameters: {type: "object", properties: {pattern: {type: "string"}, path: {type: "string"}}, required: ["pattern"]}}}, ]; } // title generation requests do not need tools const _sysParts = messages.filter(m => m.role === "system").map(m => extractContent(m)); const _sysPrompt = _sysParts.length > 0 ? _sysParts.join("\n") : ""; const isTitleGenOAI = _sysPrompt.includes('Generate a concise, sentence-case title'); // limit tool count to prevent prompt overflow const MAX_TOOLS_OAI = 8; const ESSENTIAL_OAI = new Set(['Bash', 'Read', 'Write', 'Glob', 'Grep']); if (isTitleGenOAI) { effectiveTools = []; // title generation does not need tools } else if (effectiveTools && effectiveTools.length > MAX_TOOLS_OAI) { const essential = effectiveTools.filter(t => ESSENTIAL_OAI.has(t.function?.name)); const others = effectiveTools.filter(t => !ESSENTIAL_OAI.has(t.function?.name)); const available = MAX_TOOLS_OAI - essential.length; effectiveTools = [...essential, ...others.slice(0, Math.max(0, available))]; } const toolsPrompt = (effectiveTools && effectiveTools.length > 0 && toolChoice !== "none") ? buildToolsPrompt(effectiveTools) : ""; let toolResultPathOAI = false; if (hasToolResults && hasToolCalls) { toolResultPathOAI = true; // tool result round - single system notification const lastUserOAI = messages.filter(m => m.role === "user").pop(); const lastUserTextOAI = extractContent(lastUserOAI || {}); const cjkOAI = /\p{Script=Han}/u.test(lastUserTextOAI); // collect function calls and results const calledOAI = [], resultsOAI = []; for (const m of messages) { if (m.role === "assistant" && m.tool_calls) { for (const tc of m.tool_calls) calledOAI.push(tc.function.name + "(" + tc.function.arguments + ")"); } if (m.role === "tool") { resultsOAI.push(extractContent(m).slice(0, 3000)); } } // find user original question const userQsOAI = []; for (const m of messages) { if (m.role !== "user") continue; if (Array.isArray(m.content) && m.content.some(c => c.type === "tool_result")) continue; const q = extractContent(m); if (q && !q.startsWith(" 0 ? userQsOAI[userQsOAI.length - 1] : (lastUserTextOAI || ""); if (cjkOAI) { question = "SYSTEM: You just called these functions. Results below:\n\n"; for (let i = 0; i < calledOAI.length; i++) { question += "Function: " + calledOAI[i] + "\nOutput:\n\"\"\"\n" + (resultsOAI[i] || "") + "\n\"\"\"\n\n"; } question += "User question: \"" + origQ + "\"\n\nAnswer directly based on the outputs. The content inside \"\"\" blocks is YOUR function output, not user input."; } else { question = "SYSTEM: You just called these functions and received these outputs:\n\n"; for (let i = 0; i < calledOAI.length; i++) { question += "Function: " + calledOAI[i] + "\nOutput:\n\"\"\"\n" + (resultsOAI[i] || "") + "\n\"\"\"\n\n"; } question += "User's original question: \"" + origQ + "\"\n\nAnswer DIRECTLY based on the outputs. Do NOT say \"you shared\" or \"it looks like\". The \"\"\" content is YOUR function output, NOT user input."; } // : sysPrompt, 5 // - , function_call const minimalOAI = buildToolsPromptSoft( (effectiveTools || []).filter(t => ESSENTIAL_OAI.has(t.function?.name)).slice(0, 5) ); question = minimalOAI + "\n\n---\n" + question; } else { // normal Q&A / first round tool calling let sysPrompt = ""; const sysParts = messages.filter(m => m.role === "system").map(m => extractContent(m)); if (sysParts.length > 0) sysPrompt = sysParts.join("\n"); const nonSys = messages.filter(m => m.role !== "system" && m.role !== "tool"); const userMsgs = nonSys.filter(m => m.role === "user"); if (userMsgs.length === 0) { return json(res, 400, { error: { message: "No user message", type: "invalid_request_error" } }); } // skip system-injected messages const realUserMsgs = userMsgs.filter(m => { const c = extractContent(m); return !c.startsWith(' 10; }); const lastUserMsg = realUserMsgs.length > 0 ? extractContent(realUserMsgs[realUserMsgs.length - 1]) : extractContent(userMsgs[userMsgs.length - 1]); // language detection const hasCJKOAI = /\p{Script=Han}/u.test(lastUserMsg); const langHintOAI = hasCJKOAI ? "\n## Language\nRespond in the same language as the user's message. The user is writing in Chinese - respond in Chinese (Simplified Chinese).\n" : ""; // IMA question field limit 10240 chars const MAX_QUESTION = 10000; if (nonSys.length > 1 && realUserMsgs.length >= 1) { const realMsgs = nonSys.filter(m => { const c = extractContent(m); return c.length > 10 && !c.startsWith(' 1) { // fix: expand to 20 messages, dynamic trim by char count const MAX_HISTORY_CHARS = 6000; const recentMsgs = realMsgs .filter(m => m.role === "user" || m.role === "assistant") .slice(-20); let historyParts = []; let historyLen = 0; for (let i = recentMsgs.length - 1; i >= 0; i--) { const m = recentMsgs[i]; const line = `${m.role === "user" ? "User" : "Assistant"}: ${extractContent(m)}`; if (historyLen + line.length > MAX_HISTORY_CHARS && historyParts.length > 0) break; historyParts.unshift(line); historyLen += line.length + 1; } const history = historyParts.join("\n"); question = (sysPrompt ? sysPrompt + "\n\n" : "") + (toolsPrompt ? toolsPrompt + "\n\n---\n" : "") + history; } else { question = `${sysPrompt ? "(Background)\n" + sysPrompt + "\n\n" : ""}${toolsPrompt}\n\n---\nUser message (respond to this):\n${lastUserMsg}`; } } else { // smart compress system prompt (keep toolsPrompt intact) let context = sysPrompt; const overhead = toolsPrompt.length + lastUserMsg.length + 200; const remaining = MAX_QUESTION - overhead; if (context && context.length > remaining && remaining > 0) { // fix: keep head+tail, trim middle const layoutMatch = context.match(/([\s\S]*?)<\/project_layout>/); const layoutSummary = layoutMatch ? layoutMatch[1].trim().split("\n").slice(0, 30).join("\n") : ""; const headLen = Math.min(800, Math.floor(remaining * 0.6)); const tailLen = Math.min(400, remaining - headLen); const head = context.slice(0, headLen); const tail = context.length > headLen + tailLen ? context.slice(-tailLen) : ""; const layoutPart = layoutSummary ? "\n\nDirectory (summary):\n" + layoutSummary.slice(0, Math.min(300, remaining - headLen - tailLen - 50)) : ""; context = tail ? head + "\n...[truncated]..." + layoutPart + "\n" + tail : head + layoutPart; context = context.slice(0, remaining); } question = `${context ? "(Background)\n" + context + "\n\n" : ""}${toolsPrompt}\n\n---\nUser message (respond to this):\n${lastUserMsg}`; } // inject language hint & truncate if (!toolResultPathOAI) { question += langHintOAI; // final truncation - always keep toolsPrompt if (question.length > MAX_QUESTION) { const minTemplate = "\n\n---\nUser message (respond to this):\n"; const compact = toolsPrompt + minTemplate + lastUserMsg + langHintOAI; if (compact.length > MAX_QUESTION) { const availForUser = Math.max(500, MAX_QUESTION - toolsPrompt.length - minTemplate.length - langHintOAI.length); question = toolsPrompt + minTemplate + lastUserMsg.slice(0, Math.max(0, availForUser)) + langHintOAI; } else { question = compact; } } } } if (!question) { return json(res, 400, { error: { message: "Empty question", type: "invalid_request_error" } }); } // session - reuse IMA session for conversation memory // fix: use system prompt as stable anchor let oaiConvId = req.headers["x-conversation-id"] || req.headers["x-session-id"] || ""; if (!oaiConvId && messages.length > 0) { const sysMsg = messages.find(m => m.role === "system"); const anchor = sysMsg ? extractContent(sysMsg).slice(0, 200) : extractContent(messages[0]).slice(0, 200); oaiConvId = "conv-" + crypto.createHash("md5").update(anchor).digest("hex").slice(0, 12); } let sessionId; try { sessionId = await ensureSession(oaiConvId, question.slice(0, 100)); } catch (e) { return json(res, 502, { error: { message: "Session init failed: " + e.message, type: "api_error" } }); } // --- non-stream --- if (!stream) { try { const events = await imaQaStreamWithRetry(oaiConvId, question, model.type, model.id); const text = await collectResponseText(events); const parsed = parseFunctionCalls(text); const choice = { index: 0, message: {}, finish_reason: "stop" }; if (parsed.found && parsed.calls.length > 0) { choice.message = { role: "assistant", content: parsed.text || null, tool_calls: parsed.calls, }; choice.finish_reason = "tool_calls"; } else { // fallback: placeholder for empty text choice.message = { role: "assistant", content: text || "(No content generated. Please retry or rephrase.)" }; } return json(res, 200, { id: "chatcmpl-" + crypto.randomUUID().slice(0, 8), object: "chat.completion", created: Math.floor(Date.now() / 1000), model: modelKey, choices: [choice], usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }, }); } catch (e) { return json(res, 502, { error: { message: "IMA error: " + e.message, type: "api_error" } }); } } // --- stream --- cors(res); res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", "x-request-id": "chatcmpl-" + crypto.randomUUID().slice(0, 8), }); const chatId = "chatcmpl-" + crypto.randomUUID().slice(0, 8); const created = Math.floor(Date.now() / 1000); let resClosed = false; let sentText = false; // whether any text/tool content sent (fallback check) res.on('close', () => { resClosed = true; }); function emit(delta, finishReason, toolCalls) { if (resClosed) return; try { const d = toolCalls && toolCalls.length > 0 ? { role: "assistant", tool_calls: toolCalls } : { role: "assistant", content: delta }; if ((delta && delta.length > 0) || (toolCalls && toolCalls.length > 0)) sentText = true; res.write("data: " + JSON.stringify({ id: chatId, object: "chat.completion.chunk", created, model: modelKey, choices: [{ index: 0, delta: d, finish_reason: finishReason }], }) + "\n\n"); } catch (e) { resClosed = true; } } try { const events = await imaQaStreamWithRetry(oaiConvId, question, model.type, model.id); const filter = new StreamFilter(); let done = false; let sawEvent = false; for await (const evt of events) { if (resClosed) break; if (CONTROL_EVENTS.has(evt.event)) { if (evt.event === "COMPLETED" || evt.event === "CLOSE") done = true; break; } sawEvent = true; const txt = eventText(evt); if (txt) { const clean = filter.feed(txt); if (clean) emit(clean, null, null); } } if (DEBUG_SSE && !sawEvent) console.error("[SSE-EMPTY] openai stream: no data events"); // flush residual buffer const flushed = filter.flush(); if (flushed && !resClosed) emit(flushed, null, null); // detect and send function calls if (!resClosed) { const calls = filter.parseCalls(); // unparseable function_call residual - output as text if (filter.leftover) emit(filter.leftover, null, null); if (calls.length > 0) { // OpenAI streaming tool_calls: send individually for (let i = 0; i < calls.length; i++) { const tc = calls[i]; if (resClosed) break; try { sentText = true; res.write("data: " + JSON.stringify({ id: chatId, object: "chat.completion.chunk", created, model: modelKey, choices: [{ index: 0, delta: { tool_calls: [{ index: i, id: tc.id, type: "function", function: { name: tc.function.name, arguments: tc.function.arguments } }] }, finish_reason: null }], }) + "\n\n"); } catch (e) { resClosed = true; } } if (!resClosed) { res.write("data: " + JSON.stringify({ id: chatId, object: "chat.completion.chunk", created, model: modelKey, choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }], }) + "\n\n"); } } else { // : ( / // / ) - "" if (!sentText && !resClosed) { const fb = done ? "(No content generated. Please retry or rephrase.)" : "(Response interrupted. Please retry.)"; emit(fb, null, null); } // done true res.write("data: " + JSON.stringify({ id: chatId, object: "chat.completion.chunk", created, model: modelKey, choices: [{ index: 0, delta: {}, finish_reason: done ? "stop" : "length" }], }) + "\n\n"); } } } catch (e) { if (!resClosed) { try { res.write("data: " + JSON.stringify({ id: chatId, object: "chat.completion.chunk", created, model: modelKey, choices: [{ index: 0, delta: {}, finish_reason: "error" }], }) + "\n\n"); } catch {} } console.error(`[STREAM-ERR] ${e.message}`); } if (!resClosed) { try { res.write("data: [DONE]\n\n"); res.end(); } catch {} } } // ============================================================ // 9. Anthropic /v1/messages // ============================================================ async function anthropicMessages(req, res, body) { const modelKey = body.model || DEFAULT_MODEL; const model = resolveModel(modelKey); const stream = !!body.stream; const messages = body.messages || []; // system prompt let sysPrompt = ""; if (typeof body.system === "string") sysPrompt = body.system; else if (Array.isArray(body.system)) sysPrompt = body.system.filter(s => s.type === "text").map(s => s.text).join("\n"); // tools -> prompt injection // Claude Code tools let tools = body.tools; if (!tools || tools.length === 0) { tools = [ {name: "Bash", description: "Execute a bash command. Use for: reading files (cat/ls), system info (uname/df/free), git, npm, find, grep, etc.", input_schema: {type: "object", properties: {command: {type: "string", description: "The bash command to execute"}}, required: ["command"]}}, {name: "Read", description: "Read contents of a file. Use for: inspecting file contents, reading configs, source code.", input_schema: {type: "object", properties: {file_path: {type: "string", description: "Absolute path to the file"}}, required: ["file_path"]}}, {name: "Write", description: "Write content to a file. Use for: creating new files, overwriting existing files.", input_schema: {type: "object", properties: {file_path: {type: "string", description: "Absolute path"}, content: {type: "string", description: "Content to write"}}, required: ["file_path", "content"]}}, {name: "Glob", description: "Find files matching a pattern. Use for: searching for files by name.", input_schema: {type: "object", properties: {pattern: {type: "string", description: "Glob pattern like **/*.js"}}, required: ["pattern"]}}, {name: "Grep", description: "Search file contents for a pattern. Use for: finding code, searching logs.", input_schema: {type: "object", properties: {pattern: {type: "string", description: "Regex pattern to search for"}, path: {type: "string", description: "Directory or file to search in"}}, required: ["pattern"]}}, ]; } // tools (Claude Code ) const isTitleGen = sysPrompt.includes('Generate a concise, sentence-case title'); // prompt IMA 10240 const MAX_TOOLS = 8; const ESSENTIAL_TOOLS = new Set(['Bash', 'Read', 'Write', 'Glob', 'Grep']); let displayTools = tools; if (isTitleGen) { displayTools = []; // title generation does not need tools } else if (displayTools.length > MAX_TOOLS) { const essential = displayTools.filter(t => ESSENTIAL_TOOLS.has(t.name)); const others = displayTools.filter(t => !ESSENTIAL_TOOLS.has(t.name)); const available = MAX_TOOLS - essential.length; displayTools = [...essential, ...others.slice(0, Math.max(0, available))]; } // -- -- // Anthropic tool_use / tool_result () function msgHasType(msg, type) { if (Array.isArray(msg.content)) return msg.content.some(c => c.type === type); return false; } const hasToolUses = messages.some(m => m.role === "assistant" && msgHasType(m, "tool_use")); const hasToolResults = messages.some(m => m.role === "user" && msgHasType(m, "tool_result")); function stripMetadata(text) { return text // () .replace(/[\s\S]*?<\/system-reminder>/g, '') .replace(/[\s\S]*?<\/local-command-caveat>/g, '') .replace(/[\s\S]*?<\/local-command-stdout>/g, '') .replace(/[^<]*<\/command-name>/g, '') .replace(/[^<]*<\/command-message>/g, '') .replace(/[^<]*<\/command-args>/g, '') // ( session ) .replace(/<\/?session>/g, '') .trim(); } const nonSys = messages.filter(m => m.role !== "system"); const userMsgs = nonSys.filter(m => m.role === "user"); if (userMsgs.length === 0) return json(res, 400, { type: "error", error: { type: "invalid_request_error", message: "No user message" } }); const realUserMsgs = userMsgs.filter(m => { const c = stripMetadata(extractContent(m)); return c.length > 5; }); const lastUserMsg = realUserMsgs.length > 0 ? stripMetadata(extractContent(realUserMsgs[realUserMsgs.length - 1])) : stripMetadata(extractContent(userMsgs[userMsgs.length - 1])); if (!lastUserMsg) { } // : const hasCJK = /\p{Script=Han}/u.test(lastUserMsg); const langHint = hasCJK ? "\n## Language\nRespond in the same language as the user's message. The user is writing in Chinese - respond in Chinese (Simplified Chinese).\n" : ""; // toolsPrompt ( question ) const toolsPrompt = (displayTools && displayTools.length > 0) ? buildToolsPrompt( displayTools.map(t => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.input_schema || {} } })) ) : ""; let question; let toolResultPath = false; // tool result round flag (skip langHint and truncation) const MAX_QUESTION = 10000; if (hasToolResults && hasToolUses) { toolResultPath = true; // flag: then skip langHint injection and generic truncation // - const isCJK = hasCJK; // + const calledFuncs = []; const results = []; for (const m of messages) { if (m.role === "assistant" && Array.isArray(m.content)) { for (const c of m.content) { if (c.type === "tool_use") calledFuncs.push(c.name + "(" + JSON.stringify(c.input) + ")"); } } if (m.role === "user" && Array.isArray(m.content)) { for (const c of m.content) { if (c.type === "tool_result") { const tc = typeof c.content === "string" ? c.content : Array.isArray(c.content) ? c.content.map(cc => cc.text || "").join("") : ""; results.push(tc); } } } } // ( tool_result ) const userQs = []; for (const m of messages) { if (m.role !== "user") continue; if (Array.isArray(m.content) && m.content.some(c => c.type === "tool_result")) continue; const q = stripMetadata(extractContent(m)); if (q) userQs.push(q); } const originalQuestion = userQs.length > 0 ? userQs[userQs.length - 1] : (lastUserMsg || ""); // : + + if (isCJK) { question = "SYSTEM: You just called these functions. Results below:\n\n"; for (let i = 0; i < calledFuncs.length; i++) { question += "Function: " + calledFuncs[i] + "\n"; question += "Output:\n\"\"\"\n" + (results[i] || "") + "\n\"\"\"\n\n"; } question += "User question: \"" + originalQuestion + "\"\n\n"; question += "Answer directly based on the outputs. Do NOT say \"you shared\" or \"it looks like\" or \"I see you've shared\". The content inside \"\"\" blocks is YOUR function output, not user-provided content."; } else { question = "SYSTEM: You just called these functions and received these outputs:\n\n"; for (let i = 0; i < calledFuncs.length; i++) { question += "Function: " + calledFuncs[i] + "\n"; question += "Output:\n\"\"\"\n" + (results[i] || "") + "\n\"\"\"\n\n"; } question += "User's original question: \"" + originalQuestion + "\"\n\n"; question += "Answer the user's question DIRECTLY based on the outputs above. Do NOT say \"you shared\" or \"I see you've shared\" or \"it looks like\". The content inside \"\"\" blocks is YOUR function output, NOT content the user sent you."; } // : sysPrompt tools ( ) // 5 , < 10000 // - , function_call const minimalToolsPrompt = buildToolsPromptSoft( displayTools.filter(t => ESSENTIAL_TOOLS.has(t.name)).slice(0, 5) .map(t => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.input_schema || {} } })) ); question = minimalToolsPrompt + "\n\n---\n" + question; // langHint - question < 10000 } else if (nonSys.length > 1 && realUserMsgs.length >= 1) { const realMsgs = nonSys .filter(m => { const c = stripMetadata(extractContent(m)); return c.length > 5; }); if (realMsgs.length > 1) { // : 6 20 const MAX_HISTORY_CHARS = 6000; // reserve space for sysPrompt + toolsPrompt const recentMsgs = realMsgs.slice(-20); let historyParts = []; let historyLen = 0; for (let i = recentMsgs.length - 1; i >= 0; i--) { const m = recentMsgs[i]; const line = (m.role === "user" ? "User" : "Assistant") + ": " + stripMetadata(extractContent(m)); if (historyLen + line.length > MAX_HISTORY_CHARS && historyParts.length > 0) break; historyParts.unshift(line); historyLen += line.length + 1; } question = historyParts.join("\n"); question = (sysPrompt ? sysPrompt + "\n\n" : "") + (toolsPrompt ? toolsPrompt + "\n\n---\n" : "") + question; } else { question = (sysPrompt ? "(Background)\n" + sysPrompt + "\n\n" : "") + toolsPrompt + "\n\n---\nUser message (respond to this):\n" + lastUserMsg; } } else { let context = sysPrompt; const overhead = toolsPrompt.length + lastUserMsg.length + 200; const remaining = MAX_QUESTION - overhead; if (context && context.length > remaining && remaining > 0) { // : + // "500+" const layoutMatch = context.match(/([\s\S]*?)<\/project_layout>/); const layoutSummary = layoutMatch ? layoutMatch[1].trim().split("\n").slice(0, 30).join("\n") : ""; const headLen = Math.min(800, Math.floor(remaining * 0.6)); const tailLen = Math.min(400, remaining - headLen); const head = context.slice(0, headLen); const tail = context.length > headLen + tailLen ? context.slice(-tailLen) : ""; const layoutPart = layoutSummary ? "\n\nDirectory (summary):\n" + layoutSummary.slice(0, Math.min(300, remaining - headLen - tailLen - 50)) : ""; context = tail ? head + "\n...[truncated]..." + layoutPart + "\n" + tail : head + layoutPart; context = context.slice(0, remaining); } question = (context ? "(Background)\n" + context + "\n\n" : "") + toolsPrompt + "\n\n---\nUser message (respond to this):\n" + lastUserMsg; } // & ( - < 10000) if (!toolResultPath) { question += langHint; if (question.length > MAX_QUESTION) { const minTemplate = "\n\n---\nUser message (respond to this):\n"; const compact = toolsPrompt + minTemplate + lastUserMsg + langHint; if (compact.length > MAX_QUESTION) { const availForUser = Math.max(500, MAX_QUESTION - toolsPrompt.length - minTemplate.length - langHint.length); question = toolsPrompt + minTemplate + lastUserMsg.slice(0, Math.max(0, availForUser)) + langHint; } else { question = compact; } } } // - : IMA session const convId = req.headers["x-conversation-id"] || req.headers["x-session-id"] || ""; // : prompt + hash // Claude Code x-conversation-id, system prompt let effectiveConvId = convId; if (!effectiveConvId && messages.length > 0) { // system prompt (Claude Code system ) const sysMsg = messages.find(m => m.role === "system"); const anchor = sysMsg ? extractContent(sysMsg).slice(0, 200) // system prompt first200chars, usually contains project path : extractContent(messages[0]).slice(0, 200); // fallback: first message effectiveConvId = "conv-" + crypto.createHash("md5").update(anchor).digest("hex").slice(0, 12); } let sessionId; const isNewSession = !getCachedSession(effectiveConvId); try { sessionId = await ensureSession(effectiveConvId, question.slice(0, 100)); } catch (e) { console.error(`[ANTHROPIC-SESSION] init failed: ${e.message}`); return json(res, 502, { type: "error", error: { type: "api_error", message: "Session init failed: " + e.message } }); } // --- non-stream --- if (!stream) { try { const events = await imaQaStreamWithRetry(effectiveConvId, question, model.type, model.id); const text = await collectResponseText(events); const parsed = parseFunctionCalls(text); if (parsed.found && parsed.calls.length > 0) { const content = []; if (parsed.text) content.push({ type: "text", text: parsed.text }); for (const c of parsed.calls) { content.push({ type: "tool_use", id: c.id, name: c.function.name, input: JSON.parse(c.function.arguments || "{}"), }); } return json(res, 200, { id: "msg_" + crypto.randomUUID().slice(0, 8), type: "message", role: "assistant", model: modelKey, content, stop_reason: "tool_use", stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 }, }); } return json(res, 200, { id: "msg_" + crypto.randomUUID().slice(0, 8), type: "message", role: "assistant", model: modelKey, // : , content content: [{ type: "text", text: text || "(No content generated. Please retry or rephrase.)" }], stop_reason: "end_turn", stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 }, }); } catch (e) { console.error(`[ANTHROPIC-NONSTREAM-ERR] ${e.message}`); return json(res, 502, { type: "error", error: { type: "api_error", message: "IMA error: " + e.message } }); } } // --- stream --- cors(res); res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }); const msgId = "msg_" + crypto.randomUUID().slice(0, 8); let resClosed = false; res.on('close', () => { resClosed = true; }); const em = (e, d) => { if (resClosed) return; try { if (e) res.write(`event: ${e}\n`); res.write(`data: ${JSON.stringify(d)}\n\n`); } catch (_) { resClosed = true; } }; em("message_start", { type: "message_start", message: { id: msgId, type: "message", role: "assistant", model: modelKey, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } }, }); em("content_block_start", { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }); let stopReason = "end_turn"; let done = false; let sentText = false; // whether any text has been sent delta (fallback check) const filter = new StreamFilter(); let calls = []; // in try outer scope, for later tool_use sending try { const events = await imaQaStreamWithRetry(effectiveConvId, question, model.type, model.id); for await (const evt of events) { if (resClosed) break; if (CONTROL_EVENTS.has(evt.event)) { if (evt.event === "COMPLETED" || evt.event === "CLOSE") done = true; break; } const txt = eventText(evt); if (txt) { const clean = filter.feed(txt); if (clean) { em("content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: clean } }); sentText = true; } } } // flush residual buffer const flushed = filter.flush(); if (flushed && !resClosed) { em("content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: flushed } }); sentText = true; } calls = filter.parseCalls(); // unparseable function_call residual - output as text if (filter.leftover && !resClosed) { em("content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: filter.leftover } }); sentText = true; } // : - // Claude Code "" if (!sentText && calls.length === 0 && !resClosed) { const fb = done ? "(No content generated. Please retry or rephrase.)" : "(Response interrupted. Please retry.)"; em("content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: fb } }); sentText = true; } } catch (e) { console.error(`[ANTHROPIC-STREAM-ERR] ${e.message}`); // , if (!sentText && !resClosed) { try { em("content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "(Response error. Please retry.)" } }); sentText = true; } catch {} } } // () if (!resClosed) em("content_block_stop", { type: "content_block_stop", index: 0 }); // - calls () if (!resClosed && calls.length > 0) { stopReason = "tool_use"; for (let i = 0; i < calls.length; i++) { const c = calls[i]; const idx = i + 1; if (resClosed) break; em("content_block_start", { type: "content_block_start", index: idx, content_block: { type: "tool_use", id: c.id, name: c.function.name, input: {} } }); em("content_block_delta", { type: "content_block_delta", index: idx, delta: { type: "input_json_delta", partial_json: c.function.arguments } }); em("content_block_stop", { type: "content_block_stop", index: idx }); } } if (!resClosed) { em("message_delta", { type: "message_delta", delta: { stop_reason: stopReason, stop_sequence: null }, usage: { output_tokens: 0 } }); em("message_stop", { type: "message_stop" }); try { res.end(); } catch {} } } // ============================================================ // 10. // ============================================================ async function router(req, res) { cors(res); if (req.method === "OPTIONS") { res.writeHead(204); return res.end(); } const url = req.url; const urlPath = url.split("?")[0]; if (urlPath === "/health") return json(res, 200, { status: "ok" }); if (urlPath === "/") return json(res, 200, { service: "ima2api", version: "3.0.0", endpoints: { openai: ["POST /v1/chat/completions (tools / function calling)", "GET /v1/models"], anthropic: ["POST /v1/messages (tools / tool_use)"], }, models: Object.keys(MODELS), auth: "Bearer or x-api-key header", }); if (!checkAuth(req)) { return json(res, 401, { error: { type: "authentication_error", message: "Invalid API key" } }); } let body = {}; if (req.method === "POST") { try { const raw = await new Promise((resolve, reject) => { const chunks = []; req.on("data", c => chunks.push(c)); req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8"))); req.on("error", reject); }); body = raw ? JSON.parse(raw) : {}; // body } catch (e) { return json(res, 400, { error: { type: "invalid_request_error", message: "Invalid JSON body" } }); } } if (req.method === "GET" && urlPath === "/v1/models") { const modelList = Object.entries(MODELS).map(([id, info]) => ({ id, object: "model", created: 1700000000, owned_by: "ima", type: "model", display_name: info.name, created_at: "2024-01-01T00:00:00Z", })); const ids = modelList.map(m => m.id); return json(res, 200, { object: "list", data: modelList, has_more: false, first_id: ids[0] || null, last_id: ids[ids.length - 1] || null, }); } // Anthropic SDK: GET /v1/models/{model_id} if (req.method === "GET" && urlPath.startsWith("/v1/models/")) { const modelId = url.slice("/v1/models/".length); const model = resolveModel(modelId); if (!model) return json(res, 404, { error: { type: "error", error: { type: "not_found_error", message: `Model not found: ${modelId}` } } }); return json(res, 200, { id: modelId, type: "model", display_name: model.name, created_at: "2024-01-01T00:00:00Z", }); } if (req.method === "POST" && urlPath === "/v1/chat/completions") { try { return await openaiChat(req, res, body); } catch (e) { console.error(`[OPENAI-FATAL] ${e.message}\n${e.stack}`); if (!res.headersSent) return json(res, 500, { error: { type: "api_error", message: e.message } }); else { try { res.end(); } catch {} } } } if (req.method === "POST" && urlPath === "/v1/messages") { try { return await anthropicMessages(req, res, body); } catch (e) { console.error(`[ANTHROPIC-FATAL] ${e.message}\n${e.stack}`); if (!res.headersSent) return json(res, 500, { error: { type: "api_error", message: e.message } }); else { try { res.end(); } catch {} } } } json(res, 404, { error: { message: `Not found: ${req.method} ${url}` } }); } // ============================================================ // 11. Startup // ============================================================ const PORT = process.env.PORT || CONFIG.server?.port || 8080; const HOST = process.env.HOST || CONFIG.server?.host || "0.0.0.0"; http.createServer(router).listen(PORT, HOST, () => { console.log(""); console.log("=================================================="); console.log(`[STARTUP] Listening on http://${HOST}:${PORT}`); console.log(`[STARTUP] API keys: ${API_KEYS.size} (source: ${process.env.IMA_API_KEYS ? "env" : "config"})`); for (const k of API_KEYS) console.log(`[STARTUP] key: ${maskKey(k)} (len=${k.length})`); console.log("=================================================="); console.log(""); });