@pasko70/pibo 1.4.3 → 1.4.5
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/dist/apps/chat-ui/assets/{dist-BAXNalar.js → dist-85Cc5Hut.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DnACFKyO.js → dist-B8jspmzT.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CiDSXgtg.js → dist-BGqK-7Ep.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-C3PnEkhb.js → dist-BtF63vik.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-vKlxFkTa.js → dist-CQXtvBTs.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-ZB1-ui2y.js → dist-D1Laodgo.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CYPL-B2Z.js → dist-DE8W5WKg.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BwUvs6Ph.js → dist-DcME5mJj.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BAGS_xkV.js → dist-eG89IJAV.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-C0zsJ8II.js → dist-ecTM1pdv.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-B2BEpL7n.js → dist-oLAGkW6G.js} +1 -1
- package/dist/apps/chat-ui/assets/{index-0x7tuTNX.js → index-D4uifikB.js} +17 -17
- package/dist/apps/chat-ui/index.html +1 -1
- package/dist/apps/chat-vscode-web/assets/{index-C3GTPyDo.js → index-CRUSv6iR.js} +3 -3
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/{pibo-vscode-ext-1.4.2.vsix → pibo-vscode-ext-1.4.5.vsix} +0 -0
- package/dist/plugins/codex-compat.js +2 -0
- package/dist/providers/glm.js +1 -1
- package/dist/session-ui/terminalRows.js +55 -8
- package/dist/tools/codex-image-generation.js +272 -0
- package/package.json +1 -1
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.4.3.vsix +0 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<meta name="theme-color" content="#101d22" />
|
|
7
7
|
<title>Pibo</title>
|
|
8
|
-
<script type="module" crossorigin src="/apps/chat-vscode/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/apps/chat-vscode/assets/index-CRUSv6iR.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-B5QK07zO.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
Binary file
|
package/dist/apps/vscode-artifacts/{pibo-vscode-ext-1.4.2.vsix → pibo-vscode-ext-1.4.5.vsix}
RENAMED
|
Binary file
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dirname, resolve } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { createCodexImageGenerationToolProfile } from "../tools/codex-image-generation.js";
|
|
3
4
|
import { definePiboPlugin } from "./registry.js";
|
|
4
5
|
const CODEX_COMPAT_REGISTERED_TOOL_NAMES = [
|
|
5
6
|
"apply_patch",
|
|
@@ -22,6 +23,7 @@ export const piboCodexCompatPlugin = definePiboPlugin({
|
|
|
22
23
|
description: toolDescriptions[name],
|
|
23
24
|
});
|
|
24
25
|
}
|
|
26
|
+
api.registerTool(createCodexImageGenerationToolProfile());
|
|
25
27
|
api.registerContextFile({
|
|
26
28
|
key: CODEX_BASE_PROMPT_CONTEXT_FILE_KEY,
|
|
27
29
|
label: "Codex Base Prompt",
|
package/dist/providers/glm.js
CHANGED
|
@@ -128,7 +128,7 @@ function createToolRowCandidate(node, turnId) {
|
|
|
128
128
|
const image = classifyImageTool(node);
|
|
129
129
|
if (image) {
|
|
130
130
|
const row = createImageToolRow(node, image);
|
|
131
|
-
return { row, turnId, image: image.detail };
|
|
131
|
+
return { row, turnId, image: image.groupable ? image.detail : undefined };
|
|
132
132
|
}
|
|
133
133
|
const preview = previewLines(node.error ?? node.output, COMPACT_TERMINAL_OUTPUT_PREVIEW_LINES, node.error ? "red" : "dim");
|
|
134
134
|
const row = {
|
|
@@ -159,7 +159,7 @@ function createToolRowCandidate(node, turnId) {
|
|
|
159
159
|
}
|
|
160
160
|
function createImageToolRow(node, image) {
|
|
161
161
|
const status = mapStatus(node.status);
|
|
162
|
-
const detailLabel = image.path ? `Path: ${image.path}` : image.query ? `Query: ${image.query}` : image.mimeType ? `Type: ${image.mimeType}` : "Image content returned";
|
|
162
|
+
const detailLabel = image.path ? `Path: ${image.path}` : image.artifactId ? `Artifact: ${image.artifactId}` : image.query ? `Query: ${image.query}` : image.mimeType ? `Type: ${image.mimeType}` : "Image content returned";
|
|
163
163
|
return {
|
|
164
164
|
id: node.id,
|
|
165
165
|
kind: "tool.image",
|
|
@@ -168,7 +168,7 @@ function createImageToolRow(node, image) {
|
|
|
168
168
|
lines: [
|
|
169
169
|
{
|
|
170
170
|
prefix: "bullet",
|
|
171
|
-
tokens: [token(
|
|
171
|
+
tokens: [token(image.verb, toneForStatus(node.status), "semibold")],
|
|
172
172
|
},
|
|
173
173
|
{
|
|
174
174
|
prefix: "detail",
|
|
@@ -186,7 +186,7 @@ function createToolResultRowCandidate(node, turnId) {
|
|
|
186
186
|
const image = classifyImageTool(node);
|
|
187
187
|
if (image) {
|
|
188
188
|
const row = createImageToolRow(node, image);
|
|
189
|
-
return { row, turnId, image: image.detail };
|
|
189
|
+
return { row, turnId, image: image.groupable ? image.detail : undefined };
|
|
190
190
|
}
|
|
191
191
|
return { row: createToolResultRow(node), turnId };
|
|
192
192
|
}
|
|
@@ -640,32 +640,48 @@ function detailItemsForGroup(candidates, kind) {
|
|
|
640
640
|
function classifyImageTool(node) {
|
|
641
641
|
const normalized = (node.title ?? "").trim().toLowerCase();
|
|
642
642
|
const args = isRecord(node.input) ? node.input : undefined;
|
|
643
|
-
const
|
|
643
|
+
const output = isRecord(node.output) ? node.output : undefined;
|
|
644
|
+
const details = recordField(output, "details");
|
|
645
|
+
const codexOperation = normalized === "codex_image_generation" ? codexImageOperation(details) ?? codexImageOperationFromInput(args) : undefined;
|
|
646
|
+
const path = codexOperation
|
|
647
|
+
? stringValue(details?.savedPath) ?? previewPath(details)
|
|
648
|
+
: previewPath(args) ?? previewPath(details) ?? previewPath(output);
|
|
649
|
+
const artifactId = codexOperation ? stringValue(details?.artifactId) : undefined;
|
|
644
650
|
const query = previewQuery(args);
|
|
645
651
|
const images = collectImagePayloads(node.output);
|
|
646
652
|
const isImageTool = matchesTool(normalized, ["view_image", "image", "screenshot"]);
|
|
647
|
-
if (!images.length && !isImageTool)
|
|
653
|
+
if (!images.length && !isImageTool && !codexOperation)
|
|
648
654
|
return undefined;
|
|
649
655
|
const mimeType = images.map((image) => image.mimeType).find((value) => Boolean(value));
|
|
650
656
|
const count = Math.max(1, images.length);
|
|
651
657
|
const summary = {
|
|
652
658
|
type: images.length > 1 ? "images" : images.length === 1 ? "image" : "image_reference",
|
|
659
|
+
toolName: codexOperation ? node.title : undefined,
|
|
660
|
+
operation: codexOperation,
|
|
653
661
|
path,
|
|
662
|
+
savedPath: codexOperation ? path : undefined,
|
|
663
|
+
artifactId,
|
|
664
|
+
model: codexOperation ? stringValue(details?.model) : undefined,
|
|
665
|
+
referencedImageCount: codexOperation ? numberValue(details?.referencedImageCount) : undefined,
|
|
654
666
|
query,
|
|
655
667
|
mimeType,
|
|
656
668
|
count: count > 1 ? count : undefined,
|
|
657
669
|
detail: images.length ? "Image data hidden in terminal view." : "Image path only; binary data is hidden in terminal view.",
|
|
658
670
|
};
|
|
659
|
-
const labelTarget = path ?? query ?? mimeType ?? "image";
|
|
671
|
+
const labelTarget = path ?? artifactId ?? query ?? mimeType ?? "image";
|
|
672
|
+
const verb = codexOperation ? codexImageVerb(node.status, codexOperation) : imageToolVerb(node.status, count);
|
|
660
673
|
return {
|
|
661
674
|
count,
|
|
662
675
|
path,
|
|
676
|
+
artifactId,
|
|
663
677
|
query,
|
|
664
678
|
mimeType,
|
|
679
|
+
verb,
|
|
680
|
+
groupable: !codexOperation,
|
|
665
681
|
summary,
|
|
666
682
|
detail: {
|
|
667
683
|
id: node.id,
|
|
668
|
-
label: `${
|
|
684
|
+
label: `${verb} ${labelTarget}`,
|
|
669
685
|
status: mapStatus(node.status),
|
|
670
686
|
input: sanitizeImagePayload(node.input),
|
|
671
687
|
output: summary,
|
|
@@ -725,6 +741,34 @@ function imageToolVerb(status, count) {
|
|
|
725
741
|
return count > 1 ? "Image reads failed" : "Image read failed";
|
|
726
742
|
return count > 1 ? "Viewed images" : "Viewed image";
|
|
727
743
|
}
|
|
744
|
+
function codexImageOperation(details) {
|
|
745
|
+
const operation = stringValue(details?.operation);
|
|
746
|
+
return operation === "generate" || operation === "edit" ? operation : undefined;
|
|
747
|
+
}
|
|
748
|
+
function codexImageOperationFromInput(args) {
|
|
749
|
+
if (!args)
|
|
750
|
+
return undefined;
|
|
751
|
+
const referencedPaths = args.referenced_image_paths;
|
|
752
|
+
if (Array.isArray(referencedPaths) && referencedPaths.length > 0)
|
|
753
|
+
return "edit";
|
|
754
|
+
if (numberValue(args.num_last_images_to_include) !== undefined)
|
|
755
|
+
return "edit";
|
|
756
|
+
return "generate";
|
|
757
|
+
}
|
|
758
|
+
function codexImageVerb(status, operation) {
|
|
759
|
+
if (operation === "edit") {
|
|
760
|
+
if (status === "running")
|
|
761
|
+
return "Editing image";
|
|
762
|
+
if (status === "error")
|
|
763
|
+
return "Image edit failed";
|
|
764
|
+
return "Edited image";
|
|
765
|
+
}
|
|
766
|
+
if (status === "running")
|
|
767
|
+
return "Generating image";
|
|
768
|
+
if (status === "error")
|
|
769
|
+
return "Image generation failed";
|
|
770
|
+
return "Generated image";
|
|
771
|
+
}
|
|
728
772
|
function recordField(record, key) {
|
|
729
773
|
const value = record?.[key];
|
|
730
774
|
return isRecord(value) ? value : undefined;
|
|
@@ -789,6 +833,9 @@ function previewQuery(value) {
|
|
|
789
833
|
}
|
|
790
834
|
return undefined;
|
|
791
835
|
}
|
|
836
|
+
function numberValue(value) {
|
|
837
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
838
|
+
}
|
|
792
839
|
function previewLines(value, maxVisibleLines, tone = "dim", _maxLineLength = 160) {
|
|
793
840
|
const empty = { lines: [], visibleLineCount: 0, omittedLineCount: 0, totalLineCount: 0, maxVisibleLineCount: maxVisibleLines };
|
|
794
841
|
const text = previewText(value);
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { platform, release, arch } from "node:os";
|
|
3
|
+
import { dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { Type } from "@mariozechner/pi-ai";
|
|
6
|
+
import { defineTool } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import { getPiboHome } from "../core/pibo-home.js";
|
|
8
|
+
const OPENAI_CODEX_PROVIDER = "openai-codex";
|
|
9
|
+
const DEFAULT_CODEX_BACKEND_BASE_URL = "https://chatgpt.com/backend-api";
|
|
10
|
+
const CODEX_IMAGE_MODEL = "gpt-image-2";
|
|
11
|
+
const MAX_EDIT_IMAGES = 5;
|
|
12
|
+
const OPENAI_JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
|
13
|
+
function trimTrailingSlashes(value) {
|
|
14
|
+
return value.replace(/\/+$/, "");
|
|
15
|
+
}
|
|
16
|
+
export function resolveCodexImageUrl(operation, baseUrl) {
|
|
17
|
+
const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_CODEX_BACKEND_BASE_URL;
|
|
18
|
+
let normalized = trimTrailingSlashes(raw.trim());
|
|
19
|
+
if (normalized.endsWith("/codex/responses"))
|
|
20
|
+
normalized = normalized.slice(0, -"/responses".length);
|
|
21
|
+
if (!normalized.endsWith("/codex"))
|
|
22
|
+
normalized = `${normalized}/codex`;
|
|
23
|
+
return `${normalized}/images/${operation}`;
|
|
24
|
+
}
|
|
25
|
+
function decodeJwtPayload(token) {
|
|
26
|
+
try {
|
|
27
|
+
const payload = token.split(".")[1];
|
|
28
|
+
if (!payload)
|
|
29
|
+
return undefined;
|
|
30
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
31
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
32
|
+
const parsed = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
33
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function getOpenAiAccountId(accessToken, storedAccountId) {
|
|
40
|
+
if (typeof storedAccountId === "string" && storedAccountId.trim().length > 0)
|
|
41
|
+
return storedAccountId;
|
|
42
|
+
const payload = decodeJwtPayload(accessToken);
|
|
43
|
+
const auth = payload?.[OPENAI_JWT_CLAIM_PATH];
|
|
44
|
+
if (!auth || typeof auth !== "object" || Array.isArray(auth))
|
|
45
|
+
return undefined;
|
|
46
|
+
const accountId = auth.chatgpt_account_id;
|
|
47
|
+
return typeof accountId === "string" && accountId.trim().length > 0 ? accountId : undefined;
|
|
48
|
+
}
|
|
49
|
+
async function getCodexImageAuth() {
|
|
50
|
+
const authStorage = AuthStorage.create();
|
|
51
|
+
const credential = authStorage.get(OPENAI_CODEX_PROVIDER);
|
|
52
|
+
if (credential?.type !== "oauth") {
|
|
53
|
+
throw new Error("codex_image_generation requires ChatGPT/Codex OAuth login for provider openai-codex. Use the existing OpenAI Codex login flow; API keys are not supported for this tool.");
|
|
54
|
+
}
|
|
55
|
+
const accessToken = await authStorage.getApiKey(OPENAI_CODEX_PROVIDER, { includeFallback: false });
|
|
56
|
+
if (!accessToken) {
|
|
57
|
+
throw new Error("codex_image_generation could not load a ChatGPT/Codex OAuth access token for provider openai-codex. Please log in again with the OpenAI Codex login flow.");
|
|
58
|
+
}
|
|
59
|
+
const accountId = getOpenAiAccountId(accessToken, credential.accountId);
|
|
60
|
+
if (!accountId) {
|
|
61
|
+
throw new Error("codex_image_generation could not resolve the ChatGPT account id from the openai-codex OAuth credential. Please log in again with the OpenAI Codex login flow.");
|
|
62
|
+
}
|
|
63
|
+
return { accessToken, accountId };
|
|
64
|
+
}
|
|
65
|
+
function mimeTypeForPath(path) {
|
|
66
|
+
switch (extname(path).toLowerCase()) {
|
|
67
|
+
case ".jpg":
|
|
68
|
+
case ".jpeg":
|
|
69
|
+
return "image/jpeg";
|
|
70
|
+
case ".webp":
|
|
71
|
+
return "image/webp";
|
|
72
|
+
case ".gif":
|
|
73
|
+
return "image/gif";
|
|
74
|
+
default:
|
|
75
|
+
return "image/png";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function resolveCwd(baseCwd, path) {
|
|
79
|
+
return isAbsolute(path) ? path : resolve(baseCwd, path);
|
|
80
|
+
}
|
|
81
|
+
function isImageContent(value) {
|
|
82
|
+
return Boolean(value)
|
|
83
|
+
&& typeof value === "object"
|
|
84
|
+
&& value.type === "image"
|
|
85
|
+
&& typeof value.data === "string"
|
|
86
|
+
&& typeof value.mimeType === "string";
|
|
87
|
+
}
|
|
88
|
+
function imageContentToDataUrl(image) {
|
|
89
|
+
if (image.data.startsWith("data:"))
|
|
90
|
+
return image.data;
|
|
91
|
+
return `data:${image.mimeType};base64,${image.data}`;
|
|
92
|
+
}
|
|
93
|
+
async function imageFileToDataUrl(cwd, path) {
|
|
94
|
+
const resolved = resolveCwd(cwd, path);
|
|
95
|
+
const data = await readFile(resolved);
|
|
96
|
+
return `data:${mimeTypeForPath(resolved)};base64,${data.toString("base64")}`;
|
|
97
|
+
}
|
|
98
|
+
function contentFromSessionEntry(entry) {
|
|
99
|
+
if (!entry || typeof entry !== "object")
|
|
100
|
+
return undefined;
|
|
101
|
+
const record = entry;
|
|
102
|
+
if (record.type === "message") {
|
|
103
|
+
const message = record.message;
|
|
104
|
+
return message && typeof message === "object" ? message.content : undefined;
|
|
105
|
+
}
|
|
106
|
+
if (record.type === "custom_message")
|
|
107
|
+
return record.content;
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
function recentImagesFromSessionEntries(entries, count) {
|
|
111
|
+
const images = [];
|
|
112
|
+
for (const entry of [...entries].reverse()) {
|
|
113
|
+
const content = contentFromSessionEntry(entry);
|
|
114
|
+
if (!Array.isArray(content))
|
|
115
|
+
continue;
|
|
116
|
+
for (const item of [...content].reverse()) {
|
|
117
|
+
if (!isImageContent(item))
|
|
118
|
+
continue;
|
|
119
|
+
images.push(imageContentToDataUrl(item));
|
|
120
|
+
if (images.length === count)
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
if (images.length === count)
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
images.reverse();
|
|
127
|
+
return images.map((image_url) => ({ image_url }));
|
|
128
|
+
}
|
|
129
|
+
function validateImageArgs(params) {
|
|
130
|
+
if (params.prompt.trim().length === 0)
|
|
131
|
+
throw new Error("`prompt` must not be empty.");
|
|
132
|
+
const pathCount = params.referenced_image_paths?.length ?? 0;
|
|
133
|
+
if (pathCount > MAX_EDIT_IMAGES)
|
|
134
|
+
throw new Error(`\`referenced_image_paths\` must contain at most ${MAX_EDIT_IMAGES} paths.`);
|
|
135
|
+
if (pathCount > 0 && params.num_last_images_to_include !== undefined) {
|
|
136
|
+
throw new Error("Provide only one of `referenced_image_paths` or `num_last_images_to_include`.");
|
|
137
|
+
}
|
|
138
|
+
if (params.num_last_images_to_include !== undefined) {
|
|
139
|
+
if (!Number.isInteger(params.num_last_images_to_include) || params.num_last_images_to_include < 1 || params.num_last_images_to_include > MAX_EDIT_IMAGES) {
|
|
140
|
+
throw new Error(`\`num_last_images_to_include\` must be an integer between 1 and ${MAX_EDIT_IMAGES}.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function sanitizePathPart(value) {
|
|
145
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 120) || "unknown";
|
|
146
|
+
}
|
|
147
|
+
function artifactPath(sessionId, toolCallId) {
|
|
148
|
+
const safeSessionId = sanitizePathPart(sessionId?.trim() || "local");
|
|
149
|
+
const safeToolCallId = sanitizePathPart(toolCallId || `image_${Date.now()}`);
|
|
150
|
+
const artifactId = `${safeSessionId}/${safeToolCallId}.png`;
|
|
151
|
+
return {
|
|
152
|
+
artifactId,
|
|
153
|
+
savedPath: join(getPiboHome(), "generated_images", safeSessionId, `${safeToolCallId}.png`),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async function saveGeneratedImage(sessionId, toolCallId, b64Json) {
|
|
157
|
+
const target = artifactPath(sessionId, toolCallId);
|
|
158
|
+
await mkdir(dirname(target.savedPath), { recursive: true });
|
|
159
|
+
await writeFile(target.savedPath, Buffer.from(b64Json.trim(), "base64"));
|
|
160
|
+
return target;
|
|
161
|
+
}
|
|
162
|
+
function truncateErrorBody(text) {
|
|
163
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
164
|
+
return compact.length > 1000 ? `${compact.slice(0, 1000)}…` : compact;
|
|
165
|
+
}
|
|
166
|
+
async function postCodexImageRequest(operation, body, auth, signal, baseUrl) {
|
|
167
|
+
const url = resolveCodexImageUrl(operation, baseUrl);
|
|
168
|
+
const response = await fetch(url, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
signal,
|
|
171
|
+
headers: {
|
|
172
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
173
|
+
"chatgpt-account-id": auth.accountId,
|
|
174
|
+
originator: "pi",
|
|
175
|
+
"User-Agent": `pibo (${platform()} ${release()}; ${arch()})`,
|
|
176
|
+
Accept: "application/json",
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify(body),
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
const text = await response.text().catch(() => "");
|
|
183
|
+
throw new Error(`Codex image ${operation} request failed: ${response.status}${text ? ` ${truncateErrorBody(text)}` : ""}`);
|
|
184
|
+
}
|
|
185
|
+
return await response.json();
|
|
186
|
+
}
|
|
187
|
+
function firstImageB64(response) {
|
|
188
|
+
const b64 = response.data?.[0]?.b64_json;
|
|
189
|
+
if (!b64 || typeof b64 !== "string")
|
|
190
|
+
throw new Error("Codex image generation returned no image data.");
|
|
191
|
+
return b64;
|
|
192
|
+
}
|
|
193
|
+
function createRequest(prompt, images) {
|
|
194
|
+
return {
|
|
195
|
+
...(images && images.length > 0 ? { images } : {}),
|
|
196
|
+
prompt,
|
|
197
|
+
background: "auto",
|
|
198
|
+
model: CODEX_IMAGE_MODEL,
|
|
199
|
+
quality: "auto",
|
|
200
|
+
size: "auto",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function collectEditImages(params, cwd, sessionEntries) {
|
|
204
|
+
const paths = params.referenced_image_paths ?? [];
|
|
205
|
+
if (paths.length > 0) {
|
|
206
|
+
return await Promise.all(paths.map(async (path) => ({ image_url: await imageFileToDataUrl(cwd, path) })));
|
|
207
|
+
}
|
|
208
|
+
if (params.num_last_images_to_include !== undefined) {
|
|
209
|
+
const images = recentImagesFromSessionEntries(sessionEntries, params.num_last_images_to_include);
|
|
210
|
+
if (images.length !== params.num_last_images_to_include) {
|
|
211
|
+
throw new Error(`Requested the last ${params.num_last_images_to_include} conversation images, but only ${images.length} were available.`);
|
|
212
|
+
}
|
|
213
|
+
return images;
|
|
214
|
+
}
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
export function createCodexImageGenerationToolDefinition(context = {}, options = {}) {
|
|
218
|
+
return defineTool({
|
|
219
|
+
name: "codex_image_generation",
|
|
220
|
+
label: "Codex Image Generation",
|
|
221
|
+
description: "Generates or edits images through the ChatGPT/Codex backend API using openai-codex OAuth entitlement. Does not use the public OpenAI Images API.",
|
|
222
|
+
promptSnippet: "Use codex_image_generation to create an image from a prompt, or to edit referenced/recent images with the Codex/ChatGPT image backend.",
|
|
223
|
+
promptGuidelines: [
|
|
224
|
+
"Use codex_image_generation for image generation and image edits when the user asks for pictures, visual variants, or edits to existing images.",
|
|
225
|
+
"For edits, pass either referenced_image_paths for local image files or num_last_images_to_include for recent conversation images, but not both.",
|
|
226
|
+
],
|
|
227
|
+
executionMode: "sequential",
|
|
228
|
+
parameters: Type.Object({
|
|
229
|
+
prompt: Type.String({ description: "Image generation/editing prompt. Be specific about the desired final image." }),
|
|
230
|
+
referenced_image_paths: Type.Optional(Type.Array(Type.String({ description: "Local filesystem image path to edit." }), { description: `Optional local image paths to edit. At most ${MAX_EDIT_IMAGES}.` })),
|
|
231
|
+
num_last_images_to_include: Type.Optional(Type.Number({ description: `Use the last N conversation images as edit references. Must be between 1 and ${MAX_EDIT_IMAGES}.` })),
|
|
232
|
+
}),
|
|
233
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
234
|
+
validateImageArgs(params);
|
|
235
|
+
const images = await collectEditImages(params, ctx.cwd, ctx.sessionManager.getBranch());
|
|
236
|
+
const operation = images.length > 0 ? "edit" : "generate";
|
|
237
|
+
const endpoint = operation === "edit" ? "edits" : "generations";
|
|
238
|
+
const auth = await getCodexImageAuth();
|
|
239
|
+
const response = await postCodexImageRequest(endpoint, createRequest(params.prompt, images), auth, signal, options.baseUrl);
|
|
240
|
+
const result = firstImageB64(response);
|
|
241
|
+
const saved = await saveGeneratedImage(context.piboSessionId, toolCallId, result);
|
|
242
|
+
const details = {
|
|
243
|
+
provider: OPENAI_CODEX_PROVIDER,
|
|
244
|
+
api: "codex-chatgpt-images",
|
|
245
|
+
operation,
|
|
246
|
+
model: CODEX_IMAGE_MODEL,
|
|
247
|
+
savedPath: saved.savedPath,
|
|
248
|
+
artifactId: saved.artifactId,
|
|
249
|
+
referencedImageCount: images.length,
|
|
250
|
+
endpoint,
|
|
251
|
+
created: response.created,
|
|
252
|
+
background: response.background,
|
|
253
|
+
quality: response.quality,
|
|
254
|
+
size: response.size,
|
|
255
|
+
};
|
|
256
|
+
return {
|
|
257
|
+
content: [
|
|
258
|
+
{ type: "image", data: result, mimeType: "image/png" },
|
|
259
|
+
{ type: "text", text: `Generated image saved to ${saved.savedPath}` },
|
|
260
|
+
],
|
|
261
|
+
details,
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
export function createCodexImageGenerationToolProfile() {
|
|
267
|
+
return {
|
|
268
|
+
name: "codex_image_generation",
|
|
269
|
+
description: "Generate and edit images through the ChatGPT/Codex backend API using openai-codex OAuth.",
|
|
270
|
+
createDefinition: createCodexImageGenerationToolDefinition,
|
|
271
|
+
};
|
|
272
|
+
}
|
package/package.json
CHANGED
|
Binary file
|