| import { Request, RequestHandler, Router } from "express"; |
| import { OpenAIImageGenerationResult } from "../shared/file-storage/mirror-generated-image"; |
| import { generateModelList } from "./openai"; |
| import { ipLimiter } from "./rate-limit"; |
| import { |
| addKey, |
| createPreprocessorMiddleware, |
| finalizeBody, |
| } from "./middleware/request"; |
| import { ProxyResHandlerWithBody } from "./middleware/response"; |
| import { ProxyReqManager } from "./middleware/request/proxy-req-manager"; |
| import { createQueuedProxyMiddleware } from "./middleware/request/proxy-middleware-factory"; |
|
|
| const KNOWN_MODELS = ["dall-e-2", "dall-e-3", "gpt-image-1"]; |
|
|
| let modelListCache: any = null; |
| let modelListValid = 0; |
| const handleModelRequest: RequestHandler = (_req, res) => { |
| if (new Date().getTime() - modelListValid < 1000 * 60) { |
| return res.status(200).json(modelListCache); |
| } |
| const result = generateModelList("openai").filter((m: { id: string }) => |
| KNOWN_MODELS.includes(m.id) |
| ); |
| modelListCache = { object: "list", data: result }; |
| modelListValid = new Date().getTime(); |
| res.status(200).json(modelListCache); |
| }; |
|
|
| const openaiImagesResponseHandler: ProxyResHandlerWithBody = async ( |
| _proxyRes, |
| req, |
| res, |
| body |
| ) => { |
| if (typeof body !== "object") { |
| throw new Error("Expected body to be an object"); |
| } |
|
|
| let newBody = body; |
| if (req.inboundApi === "openai") { |
| req.log.info("Transforming OpenAI image response to OpenAI chat format"); |
| newBody = transformResponseForChat( |
| body as OpenAIImageGenerationResult, |
| req |
| ); |
| } |
|
|
| res.status(200).json({ ...newBody, proxy: body.proxy }); |
| }; |
|
|
| |
| |
| |
| |
| function transformResponseForChat( |
| imageBody: OpenAIImageGenerationResult, |
| req: Request |
| ): Record<string, any> { |
| const prompt = imageBody.data[0].revised_prompt ?? req.body.prompt; |
| const isGptImage = req.body.model?.includes("gpt-image") || false; |
| |
| const content = imageBody.data |
| .map((item) => { |
| const { url, b64_json } = item; |
| |
| |
| |
| const format = req.body.output_format || "png"; |
| |
| if (b64_json) { |
| return ``; |
| } else { |
| return ``; |
| } |
| }) |
| .join("\n\n"); |
|
|
| |
| let usage = { |
| prompt_tokens: 0, |
| completion_tokens: req.outputTokens, |
| total_tokens: req.outputTokens, |
| }; |
| |
| |
| if (imageBody.usage) { |
| usage = { |
| prompt_tokens: imageBody.usage.input_tokens || 0, |
| completion_tokens: imageBody.usage.output_tokens || 0, |
| total_tokens: imageBody.usage.total_tokens || 0, |
| }; |
| } |
| |
| return { |
| id: req.body.model?.includes("gpt-image") ? "gptimage-" + req.id : "dalle-" + req.id, |
| object: "chat.completion", |
| created: Date.now(), |
| model: req.body.model, |
| usage, |
| choices: [ |
| { |
| message: { role: "assistant", content }, |
| finish_reason: "stop", |
| index: 0, |
| }, |
| ], |
| }; |
| } |
|
|
| |
| function filterModelParameters(manager: ProxyReqManager) { |
| const req = manager.request; |
| const originalBody = req.body; |
| const modelName = originalBody?.model || ""; |
| |
| |
| if (!originalBody || typeof originalBody !== 'object') return; |
| |
| |
| const filteredBody = { ...originalBody }; |
| |
| |
| if (modelName.includes('dall-e-2')) { |
| |
| const allowedParams = [ |
| 'model', 'prompt', 'n', 'size', 'response_format', 'user' |
| ]; |
| |
| |
| Object.keys(filteredBody).forEach(key => { |
| if (!allowedParams.includes(key)) { |
| delete filteredBody[key]; |
| } |
| }); |
| |
| req.log.info({ model: 'dall-e-2', params: Object.keys(filteredBody) }, "Filtered parameters for DALL-E 2"); |
| } else if (modelName.includes('dall-e-3')) { |
| |
| const allowedParams = [ |
| 'model', 'prompt', 'n', 'quality', 'size', 'style', 'response_format', 'user' |
| ]; |
| |
| |
| Object.keys(filteredBody).forEach(key => { |
| if (!allowedParams.includes(key)) { |
| delete filteredBody[key]; |
| } |
| }); |
| |
| req.log.info({ model: 'dall-e-3', params: Object.keys(filteredBody) }, "Filtered parameters for DALL-E 3"); |
| } else if (modelName.includes('gpt-image')) { |
| |
| const allowedParams = [ |
| 'model', 'prompt', 'background', 'moderation', 'n', 'output_compression', |
| 'output_format', 'quality', 'size', 'user', 'image', 'mask' |
| ]; |
| |
| |
| Object.keys(filteredBody).forEach(key => { |
| if (!allowedParams.includes(key)) { |
| req.log.info({ model: 'gpt-image-1', removedParam: key }, "Removing unsupported parameter for GPT Image"); |
| delete filteredBody[key]; |
| } |
| }); |
| |
| req.log.info({ model: 'gpt-image-1', params: Object.keys(filteredBody) }, "Filtered parameters for GPT Image"); |
| } |
| |
| |
| manager.setBody(filteredBody); |
| } |
|
|
| function replacePath(manager: ProxyReqManager) { |
| const req = manager.request; |
| const pathname = req.url.split("?")[0]; |
| req.log.debug({ pathname }, "OpenAI image path filter"); |
| if (req.path.startsWith("/v1/chat/completions")) { |
| manager.setPath("/v1/images/generations"); |
| } |
| } |
|
|
| const openaiImagesProxy = createQueuedProxyMiddleware({ |
| target: "https://api.openai.com", |
| mutations: [replacePath, filterModelParameters, addKey, finalizeBody], |
| blockingResponseHandler: openaiImagesResponseHandler, |
| }); |
|
|
| const openaiImagesRouter = Router(); |
| openaiImagesRouter.get("/v1/models", handleModelRequest); |
| openaiImagesRouter.post( |
| "/v1/images/generations", |
| ipLimiter, |
| createPreprocessorMiddleware({ |
| inApi: "openai-image", |
| outApi: "openai-image", |
| service: "openai", |
| }), |
| openaiImagesProxy |
| ); |
| |
| openaiImagesRouter.post( |
| "/v1/images/edits", |
| ipLimiter, |
| createPreprocessorMiddleware({ |
| inApi: "openai-image", |
| outApi: "openai-image", |
| service: "openai", |
| }), |
| openaiImagesProxy |
| ); |
| openaiImagesRouter.post( |
| "/v1/chat/completions", |
| ipLimiter, |
| createPreprocessorMiddleware({ |
| inApi: "openai", |
| outApi: "openai-image", |
| service: "openai", |
| }), |
| openaiImagesProxy |
| ); |
| export const openaiImage = openaiImagesRouter; |
|
|