mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
Add codex to image providers
This commit is contained in:
@@ -37,9 +37,9 @@ export const PROVIDER_MODELS = {
|
|||||||
{ id: "gpt-5-codex", name: "GPT 5 Codex" },
|
{ id: "gpt-5-codex", name: "GPT 5 Codex" },
|
||||||
{ id: "gpt-5-codex-mini", name: "GPT 5 Codex Mini" },
|
{ id: "gpt-5-codex-mini", name: "GPT 5 Codex Mini" },
|
||||||
// Image models (uses image_generation tool, requires Plus/Pro plan)
|
// 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.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"] },
|
{ 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"] },
|
{ 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
|
gc: [ // Gemini CLI
|
||||||
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
|
{ 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" },
|
{ id: "DeepSeek-V3.2", name: "DeepSeek-V3.2" },
|
||||||
],
|
],
|
||||||
byteplus: [
|
byteplus: [
|
||||||
{ id: "doubao-seed-2.0-pro", name: "Doubao Seed 2.0 Pro" },
|
{ id: "seed-2-0-pro-260328", name: "Seed 2.0 Pro" },
|
||||||
{ id: "doubao-seed-2.0-code", name: "Doubao Seed 2.0 Code" },
|
{ id: "seed-2-0-code-preview-260328", name: "Seed 2.0 Code Preview" },
|
||||||
{ id: "doubao-seed-2.0-lite", name: "Doubao Seed 2.0 Lite" },
|
{ id: "seed-2-0-mini-260215", name: "Seed 2.0 Mini" },
|
||||||
{ id: "doubao-seed-code", name: "Doubao Seed Code" },
|
{ id: "seed-2-0-lite-260228", name: "Seed 2.0 Lite" },
|
||||||
{ id: "glm-5.1", name: "GLM-5.1" },
|
{ id: "kimi-k2-thinking-251104", name: "Kimi K2 Thinking" },
|
||||||
{ id: "glm-4.7", name: "GLM-4.7" },
|
{ id: "glm-4-7-251222", name: "GLM 4.7" },
|
||||||
{ id: "kimi-k2.5", name: "Kimi-K2.5" },
|
{ id: "gpt-oss-120b-250805", name: "GPT-OSS-120B" },
|
||||||
{ id: "gpt-oss-120b", name: "GPT-OSS-120B" },
|
|
||||||
],
|
],
|
||||||
deepseek: [
|
deepseek: [
|
||||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" },
|
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" },
|
||||||
|
|||||||
@@ -83,11 +83,11 @@ function toCodexDataUrl(input) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build content array with optional reference images, mirroring codex-imagen tagging
|
// 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 = [];
|
const content = [];
|
||||||
refs.forEach((url, index) => {
|
refs.forEach((url, index) => {
|
||||||
content.push({ type: "input_text", text: `<image name=image${index + 1}>` });
|
content.push({ type: "input_text", text: `<image name=image${index + 1}>` });
|
||||||
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: "</image>" });
|
content.push({ type: "input_text", text: "</image>" });
|
||||||
});
|
});
|
||||||
content.push({ type: "input_text", text: prompt });
|
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); });
|
if (Array.isArray(images)) images.forEach((i) => { const u = toCodexDataUrl(i); if (u) refs.push(u); });
|
||||||
const single = toCodexDataUrl(image);
|
const single = toCodexDataUrl(image);
|
||||||
if (single) refs.push(single);
|
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 {
|
return {
|
||||||
model: stripCodexImageModel(model),
|
model: stripCodexImageModel(model),
|
||||||
instructions: "",
|
instructions: "",
|
||||||
input: [{ type: "message", role: "user", content: buildCodexContent(prompt, refs) }],
|
input: [{ type: "message", role: "user", content: buildCodexContent(prompt, refs, detail) }],
|
||||||
tools: [{ type: "image_generation", output_format: "png" }],
|
tools: [imgTool],
|
||||||
tool_choice: "auto",
|
tool_choice: "auto",
|
||||||
parallel_tool_calls: false,
|
parallel_tool_calls: false,
|
||||||
prompt_cache_key: randomUUID(),
|
prompt_cache_key: randomUUID(),
|
||||||
@@ -404,6 +409,7 @@ export async function handleImageGenerationCore({
|
|||||||
credentials,
|
credentials,
|
||||||
log,
|
log,
|
||||||
streamToClient = false,
|
streamToClient = false,
|
||||||
|
binaryOutput = false,
|
||||||
onCredentialsRefreshed,
|
onCredentialsRefreshed,
|
||||||
onRequestSuccess,
|
onRequestSuccess,
|
||||||
}) {
|
}) {
|
||||||
@@ -524,7 +530,26 @@ export async function handleImageGenerationCore({
|
|||||||
|
|
||||||
const normalized = normalizeImageResponse(responseBody, provider, body.prompt);
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -65,10 +65,13 @@ const KIND_EXAMPLE_CONFIG = {
|
|||||||
defaultResponse: `{\n "data": [\n { "url": "...", "b64_json": "..." }\n ]\n}`,
|
defaultResponse: `{\n "data": [\n { "url": "...", "b64_json": "..." }\n ]\n}`,
|
||||||
extraFields: [
|
extraFields: [
|
||||||
{ key: "n", label: "n", type: "number", default: 1, min: 1, max: 4 },
|
{ 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: "size", label: "Size", type: "select", default: "auto", options: ["auto", "1024x1024", "1024x1536", "1536x1024", "1024x1792", "1792x1024"] },
|
||||||
{ key: "quality", label: "Quality", type: "select", default: "", options: ["", "standard", "hd", "high", "low", "auto"] },
|
{ 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: "style", label: "Style", type: "select", default: "", options: ["", "vivid", "natural"] },
|
||||||
{ key: "response_format", label: "Format", type: "select", default: "", options: ["", "url", "b64_json"] },
|
{ 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: {
|
imageToText: {
|
||||||
@@ -879,6 +882,8 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
const [result, setResult] = useState(null);
|
const [result, setResult] = useState(null);
|
||||||
const [progress, setProgress] = useState(null); // { stage, bytesReceived }
|
const [progress, setProgress] = useState(null); // { stage, bytesReceived }
|
||||||
const [partialImage, setPartialImage] = useState(null);
|
const [partialImage, setPartialImage] = useState(null);
|
||||||
|
const [imageOutputFormat, setImageOutputFormat] = useState("json"); // json | binary
|
||||||
|
const [binaryImageUrl, setBinaryImageUrl] = useState("");
|
||||||
const [running, setRunning] = useState(false);
|
const [running, setRunning] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [connections, setConnections] = useState([]);
|
const [connections, setConnections] = useState([]);
|
||||||
@@ -928,12 +933,14 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
...(supportsEdit && refImage.trim() ? { image: refImage.trim() } : {}),
|
...(supportsEdit && refImage.trim() ? { image: refImage.trim() } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Streaming supported for codex image (Plus/Pro accounts)
|
// Streaming supported for codex image (Plus/Pro accounts) — disabled when binary output requested
|
||||||
const useStreaming = kind === "image" && providerId === "codex";
|
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 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 ")} \\
|
${headersPreview.replace(/\\\n /g, "\\\n ")} \\
|
||||||
-d '${JSON.stringify(requestBody)}'`;
|
-d '${JSON.stringify(requestBody)}'${wantBinary ? " \\\n --output image.png" : ""}`;
|
||||||
|
|
||||||
const handleRun = async () => {
|
const handleRun = async () => {
|
||||||
if (!input.trim() || !modelFull) return;
|
if (!input.trim() || !modelFull) return;
|
||||||
@@ -942,6 +949,7 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
setResult(null);
|
setResult(null);
|
||||||
setProgress(null);
|
setProgress(null);
|
||||||
setPartialImage(null);
|
setPartialImage(null);
|
||||||
|
if (binaryImageUrl) { try { URL.revokeObjectURL(binaryImageUrl); } catch {} setBinaryImageUrl(""); }
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
try {
|
try {
|
||||||
const headers = { "Content-Type": "application/json" };
|
const headers = { "Content-Type": "application/json" };
|
||||||
@@ -949,7 +957,7 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
if (pinnedConnectionId) headers["x-connection-id"] = pinnedConnectionId;
|
if (pinnedConnectionId) headers["x-connection-id"] = pinnedConnectionId;
|
||||||
if (useStreaming) headers["Accept"] = "text/event-stream";
|
if (useStreaming) headers["Accept"] = "text/event-stream";
|
||||||
const body = { ...requestBody, model: modelFull };
|
const body = { ...requestBody, model: modelFull };
|
||||||
const res = await fetch(`/api${apiPath}`, {
|
const res = await fetch(`/api${apiPathWithQuery}`, {
|
||||||
method: kindConfig.endpoint.method,
|
method: kindConfig.endpoint.method,
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
@@ -959,7 +967,16 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
setError(data?.error?.message || data?.error || `HTTP ${res.status}`);
|
setError(data?.error?.message || data?.error || `HTTP ${res.status}`);
|
||||||
return;
|
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) {
|
if (isSse && res.body) {
|
||||||
// Parse SSE: progress / partial_image / done / error
|
// Parse SSE: progress / partial_image / done / error
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
@@ -1171,6 +1188,20 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Output Format toggle (image only) — last */}
|
||||||
|
{kind === "image" && (
|
||||||
|
<Row label="Output Format">
|
||||||
|
<select
|
||||||
|
value={imageOutputFormat}
|
||||||
|
onChange={(e) => setImageOutputFormat(e.target.value)}
|
||||||
|
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
|
||||||
|
>
|
||||||
|
<option value="json">JSON (Base64)</option>
|
||||||
|
<option value="binary">Binary File</option>
|
||||||
|
</select>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Curl + Run */}
|
{/* Curl + Run */}
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
@@ -1206,7 +1237,7 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-text-muted">
|
<span className="text-xs text-text-muted">
|
||||||
{progress?.stage || "starting"}
|
{progress?.stage || "starting"}
|
||||||
{progress?.bytesReceived ? ` · ${(progress.bytesReceived / 1024).toFixed(1)} KB` : ""}
|
{!running && progress?.bytesReceived ? ` · ${(progress.bytesReceived / 1024).toFixed(1)} KB` : ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1245,12 +1276,24 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre opacity-70">
|
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre opacity-70">
|
||||||
{result ? resultJson : exConfig.defaultResponse}
|
{result ? resultJson : exConfig.defaultResponse}
|
||||||
</pre>
|
</pre>
|
||||||
{kind === "image" && result?.data?.data?.[0] && (
|
{kind === "image" && (binaryImageUrl || result?.data?.data?.[0]) && (
|
||||||
<img
|
<div className="mt-2">
|
||||||
src={result.data.data[0].b64_json ? `data:image/png;base64,${result.data.data[0].b64_json}` : result.data.data[0].url}
|
<div className="flex items-center justify-end mb-1.5">
|
||||||
alt="Generated"
|
<a
|
||||||
className="max-w-full rounded-lg border border-border mt-2"
|
href={binaryImageUrl || (result?.data?.data?.[0]?.b64_json ? `data:image/png;base64,${result.data.data[0].b64_json}` : result?.data?.data?.[0]?.url || "")}
|
||||||
/>
|
download="image.png"
|
||||||
|
className="flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[14px]">download</span>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
src={binaryImageUrl || (result?.data?.data?.[0]?.b64_json ? `data:image/png;base64,${result.data.data[0].b64_json}` : result?.data?.data?.[0]?.url)}
|
||||||
|
alt="Generated"
|
||||||
|
className="max-w-full rounded-lg border border-border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,8 +27,10 @@ export async function handleImageGeneration(request) {
|
|||||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body");
|
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 preferredConnectionId = request.headers.get("x-connection-id") || null;
|
||||||
const wantsStream = (request.headers.get("accept") || "").includes("text/event-stream");
|
const wantsStream = (request.headers.get("accept") || "").includes("text/event-stream");
|
||||||
|
const binaryOutput = url.searchParams.get("response_format") === "binary";
|
||||||
const modelStr = body.model;
|
const modelStr = body.model;
|
||||||
|
|
||||||
const apiKey = extractApiKey(request);
|
const apiKey = extractApiKey(request);
|
||||||
@@ -53,6 +55,7 @@ export async function handleImageGeneration(request) {
|
|||||||
body,
|
body,
|
||||||
modelInfo: { provider, model },
|
modelInfo: { provider, model },
|
||||||
credentials: null,
|
credentials: null,
|
||||||
|
binaryOutput,
|
||||||
});
|
});
|
||||||
if (result.success) return result.response;
|
if (result.success) return result.response;
|
||||||
return errorResponse(result.status || HTTP_STATUS.BAD_GATEWAY, result.error || "Image generation failed");
|
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 },
|
modelInfo: { provider, model },
|
||||||
credentials: refreshedCredentials,
|
credentials: refreshedCredentials,
|
||||||
streamToClient: wantsStream,
|
streamToClient: wantsStream,
|
||||||
|
binaryOutput,
|
||||||
onCredentialsRefreshed: async (newCreds) => {
|
onCredentialsRefreshed: async (newCreds) => {
|
||||||
await updateProviderCredentials(credentials.connectionId, {
|
await updateProviderCredentials(credentials.connectionId, {
|
||||||
accessToken: newCreds.accessToken,
|
accessToken: newCreds.accessToken,
|
||||||
|
|||||||
Reference in New Issue
Block a user