@oai404iao/pi-codex-imagegen 0.2.1 → 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 +1 -1
- package/README.md +32 -2
- package/THIRD_PARTY_NOTICES.md +3 -2
- package/package.json +9 -9
- package/src/background-image-generation.ts +26 -26
- package/src/index.ts +9 -6
- package/src/tools/image-generation/output.ts +29 -0
- package/src/tools/image-generation.ts +45 -21
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
# pi-codex-imagegen
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
See the package guide below for release status; source edits do not publish artifacts.
|
|
4
4
|
|
|
5
|
-
Peer floor
|
|
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.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Source and license notices
|
|
2
2
|
|
|
3
|
-
This
|
|
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
|
|
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
|
+
"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.
|
|
32
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
33
|
-
"@earendil-works/pi-tui": ">=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.
|
|
48
|
+
"@oai404iao/pi-codex-runtime": "0.5.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@earendil-works/pi-ai": "0.
|
|
52
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
53
|
-
"@earendil-works/pi-tui": "0.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
177
|
+
endpoint: ResponsesProtocol,
|
|
178
|
+
codexRequestExtensions: boolean,
|
|
189
179
|
): Headers {
|
|
190
180
|
const headers = buildCodexJsonHeaders({
|
|
191
181
|
modelHeaders: model.headers,
|
|
192
182
|
auth,
|
|
193
|
-
|
|
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
|
|
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(
|
|
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
|
-
},
|
|
384
|
+
}, settings.responsesEndpoint, settings.codexRequestExtensions),
|
|
388
385
|
body: JSON.stringify(body),
|
|
389
386
|
});
|
|
390
387
|
signal.throwIfAborted();
|
|
391
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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,
|
|
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
|
|
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(
|
|
82
|
+
const response = await fetch(resolveCodexApiEndpoint(auth.baseUrl ?? model.baseUrl, endpoint, "images/generations"), {
|
|
68
83
|
method: "POST",
|
|
69
|
-
headers: {
|
|
84
|
+
headers: buildCodexJsonHeaders({ modelHeaders: model.headers, auth, endpoint, codexRequestExtensions: settings.codexRequestExtensions }),
|
|
70
85
|
body: JSON.stringify(body),
|
|
71
86
|
signal,
|
|
72
87
|
});
|
|
73
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
},
|