From 79536810ee4d0c4350884b0825dfd11cef38c441 Mon Sep 17 00:00:00 2001 From: Gus Nguyen Date: Mon, 8 Dec 2025 13:12:50 -0800 Subject: [PATCH] feat: add LiteLLM integration for chat functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add node-fetch and http-proxy-agent for Stripe certproxy support - Configure OpenAI client to use LiteLLM via HTTP proxy - Add error handling in executeAgenticLoop to gracefully handle API errors - Support local development via certproxy (port 7891) For local development, configure server/.env with: OPENAI_API_KEY="use_case=adhoc&team=&ticket=" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 Committed-By-Agent: claude --- client/src/App.tsx | 6 ++ client/src/components/ChatTab.tsx | 6 +- client/src/utils/executeAgenticLoop.ts | 21 +++++ package-lock.json | 113 ++++++++++++++++++++++++- server/package.json | 3 + server/src/index.ts | 48 +++++++++-- 6 files changed, 185 insertions(+), 12 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index cb2affc9b..f1ba2dac5 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -941,6 +941,12 @@ const App = () => { ); }} /> + ) : ( <> diff --git a/client/src/components/ChatTab.tsx b/client/src/components/ChatTab.tsx index baae41b6e..7e661a2f1 100644 --- a/client/src/components/ChatTab.tsx +++ b/client/src/components/ChatTab.tsx @@ -114,8 +114,8 @@ const ChatTab = ({ chatURL, tools, listTools, callTool }: ChatTabProps) => { }; return ( - -
+ +
{messages.map((message, index) => (
{
)}
-
+
= 12" + } + }, "node_modules/data-urls": { "version": "3.0.2", "dev": true, @@ -4491,6 +4500,38 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fetch-blob/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "dev": true, @@ -4628,6 +4669,18 @@ "node": ">= 12.20" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "license": "MIT", @@ -6683,7 +6736,9 @@ } }, "node_modules/openai": { - "version": "4.98.0", + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", @@ -9150,6 +9205,9 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "node-fetch": "^3.3.2", "openai": "^4.95.1", "ws": "^8.18.0", "zod": "^3.25.76" @@ -9164,6 +9222,59 @@ "tsx": "^4.19.0", "typescript": "^5.6.2" } + }, + "server/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "server/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "server/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "server/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } } } } diff --git a/server/package.json b/server/package.json index 0cf34f6ed..b2edaa0bf 100644 --- a/server/package.json +++ b/server/package.json @@ -31,6 +31,9 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "node-fetch": "^3.3.2", "openai": "^4.95.1", "ws": "^8.18.0", "zod": "^3.25.76" diff --git a/server/src/index.ts b/server/src/index.ts index 5a56c2d09..f4bde8881 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -4,9 +4,34 @@ import cors from "cors"; import { parseArgs } from "node:util"; import { parse as shellParseArgs } from "shell-quote"; import * as dotenv from "dotenv"; - -// Load environment variables from .env file -dotenv.config(); +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import nodeFetch from "node-fetch"; +import { HttpProxyAgent } from "http-proxy-agent"; + +// Load environment variables from .env file in the server directory +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +dotenv.config({ path: join(__dirname, "..", ".env") }); + +// HTTP proxy agent for routing through certproxy (port 7891) +const httpProxyAgent = new HttpProxyAgent("http://127.0.0.1:7891"); + +// Custom fetch implementation using node-fetch to bypass Node.js undici's port 10080 blocking +// This is required for Stripe's service mesh which uses envoy on port 10080 +// For local development, it uses certproxy to access litellm.corp.stripe.com +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const fetchPolyfill = async (url: any, init?: any): Promise => { + const urlStr = url.toString(); + // Use HTTP proxy for Stripe corp domains via certproxy + if (urlStr.includes("litellm.corp.stripe.com")) { + console.log(`[fetchPolyfill] Using HTTP proxy for: ${urlStr}`); + return nodeFetch(urlStr, { ...init, agent: httpProxyAgent }); + } + // For service mesh URLs (envoy) or other URLs, use node-fetch directly + console.log(`[fetchPolyfill] Direct fetch for: ${urlStr}`); + return nodeFetch(urlStr, init); +}; import { SSEClientTransport, @@ -533,12 +558,19 @@ app.post( }, ); +// For local development, use http://litellm.corp.stripe.com via certproxy (HTTP proxy on port 7891) +// For service mesh, set LITELLM_BASE_URL=http://litellm-srv.service.envoy:10080/v1 +const litellmBaseUrl = + process.env.LITELLM_BASE_URL ?? + process.env.OPENAI_BASE_URL ?? + "http://litellm.corp.stripe.com/v1"; +const litellmApiKey = + process.env.LITELLM_API_KEY ?? process.env.OPENAI_API_KEY ?? "use_case=librechat"; + const openai = new OpenAI({ - baseURL: `${process.env.HTTP_PROXY}/v1/`, - apiKey: process.env.OPEN_AI_KEY, - defaultHeaders: { - Host: process.env.OPEN_AI_HOST, - }, + baseURL: litellmBaseUrl, + apiKey: litellmApiKey, + fetch: fetchPolyfill, // polyfill with node-fetch because Node's built-in undici fetch blocks port 10080 }); app.post("/chat", express.json(), async (req, res) => {