pi-better-openai 0.1.18 → 0.1.21
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 +90 -1
- package/index.ts +308 -1058
- package/package.json +9 -4
- package/src/codex-auth.ts +136 -0
- package/src/config.ts +297 -2
- package/src/fast-controller.ts +109 -0
- package/src/format.ts +81 -20
- package/src/image.ts +182 -200
- package/src/paths.ts +17 -0
- package/src/pet-footer-controller.ts +627 -0
- package/src/pets.ts +212 -63
- package/src/usage-controller.ts +299 -0
- package/src/usage.ts +99 -48
package/src/image.ts
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
5
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Box, Container, Image, Text } from "@earendil-works/pi-tui";
|
|
7
|
+
import sharp from "sharp";
|
|
6
8
|
import type { ResolvedConfig } from "./config.ts";
|
|
7
9
|
import { isRecord } from "./config.ts";
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
extractAccountIdFromJwt,
|
|
12
|
+
getCodexCredentials,
|
|
13
|
+
type CodexCredentialsWithSource,
|
|
14
|
+
} from "./codex-auth.ts";
|
|
15
|
+
import { maskIdentifier, sanitizeDiagnosticError } from "./format.ts";
|
|
16
|
+
import { piAgentDir, resolveUserPath } from "./paths.ts";
|
|
9
17
|
|
|
10
18
|
const OPENAI_IMAGE_TOOL = "openai_image";
|
|
11
19
|
const OPENAI_IMAGE_COMMAND = "openai-image";
|
|
12
20
|
const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
13
21
|
const DEFAULT_TIMEOUT_MS = 180_000;
|
|
22
|
+
const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
|
|
23
|
+
const MAX_IMAGE_INPUTS = 5;
|
|
24
|
+
const MAX_TOTAL_IMAGE_INPUT_BYTES = 50 * 1024 * 1024;
|
|
25
|
+
const SUPPORTED_INPUT_IMAGE_FORMATS = new Set(["png", "jpeg", "jpg", "webp", "gif"]);
|
|
26
|
+
const SSE_EVENT_BOUNDARY = /\r?\n\r?\n/;
|
|
14
27
|
|
|
15
28
|
export const IMAGE_SAVE_MODES = ["none", "project", "global", "custom"] as const;
|
|
16
29
|
export const IMAGE_ACTIONS = ["auto", "generate", "edit"] as const;
|
|
@@ -36,6 +49,7 @@ const TOOL_PARAMS = {
|
|
|
36
49
|
},
|
|
37
50
|
images: {
|
|
38
51
|
type: "array",
|
|
52
|
+
maxItems: MAX_IMAGE_INPUTS,
|
|
39
53
|
items: { type: "string" },
|
|
40
54
|
description: "Local image paths to use as edit targets or references.",
|
|
41
55
|
},
|
|
@@ -70,11 +84,7 @@ type ToolParams = {
|
|
|
70
84
|
saveDir?: string;
|
|
71
85
|
};
|
|
72
86
|
|
|
73
|
-
type CodexImageCredentials =
|
|
74
|
-
accessToken: string;
|
|
75
|
-
accountId: string;
|
|
76
|
-
source: "modelRegistry" | "authFile";
|
|
77
|
-
};
|
|
87
|
+
type CodexImageCredentials = CodexCredentialsWithSource;
|
|
78
88
|
|
|
79
89
|
type ImageInput = {
|
|
80
90
|
path: string;
|
|
@@ -112,65 +122,12 @@ export type ImageGenerationDebug = {
|
|
|
112
122
|
lastError?: string;
|
|
113
123
|
};
|
|
114
124
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
export function extractAccountIdFromJwt(token: string): string | undefined {
|
|
122
|
-
try {
|
|
123
|
-
const [, payload] = token.split(".");
|
|
124
|
-
if (!payload) return undefined;
|
|
125
|
-
const parsed = JSON.parse(decodeBase64Url(payload)) as unknown;
|
|
126
|
-
if (!isRecord(parsed)) return undefined;
|
|
127
|
-
const auth = parsed["https://api.openai.com/auth"];
|
|
128
|
-
if (!isRecord(auth)) return undefined;
|
|
129
|
-
const accountId = auth.chatgpt_account_id;
|
|
130
|
-
return typeof accountId === "string" && accountId.trim() ? accountId.trim() : undefined;
|
|
131
|
-
} catch {
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function parseRegistryCredentials(
|
|
137
|
-
raw: string | undefined,
|
|
138
|
-
): Omit<CodexImageCredentials, "source"> | undefined {
|
|
139
|
-
const value = raw?.trim();
|
|
140
|
-
if (!value) return undefined;
|
|
141
|
-
try {
|
|
142
|
-
const parsed = JSON.parse(value) as unknown;
|
|
143
|
-
if (isRecord(parsed)) {
|
|
144
|
-
const accessToken =
|
|
145
|
-
typeof parsed.access === "string"
|
|
146
|
-
? parsed.access
|
|
147
|
-
: typeof parsed.token === "string"
|
|
148
|
-
? parsed.token
|
|
149
|
-
: undefined;
|
|
150
|
-
const accountId =
|
|
151
|
-
typeof parsed.accountId === "string"
|
|
152
|
-
? parsed.accountId
|
|
153
|
-
: typeof parsed.account_id === "string"
|
|
154
|
-
? parsed.account_id
|
|
155
|
-
: undefined;
|
|
156
|
-
if (accessToken?.trim() && accountId?.trim())
|
|
157
|
-
return { accessToken: accessToken.trim(), accountId: accountId.trim() };
|
|
158
|
-
}
|
|
159
|
-
} catch {
|
|
160
|
-
// Plain bearer token is expected for openai-codex in pi.
|
|
161
|
-
}
|
|
162
|
-
const accountId = extractAccountIdFromJwt(value);
|
|
163
|
-
return accountId ? { accessToken: value, accountId } : undefined;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function getCredentials(ctx: ExtensionContext): Promise<CodexImageCredentials> {
|
|
167
|
-
const registryToken = await ctx.modelRegistry
|
|
168
|
-
.getApiKeyForProvider("openai-codex")
|
|
169
|
-
.catch(() => undefined);
|
|
170
|
-
const registryCredentials = parseRegistryCredentials(registryToken);
|
|
171
|
-
if (registryCredentials) return { ...registryCredentials, source: "modelRegistry" };
|
|
172
|
-
const auth = readCodexAuth();
|
|
173
|
-
if (auth) return { ...auth, source: "authFile" };
|
|
125
|
+
async function getCredentials(
|
|
126
|
+
ctx: ExtensionContext,
|
|
127
|
+
signal?: AbortSignal,
|
|
128
|
+
): Promise<CodexImageCredentials> {
|
|
129
|
+
const credentials = await getCodexCredentials(ctx, signal);
|
|
130
|
+
if (credentials) return credentials;
|
|
174
131
|
throw new Error("Missing openai-codex OAuth credentials. Run /login openai-codex.");
|
|
175
132
|
}
|
|
176
133
|
|
|
@@ -193,12 +150,14 @@ function resolveImageConfig(cfg: ResolvedConfig, params: ToolParams) {
|
|
|
193
150
|
}
|
|
194
151
|
|
|
195
152
|
function imageMimeType(path: string, outputFormat?: string): string {
|
|
153
|
+
if (outputFormat === "jpeg" || outputFormat === "jpg") return "image/jpeg";
|
|
154
|
+
if (outputFormat === "webp") return "image/webp";
|
|
155
|
+
if (outputFormat === "gif") return "image/gif";
|
|
156
|
+
if (outputFormat === "png") return "image/png";
|
|
196
157
|
const ext = extname(path).toLowerCase();
|
|
197
158
|
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
198
159
|
if (ext === ".webp") return "image/webp";
|
|
199
160
|
if (ext === ".gif") return "image/gif";
|
|
200
|
-
if (outputFormat === "jpeg") return "image/jpeg";
|
|
201
|
-
if (outputFormat === "webp") return "image/webp";
|
|
202
161
|
return "image/png";
|
|
203
162
|
}
|
|
204
163
|
|
|
@@ -206,16 +165,75 @@ function extensionForFormat(format: ImageOutputFormat): string {
|
|
|
206
165
|
return format === "jpeg" ? "jpg" : format;
|
|
207
166
|
}
|
|
208
167
|
|
|
168
|
+
function isInsideDirectory(root: string, child: string): boolean {
|
|
169
|
+
const normalizedRoot = resolve(root);
|
|
170
|
+
const normalizedChild = resolve(child);
|
|
171
|
+
return (
|
|
172
|
+
normalizedChild !== normalizedRoot && normalizedChild.startsWith(`${normalizedRoot}${sep}`)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function validateImageInput(
|
|
177
|
+
path: string,
|
|
178
|
+
realWorkspaceRoot: string,
|
|
179
|
+
): Promise<{ mimeType: string; path: string; size: number }> {
|
|
180
|
+
const realInputPath = await realpath(path).catch(() => undefined);
|
|
181
|
+
if (!realInputPath || !isInsideDirectory(realWorkspaceRoot, realInputPath))
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Image input must be a file inside the current workspace: ${displayPath(path)}`,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const pathStats = await stat(realInputPath).catch(() => undefined);
|
|
187
|
+
if (!pathStats?.isFile())
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Image input must be a file inside the current workspace: ${displayPath(path)}`,
|
|
190
|
+
);
|
|
191
|
+
if (pathStats.size > MAX_IMAGE_INPUT_BYTES)
|
|
192
|
+
throw new Error(`Image input is too large (max 20 MB): ${displayPath(path)}`);
|
|
193
|
+
|
|
194
|
+
const metadata = await sharp(realInputPath, { animated: false })
|
|
195
|
+
.metadata()
|
|
196
|
+
.catch(() => undefined);
|
|
197
|
+
if (!metadata?.format || !SUPPORTED_INPUT_IMAGE_FORMATS.has(metadata.format))
|
|
198
|
+
throw new Error(`Image input is not a readable image: ${displayPath(path)}`);
|
|
199
|
+
return {
|
|
200
|
+
mimeType: imageMimeType(path, metadata.format),
|
|
201
|
+
path: realInputPath,
|
|
202
|
+
size: pathStats.size,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
209
206
|
async function readImageInputs(paths: string[] | undefined, cwd: string): Promise<ImageInput[]> {
|
|
210
|
-
const
|
|
207
|
+
const validatedInputs: Array<{ path: string; mimeType: string }> = [];
|
|
208
|
+
const seenPaths = new Set<string>();
|
|
209
|
+
let totalBytes = 0;
|
|
210
|
+
const workspaceRoot = resolve(cwd);
|
|
211
|
+
let realWorkspaceRoot: string | undefined;
|
|
211
212
|
for (const rawPath of paths ?? []) {
|
|
212
213
|
const trimmed = rawPath.trim();
|
|
213
214
|
if (!trimmed) continue;
|
|
214
|
-
const path = isAbsolute(trimmed) ? trimmed : resolve(
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
const path = isAbsolute(trimmed) ? resolve(trimmed) : resolve(workspaceRoot, trimmed);
|
|
216
|
+
if (!isInsideDirectory(workspaceRoot, path))
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Image input must be a file inside the current workspace: ${displayPath(path)}`,
|
|
219
|
+
);
|
|
220
|
+
realWorkspaceRoot ??= await realpath(workspaceRoot).catch(() => workspaceRoot);
|
|
221
|
+
const input = await validateImageInput(path, realWorkspaceRoot);
|
|
222
|
+
if (seenPaths.has(input.path)) continue;
|
|
223
|
+
if (validatedInputs.length >= MAX_IMAGE_INPUTS)
|
|
224
|
+
throw new Error(`Too many image inputs (max ${MAX_IMAGE_INPUTS}).`);
|
|
225
|
+
totalBytes += input.size;
|
|
226
|
+
if (totalBytes > MAX_TOTAL_IMAGE_INPUT_BYTES)
|
|
227
|
+
throw new Error("Image inputs are too large in total (max 50 MB).");
|
|
228
|
+
seenPaths.add(input.path);
|
|
229
|
+
validatedInputs.push({ path: input.path, mimeType: input.mimeType });
|
|
217
230
|
}
|
|
218
|
-
return
|
|
231
|
+
return Promise.all(
|
|
232
|
+
validatedInputs.map(async (input) => ({
|
|
233
|
+
...input,
|
|
234
|
+
data: (await readFile(input.path)).toString("base64"),
|
|
235
|
+
})),
|
|
236
|
+
);
|
|
219
237
|
}
|
|
220
238
|
|
|
221
239
|
function resolveSaveDir(
|
|
@@ -225,14 +243,10 @@ function resolveSaveDir(
|
|
|
225
243
|
): string | undefined {
|
|
226
244
|
if (mode === "none") return undefined;
|
|
227
245
|
if (mode === "project") return join(cwd, ".pi", "generated-images");
|
|
228
|
-
if (mode === "global")
|
|
229
|
-
return join(
|
|
230
|
-
process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"),
|
|
231
|
-
"generated-images",
|
|
232
|
-
);
|
|
246
|
+
if (mode === "global") return join(piAgentDir(), "generated-images");
|
|
233
247
|
const dir = params.saveDir?.trim() || process.env.PI_IMAGE_SAVE_DIR?.trim();
|
|
234
248
|
if (!dir) throw new Error("save=custom requires saveDir or PI_IMAGE_SAVE_DIR.");
|
|
235
|
-
return dir;
|
|
249
|
+
return resolveUserPath(dir, cwd);
|
|
236
250
|
}
|
|
237
251
|
|
|
238
252
|
async function saveImage(
|
|
@@ -335,10 +349,15 @@ function extractImageFromEvent(
|
|
|
335
349
|
mimeType,
|
|
336
350
|
};
|
|
337
351
|
}
|
|
338
|
-
const partial =
|
|
352
|
+
const partial =
|
|
353
|
+
typeof event.partial_image_b64 === "string"
|
|
354
|
+
? event.partial_image_b64
|
|
355
|
+
: typeof event.b64_json === "string"
|
|
356
|
+
? event.b64_json
|
|
357
|
+
: undefined;
|
|
339
358
|
if (typeof partial === "string" && partial.trim()) {
|
|
340
359
|
const { data, mimeType } = dataUrlParts(partial, fallbackMimeType);
|
|
341
|
-
return { id: `ig_${randomUUID().slice(0, 8)}`, status: "
|
|
360
|
+
return { id: `ig_${randomUUID().slice(0, 8)}`, status: "partial", data, mimeType };
|
|
342
361
|
}
|
|
343
362
|
return undefined;
|
|
344
363
|
}
|
|
@@ -352,19 +371,19 @@ async function parseSseForImage(
|
|
|
352
371
|
const reader = response.body.getReader();
|
|
353
372
|
const decoder = new TextDecoder();
|
|
354
373
|
let buffer = "";
|
|
355
|
-
let lastImage: ExtractedImageResult | undefined;
|
|
356
374
|
try {
|
|
357
375
|
while (true) {
|
|
358
376
|
if (signal?.aborted) throw new Error("Image request was aborted.");
|
|
359
377
|
const { done, value } = await reader.read();
|
|
360
378
|
if (done) break;
|
|
361
379
|
buffer += decoder.decode(value, { stream: true });
|
|
362
|
-
let
|
|
363
|
-
while (
|
|
380
|
+
let boundary = SSE_EVENT_BOUNDARY.exec(buffer);
|
|
381
|
+
while (boundary) {
|
|
382
|
+
const idx = boundary.index;
|
|
364
383
|
const chunk = buffer.slice(0, idx);
|
|
365
|
-
buffer = buffer.slice(idx +
|
|
384
|
+
buffer = buffer.slice(idx + boundary[0].length);
|
|
366
385
|
const data = chunk
|
|
367
|
-
.split(
|
|
386
|
+
.split(/\r?\n/)
|
|
368
387
|
.filter((line) => line.startsWith("data:"))
|
|
369
388
|
.map((line) => line.slice(5).trim())
|
|
370
389
|
.join("\n")
|
|
@@ -377,36 +396,34 @@ async function parseSseForImage(
|
|
|
377
396
|
event = undefined;
|
|
378
397
|
}
|
|
379
398
|
const image = extractImageFromEvent(event, fallbackMimeType);
|
|
380
|
-
if (image?.data) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
await reader.cancel().catch(() => undefined);
|
|
384
|
-
return image;
|
|
385
|
-
}
|
|
399
|
+
if (image?.data && image.status === "completed") {
|
|
400
|
+
await reader.cancel().catch(() => undefined);
|
|
401
|
+
return image;
|
|
386
402
|
}
|
|
387
403
|
if (isRecord(event) && event.type === "response.failed") {
|
|
388
404
|
const error =
|
|
389
405
|
isRecord(event.response) && isRecord(event.response.error)
|
|
390
406
|
? event.response.error
|
|
391
407
|
: undefined;
|
|
392
|
-
const message =
|
|
393
|
-
typeof error?.message === "string" ? error.message : "Codex image request failed."
|
|
408
|
+
const message = sanitizeDiagnosticError(
|
|
409
|
+
typeof error?.message === "string" ? error.message : "Codex image request failed.",
|
|
410
|
+
);
|
|
394
411
|
throw new Error(message);
|
|
395
412
|
}
|
|
396
413
|
if (isRecord(event) && event.type === "error") {
|
|
397
|
-
const message =
|
|
398
|
-
typeof event.message === "string" ? event.message : JSON.stringify(event)
|
|
414
|
+
const message = sanitizeDiagnosticError(
|
|
415
|
+
typeof event.message === "string" ? event.message : JSON.stringify(event),
|
|
416
|
+
);
|
|
399
417
|
throw new Error(`Codex image error: ${message}`);
|
|
400
418
|
}
|
|
401
419
|
}
|
|
402
|
-
|
|
420
|
+
boundary = SSE_EVENT_BOUNDARY.exec(buffer);
|
|
403
421
|
}
|
|
404
422
|
}
|
|
405
423
|
} finally {
|
|
406
424
|
reader.releaseLock();
|
|
407
425
|
}
|
|
408
|
-
|
|
409
|
-
throw new Error("No image_generation_call result returned by Codex.");
|
|
426
|
+
throw new Error("No completed image_generation_call result returned by Codex.");
|
|
410
427
|
}
|
|
411
428
|
|
|
412
429
|
async function requestCodexImage(
|
|
@@ -416,14 +433,16 @@ async function requestCodexImage(
|
|
|
416
433
|
requestSignal?: AbortSignal,
|
|
417
434
|
): Promise<CodexImageResult> {
|
|
418
435
|
if (!cfg.image.enabled) throw new Error("OpenAI image generation is disabled in config.");
|
|
419
|
-
const
|
|
436
|
+
const cwd = ctx.cwd || process.cwd();
|
|
420
437
|
const model = resolveModel(params, ctx, cfg);
|
|
421
438
|
const { action, outputFormat, save } = resolveImageConfig(cfg, params);
|
|
422
|
-
const
|
|
423
|
-
const request = buildRequest(params, model, cfg, images);
|
|
439
|
+
const saveDir = resolveSaveDir(save, params, cwd);
|
|
424
440
|
const timeoutSignal = AbortSignal.timeout(cfg.image.timeoutMs);
|
|
425
441
|
const baseSignal = requestSignal ?? ctx.signal;
|
|
426
442
|
const signal = baseSignal ? AbortSignal.any([baseSignal, timeoutSignal]) : timeoutSignal;
|
|
443
|
+
const credentials = await getCredentials(ctx, signal);
|
|
444
|
+
const images = await readImageInputs(params.images, cwd);
|
|
445
|
+
const request = buildRequest(params, model, cfg, images);
|
|
427
446
|
const response = await fetch(CODEX_RESPONSES_URL, {
|
|
428
447
|
method: "POST",
|
|
429
448
|
headers: {
|
|
@@ -439,17 +458,16 @@ async function requestCodexImage(
|
|
|
439
458
|
signal,
|
|
440
459
|
});
|
|
441
460
|
if (!response.ok) {
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
);
|
|
461
|
+
const statusText = response.statusText
|
|
462
|
+
? ` ${sanitizeDiagnosticError(response.statusText, 120)}`
|
|
463
|
+
: "";
|
|
464
|
+
throw new Error(`Codex image request failed (${response.status}${statusText}).`);
|
|
446
465
|
}
|
|
447
466
|
const parsed = await parseSseForImage(
|
|
448
467
|
response,
|
|
449
468
|
imageMimeType(`image.${outputFormat}`, outputFormat),
|
|
450
469
|
signal,
|
|
451
470
|
);
|
|
452
|
-
const saveDir = resolveSaveDir(save, params, ctx.cwd || process.cwd());
|
|
453
471
|
const savedPath = saveDir
|
|
454
472
|
? await saveImage(parsed.data, outputFormat, saveDir, parsed.id)
|
|
455
473
|
: undefined;
|
|
@@ -464,37 +482,6 @@ function displayPath(path: string): string {
|
|
|
464
482
|
return path.startsWith(homePrefix) ? `~/${path.slice(homePrefix.length)}` : path;
|
|
465
483
|
}
|
|
466
484
|
|
|
467
|
-
function textFromMessageContent(content: unknown): string | undefined {
|
|
468
|
-
if (typeof content === "string") return content.trim() || undefined;
|
|
469
|
-
if (!Array.isArray(content)) return undefined;
|
|
470
|
-
const text = content
|
|
471
|
-
.filter((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
|
|
472
|
-
.map((part) => (part as { text: string }).text)
|
|
473
|
-
.join("\n")
|
|
474
|
-
.trim();
|
|
475
|
-
return text || undefined;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function latestUserPromptFromEntries(entries: unknown[]): string | undefined {
|
|
479
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
480
|
-
const entry = entries[i];
|
|
481
|
-
if (
|
|
482
|
-
!isRecord(entry) ||
|
|
483
|
-
entry.type !== "message" ||
|
|
484
|
-
!isRecord(entry.message) ||
|
|
485
|
-
entry.message.role !== "user"
|
|
486
|
-
)
|
|
487
|
-
continue;
|
|
488
|
-
const text = textFromMessageContent(entry.message.content);
|
|
489
|
-
if (text) return text;
|
|
490
|
-
}
|
|
491
|
-
return undefined;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function resolveToolPrompt(params: ToolParams, ctx: ExtensionContext): string {
|
|
495
|
-
return latestUserPromptFromEntries(ctx.sessionManager.getEntries()) ?? params.prompt;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
485
|
function resultText(result: CodexImageResult): string {
|
|
499
486
|
const parts = [
|
|
500
487
|
`Generated image using OpenAI image_generation tool via openai-codex/${result.model}.`,
|
|
@@ -526,7 +513,7 @@ export function registerOpenAIImage(
|
|
|
526
513
|
return result;
|
|
527
514
|
} catch (error) {
|
|
528
515
|
lastStatus = "error";
|
|
529
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
516
|
+
lastError = sanitizeDiagnosticError(error instanceof Error ? error.message : String(error));
|
|
530
517
|
throw error;
|
|
531
518
|
}
|
|
532
519
|
}
|
|
@@ -542,7 +529,7 @@ export function registerOpenAIImage(
|
|
|
542
529
|
return {
|
|
543
530
|
authFound: credentials !== undefined,
|
|
544
531
|
authSource: credentials?.source,
|
|
545
|
-
accountId: credentials?.accountId,
|
|
532
|
+
accountId: maskIdentifier(credentials?.accountId),
|
|
546
533
|
endpoint: CODEX_RESPONSES_URL,
|
|
547
534
|
defaultModel: ctx.model?.provider === "openai-codex" ? ctx.model.id : cfg.image.defaultModel,
|
|
548
535
|
defaultSave: cfg.image.defaultSave,
|
|
@@ -552,63 +539,57 @@ export function registerOpenAIImage(
|
|
|
552
539
|
};
|
|
553
540
|
}
|
|
554
541
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (imagePart) image = { data: imagePart.data, mimeType: imagePart.mimeType };
|
|
583
|
-
}
|
|
542
|
+
pi.registerMessageRenderer<CodexImageResult>("openai-image", (message, _options, theme) => {
|
|
543
|
+
const result = message.details;
|
|
544
|
+
const text =
|
|
545
|
+
result && isRecord(result)
|
|
546
|
+
? resultText(result as CodexImageResult)
|
|
547
|
+
: typeof message.content === "string"
|
|
548
|
+
? message.content
|
|
549
|
+
: message.content
|
|
550
|
+
.filter((part) => part.type === "text")
|
|
551
|
+
.map((part) => part.text)
|
|
552
|
+
.join("\n");
|
|
553
|
+
let image: { data: string; mimeType: string; savedPath?: string } | undefined;
|
|
554
|
+
if (
|
|
555
|
+
result &&
|
|
556
|
+
isRecord(result) &&
|
|
557
|
+
typeof result.data === "string" &&
|
|
558
|
+
typeof result.mimeType === "string"
|
|
559
|
+
) {
|
|
560
|
+
image = {
|
|
561
|
+
data: result.data,
|
|
562
|
+
mimeType: result.mimeType,
|
|
563
|
+
savedPath: typeof result.savedPath === "string" ? result.savedPath : undefined,
|
|
564
|
+
};
|
|
565
|
+
} else if (Array.isArray(message.content)) {
|
|
566
|
+
const imagePart = message.content.find(isImageContent);
|
|
567
|
+
if (imagePart) image = { data: imagePart.data, mimeType: imagePart.mimeType };
|
|
568
|
+
}
|
|
584
569
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
return container;
|
|
609
|
-
});
|
|
610
|
-
})
|
|
611
|
-
.catch(() => undefined);
|
|
570
|
+
const container = new Container();
|
|
571
|
+
const box = new Box(1, 1, (line) => theme.bg("customMessageBg", line));
|
|
572
|
+
box.addChild(new Text(`${theme.fg("accent", theme.bold("[openai-image]"))}\n\n${text}`, 0, 0));
|
|
573
|
+
if (image) {
|
|
574
|
+
box.addChild(
|
|
575
|
+
new Image(
|
|
576
|
+
image.data,
|
|
577
|
+
image.mimeType,
|
|
578
|
+
{ fallbackColor: (line) => theme.fg("dim", line) },
|
|
579
|
+
{
|
|
580
|
+
maxWidthCells: 80,
|
|
581
|
+
maxHeightCells: 24,
|
|
582
|
+
filename:
|
|
583
|
+
"savedPath" in image && typeof image.savedPath === "string"
|
|
584
|
+
? image.savedPath
|
|
585
|
+
: undefined,
|
|
586
|
+
},
|
|
587
|
+
),
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
container.addChild(box);
|
|
591
|
+
return container;
|
|
592
|
+
});
|
|
612
593
|
|
|
613
594
|
pi.registerCommand(OPENAI_IMAGE_COMMAND, {
|
|
614
595
|
description: "Generate an image with OpenAI Codex image generation",
|
|
@@ -647,7 +628,6 @@ export function registerOpenAIImage(
|
|
|
647
628
|
async execute(_toolCallId, params: ToolParams, signal, onUpdate, ctx) {
|
|
648
629
|
const cfg = getConfig(ctx);
|
|
649
630
|
const model = resolveModel(params, ctx, cfg);
|
|
650
|
-
const requestParams = { ...params, prompt: resolveToolPrompt(params, ctx) };
|
|
651
631
|
onUpdate?.({
|
|
652
632
|
content: [
|
|
653
633
|
{
|
|
@@ -657,7 +637,7 @@ export function registerOpenAIImage(
|
|
|
657
637
|
],
|
|
658
638
|
details: undefined,
|
|
659
639
|
});
|
|
660
|
-
const result = await generate(
|
|
640
|
+
const result = await generate(params, ctx, signal);
|
|
661
641
|
return {
|
|
662
642
|
content: [
|
|
663
643
|
{ type: "text", text: resultText(result) },
|
|
@@ -676,11 +656,13 @@ export const _imageTest = {
|
|
|
676
656
|
DEFAULT_TIMEOUT_MS,
|
|
677
657
|
OPENAI_IMAGE_TOOL,
|
|
678
658
|
OPENAI_IMAGE_COMMAND,
|
|
659
|
+
MAX_IMAGE_INPUT_BYTES,
|
|
660
|
+
MAX_IMAGE_INPUTS,
|
|
661
|
+
MAX_TOTAL_IMAGE_INPUT_BYTES,
|
|
679
662
|
extractAccountIdFromJwt,
|
|
680
663
|
imageMimeType,
|
|
681
664
|
dataUrlParts,
|
|
682
665
|
extractImageFromEvent,
|
|
683
666
|
displayPath,
|
|
684
|
-
latestUserPromptFromEntries,
|
|
685
667
|
buildRequest,
|
|
686
668
|
};
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function expandTildePath(path: string, home = homedir()): string {
|
|
5
|
+
if (path === "~") return home;
|
|
6
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return join(home, path.slice(2));
|
|
7
|
+
return path;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function piAgentDir(env = process.env, home = homedir()): string {
|
|
11
|
+
const configuredDir = env.PI_CODING_AGENT_DIR?.trim();
|
|
12
|
+
return configuredDir ? expandTildePath(configuredDir, home) : join(home, ".pi", "agent");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveUserPath(path: string, cwd: string, home = homedir()): string {
|
|
16
|
+
return resolve(cwd, expandTildePath(path, home));
|
|
17
|
+
}
|