🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

opencode-gpt-imagegen

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

opencode-gpt-imagegen - npm Package Compare versions

Comparing version
0.1.4
to
0.1.5
+176
-181
dist/index.js

@@ -0,198 +1,193 @@

// src/index.ts
import { tool } from "@opencode-ai/plugin";
// src/auth.ts
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { tool } from "@opencode-ai/plugin";
import { xdgData } from "xdg-basedir";
async function loadAuthData() {
if (process.env.OPENCODE_AUTH_CONTENT) {
return JSON.parse(process.env.OPENCODE_AUTH_CONTENT);
}
if (!xdgData) {
throw new Error("could not determine XDG data directory");
}
const raw = await fs.readFile(path.join(xdgData, "opencode", "auth.json"), "utf-8");
return JSON.parse(raw);
}
async function loadOpenAIAuth() {
try {
const data = await loadAuthData();
const entry = data.openai;
if (entry?.type === "oauth" && typeof entry.access === "string") {
return entry;
}
} catch {
return;
}
return;
}
// src/codex.ts
import { EventSourceParserStream } from "eventsource-parser/stream";
var CODEX_RESPONSES_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
var SUBSCRIPTION_MODEL = "gpt-5.5";
async function parseImageGenerationResultFromSSE(stream) {
const events = stream.pipeThrough(new TextDecoderStream).pipeThrough(new EventSourceParserStream);
for await (const event of events) {
if (event.data === "[DONE]")
continue;
try {
const json = JSON.parse(event.data);
if (json.type === "response.output_item.done" && json.item?.type === "image_generation_call" && typeof json.item.result === "string") {
return json.item.result;
}
} catch {}
}
throw new Error("no image_generation result returned by codex backend");
}
async function callViaCodexResponses(auth, args, inputImageDataUrls) {
const userContent = [{ type: "input_text", text: args.prompt }];
for (const dataUrl of inputImageDataUrls) {
userContent.push({ type: "input_image", image_url: dataUrl });
}
const body = {
model: SUBSCRIPTION_MODEL,
instructions: "You are an image generation assistant running inside the Codex backend. " + "Always satisfy the request by invoking the image_generation tool exactly once. " + "Do not respond with text only.",
input: [{ role: "user", content: userContent }],
tools: [
{
type: "image_generation",
output_format: "png",
quality: args.quality,
...args.size ? { size: args.size } : {}
}
],
tool_choice: { type: "image_generation" },
stream: true,
store: false
};
const res = await fetch(CODEX_RESPONSES_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${auth.access}`,
...auth.accountId ? { "ChatGPT-Account-Id": auth.accountId } : {},
originator: "opencode",
Accept: "text/event-stream"
},
body: JSON.stringify(body)
});
if (!res.ok || !res.body) {
const detail = await res.text().catch(() => "");
throw new Error(`codex responses request failed: ${res.status} ${detail.slice(0, 500)}`);
}
return parseImageGenerationResultFromSSE(res.body);
}
// src/input-image.ts
import * as fs2 from "node:fs/promises";
import * as path2 from "node:path";
import { fileTypeFromBuffer } from "file-type";
import { xdgData } from "xdg-basedir";
// Codex OAuth responses endpoint URL.
// https://github.com/openai/codex/blob/fca81eeb5bab4cad997622a359d446e6489c445b/codex-rs/model-provider-info/src/lib.rs#L37
// https://github.com/openai/codex/blob/fca81eeb5bab4cad997622a359d446e6489c445b/codex-rs/core/src/client.rs#L146
const CODEX_RESPONSES_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
// Codex model slug used for the hosted image_generation turn.
// https://github.com/openai/codex/blob/fca81eeb5bab4cad997622a359d446e6489c445b/codex-rs/models-manager/models.json#L24
const SUBSCRIPTION_MODEL = "gpt-5.5";
const MAX_OUTPUT_VERSION_SUFFIX = 999;
async function readImageAsDataUrl(filePath, ctxDir) {
const abs = path.isAbsolute(filePath) ? filePath : path.resolve(ctxDir, filePath);
const buf = await fs.readFile(abs);
const detected = await fileTypeFromBuffer(buf);
if (!detected?.mime.startsWith("image/")) {
throw new Error(`unsupported image file type: ${abs}`);
}
return `data:${detected.mime};base64,${buf.toString("base64")}`;
const abs = path2.isAbsolute(filePath) ? filePath : path2.resolve(ctxDir, filePath);
const buf = await fs2.readFile(abs);
const detected = await fileTypeFromBuffer(buf);
if (!detected?.mime.startsWith("image/")) {
throw new Error(`unsupported image file type: ${abs}`);
}
return `data:${detected.mime};base64,${buf.toString("base64")}`;
}
async function readReferenceImages(paths, ctxDir) {
return Promise.all((paths ?? []).map((p) => readImageAsDataUrl(p, ctxDir)));
}
// src/output-image.ts
import * as fs3 from "node:fs/promises";
import * as path3 from "node:path";
var MAX_OUTPUT_VERSION_SUFFIX = 999;
async function pathExists(p) {
try {
await fs.access(p);
return true;
}
catch {
return false;
}
try {
await fs3.access(p);
return true;
} catch {
return false;
}
}
async function pickNonOverwritePath(requested) {
if (!(await pathExists(requested)))
return requested;
const dir = path.dirname(requested);
const ext = path.extname(requested);
const stem = path.basename(requested, ext);
for (let n = 2; n <= MAX_OUTPUT_VERSION_SUFFIX; n++) {
const candidate = path.join(dir, `${stem}-v${n}${ext}`);
if (!(await pathExists(candidate)))
return candidate;
}
throw new Error(`could not find a non-conflicting filename under ${dir}/${stem}-vN${ext} (tried up to v${MAX_OUTPUT_VERSION_SUFFIX})`);
async function pickNonOverwritePath(requested, maxVersion = MAX_OUTPUT_VERSION_SUFFIX) {
if (!await pathExists(requested))
return requested;
const dir = path3.dirname(requested);
const ext = path3.extname(requested);
const stem = path3.basename(requested, ext);
for (let n = 2;n <= maxVersion; n++) {
const candidate = path3.join(dir, `${stem}-v${n}${ext}`);
if (!await pathExists(candidate))
return candidate;
}
throw new Error(`could not find a non-conflicting filename under ${dir}/${stem}-vN${ext} (tried up to v${maxVersion})`);
}
function buildSavedMessage(savedPath, requestedPath) {
const versionNote = savedPath !== requestedPath
? ` (the requested path ${requestedPath} already existed; the new image was versioned to avoid overwriting it)`
: "";
return `Generated image saved to ${savedPath}${versionNote}.`;
const versionNote = savedPath !== requestedPath ? ` (the requested path ${requestedPath} already existed; the new image was versioned to avoid overwriting it)` : "";
return `Generated image saved to ${savedPath}${versionNote}.`;
}
// Mirrors OpenCode's auth resolution: OPENCODE_AUTH_CONTENT overrides $XDG_DATA_HOME/opencode/auth.json.
// The Auth service is not exposed to external plugins, so this reproduces the rules directly.
async function loadAuthData() {
if (process.env.OPENCODE_AUTH_CONTENT) {
return JSON.parse(process.env.OPENCODE_AUTH_CONTENT);
}
if (!xdgData) {
throw new Error("could not determine XDG data directory");
}
const raw = await fs.readFile(path.join(xdgData, "opencode", "auth.json"), "utf-8");
return JSON.parse(raw);
async function saveGeneratedImage(out, ctxDir, base64) {
const requestedPath = path3.isAbsolute(out) ? out : path3.resolve(ctxDir, out);
await fs3.mkdir(path3.dirname(requestedPath), { recursive: true });
const savedPath = await pickNonOverwritePath(requestedPath);
await fs3.writeFile(savedPath, Buffer.from(base64, "base64"));
return {
savedPath,
versioned: savedPath !== requestedPath,
message: buildSavedMessage(savedPath, requestedPath)
};
}
async function loadOpenAIAuth() {
try {
const data = await loadAuthData();
const entry = data.openai;
if (entry?.type === "oauth" && typeof entry.access === "string") {
return entry;
}
}
catch {
return undefined;
}
return undefined;
}
async function parseImageGenerationResultFromSSE(stream) {
const events = stream
.pipeThrough(new TextDecoderStream())
.pipeThrough(new EventSourceParserStream());
for await (const event of events) {
if (event.data === "[DONE]")
continue;
try {
const json = JSON.parse(event.data);
if (json.type === "response.output_item.done" &&
json.item?.type === "image_generation_call" &&
typeof json.item.result === "string") {
return json.item.result;
// src/index.ts
var GptImagePlugin = async (_input) => {
return {
tool: {
gpt_image_gen: tool({
description: [
"Generate raster images using OpenAI's hosted image_generation tool.",
"Use for AI-created bitmap visuals such as photos, illustrations, textures, sprites, and mockups.",
"Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.",
"Reference images may be attached through `images`; label each image's role inline in `prompt`, for example: 'Image 1: reference image'.",
"For many distinct assets, invoke gpt_image_gen once per requested asset rather than relying on multi-image output; gpt_image_gen returns one image per call.",
"Requires OpenCode to be authenticated with ChatGPT OAuth. Returns the absolute path of the saved PNG."
].join(" "),
args: {
prompt: tool.schema.string().describe("Description of the image to generate."),
out: tool.schema.string().describe("Output file path, relative to the project directory unless absolute. The plugin writes a PNG."),
quality: tool.schema.enum(["low", "medium", "high", "auto"]).describe("Generation quality passed to the hosted image_generation tool."),
size: tool.schema.string().optional().describe("Optional image size passed to the hosted image_generation tool. Use `auto` or `WIDTHxHEIGHT`; width and height must be multiples of 16px, max edge <= 3840px, long-to-short ratio <= 3:1, and total pixels between 655,360 and 8,294,400."),
images: tool.schema.array(tool.schema.string()).optional().describe("Optional reference image paths, relative to the project directory unless absolute.")
},
async execute(args, ctx) {
const auth = await loadOpenAIAuth();
if (!auth) {
throw new Error("OpenAI ChatGPT OAuth credentials not configured.");
}
const inputImageDataUrls = await readReferenceImages(args.images, ctx.directory);
const base64 = await callViaCodexResponses(auth, args, inputImageDataUrls);
const { savedPath, versioned, message } = await saveGeneratedImage(args.out, ctx.directory, base64);
return {
output: message,
metadata: {
out: savedPath,
versioned,
billing: "subscription"
}
};
}
catch {
// SSE keepalive or non-JSON heartbeat
}
})
}
throw new Error("no image_generation result returned by codex backend");
}
async function callViaCodexResponses(auth, args, inputImageDataUrls) {
const userContent = [{ type: "input_text", text: args.prompt }];
for (const dataUrl of inputImageDataUrls) {
userContent.push({ type: "input_image", image_url: dataUrl });
}
// https://github.com/openai/codex/blob/fca81eeb5bab4cad997622a359d446e6489c445b/codex-rs/core/src/client.rs#L745-L763
const body = {
model: SUBSCRIPTION_MODEL,
instructions: "You are an image generation assistant running inside the Codex backend. " +
"Always satisfy the request by invoking the image_generation tool exactly once. " +
"Do not respond with text only.",
input: [{ role: "user", content: userContent }],
tools: [
{
type: "image_generation",
output_format: "png",
quality: args.quality,
...(args.size ? { size: args.size } : {}),
},
],
tool_choice: { type: "image_generation" },
stream: true,
store: false,
};
const res = await fetch(CODEX_RESPONSES_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${auth.access}`,
...(auth.accountId ? { "ChatGPT-Account-Id": auth.accountId } : {}),
originator: "opencode",
Accept: "text/event-stream",
},
body: JSON.stringify(body),
});
if (!res.ok || !res.body) {
const detail = await res.text().catch(() => "");
throw new Error(`codex responses request failed: ${res.status} ${detail.slice(0, 500)}`);
}
return parseImageGenerationResultFromSSE(res.body);
}
const GptImagePlugin = async (_input) => {
return {
tool: {
gpt_image_gen: tool({
description: [
"Generate raster images using OpenAI's hosted image_generation tool.",
"Use for AI-created bitmap visuals such as photos, illustrations, textures, sprites, and mockups.",
"Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas.",
"Reference images may be attached through `images`; label each image's role inline in `prompt`, for example: 'Image 1: reference image'.",
"For many distinct assets, invoke gpt_image_gen once per requested asset rather than relying on multi-image output; gpt_image_gen returns one image per call.",
"Requires OpenCode to be authenticated with ChatGPT OAuth. Returns the absolute path of the saved PNG.",
].join(" "),
// https://developers.openai.com/api/docs/guides/image-generation
args: {
prompt: tool.schema.string().describe("Description of the image to generate."),
out: tool.schema
.string()
.describe("Output file path, relative to the project directory unless absolute. The plugin writes a PNG."),
quality: tool.schema
.enum(["low", "medium", "high", "auto"])
.describe("Generation quality passed to the hosted image_generation tool."),
size: tool.schema
.string()
.optional()
.describe("Optional image size passed to the hosted image_generation tool. Use `auto` or `WIDTHxHEIGHT`; width and height must be multiples of 16px, max edge <= 3840px, long-to-short ratio <= 3:1, and total pixels between 655,360 and 8,294,400."),
images: tool.schema
.array(tool.schema.string())
.optional()
.describe("Optional reference image paths, relative to the project directory unless absolute."),
},
async execute(args, ctx) {
const auth = await loadOpenAIAuth();
if (!auth) {
throw new Error("OpenAI ChatGPT OAuth credentials not configured.");
}
const inputPaths = args.images ?? [];
const inputImageDataUrls = await Promise.all(inputPaths.map((path) => readImageAsDataUrl(path, ctx.directory)));
const base64 = await callViaCodexResponses(auth, args, inputImageDataUrls);
const requestedPath = path.isAbsolute(args.out) ? args.out : path.resolve(ctx.directory, args.out);
await fs.mkdir(path.dirname(requestedPath), { recursive: true });
const savedPath = await pickNonOverwritePath(requestedPath);
await fs.writeFile(savedPath, Buffer.from(base64, "base64"));
const versioned = savedPath !== requestedPath;
return {
output: buildSavedMessage(savedPath, requestedPath),
metadata: {
out: savedPath,
versioned,
billing: "subscription",
},
};
},
}),
},
};
};
};
export default {
id: "opencode-gpt-imagegen",
server: GptImagePlugin,
var src_default = {
id: "opencode-gpt-imagegen",
server: GptImagePlugin
};
export {
src_default as default
};
{
"name": "opencode-gpt-imagegen",
"version": "0.1.4",
"version": "0.1.5",
"description": "OpenCode plugin for GPT Image 2 generation through your ChatGPT subscription, with reference images and safe PNG output.",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"

@@ -23,3 +21,3 @@ }

"prebuild": "rm -rf dist",
"build": "tsc -p tsconfig.build.json",
"build": "bun build ./src/index.ts --outdir ./dist --target node --format esm --packages external",
"test": "bun test",

@@ -26,0 +24,0 @@ "test:e2e": "OPENCODE_MODEL=openai/gpt-5.5 RUN_E2E=1 bun test",

import type { Plugin } from "@opencode-ai/plugin";
declare const _default: {
id: string;
server: Plugin;
};
export default _default;