@oai404iao/pi-codex-imagegen 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -61
- package/THIRD_PARTY_NOTICES.md +6 -28
- package/package.json +6 -10
- package/src/index.ts +1 -37
- package/LICENSES/Apache-2.0.txt +0 -201
- package/LICENSES/OpenAI-Codex-NOTICE.txt +0 -6
- package/provenance/openai-codex-eb9dceba-reserved-tools.json +0 -140
- package/src/background-image-generation.ts +0 -218
- package/src/background-image-jobs.ts +0 -116
- package/src/tools/image-generation/capture.ts +0 -42
- package/src/tools/image-generation/display.ts +0 -73
- package/src/tools/image-generation/output.ts +0 -29
- package/src/tools/image-generation/preview.ts +0 -79
- package/src/tools/image-generation/storage.ts +0 -185
- package/src/tools/image-generation/types.ts +0 -36
- package/src/tools/image-generation.ts +0 -210
- package/src/utils/images.ts +0 -73
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { loadSettings } from "@oai404iao/pi-codex-runtime/internal/settings";
|
|
2
|
-
import { saveBase64Image } from "../../utils/images.js";
|
|
3
|
-
import { type SavedGeneratedImage } from "./types.js";
|
|
4
|
-
|
|
5
|
-
const OPENAI_CODEX_IMAGE_DIR = ".pi/openai-codex-images";
|
|
6
|
-
|
|
7
|
-
const OPENAI_CODEX_LATEST_IMAGE_NAME = "latest.png";
|
|
8
|
-
|
|
9
|
-
let fsPromisesPromise: Promise<typeof import("node:fs/promises")> | undefined;
|
|
10
|
-
|
|
11
|
-
const workspaceRootCache = new Map<string, Promise<string>>();
|
|
12
|
-
|
|
13
|
-
const PATH_SEPARATOR = "/";
|
|
14
|
-
|
|
15
|
-
function sanitizeFilePart(value: string | undefined, fallback: string): string {
|
|
16
|
-
const trimmed = (value ?? "").trim();
|
|
17
|
-
if (!trimmed) return fallback;
|
|
18
|
-
return trimmed.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function shortenFilePart(value: string | undefined, fallback: string): string {
|
|
22
|
-
const safe = sanitizeFilePart(value, fallback);
|
|
23
|
-
const match = /^([a-zA-Z]+_)(.+)$/.exec(safe);
|
|
24
|
-
const prefix = match?.[1] ?? "";
|
|
25
|
-
const body = match?.[2] ?? safe;
|
|
26
|
-
if (body.length <= 12) return `${prefix}${body}`;
|
|
27
|
-
return `${prefix}${body.slice(0, 8)}-${body.slice(-4)}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function normalizeImageOutputFormat(value: string | undefined): string {
|
|
31
|
-
const format = (value ?? "png").toLowerCase();
|
|
32
|
-
return format === "png" || format === "jpg" || format === "jpeg" || format === "webp" ? format : "png";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function normalizePath(value: string): string {
|
|
36
|
-
if (!value) return ".";
|
|
37
|
-
const normalized = value.replace(/\/+/g, PATH_SEPARATOR);
|
|
38
|
-
if (normalized === PATH_SEPARATOR) return normalized;
|
|
39
|
-
return normalized.replace(/\/+$/g, "") || PATH_SEPARATOR;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function joinPaths(...parts: string[]): string {
|
|
43
|
-
if (parts.length === 0) return ".";
|
|
44
|
-
let result = parts[0] ?? "";
|
|
45
|
-
for (let i = 1; i < parts.length; i++) {
|
|
46
|
-
const part = parts[i];
|
|
47
|
-
if (!part) continue;
|
|
48
|
-
if (!result || result.endsWith(PATH_SEPARATOR)) {
|
|
49
|
-
result += part.replace(/^\/+/, "");
|
|
50
|
-
} else {
|
|
51
|
-
result += `${PATH_SEPARATOR}${part.replace(/^\/+/, "")}`;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return normalizePath(result);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function dirnamePath(value: string): string {
|
|
58
|
-
const normalized = normalizePath(value);
|
|
59
|
-
if (normalized === PATH_SEPARATOR) return PATH_SEPARATOR;
|
|
60
|
-
const index = normalized.lastIndexOf(PATH_SEPARATOR);
|
|
61
|
-
if (index < 0) return ".";
|
|
62
|
-
if (index === 0) return PATH_SEPARATOR;
|
|
63
|
-
return normalized.slice(0, index);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function splitPathSegments(value: string): string[] {
|
|
67
|
-
const normalized = normalizePath(value);
|
|
68
|
-
if (normalized === PATH_SEPARATOR) return [];
|
|
69
|
-
return normalized.replace(/^\/+/, "").split(PATH_SEPARATOR).filter(Boolean);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function relativePath(from: string, to: string): string {
|
|
73
|
-
const normalizedFrom = normalizePath(from);
|
|
74
|
-
const normalizedTo = normalizePath(to);
|
|
75
|
-
if (normalizedFrom === normalizedTo) return "";
|
|
76
|
-
const fromSegments = splitPathSegments(normalizedFrom);
|
|
77
|
-
const toSegments = splitPathSegments(normalizedTo);
|
|
78
|
-
let shared = 0;
|
|
79
|
-
while (shared < fromSegments.length && shared < toSegments.length && fromSegments[shared] === toSegments[shared]) {
|
|
80
|
-
shared++;
|
|
81
|
-
}
|
|
82
|
-
const upSegments = new Array(fromSegments.length - shared).fill("..");
|
|
83
|
-
const downSegments = toSegments.slice(shared);
|
|
84
|
-
return [...upSegments, ...downSegments].join(PATH_SEPARATOR);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function getNodeFsPromises(): Promise<typeof import("node:fs/promises")> {
|
|
88
|
-
if (!fsPromisesPromise) {
|
|
89
|
-
fsPromisesPromise = import("node:fs/promises");
|
|
90
|
-
}
|
|
91
|
-
return fsPromisesPromise;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function pathExists(value: string): Promise<boolean> {
|
|
95
|
-
try {
|
|
96
|
-
const fs = await getNodeFsPromises();
|
|
97
|
-
await fs.access(value);
|
|
98
|
-
return true;
|
|
99
|
-
} catch {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function resolveWorkspaceRoot(cwd: string): Promise<string> {
|
|
105
|
-
const normalizedCwd = normalizePath(cwd);
|
|
106
|
-
const cached = workspaceRootCache.get(normalizedCwd);
|
|
107
|
-
if (cached) return cached;
|
|
108
|
-
|
|
109
|
-
const promise = (async () => {
|
|
110
|
-
let current = normalizedCwd;
|
|
111
|
-
while (true) {
|
|
112
|
-
if (await pathExists(joinPaths(current, ".git"))) {
|
|
113
|
-
return current;
|
|
114
|
-
}
|
|
115
|
-
const parent = dirnamePath(current);
|
|
116
|
-
if (parent === current || parent === ".") {
|
|
117
|
-
return normalizedCwd;
|
|
118
|
-
}
|
|
119
|
-
current = parent;
|
|
120
|
-
}
|
|
121
|
-
})();
|
|
122
|
-
|
|
123
|
-
workspaceRootCache.set(normalizedCwd, promise);
|
|
124
|
-
return promise;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function getOpenAICodexImageDirectory(cwd: string): string {
|
|
128
|
-
return joinPaths(cwd, OPENAI_CODEX_IMAGE_DIR);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function getOpenAICodexImagePath(cwd: string, responseId: string | undefined, callId: string, outputFormat?: string): string {
|
|
132
|
-
const ext = normalizeImageOutputFormat(outputFormat);
|
|
133
|
-
const safeCallId = shortenFilePart(callId, "image");
|
|
134
|
-
const safeResponseId = shortenFilePart(responseId, "response");
|
|
135
|
-
return joinPaths(getOpenAICodexImageDirectory(cwd), `${safeCallId}-${safeResponseId}.${ext}`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function getOpenAICodexLatestImagePath(cwd: string): string {
|
|
139
|
-
return joinPaths(getOpenAICodexImageDirectory(cwd), OPENAI_CODEX_LATEST_IMAGE_NAME);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function buildGeneratedImageDisplayText(savedImage: SavedGeneratedImage, options?: { expanded?: boolean }): string {
|
|
143
|
-
const lines: string[] = [];
|
|
144
|
-
if (options?.expanded && savedImage.revisedPrompt) {
|
|
145
|
-
lines.push(`Prompt: ${savedImage.revisedPrompt}`);
|
|
146
|
-
}
|
|
147
|
-
lines.push(`File: ${savedImage.relativePath}`);
|
|
148
|
-
return lines.join("\n");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export async function saveOpenAICodexGeneratedImage(
|
|
152
|
-
cwd: string,
|
|
153
|
-
image: { responseId?: string; callId: string; result: string; outputFormat?: string; imageModel?: string; revisedPrompt?: string },
|
|
154
|
-
): Promise<SavedGeneratedImage> {
|
|
155
|
-
const workspaceRoot = await resolveWorkspaceRoot(cwd);
|
|
156
|
-
const outputFormat = normalizeImageOutputFormat(image.outputFormat);
|
|
157
|
-
const saved = await saveBase64Image({
|
|
158
|
-
base64: image.result,
|
|
159
|
-
callId: image.callId,
|
|
160
|
-
cwd,
|
|
161
|
-
format: outputFormat,
|
|
162
|
-
responseId: image.responseId,
|
|
163
|
-
settings: loadSettings(cwd),
|
|
164
|
-
});
|
|
165
|
-
const absolutePath = saved.path;
|
|
166
|
-
const latestAbsolutePath = saved.latestPath ?? getOpenAICodexLatestImagePath(workspaceRoot);
|
|
167
|
-
|
|
168
|
-
const relativeFilePath = relativePath(workspaceRoot, absolutePath);
|
|
169
|
-
const latestRelativeFilePath = relativePath(workspaceRoot, latestAbsolutePath);
|
|
170
|
-
const relativePathValue = relativeFilePath && !relativeFilePath.startsWith("..") ? relativeFilePath : absolutePath;
|
|
171
|
-
const latestRelativePathValue =
|
|
172
|
-
latestRelativeFilePath && !latestRelativeFilePath.startsWith("..") ? latestRelativeFilePath : latestAbsolutePath;
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
absolutePath,
|
|
176
|
-
relativePath: relativePathValue,
|
|
177
|
-
latestAbsolutePath,
|
|
178
|
-
latestRelativePath: latestRelativePathValue,
|
|
179
|
-
responseId: image.responseId,
|
|
180
|
-
callId: image.callId,
|
|
181
|
-
outputFormat,
|
|
182
|
-
imageModel: image.imageModel,
|
|
183
|
-
revisedPrompt: image.revisedPrompt,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export const IMAGE_SAVE_DISPLAY_MESSAGE_TYPE = "codex-image-generation-display";
|
|
2
|
-
|
|
3
|
-
export interface SavedGeneratedImage {
|
|
4
|
-
absolutePath: string;
|
|
5
|
-
relativePath: string;
|
|
6
|
-
latestAbsolutePath: string;
|
|
7
|
-
latestRelativePath: string;
|
|
8
|
-
responseId: string | undefined;
|
|
9
|
-
callId: string;
|
|
10
|
-
outputFormat: string;
|
|
11
|
-
imageModel?: string;
|
|
12
|
-
revisedPrompt?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ImageDisplayMessageDetails {
|
|
16
|
-
savedImages: SavedGeneratedImage[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface PendingImageDisplay {
|
|
20
|
-
savedImage: SavedGeneratedImage;
|
|
21
|
-
imageData: { data: string; mimeType: string };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface QueuedImageActivity extends PendingImageDisplay {
|
|
25
|
-
kind: "image";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type PendingActivity = QueuedImageActivity;
|
|
29
|
-
|
|
30
|
-
export interface CachedImagePreview {
|
|
31
|
-
data: string;
|
|
32
|
-
mimeType: string;
|
|
33
|
-
bytes: number;
|
|
34
|
-
widthPx?: number;
|
|
35
|
-
heightPx?: number;
|
|
36
|
-
}
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { isAbsolute, resolve } from "node:path";
|
|
3
|
-
import { buildSessionContext, detectSupportedImageMimeTypeFromFile, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { saveBase64Image } from "../utils/images.js";
|
|
5
|
-
import { imageGenerationOutput, imageGenerationOutputSchema } from "./image-generation/output.js";
|
|
6
|
-
import type { CodexMinimalToolsSettings } from "@oai404iao/pi-codex-runtime/internal/settings";
|
|
7
|
-
import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
8
|
-
import { buildCodexJsonHeaders, hasCodexRequestAuth, resolveCodexApiEndpoint, withResolvedAuthBaseUrl } from "@oai404iao/pi-codex-runtime/internal/codex-http";
|
|
9
|
-
import { loadModelSettings, type ResolvedCodexModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
|
|
10
|
-
import { checkEndpointResponse, rememberResolvedEndpoint, requireEndpointCapability } from "@oai404iao/pi-codex-runtime/internal/endpoint-state";
|
|
11
|
-
import { currentCodexTurn, uuidV7 } from "@oai404iao/pi-codex-runtime/internal/codex-wire-identity";
|
|
12
|
-
import { fetchCodexJson } from "@oai404iao/pi-codex-runtime/internal/json-request";
|
|
13
|
-
|
|
14
|
-
export interface ImageGenerationInput {
|
|
15
|
-
prompt?: string;
|
|
16
|
-
referenced_image_paths?: string[] | null;
|
|
17
|
-
num_last_images_to_include?: number | null;
|
|
18
|
-
transparent_background?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const imageGenerationToolSchema = {
|
|
22
|
-
type: "object",
|
|
23
|
-
additionalProperties: false,
|
|
24
|
-
properties: {
|
|
25
|
-
prompt: { type: "string", description: "Image generation or editing prompt." },
|
|
26
|
-
referenced_image_paths: {
|
|
27
|
-
type: ["array", "null"], maxItems: 5,
|
|
28
|
-
items: { type: "string", minLength: 1 },
|
|
29
|
-
description: "Local image paths to edit. PNG, JPEG, WebP, GIF and BMP are supported.",
|
|
30
|
-
},
|
|
31
|
-
num_last_images_to_include: {
|
|
32
|
-
type: ["integer", "null"], minimum: 1, maximum: 5,
|
|
33
|
-
description: "Use the newest conversation images when one or more targets have no local path.",
|
|
34
|
-
},
|
|
35
|
-
transparent_background: {
|
|
36
|
-
type: "boolean",
|
|
37
|
-
description: "Whether the output should have a transparent background. Defaults to false.",
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
required: ["prompt"],
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
interface ImageGenerationToolContext {
|
|
44
|
-
cwd: string;
|
|
45
|
-
model?: Model<Api>;
|
|
46
|
-
modelRegistry?: {
|
|
47
|
-
getApiKeyAndHeaders(model: Model<Api>): Promise<
|
|
48
|
-
| { ok: true; apiKey?: string; headers?: ProviderHeaders; baseUrl?: string }
|
|
49
|
-
| { ok: false; error: string }
|
|
50
|
-
>;
|
|
51
|
-
};
|
|
52
|
-
sessionManager?: {
|
|
53
|
-
getSessionId?(): string;
|
|
54
|
-
getBranch(): SessionEntry[];
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface StandaloneImageGenerationInvocation {
|
|
59
|
-
callId?: string;
|
|
60
|
-
turnId?: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function referencedImageUrls(cwd: string, paths: readonly string[]): Promise<Array<{ image_url: string }>> {
|
|
64
|
-
if (paths.length > 5) throw new Error("referenced_image_paths must contain at most 5 paths.");
|
|
65
|
-
return Promise.all(paths.map(async (rawPath) => {
|
|
66
|
-
const normalized = rawPath.replace(/^@/, "");
|
|
67
|
-
const path = isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
|
|
68
|
-
const mimeType = await detectSupportedImageMimeTypeFromFile(path);
|
|
69
|
-
if (!mimeType) throw new Error(`Unsupported reference image type: ${rawPath}. Use PNG, JPEG, WebP, GIF, or BMP.`);
|
|
70
|
-
const data = await readFile(path);
|
|
71
|
-
if (mimeType === "image/gif" || mimeType === "image/bmp") {
|
|
72
|
-
// Codex Original mode normalizes unsupported source formats to PNG without resizing.
|
|
73
|
-
const { PhotonImage } = await import("@silvia-odwyer/photon-node");
|
|
74
|
-
const decoded = PhotonImage.new_from_byteslice(data);
|
|
75
|
-
try {
|
|
76
|
-
return { image_url: `data:image/png;base64,${Buffer.from(decoded.get_bytes()).toString("base64")}` };
|
|
77
|
-
} finally {
|
|
78
|
-
decoded.free();
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return { image_url: `data:${mimeType};base64,${data.toString("base64")}` };
|
|
82
|
-
}));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function recentConversationImageUrls(ctx: ImageGenerationToolContext, count: number): Array<{ image_url: string }> {
|
|
86
|
-
if (!ctx.sessionManager) throw new Error("Conversation images are unavailable in this tool context; use referenced_image_paths.");
|
|
87
|
-
if (!Number.isSafeInteger(count) || count < 1 || count > 5) {
|
|
88
|
-
throw new Error("num_last_images_to_include must be an integer between 1 and 5.");
|
|
89
|
-
}
|
|
90
|
-
const messages = buildSessionContext(ctx.sessionManager.getBranch()).messages;
|
|
91
|
-
const images: string[] = [];
|
|
92
|
-
for (const message of messages) {
|
|
93
|
-
if (message.role === "user" || message.role === "toolResult") {
|
|
94
|
-
if (!Array.isArray(message.content)) continue;
|
|
95
|
-
for (const item of message.content) {
|
|
96
|
-
if (item.type === "image") images.push(`data:${item.mimeType};base64,${item.data}`);
|
|
97
|
-
}
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (message.role !== "assistant") continue;
|
|
101
|
-
for (const block of message.content as unknown[]) {
|
|
102
|
-
if (!block || typeof block !== "object") continue;
|
|
103
|
-
const candidate = block as { type?: unknown; item?: { result?: unknown } };
|
|
104
|
-
if (candidate.type === "image_generation_call" && typeof candidate.item?.result === "string" && candidate.item.result) {
|
|
105
|
-
images.push(`data:image/png;base64,${candidate.item.result}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (images.length < count) throw new Error(`Requested the last ${count} conversation images, but only ${images.length} were available.`);
|
|
110
|
-
return images.slice(-count).map((image_url) => ({ image_url }));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export async function standaloneImageGeneration(
|
|
114
|
-
input: ImageGenerationInput,
|
|
115
|
-
ctx: ImageGenerationToolContext,
|
|
116
|
-
settings: ResolvedCodexModelSettings,
|
|
117
|
-
signal?: AbortSignal,
|
|
118
|
-
invocation: StandaloneImageGenerationInvocation = {},
|
|
119
|
-
) {
|
|
120
|
-
signal?.throwIfAborted();
|
|
121
|
-
const callId = invocation.callId ?? "standalone";
|
|
122
|
-
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
123
|
-
const turnId = invocation.turnId ?? currentCodexTurn(sessionId)?.turnId ?? uuidV7();
|
|
124
|
-
const model = ctx.model;
|
|
125
|
-
if (!model || !ctx.modelRegistry) throw new Error("No active model is available for standalone image generation.");
|
|
126
|
-
if (!settings.enabled) throw new Error("pi-codex-minimal-tools is disabled.");
|
|
127
|
-
if (!settings.imageGeneration) throw new Error("Image generation is disabled by the global setting or current model profile.");
|
|
128
|
-
if (settings.imageGenerationImplementation !== "standalone") throw new Error("Image generation requires a standalone model profile.");
|
|
129
|
-
if (!input.prompt?.trim()) throw new Error("A prompt is required for standalone image generation.");
|
|
130
|
-
if (input.transparent_background !== undefined && typeof input.transparent_background !== "boolean") {
|
|
131
|
-
throw new Error("transparent_background must be a boolean.");
|
|
132
|
-
}
|
|
133
|
-
if (input.num_last_images_to_include != null && input.referenced_image_paths?.length) {
|
|
134
|
-
throw new Error("Provide only one of referenced_image_paths or num_last_images_to_include.");
|
|
135
|
-
}
|
|
136
|
-
// Capture the invocation's images and turn before auth can yield to another turn.
|
|
137
|
-
const images = input.num_last_images_to_include != null
|
|
138
|
-
? recentConversationImageUrls(ctx, input.num_last_images_to_include)
|
|
139
|
-
: await referencedImageUrls(ctx.cwd, input.referenced_image_paths ?? []);
|
|
140
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
141
|
-
signal?.throwIfAborted();
|
|
142
|
-
if (!auth.ok) throw new Error(auth.error);
|
|
143
|
-
const requestModel = withResolvedAuthBaseUrl(model, auth);
|
|
144
|
-
rememberResolvedEndpoint(model, requestModel, sessionId);
|
|
145
|
-
requireEndpointCapability(settings.endpoint_config, requestModel, sessionId, "imageGeneration.standalone");
|
|
146
|
-
if (!hasCodexRequestAuth({ modelHeaders: model.headers, auth })) {
|
|
147
|
-
throw new Error(`No request authentication for provider: ${model.provider}`);
|
|
148
|
-
}
|
|
149
|
-
const response = await fetchCodexJson(
|
|
150
|
-
resolveCodexApiEndpoint(auth.baseUrl ?? model.baseUrl, settings.responsesEndpoint, images.length ? "images/edits" : "images/generations"),
|
|
151
|
-
{
|
|
152
|
-
headers: buildCodexJsonHeaders({
|
|
153
|
-
codexRequestExtensions: settings.codexRequestExtensions,
|
|
154
|
-
modelHeaders: model.headers, auth, endpoint: settings.responsesEndpoint,
|
|
155
|
-
extraHeaders: settings.codexRequestExtensions ? { "x-codex-image-turn-id": turnId } : undefined,
|
|
156
|
-
}),
|
|
157
|
-
body: JSON.stringify({
|
|
158
|
-
model: settings.imageModel, prompt: input.prompt,
|
|
159
|
-
...(images.length ? { images } : {}),
|
|
160
|
-
background: input.transparent_background === true ? "transparent" : "opaque",
|
|
161
|
-
quality: "auto", size: "auto",
|
|
162
|
-
}),
|
|
163
|
-
signal,
|
|
164
|
-
},
|
|
165
|
-
["imageGeneration.standalone"],
|
|
166
|
-
);
|
|
167
|
-
signal?.throwIfAborted();
|
|
168
|
-
await checkEndpointResponse(response, requestModel, sessionId, ["imageGeneration.standalone"], signal);
|
|
169
|
-
const result = await response.json() as { data?: Array<{ b64_json?: string; generation_id?: string }> };
|
|
170
|
-
signal?.throwIfAborted();
|
|
171
|
-
const base64 = result.data?.[0]?.b64_json;
|
|
172
|
-
if (!base64) throw new Error("Standalone image generation returned no image data.");
|
|
173
|
-
const saved = await saveBase64Image({ base64, callId, cwd: ctx.cwd, format: "png", responseId: settings.imageModel, settings });
|
|
174
|
-
signal?.throwIfAborted();
|
|
175
|
-
return {
|
|
176
|
-
content: [
|
|
177
|
-
{ type: "image", data: base64, mimeType: "image/png" },
|
|
178
|
-
{ type: "text", text: `Generated image with ${settings.imageModel}; saved to ${saved.path}${saved.latestPath ? ` (latest: ${saved.latestPath})` : ""}.` },
|
|
179
|
-
],
|
|
180
|
-
details: {
|
|
181
|
-
saved, mode: "standalone-images-api",
|
|
182
|
-
requestId: response.headers.get("x-codex-imagegen-request-id") ?? undefined,
|
|
183
|
-
generationId: result.data?.[0]?.generation_id,
|
|
184
|
-
},
|
|
185
|
-
structuredContent: imageGenerationOutput(saved, base64),
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function createImageGenerationToolDefinition(options: {
|
|
190
|
-
loadSettings?: (cwd: string, model?: Model<Api>) => CodexMinimalToolsSettings | ResolvedCodexModelSettings;
|
|
191
|
-
getCurrentTurnId?: (sessionId: string | undefined) => string | undefined;
|
|
192
|
-
} = {}) {
|
|
193
|
-
return {
|
|
194
|
-
name: "image_generation",
|
|
195
|
-
label: "Image Generation",
|
|
196
|
-
description: "Generate or edit images using the standalone Images API selected by the current model profile. Results are saved under imageOutputDir and mirrored to latest.png.",
|
|
197
|
-
promptSnippet: "Generate or edit images with the implementation selected by the current model profile.",
|
|
198
|
-
parameters: imageGenerationToolSchema,
|
|
199
|
-
outputSchema: imageGenerationOutputSchema,
|
|
200
|
-
async execute(toolCallId: string, params: ImageGenerationInput, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ImageGenerationToolContext) {
|
|
201
|
-
const cwd = ctx?.cwd ?? process.cwd();
|
|
202
|
-
const profileModel = ctx.model ? { ...ctx.model, baseUrl: "" } : undefined;
|
|
203
|
-
const settings = options.loadSettings?.(cwd, profileModel) ?? loadModelSettings(profileModel, cwd);
|
|
204
|
-
const resolved = "modelProfile" in settings ? settings as ResolvedCodexModelSettings : loadModelSettings(profileModel, cwd, settings);
|
|
205
|
-
return standaloneImageGeneration(params, { ...ctx, cwd }, resolved, signal, {
|
|
206
|
-
callId: toolCallId, turnId: options.getCurrentTurnId?.(ctx.sessionManager?.getSessionId?.()),
|
|
207
|
-
});
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
}
|
package/src/utils/images.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
-
import type { CodexMinimalToolsSettings } from "@oai404iao/pi-codex-runtime/internal/settings";
|
|
6
|
-
|
|
7
|
-
export interface SavedImageInfo {
|
|
8
|
-
path: string;
|
|
9
|
-
latestPath?: string;
|
|
10
|
-
mimeType: string;
|
|
11
|
-
format: string;
|
|
12
|
-
bytes: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function projectRoot(cwd: string): string {
|
|
16
|
-
let current = resolve(cwd);
|
|
17
|
-
while (true) {
|
|
18
|
-
if (existsSync(join(current, ".git")) || existsSync(join(current, ".pi"))) return current;
|
|
19
|
-
const parent = dirname(current);
|
|
20
|
-
if (parent === current) return resolve(cwd);
|
|
21
|
-
current = parent;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function imageOutputDir(cwd: string, settings: Pick<CodexMinimalToolsSettings, "imageOutputDir">): string {
|
|
26
|
-
const configured = settings.imageOutputDir || ".pi/openai-codex-images";
|
|
27
|
-
return isAbsolute(configured) ? resolve(configured) : resolve(projectRoot(cwd), configured);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function imageFormatToMime(format: string | undefined): string {
|
|
31
|
-
const normalized = (format || "png").toLowerCase().replace(/^\./, "");
|
|
32
|
-
if (normalized === "jpg" || normalized === "jpeg") return "image/jpeg";
|
|
33
|
-
if (normalized === "webp") return "image/webp";
|
|
34
|
-
return "image/png";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function extensionForMime(mimeType: string): string {
|
|
38
|
-
if (mimeType === "image/jpeg") return "jpeg";
|
|
39
|
-
if (mimeType === "image/webp") return "webp";
|
|
40
|
-
return "png";
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function inferImageFormat(value: unknown, fallback = "png"): string {
|
|
44
|
-
if (typeof value !== "string") return fallback;
|
|
45
|
-
const lower = value.toLowerCase();
|
|
46
|
-
if (lower.includes("jpeg") || lower.includes("jpg")) return "jpeg";
|
|
47
|
-
if (lower.includes("webp")) return "webp";
|
|
48
|
-
if (lower.includes("png")) return "png";
|
|
49
|
-
const ext = extname(lower).replace(/^\./, "");
|
|
50
|
-
return ext || fallback;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function saveBase64Image(options: {
|
|
54
|
-
base64: string;
|
|
55
|
-
callId?: string;
|
|
56
|
-
cwd: string;
|
|
57
|
-
format?: string;
|
|
58
|
-
responseId?: string;
|
|
59
|
-
settings: Pick<CodexMinimalToolsSettings, "imageOutputDir">;
|
|
60
|
-
}): Promise<SavedImageInfo> {
|
|
61
|
-
const format = inferImageFormat(options.format, "png");
|
|
62
|
-
const mimeType = imageFormatToMime(format);
|
|
63
|
-
const ext = extensionForMime(mimeType);
|
|
64
|
-
const dir = imageOutputDir(options.cwd, options.settings);
|
|
65
|
-
await mkdir(dir, { recursive: true });
|
|
66
|
-
const unique = randomUUID().slice(0, 8);
|
|
67
|
-
const filePath = join(dir, `${new Date().toISOString().replace(/[:.]/g, "-")}-${unique}.${ext}`);
|
|
68
|
-
const data = Buffer.from(options.base64, "base64");
|
|
69
|
-
await writeFile(filePath, data, { mode: 0o600 });
|
|
70
|
-
const latestPath = join(dir, `latest.${ext}`);
|
|
71
|
-
await copyFile(filePath, latestPath);
|
|
72
|
-
return { bytes: data.byteLength, format: ext, latestPath, mimeType, path: filePath };
|
|
73
|
-
}
|