From 8e6eef04abb5727c7d8708c7363a0ce3f51a2a5b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 28 Jul 2025 15:12:29 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20fix:=20Update=20Proxy=20Config?= =?UTF-8?q?=20for=20OpenAI=20Image=20Tools=20(#8712)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replaced HttpsProxyAgent with ProxyAgent from undici for improved proxy handling in DALLE3.js and OpenAIImageTools.js. - Updated fetchOptions to use dispatcher for proxy configuration. - Added new test suite for DALLE3 to verify proxy configuration behavior based on environment variables. --- api/app/clients/tools/structured/DALLE3.js | 10 +- .../tools/structured/OpenAIImageTools.js | 25 ++++- .../structured/specs/DALLE3-proxy.spec.js | 94 +++++++++++++++++++ 3 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js diff --git a/api/app/clients/tools/structured/DALLE3.js b/api/app/clients/tools/structured/DALLE3.js index 7c2a56fe71..5f6e335a17 100644 --- a/api/app/clients/tools/structured/DALLE3.js +++ b/api/app/clients/tools/structured/DALLE3.js @@ -3,8 +3,8 @@ const path = require('path'); const OpenAI = require('openai'); const fetch = require('node-fetch'); const { v4: uuidv4 } = require('uuid'); +const { ProxyAgent } = require('undici'); const { Tool } = require('@langchain/core/tools'); -const { HttpsProxyAgent } = require('https-proxy-agent'); const { FileContext, ContentTypes } = require('librechat-data-provider'); const { getImageBasename } = require('~/server/services/Files/images'); const extractBaseURL = require('~/utils/extractBaseURL'); @@ -46,7 +46,10 @@ class DALLE3 extends Tool { } if (process.env.PROXY) { - config.httpAgent = new HttpsProxyAgent(process.env.PROXY); + const proxyAgent = new ProxyAgent(process.env.PROXY); + config.fetchOptions = { + dispatcher: proxyAgent, + }; } /** @type {OpenAI} */ @@ -163,7 +166,8 @@ Error Message: ${error.message}`); if (this.isAgent) { let fetchOptions = {}; if (process.env.PROXY) { - fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY); + const proxyAgent = new ProxyAgent(process.env.PROXY); + fetchOptions.dispatcher = proxyAgent; } const imageResponse = await fetch(theImageUrl, fetchOptions); const arrayBuffer = await imageResponse.arrayBuffer(); diff --git a/api/app/clients/tools/structured/OpenAIImageTools.js b/api/app/clients/tools/structured/OpenAIImageTools.js index 411db1edf9..920555da30 100644 --- a/api/app/clients/tools/structured/OpenAIImageTools.js +++ b/api/app/clients/tools/structured/OpenAIImageTools.js @@ -3,10 +3,10 @@ const axios = require('axios'); const { v4 } = require('uuid'); const OpenAI = require('openai'); const FormData = require('form-data'); +const { ProxyAgent } = require('undici'); const { tool } = require('@langchain/core/tools'); const { logAxiosError } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); -const { HttpsProxyAgent } = require('https-proxy-agent'); const { ContentTypes, EImageOutputType } = require('librechat-data-provider'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { extractBaseURL } = require('~/utils'); @@ -189,7 +189,10 @@ function createOpenAIImageTools(fields = {}) { } const clientConfig = { ...closureConfig }; if (process.env.PROXY) { - clientConfig.httpAgent = new HttpsProxyAgent(process.env.PROXY); + const proxyAgent = new ProxyAgent(process.env.PROXY); + clientConfig.fetchOptions = { + dispatcher: proxyAgent, + }; } /** @type {OpenAI} */ @@ -335,7 +338,10 @@ Error Message: ${error.message}`); const clientConfig = { ...closureConfig }; if (process.env.PROXY) { - clientConfig.httpAgent = new HttpsProxyAgent(process.env.PROXY); + const proxyAgent = new ProxyAgent(process.env.PROXY); + clientConfig.fetchOptions = { + dispatcher: proxyAgent, + }; } const formData = new FormData(); @@ -447,6 +453,19 @@ Error Message: ${error.message}`); baseURL, }; + if (process.env.PROXY) { + try { + const url = new URL(process.env.PROXY); + axiosConfig.proxy = { + host: url.hostname.replace(/^\[|\]$/g, ''), + port: url.port ? parseInt(url.port, 10) : undefined, + protocol: url.protocol.replace(':', ''), + }; + } catch (error) { + logger.error('Error parsing proxy URL:', error); + } + } + if (process.env.IMAGE_GEN_OAI_AZURE_API_VERSION && process.env.IMAGE_GEN_OAI_BASEURL) { axiosConfig.params = { 'api-version': process.env.IMAGE_GEN_OAI_AZURE_API_VERSION, diff --git a/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js b/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js new file mode 100644 index 0000000000..768d81e888 --- /dev/null +++ b/api/app/clients/tools/structured/specs/DALLE3-proxy.spec.js @@ -0,0 +1,94 @@ +const DALLE3 = require('../DALLE3'); +const { ProxyAgent } = require('undici'); + +const processFileURL = jest.fn(); + +jest.mock('~/server/services/Files/images', () => ({ + getImageBasename: jest.fn().mockImplementation((url) => { + const parts = url.split('/'); + const lastPart = parts.pop(); + const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg)$/i; + if (imageExtensionRegex.test(lastPart)) { + return lastPart; + } + return ''; + }), +})); + +jest.mock('fs', () => { + return { + existsSync: jest.fn(), + mkdirSync: jest.fn(), + promises: { + writeFile: jest.fn(), + readFile: jest.fn(), + unlink: jest.fn(), + }, + }; +}); + +jest.mock('path', () => { + return { + resolve: jest.fn(), + join: jest.fn(), + relative: jest.fn(), + extname: jest.fn().mockImplementation((filename) => { + return filename.slice(filename.lastIndexOf('.')); + }), + }; +}); + +describe('DALLE3 Proxy Configuration', () => { + let originalEnv; + + beforeAll(() => { + originalEnv = { ...process.env }; + }); + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('should configure ProxyAgent in fetchOptions.dispatcher when PROXY env is set', () => { + // Set proxy environment variable + process.env.PROXY = 'http://proxy.example.com:8080'; + process.env.DALLE_API_KEY = 'test-api-key'; + + // Create instance + const dalleWithProxy = new DALLE3({ processFileURL }); + + // Check that the openai client exists + expect(dalleWithProxy.openai).toBeDefined(); + + // Check that _options exists and has fetchOptions with a dispatcher + expect(dalleWithProxy.openai._options).toBeDefined(); + expect(dalleWithProxy.openai._options.fetchOptions).toBeDefined(); + expect(dalleWithProxy.openai._options.fetchOptions.dispatcher).toBeDefined(); + expect(dalleWithProxy.openai._options.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent); + }); + + it('should not configure ProxyAgent when PROXY env is not set', () => { + // Ensure PROXY is not set + delete process.env.PROXY; + process.env.DALLE_API_KEY = 'test-api-key'; + + // Create instance + const dalleWithoutProxy = new DALLE3({ processFileURL }); + + // Check that the openai client exists + expect(dalleWithoutProxy.openai).toBeDefined(); + + // Check that _options exists but fetchOptions either doesn't exist or doesn't have a dispatcher + expect(dalleWithoutProxy.openai._options).toBeDefined(); + + // fetchOptions should either not exist or not have a dispatcher + if (dalleWithoutProxy.openai._options.fetchOptions) { + expect(dalleWithoutProxy.openai._options.fetchOptions.dispatcher).toBeUndefined(); + } + }); +});