@oai404iao/pi-codex-imagegen 0.4.0 → 2.0.0

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 CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  See the package guide below for release status; source edits do not publish artifacts.
4
4
 
5
- Peer floor and development target: Pi 0.99.1.
5
+ Peer floor and development target: Pi 1.2.0.
6
6
 
7
- Hosted generation is model-only, including with native codemode in `only` mode.
8
- Standalone generation remains directly callable and is also available to
9
- codemode while active. Exposure follows the selected model profile; this does
10
- not enable codemode itself. A hosted profile with explicit direct fallback
11
- remains model-only.
7
+ Image generation uses standalone Images endpoints and is directly callable,
8
+ including through codemode while active. This does not enable codemode itself.
9
+ Hosted Responses image generation and `directImageApiFallback` have been removed.
10
+ Legacy hosted profiles are disabled with a diagnostic rather than silently routed
11
+ to a different implementation. Historical image outputs still replay and display.
12
12
 
13
13
  Codemode scripts receive `{ path, latestPath?, image: { type, data, mimeType } }`.
14
14
  The image data is base64; use `image(result.image)` to forward it to the model.
@@ -24,17 +24,19 @@ new image writes. Already-started server generation or disk writes cannot be
24
24
  rolled back; cancellation is not a guarantee of avoiding provider charges.
25
25
 
26
26
  Installs `image_generation`, background image commands, image persistence and
27
- presentation. Depends only on `pi-codex-runtime`, not core, web-search or the bundle.
27
+ presentation. Its only workspace dependency is `pi-codex-runtime`, not core,
28
+ web-search or the bundle. Photon supplies image decoding and PNG encoding.
28
29
  When global `config.json.imageGeneration` is `false`, none of those generation
29
- surfaces are registered. The gate also blocks late/manual standalone and direct
30
- fallback execution before authentication or network I/O.
30
+ surfaces are registered. The gate also blocks late/manual standalone execution
31
+ before authentication or network I/O.
31
32
  Catalog-supported standalone profiles call Images generation/edit endpoints with
32
- Pi authentication. Hosted profiles need core unless the existing
33
- `directImageApiFallback` option was explicitly enabled.
34
- All image paths, including this explicit fallback and background jobs, use
33
+ Pi authentication. `transparent_background:true` requests transparency;
34
+ omitted or false requests an opaque background. References are sniffed by content,
35
+ including extensionless PNG/JPEG/WebP; GIF/BMP normalize to PNG without resizing.
36
+ All image paths, including background jobs, use
35
37
  the selected provider's Pi-resolved credentials, headers and base URL. The
36
- fallback no longer reads a separate `OPENAI_API_KEY` or forces the public
37
- OpenAI endpoint. Authentication failure never switches accounts or providers.
38
+ client never reads a separate `OPENAI_API_KEY` or forces the public OpenAI endpoint.
39
+ Authentication failure never switches accounts or providers.
38
40
  `endpoint_config.imageGeneration` limits allowed modes at the authenticated
39
41
  endpoint. Rejected/denied standalone jobs do not fall through to hosted jobs.
40
42
  Explicit protocol rejection temporarily disables that mode for the session,
@@ -53,13 +55,13 @@ providers. Reserved endpoints remain unsupported compatibility surfaces.
53
55
  See [the package guide](../../docs/codex-packages.md). From repository root:
54
56
 
55
57
  ```bash
56
- npm run check -w @oai404iao/pi-codex-imagegen
57
- npm run test:codex-composition
58
- npm run test:codex-packages
58
+ pnpm --filter @oai404iao/pi-codex-imagegen run check
59
+ pnpm run test:codex-composition
60
+ pnpm run test:codex-packages
59
61
  ```
60
62
 
61
- Client/capture/display/background-job regressions are in `tests/`; hosted
62
- transport integration is in root `tests/codex/`. See the
63
+ Client/capture/display/background-job regressions are in `tests/`; cross-package
64
+ integration is in root `tests/codex/`. See the
63
65
  [shared configuration guide](../pi-codex-runtime/reference/configuration.md) and
64
66
  [image-generation example](assets/image-generation.gif). The example is a
65
67
  repository asset, not part of the runtime tarball.
@@ -11,8 +11,19 @@ apply-patch compatibility evidence remains pinned to
11
11
  The unmodified provenance record preserves upstream source identifiers, not
12
12
  claims that every referenced source file is shipped in this package.
13
13
 
14
+ Image request behavior and the runtime-owned namespace declaration now follow
15
+ Codex `5a3140176e668a2f72f3c098490eb7f7052d9d85`, including transparent-background
16
+ requests. The runtime package ships the exact source and canonical-JSON hashes
17
+ in `provenance/openai-codex-5a314017-image-generation.json`.
18
+ The earlier provenance record remains historical evidence.
19
+
14
20
  Endpoint clients and presentation code are project implementations. Protocol
15
21
  shapes are consumed from pi-codex-runtime rather than copied here.
16
22
 
23
+ Reference image normalization uses the separately distributed
24
+ `@silvia-odwyer/photon-node` 0.3.4 dependency (Apache-2.0), retaining original
25
+ dimensions and transparency when encoding GIF/BMP input as PNG. Its package
26
+ includes its own license; no Photon source or WASM binary is vendored here.
27
+
17
28
  See `pi-codex-runtime/reference/source-map.md` in the repository for the pinned
18
29
  evidence map and `reference/configuration.md` there for image configuration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oai404iao/pi-codex-imagegen",
3
- "version": "0.4.0",
3
+ "version": "2.0.0",
4
4
  "private": false,
5
5
  "description": "Unpublished pi-codex-imagegen workspace for the staged Codex split",
6
6
  "type": "module",
@@ -28,9 +28,9 @@
28
28
  ]
29
29
  },
30
30
  "peerDependencies": {
31
- "@earendil-works/pi-ai": ">=0.99.1",
32
- "@earendil-works/pi-coding-agent": ">=0.99.1",
33
- "@earendil-works/pi-tui": ">=0.99.1",
31
+ "@earendil-works/pi-ai": ">=1.2.0",
32
+ "@earendil-works/pi-coding-agent": ">=1.2.0",
33
+ "@earendil-works/pi-tui": ">=1.2.0",
34
34
  "typebox": "*"
35
35
  },
36
36
  "peerDependenciesMeta": {
@@ -45,22 +45,18 @@
45
45
  }
46
46
  },
47
47
  "dependencies": {
48
- "@oai404iao/pi-codex-runtime": "0.5.0"
48
+ "@oai404iao/pi-codex-runtime": "2.0.0",
49
+ "@silvia-odwyer/photon-node": "0.3.4"
49
50
  },
50
51
  "devDependencies": {
51
- "@earendil-works/pi-ai": "0.99.1",
52
- "@earendil-works/pi-coding-agent": "0.99.1",
53
- "@earendil-works/pi-tui": "0.99.1",
52
+ "@earendil-works/pi-ai": "1.2.0",
53
+ "@earendil-works/pi-coding-agent": "1.2.0",
54
+ "@earendil-works/pi-tui": "1.2.0",
54
55
  "@types/node": "^26.2.0",
55
56
  "tsx": "^4.20.6",
56
57
  "typebox": "^1.1.24",
57
58
  "typescript": "^7.0.2"
58
59
  },
59
- "scripts": {
60
- "typecheck": "tsc -p tsconfig.json --noEmit",
61
- "test": "tsx --test tests/**/*.test.ts",
62
- "check": "npm run typecheck && npm test"
63
- },
64
60
  "publishConfig": {
65
61
  "access": "public",
66
62
  "registry": "https://registry.npmjs.org/"
@@ -77,5 +73,10 @@
77
73
  "engines": {
78
74
  "node": ">=22.19.0"
79
75
  },
80
- "gitHead": "8c96ca920ee51fe23c5c50d2cb2d8e1b304efe94"
81
- }
76
+ "gitHead": "cf3a22fc7855afd0d31e21999b3868da8cf67d5f",
77
+ "scripts": {
78
+ "typecheck": "tsc -p tsconfig.json --noEmit",
79
+ "test": "tsx --test tests/**/*.test.ts",
80
+ "check": "pnpm run typecheck && pnpm run test"
81
+ }
82
+ }
@@ -1,31 +1,17 @@
1
- import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
3
3
  import { truncateToWidth, type Component } from "@earendil-works/pi-tui";
4
- import { readFile } from "node:fs/promises";
5
- import { extname, isAbsolute, relative, resolve } from "node:path";
4
+ import { relative } from "node:path";
6
5
  import { supportsImageInput, type ModelLike } from "@oai404iao/pi-codex-runtime/internal/capabilities";
7
- import {
8
- buildCodexJsonHeaders,
9
- hasCodexRequestAuth,
10
- resolveResponsesUrl,
11
- type ResponsesProtocol,
12
- withResolvedAuthBaseUrl,
13
- } from "@oai404iao/pi-codex-runtime/internal/codex-http";
14
6
  import { createBackgroundImageJobs, panelBranch } from "./background-image-jobs.js";
15
7
  import { listResolvedModelProfiles } from "@oai404iao/pi-codex-runtime/internal/model-catalog/catalog";
16
8
  import { loadModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
17
- import { setProviderGeneratedHeader } from "@oai404iao/pi-codex-runtime/internal/provider-headers";
18
9
  import { loadSettings } from "@oai404iao/pi-codex-runtime/internal/settings";
19
10
  import { standaloneImageGeneration } from "./tools/image-generation.js";
20
- import {
21
- buildGeneratedImageDisplayText,
22
- saveOpenAICodexGeneratedImage,
23
- } from "./tools/image-generation/storage.js";
11
+ import { buildGeneratedImageDisplayText } from "./tools/image-generation/storage.js";
24
12
  import { IMAGE_SAVE_DISPLAY_MESSAGE_TYPE, type SavedGeneratedImage } from "./tools/image-generation/types.js";
25
13
  import { projectRoot } from "./utils/images.js";
26
- import { checkEndpointResponse, rememberResolvedEndpoint, reportEndpointFailure, requireEndpointCapability } from "@oai404iao/pi-codex-runtime/internal/endpoint-state";
27
14
 
28
- const BACKGROUND_IMAGE_INSTRUCTIONS = "Generate or edit images with the hosted image_generation tool. Use the user's prompt and any provided reference images. Return the image_generation_call result.";
29
15
  const IMAGE_GEN_ERROR_MESSAGE_TYPE = "codex-image-generation-error";
30
16
 
31
17
  export interface ParsedImageGenCommand {
@@ -33,20 +19,6 @@ export interface ParsedImageGenCommand {
33
19
  imagePaths: string[];
34
20
  }
35
21
 
36
- interface ReferenceImage {
37
- path: string;
38
- mimeType: string;
39
- base64: string;
40
- }
41
-
42
- interface CodexImageResult {
43
- id: string;
44
- result: string;
45
- outputFormat?: string;
46
- revisedPrompt?: string;
47
- imageModel?: string;
48
- }
49
-
50
22
  interface ImageGenerationErrorDetails {
51
23
  message: string;
52
24
  prompt?: string;
@@ -60,12 +32,10 @@ interface ModelRegistryLike {
60
32
  find?: (provider: string, id: string) => unknown;
61
33
  }
62
34
 
63
- function isRecord(value: unknown): value is Record<string, unknown> {
64
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
65
- }
66
-
67
35
  function isModelLike(value: unknown): value is ModelLike {
68
- return isRecord(value) && typeof value.provider === "string" && (typeof value.id === "string" || typeof value.name === "string");
36
+ if (!value || typeof value !== "object") return false;
37
+ const record = value as Record<string, unknown>;
38
+ return typeof record.provider === "string" && (typeof record.id === "string" || typeof record.name === "string");
69
39
  }
70
40
 
71
41
  function registryModels(registry: ModelRegistryLike | undefined): ModelLike[] {
@@ -85,21 +55,21 @@ function registryModels(registry: ModelRegistryLike | undefined): ModelLike[] {
85
55
  function isConfiguredImageModel(model: ModelLike | undefined): boolean {
86
56
  if (!model || !supportsImageInput(model)) return false;
87
57
  const settings = loadModelSettings({ ...model, baseUrl: undefined });
88
- return Boolean(settings.modelProfile?.effective.enabled && settings.imageGenerationImplementation);
58
+ return Boolean(settings.modelProfile?.effective.enabled && settings.imageGenerationImplementation === "standalone");
89
59
  }
90
60
 
91
61
  export function selectCodexImageModel(currentModel: ModelLike | undefined, registry: ModelRegistryLike | undefined): ModelLike | undefined {
92
- const currentSettings = currentModel && loadModelSettings({ ...currentModel, baseUrl: undefined });
93
- if (currentModel && supportsImageInput(currentModel) && currentSettings?.imageGenerationImplementation) return currentModel;
62
+ const profile = currentModel && loadModelSettings({ ...currentModel, baseUrl: undefined }).modelProfile;
63
+ if (profile?.removedHostedImageGeneration) {
64
+ throw new Error(`Hosted image generation was removed for ${profile.id}. Change tools.imageGeneration to standalone or false in models.json; no alternate model was selected.`);
65
+ }
66
+ if (isConfiguredImageModel(currentModel)) return currentModel;
94
67
  const discovered = registryModels(registry).find(isConfiguredImageModel);
95
68
  if (discovered) return discovered;
96
69
  for (const profile of listResolvedModelProfiles()) {
97
- if (!profile.effective.enabled || !profile.effective.tools.imageGeneration) continue;
70
+ if (!profile.effective.enabled || profile.effective.tools.imageGeneration !== "standalone") continue;
98
71
  const slash = profile.id.indexOf("/");
99
- const candidate = registry?.find?.(
100
- profile.id.slice(0, slash),
101
- profile.id.slice(slash + 1),
102
- );
72
+ const candidate = registry?.find?.(profile.id.slice(0, slash), profile.id.slice(slash + 1));
103
73
  if (isModelLike(candidate) && isConfiguredImageModel(candidate)) return candidate;
104
74
  }
105
75
  return undefined;
@@ -143,7 +113,7 @@ function tokenizeArgs(input: string): string[] {
143
113
  function isSupportedImagePathToken(token: string): boolean {
144
114
  const normalized = token.replace(/^file:\/\//, "").replace(/[),.;:]+$/, "");
145
115
  if (/^https?:\/\//i.test(normalized) || normalized.startsWith("data:")) return false;
146
- return /\.(?:png|jpe?g|webp)$/i.test(normalized);
116
+ return /\.(?:png|jpe?g|webp|gif|bmp)$/i.test(normalized);
147
117
  }
148
118
 
149
119
  export function parseImageGenCommandArgs(input: string): ParsedImageGenCommand {
@@ -157,140 +127,6 @@ export function parseImageGenCommandArgs(input: string): ParsedImageGenCommand {
157
127
  return { prompt: promptParts.join(" ").trim(), imagePaths };
158
128
  }
159
129
 
160
- function mimeTypeForPath(path: string): string {
161
- const ext = extname(path).toLowerCase();
162
- if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
163
- if (ext === ".webp") return "image/webp";
164
- if (ext === ".png") return "image/png";
165
- throw new Error(`Unsupported reference image type: ${path}. Use PNG, JPEG, or WebP.`);
166
- }
167
-
168
- async function loadReferenceImage(cwd: string, rawPath: string): Promise<ReferenceImage> {
169
- const path = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
170
- const buffer = await readFile(path);
171
- return { path, mimeType: mimeTypeForPath(path), base64: buffer.toString("base64") };
172
- }
173
-
174
- function buildHeaders(
175
- model: Model<Api>,
176
- auth: { apiKey?: string; headers?: ProviderHeaders },
177
- endpoint: ResponsesProtocol,
178
- codexRequestExtensions: boolean,
179
- ): Headers {
180
- const headers = buildCodexJsonHeaders({
181
- modelHeaders: model.headers,
182
- auth,
183
- endpoint,
184
- codexRequestExtensions,
185
- });
186
- if (codexRequestExtensions || endpoint === "codex") setProviderGeneratedHeader(headers, "OpenAI-Beta", "responses=experimental");
187
- setProviderGeneratedHeader(headers, "accept", "text/event-stream");
188
- return headers;
189
- }
190
-
191
- export function buildBackgroundImageRequest(options: {
192
- prompt: string;
193
- referenceImages: ReferenceImage[];
194
- responsesModel: string;
195
- imageModel: string;
196
- }): Record<string, unknown> {
197
- const content: Array<Record<string, unknown>> = [
198
- { type: "input_text", text: options.referenceImages.length > 0 ? `Edit the provided image(s): ${options.prompt}` : options.prompt },
199
- ...options.referenceImages.map((image) => ({
200
- type: "input_image",
201
- detail: "auto",
202
- image_url: `data:${image.mimeType};base64,${image.base64}`,
203
- })),
204
- ];
205
- return {
206
- model: options.responsesModel,
207
- store: false,
208
- stream: true,
209
- instructions: BACKGROUND_IMAGE_INSTRUCTIONS,
210
- input: [{ role: "user", content }],
211
- tools: [{
212
- type: "image_generation",
213
- model: options.imageModel,
214
- output_format: "png",
215
- action: options.referenceImages.length > 0 ? "edit" : "generate",
216
- }],
217
- tool_choice: { type: "image_generation" },
218
- };
219
- }
220
-
221
- async function* parseSseEvents(response: Response, signal: AbortSignal): AsyncIterable<Record<string, unknown>> {
222
- if (!response.body) return;
223
- const reader = response.body.getReader();
224
- const decoder = new TextDecoder();
225
- let buffer = "";
226
- const cancel = () => { void reader.cancel(signal.reason).catch(() => {}); };
227
- signal.addEventListener("abort", cancel, { once: true });
228
- if (signal.aborted) cancel();
229
- try {
230
- while (true) {
231
- signal.throwIfAborted();
232
- const { done, value } = await reader.read();
233
- signal.throwIfAborted();
234
- if (done) break;
235
- buffer += decoder.decode(value, { stream: true });
236
- let boundary: number;
237
- while ((boundary = buffer.indexOf("\n\n")) >= 0) {
238
- const raw = buffer.slice(0, boundary);
239
- buffer = buffer.slice(boundary + 2);
240
- for (const line of raw.split(/\r?\n/)) {
241
- if (!line.startsWith("data:")) continue;
242
- const data = line.slice(5).trim();
243
- if (!data || data === "[DONE]") continue;
244
- yield JSON.parse(data) as Record<string, unknown>;
245
- }
246
- }
247
- }
248
- } finally {
249
- signal.removeEventListener("abort", cancel);
250
- reader.releaseLock();
251
- }
252
- }
253
-
254
- function collectImageResult(results: CodexImageResult[], item: unknown, fallbackImageModel: string): void {
255
- if (!item || typeof item !== "object") return;
256
- const candidate = item as Record<string, unknown>;
257
- if (candidate.type !== "image_generation_call" || typeof candidate.result !== "string") return;
258
- const id = typeof candidate.id === "string" ? candidate.id : `ig_${results.length}`;
259
- if (results.some((result) => result.id === id)) return;
260
- results.push({
261
- id,
262
- result: candidate.result,
263
- outputFormat: typeof candidate.output_format === "string" ? candidate.output_format : "png",
264
- revisedPrompt: typeof candidate.revised_prompt === "string" ? candidate.revised_prompt : undefined,
265
- imageModel: typeof candidate.model === "string" ? candidate.model : fallbackImageModel,
266
- });
267
- }
268
-
269
- function collectResponseText(value: unknown, out: string[]): void {
270
- if (!value || typeof value !== "object") return;
271
- const item = value as Record<string, unknown>;
272
- for (const key of ["text", "refusal", "summary_text", "message"] as const) {
273
- const text = item[key];
274
- if (typeof text === "string" && text.trim()) out.push(text.trim());
275
- }
276
- for (const key of ["content", "output", "summary"] as const) {
277
- const child = item[key];
278
- if (Array.isArray(child)) for (const part of child) collectResponseText(part, out);
279
- }
280
- }
281
-
282
- export function summarizeNonImageResponse(response: Record<string, unknown> | undefined): string {
283
- if (!response) return "No image was returned by Codex.";
284
- const status = typeof response.status === "string" ? response.status : undefined;
285
- const error = response.error && typeof response.error === "object" ? response.error as Record<string, unknown> : undefined;
286
- const errorMessage = typeof error?.message === "string" ? error.message : undefined;
287
- const texts: string[] = [];
288
- collectResponseText(response, texts);
289
- const text = [...new Set(texts)].join(" ").replace(/\s+/g, " ").trim();
290
- const details = [status && status !== "completed" ? `status ${status}` : undefined, errorMessage, text].filter(Boolean).join(" · ");
291
- return details ? `No image was returned by Codex: ${details}` : "No image was returned by Codex.";
292
- }
293
-
294
130
  function renderImageGenError(details: ImageGenerationErrorDetails, theme: Theme): Component {
295
131
  return {
296
132
  invalidate() {},
@@ -313,117 +149,32 @@ function renderImageGenError(details: ImageGenerationErrorDetails, theme: Theme)
313
149
  async function runBackgroundImageGeneration(pi: ExtensionAPI, ctx: ExtensionCommandContext, parsed: ParsedImageGenCommand, signal: AbortSignal): Promise<void> {
314
150
  signal.throwIfAborted();
315
151
  const model = selectCodexImageModel(ctx.model as ModelLike | undefined, ctx.modelRegistry as ModelRegistryLike | undefined) as Model<Api> | undefined;
316
- if (!model) throw new Error("No image-capable model with an enabled model catalog profile is available.");
152
+ if (!model) throw new Error("No image-capable model with an enabled standalone model catalog profile is available.");
317
153
  const settings = loadModelSettings({ ...model, baseUrl: undefined }, ctx.cwd);
318
- if (!settings.imageGenerationImplementation) throw new Error("Image generation is disabled for the selected model.");
319
- if (!settings.enabled) throw new Error("pi-codex-minimal-tools is disabled.");
320
- if (settings.imageGenerationImplementation === "standalone") {
321
- const result = await standaloneImageGeneration({
322
- prompt: parsed.prompt,
323
- referenced_image_paths: parsed.imagePaths,
324
- }, {
325
- cwd: ctx.cwd,
326
- model,
327
- modelRegistry: ctx.modelRegistry,
328
- sessionManager: ctx.sessionManager,
329
- }, settings, signal, { callId: "standalone" });
330
- signal.throwIfAborted();
331
- const saved = result.details.saved;
332
- const workspaceRoot = projectRoot(ctx.cwd);
333
- const relativePath = relative(workspaceRoot, saved.path);
334
- const latestAbsolutePath = saved.latestPath ?? saved.path;
335
- const latestRelativePath = relative(workspaceRoot, latestAbsolutePath);
336
- const savedImage: SavedGeneratedImage = {
337
- absolutePath: saved.path,
338
- relativePath: relativePath && !relativePath.startsWith("..") ? relativePath : saved.path,
339
- latestAbsolutePath,
340
- latestRelativePath: latestRelativePath && !latestRelativePath.startsWith("..")
341
- ? latestRelativePath
342
- : latestAbsolutePath,
343
- responseId: undefined,
344
- callId: "standalone",
345
- outputFormat: saved.format,
346
- imageModel: settings.imageModel,
347
- revisedPrompt: parsed.prompt,
348
- };
349
- pi.sendMessage({
350
- customType: IMAGE_SAVE_DISPLAY_MESSAGE_TYPE,
351
- content: [{ type: "text", text: buildGeneratedImageDisplayText(savedImage, { expanded: false }) }],
352
- display: true,
353
- details: { savedImages: [savedImage] },
354
- }, { triggerTurn: false });
355
- return;
356
- }
357
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
358
- signal.throwIfAborted();
359
- if (!auth.ok) throw new Error(auth.error);
360
- const requestModel = withResolvedAuthBaseUrl(model, auth);
361
- const sessionId = ctx.sessionManager.getSessionId();
362
- rememberResolvedEndpoint(model, requestModel, sessionId);
363
- requireEndpointCapability(settings.endpoint_config, requestModel, sessionId, "imageGeneration.hosted");
364
- if (!hasCodexRequestAuth({
365
- modelHeaders: model.headers,
366
- auth: { apiKey: auth.apiKey, headers: auth.headers },
367
- })) {
368
- throw new Error(`No request authentication is configured for ${model.provider}.`);
369
- }
370
- const referenceImages = await Promise.all(parsed.imagePaths.map((path) => loadReferenceImage(ctx.cwd, path)));
371
- signal.throwIfAborted();
372
- const body = buildBackgroundImageRequest({
373
- prompt: parsed.prompt,
374
- referenceImages,
375
- responsesModel: model.id,
376
- imageModel: settings.imageModel,
377
- });
378
- const response = await fetch(resolveResponsesUrl(auth.baseUrl ?? model.baseUrl, settings.responsesEndpoint), {
379
- signal,
380
- method: "POST",
381
- headers: buildHeaders(model, {
382
- apiKey: auth.apiKey,
383
- headers: auth.headers,
384
- }, settings.responsesEndpoint, settings.codexRequestExtensions),
385
- body: JSON.stringify(body),
386
- });
387
- signal.throwIfAborted();
388
- await checkEndpointResponse(response, requestModel, sessionId, ["imageGeneration.hosted"], signal);
389
- const results: CodexImageResult[] = [];
390
- let responseId: string | undefined;
391
- let lastResponse: Record<string, unknown> | undefined;
392
- for await (const event of parseSseEvents(response, signal)) {
393
- if (event.type === "error" || event.type === "response.failed") {
394
- reportEndpointFailure(requestModel, sessionId, ["imageGeneration.hosted"], event.error ?? (event.response as { error?: unknown } | undefined)?.error);
395
- }
396
- if (event.type === "response.created" && event.response && typeof event.response === "object") {
397
- lastResponse = event.response as Record<string, unknown>;
398
- const id = (event.response as { id?: unknown }).id;
399
- if (typeof id === "string") responseId = id;
400
- }
401
- if (event.type === "response.output_item.done") collectImageResult(results, event.item, settings.imageModel);
402
- if ((event.type === "response.completed" || event.type === "response.done") && event.response && typeof event.response === "object") {
403
- lastResponse = event.response as Record<string, unknown>;
404
- const responseOutput = (event.response as { output?: unknown }).output;
405
- if (Array.isArray(responseOutput)) for (const item of responseOutput) collectImageResult(results, item, settings.imageModel);
406
- }
407
- }
408
- if (results.length === 0) throw new Error(summarizeNonImageResponse(lastResponse));
409
- const savedImages = [];
410
- for (const result of results) {
411
- signal.throwIfAborted();
412
- savedImages.push(await saveOpenAICodexGeneratedImage(ctx.cwd, {
413
- responseId,
414
- callId: result.id,
415
- result: result.result,
416
- outputFormat: result.outputFormat,
417
- imageModel: result.imageModel,
418
- revisedPrompt: result.revisedPrompt ?? parsed.prompt,
419
- }));
420
- }
154
+ const result = await standaloneImageGeneration({
155
+ prompt: parsed.prompt, referenced_image_paths: parsed.imagePaths,
156
+ }, {
157
+ cwd: ctx.cwd, model, modelRegistry: ctx.modelRegistry, sessionManager: ctx.sessionManager,
158
+ }, settings, signal, { callId: "standalone" });
421
159
  signal.throwIfAborted();
160
+ const saved = result.details.saved;
161
+ const workspaceRoot = projectRoot(ctx.cwd);
162
+ const relativePath = relative(workspaceRoot, saved.path);
163
+ const latestAbsolutePath = saved.latestPath ?? saved.path;
164
+ const latestRelativePath = relative(workspaceRoot, latestAbsolutePath);
165
+ const savedImage: SavedGeneratedImage = {
166
+ absolutePath: saved.path,
167
+ relativePath: relativePath && !relativePath.startsWith("..") ? relativePath : saved.path,
168
+ latestAbsolutePath,
169
+ latestRelativePath: latestRelativePath && !latestRelativePath.startsWith("..") ? latestRelativePath : latestAbsolutePath,
170
+ responseId: result.details.requestId,
171
+ callId: "standalone", outputFormat: saved.format, imageModel: settings.imageModel,
172
+ revisedPrompt: parsed.prompt,
173
+ };
422
174
  pi.sendMessage({
423
175
  customType: IMAGE_SAVE_DISPLAY_MESSAGE_TYPE,
424
- content: [{ type: "text", text: buildGeneratedImageDisplayText(savedImages[0], { expanded: false }) }],
425
- display: true,
426
- details: { savedImages },
176
+ content: [{ type: "text", text: buildGeneratedImageDisplayText(savedImage, { expanded: false }) }],
177
+ display: true, details: { savedImages: [savedImage] },
427
178
  }, { triggerTurn: false });
428
179
  }
429
180
 
@@ -432,23 +183,15 @@ export function registerBackgroundImageGenerationCommand(pi: ExtensionAPI): void
432
183
  pi.on("session_start", (_event, ctx) => jobs.reset(ctx));
433
184
  pi.on("session_shutdown", (_event, ctx) => jobs.reset(ctx));
434
185
  pi.registerMessageRenderer<ImageGenerationErrorDetails>(IMAGE_GEN_ERROR_MESSAGE_TYPE, (message, _options, theme) => {
435
- const rawContent = typeof message.content === "string"
436
- ? message.content
437
- : message.content
438
- .filter((item) => item.type === "text")
439
- .map((item) => item.text)
440
- .join("\n");
441
- const details: ImageGenerationErrorDetails = {
186
+ const rawContent = typeof message.content === "string" ? message.content
187
+ : message.content.filter((item) => item.type === "text").map((item) => item.text).join("\n");
188
+ return renderImageGenError({
442
189
  message: message.details?.message ?? rawContent.replace(/^Image generation failed:\s*/i, "") ?? "Image generation failed.",
443
- prompt: message.details?.prompt,
444
- imageModel: message.details?.imageModel,
445
- referenceCount: message.details?.referenceCount,
446
- };
447
- return renderImageGenError(details, theme);
190
+ prompt: message.details?.prompt, imageModel: message.details?.imageModel, referenceCount: message.details?.referenceCount,
191
+ }, theme);
448
192
  });
449
-
450
193
  pi.registerCommand("image-gen", {
451
- description: "Generate or edit an image in the background with the current model catalog. Usage: /image-gen prompt text [@reference.png]",
194
+ description: "Generate or edit an image in the background with the standalone Images API. Usage: /image-gen prompt text [@reference.png]",
452
195
  handler: async (args, ctx) => {
453
196
  const parsed = parseImageGenCommandArgs(args);
454
197
  if (!parsed.prompt) {
@@ -464,8 +207,7 @@ export function registerBackgroundImageGenerationCommand(pi: ExtensionAPI): void
464
207
  const message = error instanceof Error ? error.message : String(error);
465
208
  pi.sendMessage({
466
209
  customType: IMAGE_GEN_ERROR_MESSAGE_TYPE,
467
- content: `Image generation failed: ${message}`,
468
- display: true,
210
+ content: `Image generation failed: ${message}`, display: true,
469
211
  details: { message, prompt: parsed.prompt, imageModel: settings.imageModel, referenceCount: parsed.imagePaths.length } satisfies ImageGenerationErrorDetails,
470
212
  }, { triggerTurn: false });
471
213
  })
package/src/index.ts CHANGED
@@ -27,7 +27,6 @@ export default function codexImagegen(pi: ExtensionAPI): void {
27
27
  const definition = createImageGenerationToolDefinition({
28
28
  loadSettings: (cwd, model) => loadModelSettings(model, cwd),
29
29
  getCurrentTurnId: sessionId => currentCodexTurn(sessionId)?.turnId,
30
- hasProviderRuntime: () => broker.coreEnabled,
31
30
  });
32
31
  const parameters = { ...definition.parameters };
33
32
  broker.ownedTools.set("image_generation", { parameters, description: definition.description });
@@ -1,28 +1,21 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import { readFile } from "node:fs/promises";
3
- import { extname, isAbsolute, resolve } from "node:path";
4
- import { buildSessionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import { buildSessionContext, detectSupportedImageMimeTypeFromFile, type SessionEntry } from "@earendil-works/pi-coding-agent";
5
4
  import { saveBase64Image } from "../utils/images.js";
6
5
  import { imageGenerationOutput, imageGenerationOutputSchema } from "./image-generation/output.js";
7
6
  import type { CodexMinimalToolsSettings } from "@oai404iao/pi-codex-runtime/internal/settings";
8
7
  import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
9
- import {
10
- buildCodexJsonHeaders,
11
- hasCodexRequestAuth,
12
- resolveCodexApiEndpoint,
13
- withResolvedAuthBaseUrl,
14
- } from "@oai404iao/pi-codex-runtime/internal/codex-http";
8
+ import { buildCodexJsonHeaders, hasCodexRequestAuth, resolveCodexApiEndpoint, withResolvedAuthBaseUrl } from "@oai404iao/pi-codex-runtime/internal/codex-http";
15
9
  import { loadModelSettings, type ResolvedCodexModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
16
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";
17
13
 
18
14
  export interface ImageGenerationInput {
19
15
  prompt?: string;
20
- referenced_image_paths?: string[];
21
- num_last_images_to_include?: number;
22
- size?: "1024x1024" | "1024x1536" | "1536x1024" | "auto";
23
- quality?: "low" | "medium" | "high" | "auto";
24
- background?: "transparent" | "opaque" | "auto";
25
- output_format?: "png" | "webp" | "jpeg";
16
+ referenced_image_paths?: string[] | null;
17
+ num_last_images_to_include?: number | null;
18
+ transparent_background?: boolean;
26
19
  }
27
20
 
28
21
  export const imageGenerationToolSchema = {
@@ -31,74 +24,22 @@ export const imageGenerationToolSchema = {
31
24
  properties: {
32
25
  prompt: { type: "string", description: "Image generation or editing prompt." },
33
26
  referenced_image_paths: {
34
- type: "array",
35
- maxItems: 5,
27
+ type: ["array", "null"], maxItems: 5,
36
28
  items: { type: "string", minLength: 1 },
37
- description: "Local PNG, JPEG, or WebP paths to edit.",
29
+ description: "Local image paths to edit. PNG, JPEG, WebP, GIF and BMP are supported.",
38
30
  },
39
31
  num_last_images_to_include: {
40
- type: "integer",
41
- minimum: 1,
42
- maximum: 5,
32
+ type: ["integer", "null"], minimum: 1, maximum: 5,
43
33
  description: "Use the newest conversation images when one or more targets have no local path.",
44
34
  },
35
+ transparent_background: {
36
+ type: "boolean",
37
+ description: "Whether the output should have a transparent background. Defaults to false.",
38
+ },
45
39
  },
46
40
  required: ["prompt"],
47
41
  };
48
42
 
49
- async function urlToBase64(url: string, signal?: AbortSignal): Promise<string> {
50
- const response = await fetch(url, { signal });
51
- if (!response.ok) throw new Error(`Failed to download generated image: ${response.status} ${await response.text()}`);
52
- const buffer = Buffer.from(await response.arrayBuffer());
53
- return buffer.toString("base64");
54
- }
55
-
56
- export async function directImageGeneration(input: ImageGenerationInput, ctx: ImageGenerationToolContext, settings: CodexMinimalToolsSettings, signal?: AbortSignal) {
57
- if (!settings.imageGeneration) throw new Error("Image generation is disabled by the global imageGeneration setting.");
58
- if (!settings.directImageApiFallback) throw new Error("Direct Images API fallback is disabled. Use the configured image implementation, or enable directImageApiFallback.");
59
- if (!input.prompt?.trim()) throw new Error("A prompt is required for direct image_generation fallback.");
60
- signal?.throwIfAborted();
61
- const model = ctx.model;
62
- if (!model || !ctx.modelRegistry) throw new Error("No active model is available for direct image generation.");
63
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
64
- signal?.throwIfAborted();
65
- if (!auth.ok) throw new Error(auth.error);
66
- const requestModel = withResolvedAuthBaseUrl(model, auth);
67
- const sessionId = ctx.sessionManager?.getSessionId?.();
68
- rememberResolvedEndpoint(model, requestModel, sessionId);
69
- requireEndpointCapability(settings.endpoint_config, requestModel, sessionId, "imageGeneration.standalone");
70
- if (!hasCodexRequestAuth({ modelHeaders: model.headers, auth })) {
71
- throw new Error(`No request authentication for provider: ${model.provider}`);
72
- }
73
- const endpoint = loadModelSettings(model, ctx.cwd, settings).responsesEndpoint;
74
- const body: Record<string, unknown> = {
75
- model: settings.imageModel,
76
- prompt: input.prompt,
77
- };
78
- if (input.size && input.size !== "auto") body.size = input.size;
79
- if (input.quality && input.quality !== "auto") body.quality = input.quality;
80
- if (input.background && input.background !== "auto") body.background = input.background;
81
- if (input.output_format) body.output_format = input.output_format;
82
- const response = await fetch(resolveCodexApiEndpoint(auth.baseUrl ?? model.baseUrl, endpoint, "images/generations"), {
83
- method: "POST",
84
- headers: buildCodexJsonHeaders({ modelHeaders: model.headers, auth, endpoint, codexRequestExtensions: settings.codexRequestExtensions }),
85
- body: JSON.stringify(body),
86
- signal,
87
- });
88
- await checkEndpointResponse(response, requestModel, sessionId, ["imageGeneration.standalone"], signal);
89
- const json = await response.json() as { data?: Array<{ b64_json?: string; url?: string; revised_prompt?: string }> };
90
- const first = json.data?.[0];
91
- const base64 = first?.b64_json ?? (first?.url ? await urlToBase64(first.url, signal) : undefined);
92
- if (!base64) throw new Error("OpenAI Images API returned no image data.");
93
- signal?.throwIfAborted();
94
- const saved = await saveBase64Image({ base64, callId: "direct", cwd: ctx.cwd, format: input.output_format, responseId: settings.imageModel, settings });
95
- return {
96
- content: [{ type: "text", text: `Generated image with ${settings.imageModel}; saved to ${saved.path}${saved.latestPath ? ` (latest: ${saved.latestPath})` : ""}.` }],
97
- details: { saved, revisedPrompt: first?.revised_prompt, mode: "direct-images-api" },
98
- structuredContent: imageGenerationOutput(saved, base64),
99
- };
100
- }
101
-
102
43
  interface ImageGenerationToolContext {
103
44
  cwd: string;
104
45
  model?: Model<Api>;
@@ -124,26 +65,27 @@ async function referencedImageUrls(cwd: string, paths: readonly string[]): Promi
124
65
  return Promise.all(paths.map(async (rawPath) => {
125
66
  const normalized = rawPath.replace(/^@/, "");
126
67
  const path = isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
127
- const extension = extname(path).toLowerCase();
128
- const mimeType = extension === ".png"
129
- ? "image/png"
130
- : extension === ".jpg" || extension === ".jpeg"
131
- ? "image/jpeg"
132
- : extension === ".webp"
133
- ? "image/webp"
134
- : undefined;
135
- if (!mimeType) throw new Error(`Unsupported reference image type: ${rawPath}. Use PNG, JPEG, or WebP.`);
68
+ const mimeType = await detectSupportedImageMimeTypeFromFile(path);
69
+ if (!mimeType) throw new Error(`Unsupported reference image type: ${rawPath}. Use PNG, JPEG, WebP, GIF, or BMP.`);
136
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
+ }
137
81
  return { image_url: `data:${mimeType};base64,${data.toString("base64")}` };
138
82
  }));
139
83
  }
140
84
 
141
- function recentConversationImageUrls(
142
- ctx: ImageGenerationToolContext,
143
- count: number,
144
- ): Array<{ image_url: string }> {
145
- if (!ctx.sessionManager) {
146
- throw new Error("Conversation images are unavailable in this tool context; use referenced_image_paths.");
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.");
147
89
  }
148
90
  const messages = buildSessionContext(ctx.sessionManager.getBranch()).messages;
149
91
  const images: string[] = [];
@@ -151,31 +93,20 @@ function recentConversationImageUrls(
151
93
  if (message.role === "user" || message.role === "toolResult") {
152
94
  if (!Array.isArray(message.content)) continue;
153
95
  for (const item of message.content) {
154
- if (item.type === "image") {
155
- images.push(`data:${item.mimeType};base64,${item.data}`);
156
- }
96
+ if (item.type === "image") images.push(`data:${item.mimeType};base64,${item.data}`);
157
97
  }
158
98
  continue;
159
99
  }
160
100
  if (message.role !== "assistant") continue;
161
101
  for (const block of message.content as unknown[]) {
162
102
  if (!block || typeof block !== "object") continue;
163
- const candidate = block as {
164
- type?: unknown;
165
- item?: { result?: unknown };
166
- };
167
- if (
168
- candidate.type === "image_generation_call"
169
- && typeof candidate.item?.result === "string"
170
- && candidate.item.result
171
- ) {
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) {
172
105
  images.push(`data:image/png;base64,${candidate.item.result}`);
173
106
  }
174
107
  }
175
108
  }
176
- if (images.length < count) {
177
- throw new Error(`Requested the last ${count} conversation images, but only ${images.length} were available.`);
178
- }
109
+ if (images.length < count) throw new Error(`Requested the last ${count} conversation images, but only ${images.length} were available.`);
179
110
  return images.slice(-count).map((image_url) => ({ image_url }));
180
111
  }
181
112
 
@@ -188,84 +119,69 @@ export async function standaloneImageGeneration(
188
119
  ) {
189
120
  signal?.throwIfAborted();
190
121
  const callId = invocation.callId ?? "standalone";
191
- const turnId = invocation.turnId ?? randomUUID();
122
+ const sessionId = ctx.sessionManager?.getSessionId?.();
123
+ const turnId = invocation.turnId ?? currentCodexTurn(sessionId)?.turnId ?? uuidV7();
192
124
  const model = ctx.model;
193
125
  if (!model || !ctx.modelRegistry) throw new Error("No active model is available for standalone image generation.");
194
126
  if (!settings.enabled) throw new Error("pi-codex-minimal-tools is disabled.");
195
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.");
196
129
  if (!input.prompt?.trim()) throw new Error("A prompt is required for standalone image generation.");
197
- if (input.num_last_images_to_include !== undefined && input.referenced_image_paths?.length) {
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) {
198
134
  throw new Error("Provide only one of referenced_image_paths or num_last_images_to_include.");
199
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 ?? []);
200
140
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
201
141
  signal?.throwIfAborted();
202
142
  if (!auth.ok) throw new Error(auth.error);
203
143
  const requestModel = withResolvedAuthBaseUrl(model, auth);
204
- const sessionId = ctx.sessionManager?.getSessionId?.();
205
144
  rememberResolvedEndpoint(model, requestModel, sessionId);
206
145
  requireEndpointCapability(settings.endpoint_config, requestModel, sessionId, "imageGeneration.standalone");
207
- if (!hasCodexRequestAuth({
208
- modelHeaders: model.headers,
209
- auth: { apiKey: auth.apiKey, headers: auth.headers },
210
- })) {
146
+ if (!hasCodexRequestAuth({ modelHeaders: model.headers, auth })) {
211
147
  throw new Error(`No request authentication for provider: ${model.provider}`);
212
148
  }
213
- const images = input.num_last_images_to_include !== undefined
214
- ? recentConversationImageUrls(ctx, input.num_last_images_to_include)
215
- : await referencedImageUrls(ctx.cwd, input.referenced_image_paths ?? []);
216
- const edit = images.length > 0;
217
- signal?.throwIfAborted();
218
- const response = await fetch(
219
- resolveCodexApiEndpoint(
220
- auth.baseUrl ?? model.baseUrl,
221
- settings.responsesEndpoint,
222
- edit ? "images/edits" : "images/generations",
223
- ),
149
+ const response = await fetchCodexJson(
150
+ resolveCodexApiEndpoint(auth.baseUrl ?? model.baseUrl, settings.responsesEndpoint, images.length ? "images/edits" : "images/generations"),
224
151
  {
225
- method: "POST",
226
152
  headers: buildCodexJsonHeaders({
227
153
  codexRequestExtensions: settings.codexRequestExtensions,
228
- modelHeaders: model.headers,
229
- auth: { apiKey: auth.apiKey, headers: auth.headers },
230
- endpoint: settings.responsesEndpoint,
231
- extraHeaders: settings.codexRequestExtensions ? {
232
- "x-codex-image-turn-id": turnId,
233
- } : undefined,
154
+ modelHeaders: model.headers, auth, endpoint: settings.responsesEndpoint,
155
+ extraHeaders: settings.codexRequestExtensions ? { "x-codex-image-turn-id": turnId } : undefined,
234
156
  }),
235
157
  body: JSON.stringify({
236
- model: settings.imageModel,
237
- prompt: input.prompt.trim(),
238
- ...(edit ? { images } : {}),
239
- background: "auto",
240
- quality: "auto",
241
- size: "auto",
158
+ model: settings.imageModel, prompt: input.prompt,
159
+ ...(images.length ? { images } : {}),
160
+ background: input.transparent_background === true ? "transparent" : "opaque",
161
+ quality: "auto", size: "auto",
242
162
  }),
243
163
  signal,
244
164
  },
165
+ ["imageGeneration.standalone"],
245
166
  );
246
167
  signal?.throwIfAborted();
247
168
  await checkEndpointResponse(response, requestModel, sessionId, ["imageGeneration.standalone"], signal);
248
- const result = await response.json() as {
249
- data?: Array<{ b64_json?: string }>;
250
- };
169
+ const result = await response.json() as { data?: Array<{ b64_json?: string; generation_id?: string }> };
251
170
  signal?.throwIfAborted();
252
171
  const base64 = result.data?.[0]?.b64_json;
253
172
  if (!base64) throw new Error("Standalone image generation returned no image data.");
254
- const saved = await saveBase64Image({
255
- base64,
256
- callId,
257
- cwd: ctx.cwd,
258
- format: "png",
259
- responseId: settings.imageModel,
260
- settings,
261
- });
173
+ const saved = await saveBase64Image({ base64, callId, cwd: ctx.cwd, format: "png", responseId: settings.imageModel, settings });
262
174
  signal?.throwIfAborted();
263
175
  return {
264
176
  content: [
265
177
  { type: "image", data: base64, mimeType: "image/png" },
266
178
  { type: "text", text: `Generated image with ${settings.imageModel}; saved to ${saved.path}${saved.latestPath ? ` (latest: ${saved.latestPath})` : ""}.` },
267
179
  ],
268
- details: { saved, mode: "standalone-images-api" },
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
+ },
269
185
  structuredContent: imageGenerationOutput(saved, base64),
270
186
  };
271
187
  }
@@ -273,12 +189,11 @@ export async function standaloneImageGeneration(
273
189
  export function createImageGenerationToolDefinition(options: {
274
190
  loadSettings?: (cwd: string, model?: Model<Api>) => CodexMinimalToolsSettings | ResolvedCodexModelSettings;
275
191
  getCurrentTurnId?: (sessionId: string | undefined) => string | undefined;
276
- hasProviderRuntime?: () => boolean;
277
192
  } = {}) {
278
193
  return {
279
194
  name: "image_generation",
280
195
  label: "Image Generation",
281
- description: "Generate or edit images using the hosted or standalone implementation selected by the current model profile. Results are saved under imageOutputDir and mirrored to latest.<ext>.",
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.",
282
197
  promptSnippet: "Generate or edit images with the implementation selected by the current model profile.",
283
198
  parameters: imageGenerationToolSchema,
284
199
  outputSchema: imageGenerationOutputSchema,
@@ -286,28 +201,10 @@ export function createImageGenerationToolDefinition(options: {
286
201
  const cwd = ctx?.cwd ?? process.cwd();
287
202
  const profileModel = ctx.model ? { ...ctx.model, baseUrl: "" } : undefined;
288
203
  const settings = options.loadSettings?.(cwd, profileModel) ?? loadModelSettings(profileModel, cwd);
289
- const resolvedSettings = "modelProfile" in settings
290
- ? settings as ResolvedCodexModelSettings
291
- : loadModelSettings(profileModel, cwd, settings);
292
- if (!resolvedSettings.imageGeneration) {
293
- throw new Error("Image generation is disabled by the global setting or current model profile.");
294
- }
295
- if (resolvedSettings.imageGenerationImplementation === "standalone") {
296
- const sessionId = ctx.sessionManager?.getSessionId?.();
297
- return standaloneImageGeneration(params, ctx, resolvedSettings, signal, {
298
- callId: toolCallId,
299
- turnId: options.getCurrentTurnId?.(sessionId),
300
- });
301
- }
302
- if (settings?.directImageApiFallback) return directImageGeneration(params, { ...ctx, cwd }, settings, signal);
303
- if (options.hasProviderRuntime?.() === false) {
304
- throw new Error("Hosted image_generation requires pi-codex-core. Use a catalog-supported standalone profile or explicitly enable directImageApiFallback.");
305
- }
306
- return {
307
- isError: true,
308
- content: [{ type: "text", text: "image_generation should be handled by the current model profile. If no hosted or standalone implementation is configured, explicitly enable directImageApiFallback using the current provider's Pi authentication." }],
309
- details: { phase: "native-provider", nativeTool: "image_generation" },
310
- };
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
+ });
311
208
  },
312
209
  };
313
210
  }