From 83418e8a9d236922b64d5d52cc2968f91a02c103 Mon Sep 17 00:00:00 2001 From: decolua Date: Sat, 25 Apr 2026 17:01:40 +0700 Subject: [PATCH] Add codex to image providers --- open-sse/config/providerModels.js | 21 +++--- open-sse/handlers/imageGenerationCore.js | 35 +++++++-- .../media-providers/[kind]/[id]/page.js | 73 +++++++++++++++---- src/sse/handlers/imageGeneration.js | 4 + 4 files changed, 102 insertions(+), 31 deletions(-) diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js index f8ce257..37b7c70 100644 --- a/open-sse/config/providerModels.js +++ b/open-sse/config/providerModels.js @@ -37,9 +37,9 @@ export const PROVIDER_MODELS = { { id: "gpt-5-codex", name: "GPT 5 Codex" }, { id: "gpt-5-codex-mini", name: "GPT 5 Codex Mini" }, // Image models (uses image_generation tool, requires Plus/Pro plan) - { id: "gpt-5.4-image", name: "GPT 5.4 Image", type: "image", capabilities: ["text2img", "edit"] }, - { id: "gpt-5.3-image", name: "GPT 5.3 Image", type: "image", capabilities: ["text2img", "edit"] }, - { id: "gpt-5.2-image", name: "GPT 5.2 Image", type: "image", capabilities: ["text2img", "edit"] }, + { id: "gpt-5.4-image", name: "GPT 5.4 Image", type: "image", capabilities: ["text2img", "edit"], params: ["size", "quality", "background", "image_detail", "output_format"] }, + { id: "gpt-5.3-image", name: "GPT 5.3 Image", type: "image", capabilities: ["text2img", "edit"], params: ["size", "quality", "background", "image_detail", "output_format"] }, + { id: "gpt-5.2-image", name: "GPT 5.2 Image", type: "image", capabilities: ["text2img", "edit"], params: ["size", "quality", "background", "image_detail", "output_format"] }, ], gc: [ // Gemini CLI { id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" }, @@ -345,14 +345,13 @@ export const PROVIDER_MODELS = { { id: "DeepSeek-V3.2", name: "DeepSeek-V3.2" }, ], byteplus: [ - { id: "doubao-seed-2.0-pro", name: "Doubao Seed 2.0 Pro" }, - { id: "doubao-seed-2.0-code", name: "Doubao Seed 2.0 Code" }, - { id: "doubao-seed-2.0-lite", name: "Doubao Seed 2.0 Lite" }, - { id: "doubao-seed-code", name: "Doubao Seed Code" }, - { id: "glm-5.1", name: "GLM-5.1" }, - { id: "glm-4.7", name: "GLM-4.7" }, - { id: "kimi-k2.5", name: "Kimi-K2.5" }, - { id: "gpt-oss-120b", name: "GPT-OSS-120B" }, + { id: "seed-2-0-pro-260328", name: "Seed 2.0 Pro" }, + { id: "seed-2-0-code-preview-260328", name: "Seed 2.0 Code Preview" }, + { id: "seed-2-0-mini-260215", name: "Seed 2.0 Mini" }, + { id: "seed-2-0-lite-260228", name: "Seed 2.0 Lite" }, + { id: "kimi-k2-thinking-251104", name: "Kimi K2 Thinking" }, + { id: "glm-4-7-251222", name: "GLM 4.7" }, + { id: "gpt-oss-120b-250805", name: "GPT-OSS-120B" }, ], deepseek: [ { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" }, diff --git a/open-sse/handlers/imageGenerationCore.js b/open-sse/handlers/imageGenerationCore.js index 853eaf9..1d5d12d 100644 --- a/open-sse/handlers/imageGenerationCore.js +++ b/open-sse/handlers/imageGenerationCore.js @@ -83,11 +83,11 @@ function toCodexDataUrl(input) { } // Build content array with optional reference images, mirroring codex-imagen tagging -function buildCodexContent(prompt, refs) { +function buildCodexContent(prompt, refs, detail = CODEX_REF_DETAIL) { const content = []; refs.forEach((url, index) => { content.push({ type: "input_text", text: `` }); - content.push({ type: "input_image", image_url: url, detail: CODEX_REF_DETAIL }); + content.push({ type: "input_image", image_url: url, detail }); content.push({ type: "input_text", text: "" }); }); content.push({ type: "input_text", text: prompt }); @@ -280,11 +280,16 @@ function buildImageBody(provider, model, body) { if (Array.isArray(images)) images.forEach((i) => { const u = toCodexDataUrl(i); if (u) refs.push(u); }); const single = toCodexDataUrl(image); if (single) refs.push(single); + const detail = body.image_detail || CODEX_REF_DETAIL; + const imgTool = { type: "image_generation", output_format: (body.output_format || "png").toLowerCase() }; + if (body.size && body.size !== "") imgTool.size = body.size; + if (body.quality && body.quality !== "") imgTool.quality = body.quality; + if (body.background && body.background !== "") imgTool.background = body.background; return { model: stripCodexImageModel(model), instructions: "", - input: [{ type: "message", role: "user", content: buildCodexContent(prompt, refs) }], - tools: [{ type: "image_generation", output_format: "png" }], + input: [{ type: "message", role: "user", content: buildCodexContent(prompt, refs, detail) }], + tools: [imgTool], tool_choice: "auto", parallel_tool_calls: false, prompt_cache_key: randomUUID(), @@ -404,6 +409,7 @@ export async function handleImageGenerationCore({ credentials, log, streamToClient = false, + binaryOutput = false, onCredentialsRefreshed, onRequestSuccess, }) { @@ -524,7 +530,26 @@ export async function handleImageGenerationCore({ const normalized = normalizeImageResponse(responseBody, provider, body.prompt); - log?.debug?.("IMAGE", `Success | images=${normalized.data?.length || 0}`); + // Binary output: decode first b64_json into raw bytes + if (binaryOutput) { + const first = normalized.data?.[0]; + const b64 = first?.b64_json; + if (b64) { + const buf = Buffer.from(b64, "base64"); + const fmt = (body.output_format || "png").toLowerCase(); + const mime = fmt === "jpeg" || fmt === "jpg" ? "image/jpeg" : fmt === "webp" ? "image/webp" : "image/png"; + return { + success: true, + response: new Response(buf, { + headers: { + "Content-Type": mime, + "Content-Disposition": `inline; filename="image.${fmt === "jpeg" ? "jpg" : fmt}"`, + "Access-Control-Allow-Origin": "*", + }, + }), + }; + } + } return { success: true, diff --git a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js index dbd3e02..e3abbf3 100644 --- a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js +++ b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js @@ -65,10 +65,13 @@ const KIND_EXAMPLE_CONFIG = { defaultResponse: `{\n "data": [\n { "url": "...", "b64_json": "..." }\n ]\n}`, extraFields: [ { key: "n", label: "n", type: "number", default: 1, min: 1, max: 4 }, - { key: "size", label: "Size", type: "select", default: "1024x1024", options: ["1024x1024", "1024x1792", "1792x1024", "auto"] }, - { key: "quality", label: "Quality", type: "select", default: "", options: ["", "standard", "hd", "high", "low", "auto"] }, + { key: "size", label: "Size", type: "select", default: "auto", options: ["auto", "1024x1024", "1024x1536", "1536x1024", "1024x1792", "1792x1024"] }, + { key: "quality", label: "Quality", type: "select", default: "auto", options: ["auto", "low", "medium", "high", "standard", "hd"] }, + { key: "background", label: "Background", type: "select", default: "auto", options: ["auto", "transparent", "opaque"] }, { key: "style", label: "Style", type: "select", default: "", options: ["", "vivid", "natural"] }, { key: "response_format", label: "Format", type: "select", default: "", options: ["", "url", "b64_json"] }, + { key: "image_detail", label: "Image Detail", type: "select", default: "high", options: ["auto", "low", "high", "original"] }, + { key: "output_format", label: "Codec", type: "select", default: "png", options: ["png", "jpeg", "webp"] }, ], }, imageToText: { @@ -879,6 +882,8 @@ function GenericExampleCard({ providerId, kind }) { const [result, setResult] = useState(null); const [progress, setProgress] = useState(null); // { stage, bytesReceived } const [partialImage, setPartialImage] = useState(null); + const [imageOutputFormat, setImageOutputFormat] = useState("json"); // json | binary + const [binaryImageUrl, setBinaryImageUrl] = useState(""); const [running, setRunning] = useState(false); const [error, setError] = useState(""); const [connections, setConnections] = useState([]); @@ -928,12 +933,14 @@ function GenericExampleCard({ providerId, kind }) { ...(supportsEdit && refImage.trim() ? { image: refImage.trim() } : {}), }; - // Streaming supported for codex image (Plus/Pro accounts) - const useStreaming = kind === "image" && providerId === "codex"; + // Streaming supported for codex image (Plus/Pro accounts) — disabled when binary output requested + const wantBinary = kind === "image" && imageOutputFormat === "binary"; + const useStreaming = kind === "image" && providerId === "codex" && !wantBinary; + const apiPathWithQuery = `${apiPath}${wantBinary ? "?response_format=binary" : ""}`; const headersPreview = `-H "Content-Type: application/json" \\\n -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}"${pinnedConnectionId ? ` \\\n -H "x-connection-id: ${pinnedConnectionId}"` : ""}${useStreaming ? ` \\\n -H "Accept: text/event-stream"` : ""}`; - const curlSnippet = `curl -X ${kindConfig.endpoint.method} ${endpoint}${apiPath} \\ + const curlSnippet = `curl -X ${kindConfig.endpoint.method} ${endpoint}${apiPathWithQuery} \\ ${headersPreview.replace(/\\\n /g, "\\\n ")} \\ - -d '${JSON.stringify(requestBody)}'`; + -d '${JSON.stringify(requestBody)}'${wantBinary ? " \\\n --output image.png" : ""}`; const handleRun = async () => { if (!input.trim() || !modelFull) return; @@ -942,6 +949,7 @@ function GenericExampleCard({ providerId, kind }) { setResult(null); setProgress(null); setPartialImage(null); + if (binaryImageUrl) { try { URL.revokeObjectURL(binaryImageUrl); } catch {} setBinaryImageUrl(""); } const start = Date.now(); try { const headers = { "Content-Type": "application/json" }; @@ -949,7 +957,7 @@ function GenericExampleCard({ providerId, kind }) { if (pinnedConnectionId) headers["x-connection-id"] = pinnedConnectionId; if (useStreaming) headers["Accept"] = "text/event-stream"; const body = { ...requestBody, model: modelFull }; - const res = await fetch(`/api${apiPath}`, { + const res = await fetch(`/api${apiPathWithQuery}`, { method: kindConfig.endpoint.method, headers, body: JSON.stringify(body), @@ -959,7 +967,16 @@ function GenericExampleCard({ providerId, kind }) { setError(data?.error?.message || data?.error || `HTTP ${res.status}`); return; } - const isSse = (res.headers.get("content-type") || "").includes("text/event-stream"); + const ctype = res.headers.get("content-type") || ""; + // Binary image response — convert to blob URL + if (ctype.startsWith("image/")) { + const blob = await res.blob(); + const objUrl = URL.createObjectURL(blob); + setBinaryImageUrl(objUrl); + setResult({ data: { binary: true, mime: ctype, size: blob.size }, latencyMs: Date.now() - start }); + return; + } + const isSse = ctype.includes("text/event-stream"); if (isSse && res.body) { // Parse SSE: progress / partial_image / done / error const reader = res.body.getReader(); @@ -1171,6 +1188,20 @@ function GenericExampleCard({ providerId, kind }) { ))} + {/* Output Format toggle (image only) — last */} + {kind === "image" && ( + + + + )} + {/* Curl + Run */}
@@ -1206,7 +1237,7 @@ function GenericExampleCard({ providerId, kind }) { {progress?.stage || "starting"} - {progress?.bytesReceived ? ` · ${(progress.bytesReceived / 1024).toFixed(1)} KB` : ""} + {!running && progress?.bytesReceived ? ` · ${(progress.bytesReceived / 1024).toFixed(1)} KB` : ""}
)} @@ -1245,12 +1276,24 @@ function GenericExampleCard({ providerId, kind }) {
             {result ? resultJson : exConfig.defaultResponse}
           
- {kind === "image" && result?.data?.data?.[0] && ( - Generated + {kind === "image" && (binaryImageUrl || result?.data?.data?.[0]) && ( +
+
+ + download + Download + +
+ Generated +
)}
diff --git a/src/sse/handlers/imageGeneration.js b/src/sse/handlers/imageGeneration.js index 87ecd41..3db18b5 100644 --- a/src/sse/handlers/imageGeneration.js +++ b/src/sse/handlers/imageGeneration.js @@ -27,8 +27,10 @@ export async function handleImageGeneration(request) { return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body"); } + const url = new URL(request.url); const preferredConnectionId = request.headers.get("x-connection-id") || null; const wantsStream = (request.headers.get("accept") || "").includes("text/event-stream"); + const binaryOutput = url.searchParams.get("response_format") === "binary"; const modelStr = body.model; const apiKey = extractApiKey(request); @@ -53,6 +55,7 @@ export async function handleImageGeneration(request) { body, modelInfo: { provider, model }, credentials: null, + binaryOutput, }); if (result.success) return result.response; return errorResponse(result.status || HTTP_STATUS.BAD_GATEWAY, result.error || "Image generation failed"); @@ -85,6 +88,7 @@ export async function handleImageGeneration(request) { modelInfo: { provider, model }, credentials: refreshedCredentials, streamToClient: wantsStream, + binaryOutput, onCredentialsRefreshed: async (newCreds) => { await updateProviderCredentials(credentials.connectionId, { accessToken: newCreds.accessToken,