"use strict"; /** * translationProxy.js — Self-contained Node.js translation proxy for AG X. * * Replaces the Python translate-proxy.py entirely. * Supports: openai-compat, anthropic, command-code backends. * Handles the AG X language server's Gemini-format requests and * translates them to the appropriate backend API format. * * Routes: * GET /codex/list-endpoints — list configured providers * POST /codex/switch-endpoint — switch active provider at runtime * GET /v1/models — list models * GET /health — health check * POST /v1/responses — Responses API (translated to backend) * POST /v1internal:* — Gemini internal format (translated) * GET /v1internal:fetchAvailableModels — model list in Gemini format */ const http = require("http"); const https = require("https"); const fs = require("fs"); const path = require("path"); const os = require("os"); const url = require("url"); // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- let BACKEND = "openai-compat"; let TARGET_URL = "http://localhost:11434/v1"; let API_KEY = ""; let OAUTH_PROVIDER = ""; let MODELS = []; let CC_VERSION = ""; let REASONING_ENABLED = true; let REASONING_EFFORT = "medium"; let PORT = 48080; const CONFIG_DIR = path.join(os.homedir(), ".cache", "codex-proxy"); const ENDPOINTS_PATH = path.join(os.homedir(), ".codex", "endpoints.json"); const ACTIVE_PATH = path.join(os.homedir(), ".codex", ".active-endpoint.json"); // --------------------------------------------------------------------------- // Init from config // --------------------------------------------------------------------------- function initFromConfig() { // Try loading from active config file first const activeConfigPath = path.join(CONFIG_DIR, "proxy-active.json"); if (fs.existsSync(activeConfigPath)) { try { const cfg = JSON.parse(fs.readFileSync(activeConfigPath, "utf8")); applyConfig(cfg); console.log("[Proxy] Loaded active config:", cfg.backend_type, cfg.target_url); return; } catch (e) { /* fallthrough */ } } // Try loading from endpoints.json if (fs.existsSync(ENDPOINTS_PATH)) { try { const ep = JSON.parse(fs.readFileSync(ENDPOINTS_PATH, "utf8")); let activeName = ""; if (fs.existsSync(ACTIVE_PATH)) { try { activeName = JSON.parse(fs.readFileSync(ACTIVE_PATH, "utf8")).active; } catch (e) {} } if (!activeName) activeName = ep.default || ""; const endpoint = ep.endpoints?.find(e => e.name === activeName) || ep.endpoints?.[0]; if (endpoint) { applyEndpoint(endpoint); console.log("[Proxy] Loaded endpoint:", endpoint.name); } } catch (e) { /* fallthrough */ } } } function applyConfig(cfg) { PORT = cfg.port || 48080; BACKEND = cfg.backend_type || "openai-compat"; TARGET_URL = cfg.target_url || "http://localhost:11434/v1"; API_KEY = cfg.api_key || ""; OAUTH_PROVIDER = cfg.oauth_provider || ""; REASONING_ENABLED = cfg.reasoning_enabled !== undefined ? cfg.reasoning_enabled : true; REASONING_EFFORT = cfg.reasoning_effort || "medium"; CC_VERSION = cfg.cc_version || ""; MODELS = cfg.models || []; } function applyEndpoint(endpoint) { BACKEND = endpoint.backend_type || "openai-compat"; TARGET_URL = endpoint.base_url || ""; if (BACKEND === "openai-compat" && !TARGET_URL.endsWith("/v1")) { TARGET_URL = TARGET_URL.replace(/\/+$/, "") + "/v1"; } API_KEY = endpoint.api_key || ""; OAUTH_PROVIDER = endpoint.oauth_provider || ""; REASONING_ENABLED = endpoint.reasoning_enabled !== undefined ? endpoint.reasoning_enabled : true; REASONING_EFFORT = endpoint.reasoning_effort || "medium"; CC_VERSION = endpoint.cc_version || ""; MODELS = (endpoint.models || []).map(m => ({ id: typeof m === "string" ? m : m.id, object: "model", created: 1700000000, owned_by: endpoint.name || "custom" })); } // --------------------------------------------------------------------------- // HTTP helpers // --------------------------------------------------------------------------- function jsonResponse(res, statusCode, data) { const body = JSON.stringify(data); res.writeHead(statusCode, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), }); res.end(body); } function readBody(req) { return new Promise((resolve, reject) => { const chunks = []; req.on("data", chunk => chunks.push(chunk)); req.on("end", () => resolve(Buffer.concat(chunks).toString())); req.on("error", reject); }); } function proxyRequest(targetUrl, method, headers, bodyBuffer, res, isStream) { return new Promise((resolve, reject) => { const urlObj = new URL(targetUrl); const isHttps = urlObj.protocol === "https:"; const mod = isHttps ? https : http; const opts = { hostname: urlObj.hostname, port: urlObj.port || (isHttps ? 443 : 80), path: urlObj.pathname + urlObj.search, method: method, headers: { ...headers }, }; // Remove host header to avoid conflicts delete opts.headers.host; delete opts.headers["host"]; // Set correct content-length if (bodyBuffer) { opts.headers["content-length"] = Buffer.byteLength(bodyBuffer); } const upstream = mod.request(opts, (upRes) => { if (isStream && (upRes.headers["content-type"]?.includes("text/event-stream") || upRes.headers["content-type"]?.includes("application/octet-stream"))) { res.writeHead(upRes.statusCode, upRes.headers); upRes.pipe(res); upRes.on("end", resolve); upRes.on("error", reject); } else { const respChunks = []; upRes.on("data", chunk => respChunks.push(chunk)); upRes.on("end", () => { const respBody = Buffer.concat(respChunks).toString(); if (!res.headersSent) { res.writeHead(upRes.statusCode, upRes.headers); } res.end(respBody); resolve(respBody); }); upRes.on("error", reject); } }); upstream.on("error", (err) => { console.error("[Proxy] Upstream error:", err.message); if (!res.headersSent) { jsonResponse(res, 502, { error: { message: `Upstream error: ${err.message}` } }); } reject(err); }); if (bodyBuffer) upstream.write(bodyBuffer); upstream.end(); }); } // --------------------------------------------------------------------------- // Translation: Responses API → OpenAI Chat Completions // --------------------------------------------------------------------------- function responsesToChatCompletions(body) { const parsed = typeof body === "string" ? JSON.parse(body) : body; const messages = []; // System instructions if (parsed.instructions) { messages.push({ role: "system", content: parsed.instructions }); } // Input items if (parsed.input) { const inputItems = Array.isArray(parsed.input) ? parsed.input : [{ role: "user", content: parsed.input }]; for (const item of inputItems) { if (typeof item === "string") { messages.push({ role: "user", content: item }); } else if (item.type === "message") { const content = item.content; if (Array.isArray(content)) { const textParts = content.filter(c => c.type === "input_text" || c.type === "text").map(c => c.text).join("\n"); if (textParts) messages.push({ role: item.role || "user", content: textParts }); } else if (typeof content === "string") { messages.push({ role: item.role || "user", content }); } } else if (item.type === "function_call_output" || item.type === "function_call_output") { messages.push({ role: "tool", content: item.output || "", tool_call_id: item.call_id || item.id || "" }); } else if (item.type === "function_call") { // Add assistant message with tool call const lastMsg = messages[messages.length - 1]; if (lastMsg && lastMsg.role === "assistant" && lastMsg.tool_calls) { lastMsg.tool_calls.push({ id: item.call_id || item.id || "", type: "function", function: { name: item.name || "", arguments: item.arguments || "{}" } }); } else { messages.push({ role: "assistant", content: null, tool_calls: [{ id: item.call_id || item.id || "", type: "function", function: { name: item.name || "", arguments: item.arguments || "{}" } }] }); } } else if (item.role) { messages.push({ role: item.role, content: item.content || "" }); } } } // Tools const tools = []; if (parsed.tools) { for (const t of parsed.tools) { if (t.type === "function" && t.function) { tools.push({ type: "function", function: t.function }); } } } const result = { model: parsed.model || "gpt-4o", messages, stream: parsed.stream || false, }; if (tools.length > 0) result.tools = tools; if (parsed.temperature !== undefined) result.temperature = parsed.temperature; if (parsed.max_output_tokens !== undefined) result.max_tokens = parsed.max_output_tokens; if (parsed.top_p !== undefined) result.top_p = parsed.top_p; return result; } // --------------------------------------------------------------------------- // Translation: Chat Completions response → Responses API // --------------------------------------------------------------------------- function chatToResponses(chatResp, reqModel) { const choice = chatResp.choices?.[0]; if (!choice) { return { id: "resp_" + Date.now(), object: "response", model: reqModel || chatResp.model || "unknown", created: Math.floor(Date.now() / 1000), status: "failed", output: [] }; } const output = []; const msg = choice.message || {}; if (msg.content) { output.push({ type: "message", id: "msg_" + Date.now(), role: "assistant", content: [{ type: "output_text", text: msg.content }] }); } if (msg.tool_calls) { for (const tc of msg.tool_calls) { output.push({ type: "function_call", id: tc.id || "fc_" + Date.now(), call_id: tc.id || "fc_" + Date.now(), name: tc.function?.name || "", arguments: tc.function?.arguments || "{}" }); } } return { id: "resp_" + Date.now(), object: "response", model: reqModel || chatResp.model || "unknown", created: Math.floor(Date.now() / 1000), status: "completed", output, usage: { input_tokens: chatResp.usage?.prompt_tokens || 0, output_tokens: chatResp.usage?.completion_tokens || 0, total_tokens: chatResp.usage?.total_tokens || 0, } }; } // --------------------------------------------------------------------------- // Translation: Responses API → Anthropic Messages // --------------------------------------------------------------------------- function responsesToAnthropic(body) { const parsed = typeof body === "string" ? JSON.parse(body) : body; const messages = []; let systemPrompt = ""; if (parsed.instructions) { systemPrompt = parsed.instructions; } if (parsed.input) { const inputItems = Array.isArray(parsed.input) ? parsed.input : [{ role: "user", content: parsed.input }]; for (const item of inputItems) { if (typeof item === "string") { messages.push({ role: "user", content: item }); } else if (item.type === "message") { const content = item.content; if (Array.isArray(content)) { const textParts = content.filter(c => c.type === "input_text" || c.type === "text").map(c => c.text).join("\n"); if (textParts) messages.push({ role: item.role === "assistant" ? "assistant" : "user", content: textParts }); } else if (typeof content === "string") { messages.push({ role: item.role === "assistant" ? "assistant" : "user", content }); } } else if (item.type === "function_call") { messages.push({ role: "assistant", content: [{ type: "tool_use", id: item.call_id || item.id || "", name: item.name || "", input: JSON.parse(item.arguments || "{}") }] }); } else if (item.type === "function_call_output") { messages.push({ role: "user", content: [{ type: "tool_result", tool_use_id: item.call_id || item.id || "", content: item.output || "" }] }); } else if (item.role) { messages.push({ role: item.role === "assistant" ? "assistant" : "user", content: item.content || "" }); } } } const tools = []; if (parsed.tools) { for (const t of parsed.tools) { if (t.type === "function" && t.function) { tools.push({ name: t.function.name, description: t.function.description || "", input_schema: t.function.parameters || { type: "object", properties: {} } }); } } } const result = { model: parsed.model || "claude-sonnet-4-20250514", messages, max_tokens: parsed.max_output_tokens || 16384, stream: parsed.stream || false, }; if (systemPrompt) result.system = systemPrompt; if (tools.length > 0) result.tools = tools; if (parsed.temperature !== undefined) result.temperature = parsed.temperature; if (parsed.top_p !== undefined) result.top_p = parsed.top_p; return result; } // --------------------------------------------------------------------------- // Translation: Anthropic response → Responses API // --------------------------------------------------------------------------- function anthropicToResponses(anthroResp, reqModel) { const output = []; const content = anthroResp.content || []; for (const block of content) { if (block.type === "text") { output.push({ type: "message", id: "msg_" + Date.now(), role: "assistant", content: [{ type: "output_text", text: block.text }] }); } else if (block.type === "tool_use") { output.push({ type: "function_call", id: block.id || "fc_" + Date.now(), call_id: block.id || "fc_" + Date.now(), name: block.name || "", arguments: JSON.stringify(block.input || {}) }); } } return { id: "resp_" + Date.now(), object: "response", model: reqModel || anthroResp.model || "unknown", created: Math.floor(Date.now() / 1000), status: anthroResp.stop_reason === "end_turn" || anthroResp.stop_reason === "stop" ? "completed" : "incomplete", output, usage: { input_tokens: anthroResp.usage?.input_tokens || 0, output_tokens: anthroResp.usage?.output_tokens || 0, total_tokens: (anthroResp.usage?.input_tokens || 0) + (anthroResp.usage?.output_tokens || 0), } }; } // --------------------------------------------------------------------------- // Gemini internal format → Responses API // --------------------------------------------------------------------------- function geminiToResponses(geminiReq, stream) { const parts = geminiReq.contents || []; const messages = []; for (const part of parts) { const role = part.role === "model" ? "assistant" : "user"; const contentParts = part.parts || []; for (const cp of contentParts) { if (cp.text) { messages.push({ type: "message", role, content: [{ type: "input_text", text: cp.text }] }); } else if (cp.functionCall) { messages.push({ type: "function_call", id: cp.functionCall.name + "_" + Date.now(), call_id: cp.functionCall.name + "_" + Date.now(), name: cp.functionCall.name, arguments: JSON.stringify(cp.functionCall.args || {}) }); } else if (cp.functionResponse) { messages.push({ type: "function_call_output", id: cp.functionResponse.name + "_" + Date.now(), call_id: cp.functionResponse.name + "_" + Date.now(), output: JSON.stringify(cp.functionResponse.response?.result || cp.functionResponse.response || "") }); } } } // Convert tools const tools = []; if (geminiReq.tools) { for (const t of geminiReq.tools) { if (t.functionDeclarations) { for (const fd of t.functionDeclarations) { tools.push({ type: "function", function: { name: fd.name, description: fd.description || "", parameters: fd.parameters || { type: "object", properties: {} } } }); } } } } return { model: (MODELS[0]?.id) || "default", input: messages, tools: tools.length > 0 ? tools : undefined, stream, }; } // --------------------------------------------------------------------------- // SSE streaming helpers // --------------------------------------------------------------------------- function parseSSE(data) { const events = []; let currentEvent = { event: "", data: "" }; for (const line of data.split("\n")) { if (line.startsWith("event: ")) { currentEvent.event = line.slice(7).trim(); } else if (line.startsWith("data: ")) { currentEvent.data = line.slice(6); events.push({ ...currentEvent }); currentEvent = { event: "", data: "" }; } else if (line.trim() === "" && currentEvent.data) { events.push({ ...currentEvent }); currentEvent = { event: "", data: "" }; } } return events; } function writeSSE(res, event, data) { if (!res.writableEnded && !res.destroyed) { res.write(`event: ${event}\ndata: ${typeof data === "string" ? data : JSON.stringify(data)}\n\n`); } } // --------------------------------------------------------------------------- // Stream translation: OpenAI SSE → Responses API SSE // --------------------------------------------------------------------------- async function handleOpenAIStream(req, res, body, config) { const chatBody = responsesToChatCompletions(body); chatBody.stream = true; const reqModel = chatBody.model; const baseUrl = (TARGET_URL || "").replace(/\/+$/, ""); let targetPath = "/v1/chat/completions"; const targetUrl = baseUrl + targetPath; const headers = { "Content-Type": "application/json" }; if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`; const bodyStr = JSON.stringify(chatBody); res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", }); writeSSE(res, "response.created", { id: "resp_" + Date.now(), object: "response", model: reqModel, created: Math.floor(Date.now() / 1000), status: "in_progress", output: [] }); const urlObj = new URL(targetUrl); const mod = urlObj.protocol === "https:" ? https : http; let contentAccum = ""; let toolCallsAccum = {}; let respId = "resp_" + Date.now(); return new Promise((resolve) => { const upstream = mod.request({ hostname: urlObj.hostname, port: urlObj.port || (urlObj.protocol === "https:" ? 443 : 80), path: urlObj.pathname + urlObj.search, method: "POST", headers: { ...headers, "Content-Length": Buffer.byteLength(bodyStr) }, }, (upRes) => { let buffer = ""; upRes.on("data", (chunk) => { buffer += chunk.toString(); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.startsWith("data: ")) { const dataStr = line.slice(6).trim(); if (dataStr === "[DONE]") { // Flush any remaining tool calls for (const [id, tc] of Object.entries(toolCallsAccum)) { writeSSE(res, "response.function_call_arguments.done", { item_id: id, arguments: tc.args }); } writeSSE(res, "response.completed", { id: respId, object: "response", model: reqModel, status: "completed", output: [{ type: "message", role: "assistant", content: [{ type: "output_text", text: contentAccum }] }] }); res.end(); resolve(); return; } try { const chunk2 = JSON.parse(dataStr); const delta = chunk2.choices?.[0]?.delta; if (delta?.content) { contentAccum += delta.content; writeSSE(res, "response.output_text.delta", { delta: delta.content }); } if (delta?.tool_calls) { for (const tc of delta.tool_calls) { if (!toolCallsAccum[tc.id || "tc_0"]) { toolCallsAccum[tc.id || "tc_0"] = { name: tc.function?.name || "", args: "" }; writeSSE(res, "response.output_item.added", { item: { type: "function_call", id: tc.id || "tc_0", call_id: tc.id || "tc_0", name: tc.function?.name || "" } }); } if (tc.function?.arguments) { toolCallsAccum[tc.id || "tc_0"].args += tc.function.arguments; writeSSE(res, "response.function_call_arguments.delta", { item_id: tc.id || "tc_0", delta: tc.function.arguments }); } } } } catch (e) { /* skip unparseable chunks */ } } } }); upRes.on("end", () => { if (!res.writableEnded) { writeSSE(res, "response.completed", { id: respId, object: "response", model: reqModel, status: "completed", output: [] }); res.end(); } resolve(); }); upRes.on("error", () => { if (!res.writableEnded) res.end(); resolve(); }); }); upstream.on("error", (err) => { console.error("[Proxy] Stream upstream error:", err.message); writeSSE(res, "response.completed", { id: respId, status: "failed", error: { message: err.message } }); if (!res.writableEnded) res.end(); resolve(); }); upstream.write(bodyStr); upstream.end(); }); } // --------------------------------------------------------------------------- // Stream translation: Anthropic SSE → Responses API SSE // --------------------------------------------------------------------------- async function handleAnthropicStream(req, res, body, config) { const antBody = responsesToAnthropic(body); antBody.stream = true; const reqModel = antBody.model; const baseUrl = (TARGET_URL || "").replace(/\/+$/, ""); const targetUrl = baseUrl + "/v1/messages"; const headers = { "Content-Type": "application/json", "anthropic-version": "2023-06-01", }; if (API_KEY) headers["x-api-key"] = API_KEY; const bodyStr = JSON.stringify(antBody); res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", }); const respId = "resp_" + Date.now(); writeSSE(res, "response.created", { id: respId, object: "response", model: reqModel, created: Math.floor(Date.now() / 1000), status: "in_progress", output: [] }); const urlObj = new URL(targetUrl); const mod = urlObj.protocol === "https:" ? https : http; let contentAccum = ""; let currentToolId = null; let currentToolName = ""; let currentToolArgs = ""; return new Promise((resolve) => { const upstream = mod.request({ hostname: urlObj.hostname, port: urlObj.port || (urlObj.protocol === "https:" ? 443 : 80), path: urlObj.pathname + urlObj.search, method: "POST", headers: { ...headers, "Content-Length": Buffer.byteLength(bodyStr) }, }, (upRes) => { let buffer = ""; upRes.on("data", (chunk) => { buffer += chunk.toString(); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.startsWith("data: ")) { const dataStr = line.slice(6).trim(); try { const evt = JSON.parse(dataStr); if (evt.type === "content_block_delta" && evt.delta?.type === "text_delta") { contentAccum += evt.delta.text; writeSSE(res, "response.output_text.delta", { delta: evt.delta.text }); } else if (evt.type === "content_block_start" && evt.content_block?.type === "tool_use") { currentToolId = evt.content_block.id; currentToolName = evt.content_block.name; currentToolArgs = ""; writeSSE(res, "response.output_item.added", { item: { type: "function_call", id: currentToolId, call_id: currentToolId, name: currentToolName } }); } else if (evt.type === "input_json_delta" && evt.partial_json) { currentToolArgs += evt.partial_json; writeSSE(res, "response.function_call_arguments.delta", { item_id: currentToolId, delta: evt.partial_json }); } else if (evt.type === "message_stop") { if (currentToolId) { writeSSE(res, "response.function_call_arguments.done", { item_id: currentToolId, arguments: currentToolArgs }); } writeSSE(res, "response.completed", { id: respId, object: "response", model: reqModel, status: "completed", output: [{ type: "message", role: "assistant", content: [{ type: "output_text", text: contentAccum }] }] }); res.end(); resolve(); return; } } catch (e) { /* skip */ } } } }); upRes.on("end", () => { if (!res.writableEnded) { writeSSE(res, "response.completed", { id: respId, status: "completed", output: [] }); res.end(); } resolve(); }); upRes.on("error", () => { if (!res.writableEnded) res.end(); resolve(); }); }); upstream.on("error", (err) => { writeSSE(res, "response.completed", { id: respId, status: "failed", error: { message: err.message } }); if (!res.writableEnded) res.end(); resolve(); }); upstream.write(bodyStr); upstream.end(); }); } // --------------------------------------------------------------------------- // Command-Code backend (passthrough with headers) // --------------------------------------------------------------------------- async function handleCommandCode(req, res, bodyStr) { const parsed = typeof bodyStr === "string" ? JSON.parse(bodyStr) : bodyStr; const isStream = parsed.stream || false; const baseUrl = (TARGET_URL || "").replace(/\/+$/, ""); const targetUrl = baseUrl + "/alpha/generate"; const headers = { "Content-Type": "application/json", "x-command-code-version": CC_VERSION || "0.26.8", }; if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`; if (isStream) { // SSE passthrough with Responses API wrapping res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", }); const respId = "resp_" + Date.now(); const reqModel = parsed.model || MODELS[0]?.id || "default"; writeSSE(res, "response.created", { id: respId, object: "response", model: reqModel, created: Math.floor(Date.now() / 1000), status: "in_progress", output: [] }); const bodyData = JSON.stringify(parsed); const urlObj = new URL(targetUrl); const mod = urlObj.protocol === "https:" ? https : http; let contentAccum = ""; return new Promise((resolve) => { const upstream = mod.request({ hostname: urlObj.hostname, port: urlObj.port || (urlObj.protocol === "https:" ? 443 : 80), path: urlObj.pathname + urlObj.search, method: "POST", headers: { ...headers, "Content-Length": Buffer.byteLength(bodyData) }, }, (upRes) => { let buffer = ""; upRes.on("data", (chunk) => { buffer += chunk.toString(); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.startsWith("data: ")) { const dataStr = line.slice(6).trim(); if (dataStr === "[DONE]") { writeSSE(res, "response.completed", { id: respId, object: "response", model: reqModel, status: "completed", output: [{ type: "message", role: "assistant", content: [{ type: "output_text", text: contentAccum }] }] }); res.end(); resolve(); return; } try { const chunk2 = JSON.parse(dataStr); // CC streaming format varies, try common patterns const delta = chunk2.choices?.[0]?.delta?.content || chunk2.text || chunk2.delta || chunk2.content || ""; if (delta) { contentAccum += delta; writeSSE(res, "response.output_text.delta", { delta }); } } catch (e) { /* skip */ } } } }); upRes.on("end", () => { if (!res.writableEnded) { writeSSE(res, "response.completed", { id: respId, status: "completed", output: [] }); res.end(); } resolve(); }); upRes.on("error", () => { if (!res.writableEnded) res.end(); resolve(); }); }); upstream.on("error", (err) => { writeSSE(res, "response.completed", { id: respId, status: "failed", error: { message: err.message } }); if (!res.writableEnded) res.end(); resolve(); }); upstream.write(bodyData); upstream.end(); }); } else { // Non-stream: forward and wrap response const bodyData = JSON.stringify(parsed); const urlObj = new URL(targetUrl); const mod = urlObj.protocol === "https:" ? https : http; return new Promise((resolve) => { const upstream = mod.request({ hostname: urlObj.hostname, port: urlObj.port || (urlObj.protocol === "https:" ? 443 : 80), path: urlObj.pathname + urlObj.search, method: "POST", headers: { ...headers, "Content-Length": Buffer.byteLength(bodyData) }, }, (upRes) => { const chunks = []; upRes.on("data", chunk => chunks.push(chunk)); upRes.on("end", () => { const respBody = Buffer.concat(chunks).toString(); try { const ccResp = JSON.parse(respBody); // Wrap in Responses API format const text = ccResp.choices?.[0]?.message?.content || ccResp.content || ccResp.text || ccResp.output || respBody; const result = { id: "resp_" + Date.now(), object: "response", model: parsed.model || "unknown", created: Math.floor(Date.now() / 1000), status: "completed", output: [{ type: "message", role: "assistant", content: [{ type: "output_text", text }] }], }; jsonResponse(res, upRes.statusCode, result); } catch (e) { jsonResponse(res, upRes.statusCode, { id: "resp_" + Date.now(), status: "completed", output: [{ type: "message", role: "assistant", content: [{ type: "output_text", text: respBody }] }] }); } resolve(); }); upRes.on("error", () => { resolve(); }); }); upstream.on("error", (err) => { jsonResponse(res, 502, { error: { message: err.message } }); resolve(); }); upstream.write(bodyData); upstream.end(); }); } } // --------------------------------------------------------------------------- // Gemini internal → backend translation // --------------------------------------------------------------------------- async function handleGeminiInternal(req, res) { const isStream = req.url?.includes("streamGenerateContent") || false; const bodyStr = await readBody(req); let geminiReq; try { geminiReq = JSON.parse(bodyStr); } catch (e) { return jsonResponse(res, 400, { error: { message: "Invalid JSON" } }); } const responsesBody = geminiToResponses(geminiReq, isStream); if (BACKEND === "openai-compat") { if (isStream) { return await handleOpenAIStream(req, res, JSON.stringify(responsesBody)); } const chatBody = responsesToChatCompletions(responsesBody); const baseUrl = (TARGET_URL || "").replace(/\/+$/, ""); const targetUrl = baseUrl + "/chat/completions"; const headers = { "Content-Type": "application/json" }; if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`; const resp = await proxyRequest(targetUrl, "POST", headers, JSON.stringify(chatBody), res, false); try { const chatResp = JSON.parse(resp); // Convert back to Gemini format const text = chatResp.choices?.[0]?.message?.content || ""; const geminiResp = { candidates: [{ content: { role: "model", parts: [{ text }] }, finishReason: "STOP", index: 0 }] }; jsonResponse(res, 200, geminiResp); } catch (e) { /* already sent */ } } else if (BACKEND === "anthropic") { if (isStream) { return await handleAnthropicStream(req, res, JSON.stringify(responsesBody)); } const antBody = responsesToAnthropic(responsesBody); const baseUrl = (TARGET_URL || "").replace(/\/+$/, ""); const targetUrl = baseUrl + "/v1/messages"; const headers = { "Content-Type": "application/json", "anthropic-version": "2023-06-01" }; if (API_KEY) headers["x-api-key"] = API_KEY; const resp = await proxyRequest(targetUrl, "POST", headers, JSON.stringify(antBody), res, false); try { const antResp = JSON.parse(resp); const text = antResp.content?.find(c => c.type === "text")?.text || ""; const geminiResp = { candidates: [{ content: { role: "model", parts: [{ text }] }, finishReason: "STOP", index: 0 }] }; jsonResponse(res, 200, geminiResp); } catch (e) { /* already sent */ } } else if (BACKEND === "command-code") { if (isStream) { // Need to handle Gemini SSE format res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" }); // Simplified: translate to CC, stream back as Gemini chunks const bodyData = JSON.stringify(responsesBody); const baseUrl = (TARGET_URL || "").replace(/\/+$/, ""); const targetUrl = baseUrl + "/alpha/generate"; const headers = { "Content-Type": "application/json", "x-command-code-version": CC_VERSION || "0.26.8" }; if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`; const urlObj = new URL(targetUrl); const mod = urlObj.protocol === "https:" ? https : http; return new Promise((resolve) => { const upstream = mod.request({ hostname: urlObj.hostname, port: urlObj.port || (urlObj.protocol === "https:" ? 443 : 80), path: urlObj.pathname + urlObj.search, method: "POST", headers: { ...headers, "Content-Length": Buffer.byteLength(bodyData) }, }, (upRes) => { let buffer = ""; upRes.on("data", (chunk) => { buffer += chunk.toString(); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.startsWith("data: ")) { const dataStr = line.slice(6).trim(); if (dataStr === "[DONE]") { res.end(); resolve(); return; } try { const c = JSON.parse(dataStr); const delta = c.choices?.[0]?.delta?.content || c.text || c.delta || c.content || ""; if (delta) { const geminiChunk = { candidates: [{ content: { role: "model", parts: [{ text: delta }] }, index: 0 }] }; res.write(`data: ${JSON.stringify(geminiChunk)}\n\n`); } } catch (e) { /* skip */ } } } }); upRes.on("end", () => { if (!res.writableEnded) res.end(); resolve(); }); upRes.on("error", () => { if (!res.writableEnded) res.end(); resolve(); }); }); upstream.on("error", () => { if (!res.writableEnded) res.end(); resolve(); }); upstream.write(bodyData); upstream.end(); }); } // Non-stream CC const bodyData = JSON.stringify(responsesBody); const baseUrl = (TARGET_URL || "").replace(/\/+$/, ""); const targetUrl = baseUrl + "/alpha/generate"; const headers = { "Content-Type": "application/json", "x-command-code-version": CC_VERSION || "0.26.8" }; if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`; const resp = await proxyRequest(targetUrl, "POST", headers, bodyData, res, false); try { const ccResp = JSON.parse(resp); const text = ccResp.choices?.[0]?.message?.content || ccResp.content || ccResp.text || ""; const geminiResp = { candidates: [{ content: { role: "model", parts: [{ text }] }, finishReason: "STOP", index: 0 }] }; jsonResponse(res, 200, geminiResp); } catch (e) { /* already sent */ } } else { jsonResponse(res, 400, { error: { message: `Unsupported backend: ${BACKEND}` } }); } } // --------------------------------------------------------------------------- // Main request handler // --------------------------------------------------------------------------- async function handleRequest(req, res) { const parsedUrl = url.parse(req.url || "/"); // CORS res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "*"); if (req.method === "OPTIONS") { res.writeHead(204); return res.end(); } try { // --- Management routes --- if (parsedUrl.pathname === "/codex/list-endpoints" && req.method === "GET") { let endpointsData = { default: "", endpoints: [] }; if (fs.existsSync(ENDPOINTS_PATH)) { try { endpointsData = JSON.parse(fs.readFileSync(ENDPOINTS_PATH, "utf8")); } catch (e) {} } let activeName = ""; if (fs.existsSync(ACTIVE_PATH)) { try { activeName = JSON.parse(fs.readFileSync(ACTIVE_PATH, "utf8")).active; } catch (e) {} } if (!activeName) activeName = endpointsData.default || ""; return jsonResponse(res, 200, { endpoints: endpointsData.endpoints || [], active: activeName }); } if (parsedUrl.pathname === "/codex/switch-endpoint" && req.method === "POST") { const body = await readBody(req); let parsed; try { parsed = JSON.parse(body); } catch (e) { return jsonResponse(res, 400, { error: { message: "Invalid JSON" } }); } const endpointName = parsed.name; if (!fs.existsSync(ENDPOINTS_PATH)) { return jsonResponse(res, 404, { error: { message: "endpoints.json not found" } }); } let endpointsData; try { endpointsData = JSON.parse(fs.readFileSync(ENDPOINTS_PATH, "utf8")); } catch (e) { return jsonResponse(res, 500, { error: { message: "Failed to read endpoints" } }); } const endpoint = endpointsData.endpoints?.find(e => e.name === endpointName); if (!endpoint) { return jsonResponse(res, 404, { error: { message: `Endpoint '${endpointName}' not found` } }); } applyEndpoint(endpoint); // Save active endpoint try { fs.mkdirSync(path.dirname(ACTIVE_PATH), { recursive: true }); fs.writeFileSync(ACTIVE_PATH, JSON.stringify({ active: endpointName })); } catch (e) {} // Save active config try { fs.mkdirSync(CONFIG_DIR, { recursive: true }); fs.writeFileSync(path.join(CONFIG_DIR, "proxy-active.json"), JSON.stringify({ port: PORT, backend_type: BACKEND, target_url: TARGET_URL, api_key: API_KEY, cc_version: CC_VERSION, oauth_provider: OAUTH_PROVIDER, reasoning_enabled: REASONING_ENABLED, reasoning_effort: REASONING_EFFORT, models: MODELS }, null, 2)); } catch (e) {} console.log(`[Proxy] Switched to: ${endpointName} (${BACKEND})`); return jsonResponse(res, 200, { success: true, active: endpointName }); } if (parsedUrl.pathname === "/v1/models" && req.method === "GET") { return jsonResponse(res, 200, { object: "list", data: MODELS }); } if (parsedUrl.pathname === "/health" && req.method === "GET") { return jsonResponse(res, 200, { ok: true, backend: BACKEND, target_url: TARGET_URL, models: MODELS.map(m => m.id || m) }); } // --- Gemini internal routes --- if (parsedUrl.pathname?.startsWith("/v1internal:")) { if (parsedUrl.pathname.startsWith("/v1internal:fetchAvailableModels")) { const modelsList = MODELS.map(m => ({ name: `models/${typeof m === "string" ? m : m.id}`, version: "1.0", displayName: typeof m === "string" ? m : m.id, description: `Model via AG X Proxy`, supportedGenerationMethods: ["generateContent", "streamGenerateContent"] })); return jsonResponse(res, 200, { models: modelsList }); } return await handleGeminiInternal(req, res); } // --- Responses API route --- if (parsedUrl.pathname === "/v1/responses" || parsedUrl.pathname === "/responses") { const bodyStr = await readBody(req); let parsed; try { parsed = typeof bodyStr === "string" ? JSON.parse(bodyStr) : bodyStr; } catch (e) { return jsonResponse(res, 400, { error: { message: "Invalid JSON" } }); } const isStream = parsed.stream || false; if (BACKEND === "openai-compat") { if (isStream) { return await handleOpenAIStream(req, res, bodyStr); } const chatBody = responsesToChatCompletions(bodyStr); const baseUrl = (TARGET_URL || "").replace(/\/+$/, ""); const targetUrl = baseUrl + "/chat/completions"; const headers = { "Content-Type": "application/json" }; if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`; const resp = await proxyRequest(targetUrl, "POST", headers, JSON.stringify(chatBody), res, false); try { const chatResp = JSON.parse(resp); jsonResponse(res, 200, chatToResponses(chatResp, chatBody.model)); } catch (e) { /* response already sent */ } } else if (BACKEND === "anthropic") { if (isStream) { return await handleAnthropicStream(req, res, bodyStr); } const antBody = responsesToAnthropic(bodyStr); const baseUrl = (TARGET_URL || "").replace(/\/+$/, ""); const targetUrl = baseUrl + "/v1/messages"; const headers = { "Content-Type": "application/json", "anthropic-version": "2023-06-01" }; if (API_KEY) headers["x-api-key"] = API_KEY; const resp = await proxyRequest(targetUrl, "POST", headers, JSON.stringify(antBody), res, false); try { const antResp = JSON.parse(resp); jsonResponse(res, 200, anthropicToResponses(antResp, antBody.model)); } catch (e) { /* response already sent */ } } else if (BACKEND === "command-code") { return await handleCommandCode(req, res, bodyStr); } else { jsonResponse(res, 400, { error: { message: `Unsupported backend: ${BACKEND}` } }); } return; } // 404 jsonResponse(res, 404, { error: { message: "Not found" } }); } catch (err) { console.error("[Proxy] Unhandled error:", err); if (!res.headersSent) { jsonResponse(res, 500, { error: { message: err.message } }); } } } // --------------------------------------------------------------------------- // Start server // --------------------------------------------------------------------------- function start() { initFromConfig(); const server = http.createServer(handleRequest); server.listen(PORT, "127.0.0.1", () => { console.log(`[AG X Proxy] Listening on http://127.0.0.1:${PORT}`); console.log(`[AG X Proxy] Backend: ${BACKEND}, Target: ${TARGET_URL}`); console.log(`[AG X Proxy] Models: ${MODELS.map(m => typeof m === "string" ? m : m.id).join(", ")}`); }); server.on("error", (err) => { console.error("[AG X Proxy] Server error:", err.message); process.exit(1); }); process.on("SIGTERM", () => { server.close(); process.exit(0); }); process.on("SIGINT", () => { server.close(); process.exit(0); }); } // Allow running standalone if (require.main === module) { start(); } module.exports = { start, handleRequest, applyEndpoint };