@oai404iao/pi-codex-imagegen 0.3.0 → 0.4.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/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Licensing for pi-codex-minimal-tools
1
+ Licensing for pi-codex-imagegen
2
2
 
3
3
  Except for the third-party materials identified in THIRD_PARTY_NOTICES.md,
4
4
  the project-authored portions of this package are available under the
package/README.md CHANGED
@@ -1,8 +1,22 @@
1
1
  # pi-codex-imagegen
2
2
 
3
- Alpha bootstrap candidate; initial npm publication is still pending.
3
+ See the package guide below for release status; source edits do not publish artifacts.
4
4
 
5
- Peer floor: Pi 0.87.0; development target: 0.87.1.
5
+ Peer floor and development target: Pi 0.99.1.
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.
12
+
13
+ Codemode scripts receive `{ path, latestPath?, image: { type, data, mimeType } }`.
14
+ The image data is base64; use `image(result.image)` to forward it to the model.
15
+ Saving and direct model-facing image/text output retain their existing behavior.
16
+ Failed generation rejects the call instead of returning success-shaped data.
17
+ Pi intentionally resolves retained structured data if a `tool_result` hook
18
+ only sets `isError: true`. To reject or redact, that hook must also replace
19
+ `content` without supplying `structuredContent`.
6
20
 
7
21
  `/image-gen` jobs are cancelled when the session is replaced or closed.
8
22
  Late authentication/results cannot notify the replacement session or initiate
@@ -17,6 +31,15 @@ fallback execution before authentication or network I/O.
17
31
  Catalog-supported standalone profiles call Images generation/edit endpoints with
18
32
  Pi authentication. Hosted profiles need core unless the existing
19
33
  `directImageApiFallback` option was explicitly enabled.
34
+ All image paths, including this explicit fallback and background jobs, use
35
+ 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
+ `endpoint_config.imageGeneration` limits allowed modes at the authenticated
39
+ endpoint. Rejected/denied standalone jobs do not fall through to hosted jobs.
40
+ Explicit protocol rejection temporarily disables that mode for the session,
41
+ including background jobs. With `codexRequestExtensions:false`, standalone
42
+ requests omit generated Codex image-turn headers.
20
43
 
21
44
  Uses the existing
22
45
  `<agentDir>/extensions/pi-codex-minimal-tools/{config,models}.json` settings and
@@ -31,5 +54,12 @@ See [the package guide](../../docs/codex-packages.md). From repository root:
31
54
 
32
55
  ```bash
33
56
  npm run check -w @oai404iao/pi-codex-imagegen
57
+ npm run test:codex-composition
34
58
  npm run test:codex-packages
35
59
  ```
60
+
61
+ Client/capture/display/background-job regressions are in `tests/`; hosted
62
+ transport integration is in root `tests/codex/`. See the
63
+ [shared configuration guide](../pi-codex-runtime/reference/configuration.md) and
64
+ [image-generation example](assets/image-generation.gif). The example is a
65
+ repository asset, not part of the runtime tarball.
@@ -1,6 +1,6 @@
1
1
  # Source and license notices
2
2
 
3
- This unpublished package is split from `@oai404iao/pi-codex-minimal-tools`.
3
+ This package owns image code split from `@oai404iao/pi-codex-minimal-tools`.
4
4
  Project material remains Copyright (c) 2026 oai404iao. The composite LICENSE
5
5
  and verified Apache license/NOTICE snapshots are retained.
6
6
 
@@ -14,4 +14,5 @@ claims that every referenced source file is shipped in this package.
14
14
  Endpoint clients and presentation code are project implementations. Protocol
15
15
  shapes are consumed from pi-codex-runtime rather than copied here.
16
16
 
17
- See the original package's reference/source-map.md for the pinned evidence map.
17
+ See `pi-codex-runtime/reference/source-map.md` in the repository for the pinned
18
+ 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.3.0",
3
+ "version": "0.4.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.87.0",
32
- "@earendil-works/pi-coding-agent": ">=0.87.0",
33
- "@earendil-works/pi-tui": ">=0.87.0",
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",
34
34
  "typebox": "*"
35
35
  },
36
36
  "peerDependenciesMeta": {
@@ -45,12 +45,12 @@
45
45
  }
46
46
  },
47
47
  "dependencies": {
48
- "@oai404iao/pi-codex-runtime": "0.4.0"
48
+ "@oai404iao/pi-codex-runtime": "0.5.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@earendil-works/pi-ai": "0.87.1",
52
- "@earendil-works/pi-coding-agent": "0.87.1",
53
- "@earendil-works/pi-tui": "0.87.1",
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",
54
54
  "@types/node": "^26.2.0",
55
55
  "tsx": "^4.20.6",
56
56
  "typebox": "^1.1.24",
@@ -77,5 +77,5 @@
77
77
  "engines": {
78
78
  "node": ">=22.19.0"
79
79
  },
80
- "gitHead": "351c1943c7402b56f36abbe22fe45bb639bf5f81"
80
+ "gitHead": "8c96ca920ee51fe23c5c50d2cb2d8e1b304efe94"
81
81
  }
@@ -7,6 +7,9 @@ import { supportsImageInput, type ModelLike } from "@oai404iao/pi-codex-runtime/
7
7
  import {
8
8
  buildCodexJsonHeaders,
9
9
  hasCodexRequestAuth,
10
+ resolveResponsesUrl,
11
+ type ResponsesProtocol,
12
+ withResolvedAuthBaseUrl,
10
13
  } from "@oai404iao/pi-codex-runtime/internal/codex-http";
11
14
  import { createBackgroundImageJobs, panelBranch } from "./background-image-jobs.js";
12
15
  import { listResolvedModelProfiles } from "@oai404iao/pi-codex-runtime/internal/model-catalog/catalog";
@@ -20,8 +23,8 @@ import {
20
23
  } from "./tools/image-generation/storage.js";
21
24
  import { IMAGE_SAVE_DISPLAY_MESSAGE_TYPE, type SavedGeneratedImage } from "./tools/image-generation/types.js";
22
25
  import { projectRoot } from "./utils/images.js";
26
+ import { checkEndpointResponse, rememberResolvedEndpoint, reportEndpointFailure, requireEndpointCapability } from "@oai404iao/pi-codex-runtime/internal/endpoint-state";
23
27
 
24
- const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
25
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.";
26
29
  const IMAGE_GEN_ERROR_MESSAGE_TYPE = "codex-image-generation-error";
27
30
 
@@ -81,15 +84,13 @@ function registryModels(registry: ModelRegistryLike | undefined): ModelLike[] {
81
84
 
82
85
  function isConfiguredImageModel(model: ModelLike | undefined): boolean {
83
86
  if (!model || !supportsImageInput(model)) return false;
84
- const settings = loadModelSettings(model);
85
- return Boolean(
86
- settings.modelProfile?.effective.enabled
87
- && settings.imageGenerationImplementation,
88
- );
87
+ const settings = loadModelSettings({ ...model, baseUrl: undefined });
88
+ return Boolean(settings.modelProfile?.effective.enabled && settings.imageGenerationImplementation);
89
89
  }
90
90
 
91
91
  export function selectCodexImageModel(currentModel: ModelLike | undefined, registry: ModelRegistryLike | undefined): ModelLike | undefined {
92
- if (isConfiguredImageModel(currentModel)) return currentModel;
92
+ const currentSettings = currentModel && loadModelSettings({ ...currentModel, baseUrl: undefined });
93
+ if (currentModel && supportsImageInput(currentModel) && currentSettings?.imageGenerationImplementation) return currentModel;
93
94
  const discovered = registryModels(registry).find(isConfiguredImageModel);
94
95
  if (discovered) return discovered;
95
96
  for (const profile of listResolvedModelProfiles()) {
@@ -170,29 +171,19 @@ async function loadReferenceImage(cwd: string, rawPath: string): Promise<Referen
170
171
  return { path, mimeType: mimeTypeForPath(path), base64: buffer.toString("base64") };
171
172
  }
172
173
 
173
- function resolveCodexUrl(baseUrl: string | undefined, options?: { apiKeyMode?: boolean }): string {
174
- const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_CODEX_BASE_URL;
175
- const normalized = raw.replace(/\/+$/, "");
176
- if (options?.apiKeyMode) {
177
- if (normalized.endsWith("/responses")) return normalized;
178
- return `${normalized}/responses`;
179
- }
180
- if (normalized.endsWith("/codex/responses")) return normalized;
181
- if (normalized.endsWith("/codex")) return `${normalized}/responses`;
182
- return `${normalized}/codex/responses`;
183
- }
184
-
185
174
  function buildHeaders(
186
175
  model: Model<Api>,
187
176
  auth: { apiKey?: string; headers?: ProviderHeaders },
188
- options?: { apiKeyMode?: boolean },
177
+ endpoint: ResponsesProtocol,
178
+ codexRequestExtensions: boolean,
189
179
  ): Headers {
190
180
  const headers = buildCodexJsonHeaders({
191
181
  modelHeaders: model.headers,
192
182
  auth,
193
- apiKeyMode: options?.apiKeyMode ?? false,
183
+ endpoint,
184
+ codexRequestExtensions,
194
185
  });
195
- setProviderGeneratedHeader(headers, "OpenAI-Beta", "responses=experimental");
186
+ if (codexRequestExtensions || endpoint === "codex") setProviderGeneratedHeader(headers, "OpenAI-Beta", "responses=experimental");
196
187
  setProviderGeneratedHeader(headers, "accept", "text/event-stream");
197
188
  return headers;
198
189
  }
@@ -323,7 +314,8 @@ async function runBackgroundImageGeneration(pi: ExtensionAPI, ctx: ExtensionComm
323
314
  signal.throwIfAborted();
324
315
  const model = selectCodexImageModel(ctx.model as ModelLike | undefined, ctx.modelRegistry as ModelRegistryLike | undefined) as Model<Api> | undefined;
325
316
  if (!model) throw new Error("No image-capable model with an enabled model catalog profile is available.");
326
- const settings = loadModelSettings(model as ModelLike, ctx.cwd);
317
+ const settings = loadModelSettings({ ...model, baseUrl: undefined }, ctx.cwd);
318
+ if (!settings.imageGenerationImplementation) throw new Error("Image generation is disabled for the selected model.");
327
319
  if (!settings.enabled) throw new Error("pi-codex-minimal-tools is disabled.");
328
320
  if (settings.imageGenerationImplementation === "standalone") {
329
321
  const result = await standaloneImageGeneration({
@@ -333,6 +325,7 @@ async function runBackgroundImageGeneration(pi: ExtensionAPI, ctx: ExtensionComm
333
325
  cwd: ctx.cwd,
334
326
  model,
335
327
  modelRegistry: ctx.modelRegistry,
328
+ sessionManager: ctx.sessionManager,
336
329
  }, settings, signal, { callId: "standalone" });
337
330
  signal.throwIfAborted();
338
331
  const saved = result.details.saved;
@@ -364,6 +357,10 @@ async function runBackgroundImageGeneration(pi: ExtensionAPI, ctx: ExtensionComm
364
357
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
365
358
  signal.throwIfAborted();
366
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");
367
364
  if (!hasCodexRequestAuth({
368
365
  modelHeaders: model.headers,
369
366
  auth: { apiKey: auth.apiKey, headers: auth.headers },
@@ -378,21 +375,24 @@ async function runBackgroundImageGeneration(pi: ExtensionAPI, ctx: ExtensionComm
378
375
  responsesModel: model.id,
379
376
  imageModel: settings.imageModel,
380
377
  });
381
- const response = await fetch(resolveCodexUrl(model.baseUrl, { apiKeyMode: settings.apiKeyMode }), {
378
+ const response = await fetch(resolveResponsesUrl(auth.baseUrl ?? model.baseUrl, settings.responsesEndpoint), {
382
379
  signal,
383
380
  method: "POST",
384
381
  headers: buildHeaders(model, {
385
382
  apiKey: auth.apiKey,
386
383
  headers: auth.headers,
387
- }, { apiKeyMode: settings.apiKeyMode }),
384
+ }, settings.responsesEndpoint, settings.codexRequestExtensions),
388
385
  body: JSON.stringify(body),
389
386
  });
390
387
  signal.throwIfAborted();
391
- if (!response.ok) throw new Error(`Codex image generation failed: ${response.status} ${await response.text()}`);
388
+ await checkEndpointResponse(response, requestModel, sessionId, ["imageGeneration.hosted"], signal);
392
389
  const results: CodexImageResult[] = [];
393
390
  let responseId: string | undefined;
394
391
  let lastResponse: Record<string, unknown> | undefined;
395
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
396
  if (event.type === "response.created" && event.response && typeof event.response === "object") {
397
397
  lastResponse = event.response as Record<string, unknown>;
398
398
  const id = (event.response as { id?: unknown }).id;
package/src/index.ts CHANGED
@@ -24,12 +24,15 @@ export default function codexImagegen(pi: ExtensionAPI): void {
24
24
  return { createEventObserver: context => createImageCapture(context, sink) };
25
25
  },
26
26
  });
27
- addPackageTool(broker, "image_generation", () => {
28
- pi.registerTool(createImageGenerationToolDefinition({
29
- loadSettings: (cwd, model) => loadModelSettings(model, cwd),
30
- getCurrentTurnId: sessionId => currentCodexTurn(sessionId)?.turnId,
31
- hasProviderRuntime: () => broker.coreEnabled,
32
- }) as never);
27
+ const definition = createImageGenerationToolDefinition({
28
+ loadSettings: (cwd, model) => loadModelSettings(model, cwd),
29
+ getCurrentTurnId: sessionId => currentCodexTurn(sessionId)?.turnId,
30
+ hasProviderRuntime: () => broker.coreEnabled,
31
+ });
32
+ const parameters = { ...definition.parameters };
33
+ broker.ownedTools.set("image_generation", { parameters, description: definition.description });
34
+ addPackageTool(broker, "image_generation", exposure => {
35
+ pi.registerTool({ ...definition, parameters, exposure } as never);
33
36
  });
34
37
  registerBackgroundImageGenerationCommand(pi);
35
38
  }
@@ -0,0 +1,29 @@
1
+ import type { SavedImageInfo } from "../../utils/images.js";
2
+
3
+ export const imageGenerationOutputSchema = {
4
+ type: "object",
5
+ additionalProperties: false,
6
+ required: ["path", "image"],
7
+ properties: {
8
+ path: { type: "string" },
9
+ latestPath: { type: "string" },
10
+ image: {
11
+ type: "object",
12
+ additionalProperties: false,
13
+ required: ["type", "data", "mimeType"],
14
+ properties: {
15
+ type: { type: "string", const: "image" },
16
+ data: { type: "string", description: "Base64 image data. Use image(result.image) to show it to the model." },
17
+ mimeType: { type: "string" },
18
+ },
19
+ },
20
+ },
21
+ };
22
+
23
+ export function imageGenerationOutput(saved: SavedImageInfo, base64: string) {
24
+ return {
25
+ path: saved.path,
26
+ ...(saved.latestPath ? { latestPath: saved.latestPath } : {}),
27
+ image: { type: "image" as const, data: base64, mimeType: saved.mimeType },
28
+ };
29
+ }
@@ -3,14 +3,17 @@ import { readFile } from "node:fs/promises";
3
3
  import { extname, isAbsolute, resolve } from "node:path";
4
4
  import { buildSessionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
5
5
  import { saveBase64Image } from "../utils/images.js";
6
+ import { imageGenerationOutput, imageGenerationOutputSchema } from "./image-generation/output.js";
6
7
  import type { CodexMinimalToolsSettings } from "@oai404iao/pi-codex-runtime/internal/settings";
7
8
  import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
8
9
  import {
9
10
  buildCodexJsonHeaders,
10
11
  hasCodexRequestAuth,
11
12
  resolveCodexApiEndpoint,
13
+ withResolvedAuthBaseUrl,
12
14
  } from "@oai404iao/pi-codex-runtime/internal/codex-http";
13
15
  import { loadModelSettings, type ResolvedCodexModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
16
+ import { checkEndpointResponse, rememberResolvedEndpoint, requireEndpointCapability } from "@oai404iao/pi-codex-runtime/internal/endpoint-state";
14
17
 
15
18
  export interface ImageGenerationInput {
16
19
  prompt?: string;
@@ -50,12 +53,24 @@ async function urlToBase64(url: string, signal?: AbortSignal): Promise<string> {
50
53
  return buffer.toString("base64");
51
54
  }
52
55
 
53
- export async function directImageGeneration(input: ImageGenerationInput, cwd: string, settings: CodexMinimalToolsSettings, signal?: AbortSignal) {
56
+ export async function directImageGeneration(input: ImageGenerationInput, ctx: ImageGenerationToolContext, settings: CodexMinimalToolsSettings, signal?: AbortSignal) {
54
57
  if (!settings.imageGeneration) throw new Error("Image generation is disabled by the global imageGeneration setting.");
55
- if (!settings.directImageApiFallback) throw new Error("Direct Images API fallback is disabled. Use native openai or openai-codex handling, or enable directImageApiFallback.");
56
- const apiKey = process.env.OPENAI_API_KEY;
57
- if (!apiKey) throw new Error("OPENAI_API_KEY is required for direct image_generation fallback.");
58
+ if (!settings.directImageApiFallback) throw new Error("Direct Images API fallback is disabled. Use the configured image implementation, or enable directImageApiFallback.");
58
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;
59
74
  const body: Record<string, unknown> = {
60
75
  model: settings.imageModel,
61
76
  prompt: input.prompt,
@@ -64,21 +79,23 @@ export async function directImageGeneration(input: ImageGenerationInput, cwd: st
64
79
  if (input.quality && input.quality !== "auto") body.quality = input.quality;
65
80
  if (input.background && input.background !== "auto") body.background = input.background;
66
81
  if (input.output_format) body.output_format = input.output_format;
67
- const response = await fetch("https://api.openai.com/v1/images/generations", {
82
+ const response = await fetch(resolveCodexApiEndpoint(auth.baseUrl ?? model.baseUrl, endpoint, "images/generations"), {
68
83
  method: "POST",
69
- headers: { Authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
84
+ headers: buildCodexJsonHeaders({ modelHeaders: model.headers, auth, endpoint, codexRequestExtensions: settings.codexRequestExtensions }),
70
85
  body: JSON.stringify(body),
71
86
  signal,
72
87
  });
73
- if (!response.ok) throw new Error(`OpenAI Images API failed: ${response.status} ${await response.text()}`);
88
+ await checkEndpointResponse(response, requestModel, sessionId, ["imageGeneration.standalone"], signal);
74
89
  const json = await response.json() as { data?: Array<{ b64_json?: string; url?: string; revised_prompt?: string }> };
75
90
  const first = json.data?.[0];
76
91
  const base64 = first?.b64_json ?? (first?.url ? await urlToBase64(first.url, signal) : undefined);
77
92
  if (!base64) throw new Error("OpenAI Images API returned no image data.");
78
- const saved = await saveBase64Image({ base64, callId: "direct", cwd, format: input.output_format, responseId: settings.imageModel, settings });
93
+ signal?.throwIfAborted();
94
+ const saved = await saveBase64Image({ base64, callId: "direct", cwd: ctx.cwd, format: input.output_format, responseId: settings.imageModel, settings });
79
95
  return {
80
96
  content: [{ type: "text", text: `Generated image with ${settings.imageModel}; saved to ${saved.path}${saved.latestPath ? ` (latest: ${saved.latestPath})` : ""}.` }],
81
97
  details: { saved, revisedPrompt: first?.revised_prompt, mode: "direct-images-api" },
98
+ structuredContent: imageGenerationOutput(saved, base64),
82
99
  };
83
100
  }
84
101
 
@@ -87,7 +104,7 @@ interface ImageGenerationToolContext {
87
104
  model?: Model<Api>;
88
105
  modelRegistry?: {
89
106
  getApiKeyAndHeaders(model: Model<Api>): Promise<
90
- | { ok: true; apiKey?: string; headers?: ProviderHeaders }
107
+ | { ok: true; apiKey?: string; headers?: ProviderHeaders; baseUrl?: string }
91
108
  | { ok: false; error: string }
92
109
  >;
93
110
  };
@@ -183,6 +200,10 @@ export async function standaloneImageGeneration(
183
200
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
184
201
  signal?.throwIfAborted();
185
202
  if (!auth.ok) throw new Error(auth.error);
203
+ const requestModel = withResolvedAuthBaseUrl(model, auth);
204
+ const sessionId = ctx.sessionManager?.getSessionId?.();
205
+ rememberResolvedEndpoint(model, requestModel, sessionId);
206
+ requireEndpointCapability(settings.endpoint_config, requestModel, sessionId, "imageGeneration.standalone");
186
207
  if (!hasCodexRequestAuth({
187
208
  modelHeaders: model.headers,
188
209
  auth: { apiKey: auth.apiKey, headers: auth.headers },
@@ -196,19 +217,20 @@ export async function standaloneImageGeneration(
196
217
  signal?.throwIfAborted();
197
218
  const response = await fetch(
198
219
  resolveCodexApiEndpoint(
199
- model.baseUrl,
200
- settings.apiKeyMode,
220
+ auth.baseUrl ?? model.baseUrl,
221
+ settings.responsesEndpoint,
201
222
  edit ? "images/edits" : "images/generations",
202
223
  ),
203
224
  {
204
225
  method: "POST",
205
226
  headers: buildCodexJsonHeaders({
227
+ codexRequestExtensions: settings.codexRequestExtensions,
206
228
  modelHeaders: model.headers,
207
229
  auth: { apiKey: auth.apiKey, headers: auth.headers },
208
- apiKeyMode: settings.apiKeyMode,
209
- extraHeaders: {
230
+ endpoint: settings.responsesEndpoint,
231
+ extraHeaders: settings.codexRequestExtensions ? {
210
232
  "x-codex-image-turn-id": turnId,
211
- },
233
+ } : undefined,
212
234
  }),
213
235
  body: JSON.stringify({
214
236
  model: settings.imageModel,
@@ -222,9 +244,7 @@ export async function standaloneImageGeneration(
222
244
  },
223
245
  );
224
246
  signal?.throwIfAborted();
225
- if (!response.ok) {
226
- throw new Error(`Standalone image generation failed: HTTP ${response.status}: ${await response.text()}`);
227
- }
247
+ await checkEndpointResponse(response, requestModel, sessionId, ["imageGeneration.standalone"], signal);
228
248
  const result = await response.json() as {
229
249
  data?: Array<{ b64_json?: string }>;
230
250
  };
@@ -246,6 +266,7 @@ export async function standaloneImageGeneration(
246
266
  { type: "text", text: `Generated image with ${settings.imageModel}; saved to ${saved.path}${saved.latestPath ? ` (latest: ${saved.latestPath})` : ""}.` },
247
267
  ],
248
268
  details: { saved, mode: "standalone-images-api" },
269
+ structuredContent: imageGenerationOutput(saved, base64),
249
270
  };
250
271
  }
251
272
 
@@ -260,12 +281,14 @@ export function createImageGenerationToolDefinition(options: {
260
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>.",
261
282
  promptSnippet: "Generate or edit images with the implementation selected by the current model profile.",
262
283
  parameters: imageGenerationToolSchema,
284
+ outputSchema: imageGenerationOutputSchema,
263
285
  async execute(toolCallId: string, params: ImageGenerationInput, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ImageGenerationToolContext) {
264
286
  const cwd = ctx?.cwd ?? process.cwd();
265
- const settings = options.loadSettings?.(cwd, ctx.model) ?? loadModelSettings(ctx.model, cwd);
287
+ const profileModel = ctx.model ? { ...ctx.model, baseUrl: "" } : undefined;
288
+ const settings = options.loadSettings?.(cwd, profileModel) ?? loadModelSettings(profileModel, cwd);
266
289
  const resolvedSettings = "modelProfile" in settings
267
290
  ? settings as ResolvedCodexModelSettings
268
- : loadModelSettings(ctx.model, cwd, settings);
291
+ : loadModelSettings(profileModel, cwd, settings);
269
292
  if (!resolvedSettings.imageGeneration) {
270
293
  throw new Error("Image generation is disabled by the global setting or current model profile.");
271
294
  }
@@ -276,12 +299,13 @@ export function createImageGenerationToolDefinition(options: {
276
299
  turnId: options.getCurrentTurnId?.(sessionId),
277
300
  });
278
301
  }
279
- if (settings?.directImageApiFallback) return directImageGeneration(params, cwd, settings, signal);
302
+ if (settings?.directImageApiFallback) return directImageGeneration(params, { ...ctx, cwd }, settings, signal);
280
303
  if (options.hasProviderRuntime?.() === false) {
281
304
  throw new Error("Hosted image_generation requires pi-codex-core. Use a catalog-supported standalone profile or explicitly enable directImageApiFallback.");
282
305
  }
283
306
  return {
284
- content: [{ type: "text", text: "image_generation should be handled by the current model profile. If no hosted or standalone implementation is configured, enable directImageApiFallback with OPENAI_API_KEY." }],
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." }],
285
309
  details: { phase: "native-provider", nativeTool: "image_generation" },
286
310
  };
287
311
  },