pi-openai-toolkit 0.14.4 → 0.14.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,243 +1,249 @@
1
- import { getAgentDir, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { loadToolkitConfig } from "../config";
3
- import { resolveResponsesEnvironment } from "../runtime";
4
- import {
5
- copyImageToExplicitPath,
6
- prepareExplicitOutputPath,
7
- saveCanonicalImage,
8
- } from "./artifacts";
9
- import { requestGeneratedImage } from "./client";
10
- import { isImageGenerationEnabledForModel } from "./eligibility";
11
- import {
12
- buildImageGenerationRequest,
13
- normalizeGenerateImageParams,
14
- } from "./protocol";
15
- import {
16
- clearPreparedReferences,
17
- prepareReferenceImages,
18
- } from "./references";
19
- import {
20
- IMAGE_GENERATION_CAPABLE_APIS,
21
- IMAGE_GENERATION_MIME_TYPE,
22
- IMAGE_GENERATION_MODEL,
23
- ImageGenerationError,
24
- sanitizeImageDiagnostic,
25
- type GenerateImageParams,
26
- type ImageGenerationExecutionResult,
27
- type ImageGenerationFailureCode,
28
- type ParsedGeneratedImage,
29
- } from "./types";
30
-
31
- export type ImageGenerationServiceDependencies = {
32
- loadConfig: typeof loadToolkitConfig;
33
- resolveRuntime: typeof resolveResponsesEnvironment;
34
- getAgentDir: typeof getAgentDir;
35
- prepareOutput: typeof prepareExplicitOutputPath;
36
- prepareReferences: typeof prepareReferenceImages;
37
- requestImage: typeof requestGeneratedImage;
38
- saveCanonical: typeof saveCanonicalImage;
39
- copyExplicit: typeof copyImageToExplicitPath;
40
- clearReferences: typeof clearPreparedReferences;
41
- };
42
-
43
- const DEFAULT_SERVICE_DEPS: ImageGenerationServiceDependencies = {
44
- loadConfig: loadToolkitConfig,
45
- resolveRuntime: resolveResponsesEnvironment,
46
- getAgentDir,
47
- prepareOutput: prepareExplicitOutputPath,
48
- prepareReferences: prepareReferenceImages,
49
- requestImage: requestGeneratedImage,
50
- saveCanonical: saveCanonicalImage,
51
- copyExplicit: copyImageToExplicitPath,
52
- clearReferences: clearPreparedReferences,
53
- };
54
-
55
- function runtimeFailure(
56
- reason: string,
57
- errorMessage?: string,
58
- ): ImageGenerationError {
59
- switch (reason) {
60
- case "disabled":
61
- return new ImageGenerationError("disabled", "Image generation is disabled.");
62
- case "unsupported-api":
63
- case "missing-model":
64
- return new ImageGenerationError(
65
- "unsupported-model",
66
- "Image generation is unavailable for the current model.",
67
- );
68
- case "missing-api-key":
69
- case "auth-resolution-failed":
70
- return new ImageGenerationError(
71
- "authentication",
72
- sanitizeImageDiagnostic(
73
- errorMessage,
74
- "Unable to resolve authentication for image generation with the current model.",
75
- ),
76
- );
77
- default:
78
- return new ImageGenerationError(
79
- "missing-runtime",
80
- "Unable to resolve the current model's Responses endpoint for image generation.",
81
- );
82
- }
83
- }
84
-
85
- function throwIfAborted(signal: AbortSignal | undefined): void {
86
- if (signal?.aborted) {
87
- throw new ImageGenerationError("aborted", "Image generation was cancelled.");
88
- }
89
- }
90
-
91
- function clientFailureCode(reason: string): ImageGenerationFailureCode {
92
- switch (reason) {
93
- case "aborted":
94
- case "timeout":
95
- case "authentication":
96
- case "rate-limit":
97
- case "request-rejected":
98
- case "backend-unavailable":
99
- case "network":
100
- case "oversized-response":
101
- case "malformed-response":
102
- case "no-image":
103
- return reason;
104
- default:
105
- return "network";
106
- }
107
- }
108
-
109
- function formatResultText(args: {
110
- artifactPath: string;
111
- outputPath?: string;
112
- warning?: string;
113
- edited: boolean;
114
- width: number;
115
- height: number;
116
- }): string {
117
- const lines = [
118
- `${args.edited ? "Edited" : "Generated"} PNG image (${args.width}x${args.height}).`,
119
- `Artifact: ${args.artifactPath}`,
120
- ];
121
- if (args.outputPath) lines.push(`Copied to: ${args.outputPath}`);
122
- if (args.warning) lines.push(`Warning: ${args.warning}`);
123
- return lines.join("\n");
124
- }
125
-
126
- export function createImageGenerationExecutor(
127
- deps: ImageGenerationServiceDependencies = DEFAULT_SERVICE_DEPS,
128
- ): (args: {
129
- params: GenerateImageParams;
130
- toolCallId: string;
131
- signal?: AbortSignal;
132
- ctx: ExtensionContext;
133
- }) => Promise<ImageGenerationExecutionResult> {
134
- return async (args) => {
135
- const params = normalizeGenerateImageParams(args.params);
136
- throwIfAborted(args.signal);
137
- const { config } = deps.loadConfig();
138
- if (!isImageGenerationEnabledForModel(args.ctx.model, config.imageGeneration)) {
139
- throw new ImageGenerationError(
140
- "unsupported-model",
141
- "Image generation is not enabled for the current provider/model-id.",
142
- );
143
- }
144
-
145
- const runtimeResolution = await deps.resolveRuntime(args.ctx, {
146
- enabled: config.imageGeneration.enabled,
147
- responsesApis: IMAGE_GENERATION_CAPABLE_APIS,
148
- });
149
- if (!runtimeResolution.ok) {
150
- throw runtimeFailure(runtimeResolution.reason, runtimeResolution.errorMessage);
151
- }
152
-
153
- const agentDir = deps.getAgentDir();
154
- const explicitOutput = await deps.prepareOutput({
155
- rawPath: params.outputPath,
156
- agentDir,
157
- ctx: args.ctx,
158
- signal: args.signal,
159
- });
160
-
161
- const references = await deps.prepareReferences({
162
- paths: params.referenceImagePaths,
163
- ctx: args.ctx,
164
- signal: args.signal,
165
- });
166
- let generated: ParsedGeneratedImage | undefined;
167
- try {
168
- throwIfAborted(args.signal);
169
- const body = buildImageGenerationRequest({
170
- routingModel: runtimeResolution.runtime.model,
171
- params,
172
- references,
173
- });
174
- const response = await deps.requestImage({
175
- runtime: runtimeResolution.runtime,
176
- body,
177
- signal: args.signal,
178
- });
179
- if (!response.ok) {
180
- throw new ImageGenerationError(
181
- clientFailureCode(response.reason),
182
- response.errorMessage,
183
- );
184
- }
185
- generated = response.image;
186
-
187
- const artifactPath = await deps.saveCanonical({
188
- bytes: generated.bytes,
189
- agentDir,
190
- sessionId: args.ctx.sessionManager.getSessionId(),
191
- imageCallId: generated.imageCallId || args.toolCallId,
192
- });
193
-
194
- let outputPath: string | undefined;
195
- let warning: string | undefined;
196
- if (explicitOutput) {
197
- try {
198
- await deps.copyExplicit({ bytes: generated.bytes, plan: explicitOutput });
199
- outputPath = explicitOutput.path;
200
- } catch (error) {
201
- warning = sanitizeImageDiagnostic(
202
- error instanceof Error ? error.message : error,
203
- `The generated image was saved to the canonical artifact, but copying to ${explicitOutput.path} failed.`,
204
- );
205
- }
206
- }
207
-
208
- const edited = references.length > 0;
209
- const details = {
210
- artifactPath,
211
- ...(outputPath ? { outputPath } : {}),
212
- routingModel: `${runtimeResolution.runtime.provider}/${runtimeResolution.runtime.model}`,
213
- imageModel: IMAGE_GENERATION_MODEL,
214
- imageCallId: generated.imageCallId,
215
- ...(generated.responseId ? { responseId: generated.responseId } : {}),
216
- mimeType: IMAGE_GENERATION_MIME_TYPE,
217
- byteCount: generated.bytes.length,
218
- width: generated.width,
219
- height: generated.height,
220
- edited,
221
- referenceCount: references.length,
222
- ...(generated.revisedPrompt ? { revisedPrompt: generated.revisedPrompt } : {}),
223
- ...(warning ? { warning } : {}),
224
- };
225
- return {
226
- details,
227
- text: formatResultText({
228
- artifactPath,
229
- outputPath,
230
- warning,
231
- edited,
232
- width: generated.width,
233
- height: generated.height,
234
- }),
235
- };
236
- } finally {
237
- deps.clearReferences(references);
238
- generated?.bytes.fill(0);
239
- }
240
- };
241
- }
242
-
243
- export const executeImageGeneration = createImageGenerationExecutor();
1
+ import { getAgentDir, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { loadToolkitConfig } from "../config";
3
+ import { resolveResponsesEnvironment } from "../runtime";
4
+ import {
5
+ copyImageToExplicitPath,
6
+ prepareExplicitOutputPath,
7
+ saveCanonicalImage,
8
+ } from "./artifacts";
9
+ import { requestGeneratedImage } from "./client";
10
+ import { isImageGenerationEnabledForModel } from "./eligibility";
11
+ import {
12
+ buildImageGenerationRequest,
13
+ normalizeGenerateImageParams,
14
+ selectImageGenerationModel,
15
+ } from "./protocol";
16
+ import {
17
+ clearPreparedReferences,
18
+ prepareReferenceImages,
19
+ } from "./references";
20
+ import {
21
+ IMAGE_GENERATION_CAPABLE_APIS,
22
+ IMAGE_GENERATION_MIME_TYPE,
23
+ ImageGenerationError,
24
+ sanitizeImageDiagnostic,
25
+ type GenerateImageParams,
26
+ type ImageGenerationExecutionResult,
27
+ type ImageGenerationFailureCode,
28
+ type ParsedGeneratedImage,
29
+ } from "./types";
30
+
31
+ export type ImageGenerationServiceDependencies = {
32
+ loadConfig: typeof loadToolkitConfig;
33
+ resolveRuntime: typeof resolveResponsesEnvironment;
34
+ getAgentDir: typeof getAgentDir;
35
+ prepareOutput: typeof prepareExplicitOutputPath;
36
+ prepareReferences: typeof prepareReferenceImages;
37
+ requestImage: typeof requestGeneratedImage;
38
+ saveCanonical: typeof saveCanonicalImage;
39
+ copyExplicit: typeof copyImageToExplicitPath;
40
+ clearReferences: typeof clearPreparedReferences;
41
+ };
42
+
43
+ const DEFAULT_SERVICE_DEPS: ImageGenerationServiceDependencies = {
44
+ loadConfig: loadToolkitConfig,
45
+ resolveRuntime: resolveResponsesEnvironment,
46
+ getAgentDir,
47
+ prepareOutput: prepareExplicitOutputPath,
48
+ prepareReferences: prepareReferenceImages,
49
+ requestImage: requestGeneratedImage,
50
+ saveCanonical: saveCanonicalImage,
51
+ copyExplicit: copyImageToExplicitPath,
52
+ clearReferences: clearPreparedReferences,
53
+ };
54
+
55
+ function runtimeFailure(
56
+ reason: string,
57
+ errorMessage?: string,
58
+ ): ImageGenerationError {
59
+ switch (reason) {
60
+ case "disabled":
61
+ return new ImageGenerationError("disabled", "Image generation is disabled.");
62
+ case "unsupported-api":
63
+ case "missing-model":
64
+ return new ImageGenerationError(
65
+ "unsupported-model",
66
+ "Image generation is unavailable for the current model.",
67
+ );
68
+ case "missing-api-key":
69
+ case "auth-resolution-failed":
70
+ return new ImageGenerationError(
71
+ "authentication",
72
+ sanitizeImageDiagnostic(
73
+ errorMessage,
74
+ "Unable to resolve authentication for image generation with the current model.",
75
+ ),
76
+ );
77
+ default:
78
+ return new ImageGenerationError(
79
+ "missing-runtime",
80
+ "Unable to resolve the current model's Responses endpoint for image generation.",
81
+ );
82
+ }
83
+ }
84
+
85
+ function throwIfAborted(signal: AbortSignal | undefined): void {
86
+ if (signal?.aborted) {
87
+ throw new ImageGenerationError("aborted", "Image generation was cancelled.");
88
+ }
89
+ }
90
+
91
+ function clientFailureCode(reason: string): ImageGenerationFailureCode {
92
+ switch (reason) {
93
+ case "aborted":
94
+ case "timeout":
95
+ case "authentication":
96
+ case "rate-limit":
97
+ case "request-rejected":
98
+ case "backend-unavailable":
99
+ case "network":
100
+ case "oversized-response":
101
+ case "malformed-response":
102
+ case "no-image":
103
+ return reason;
104
+ default:
105
+ return "network";
106
+ }
107
+ }
108
+
109
+ function formatResultText(args: {
110
+ artifactPath: string;
111
+ outputPath?: string;
112
+ warning?: string;
113
+ edited: boolean;
114
+ width: number;
115
+ height: number;
116
+ }): string {
117
+ const lines = [
118
+ `${args.edited ? "Edited" : "Generated"} PNG image (${args.width}x${args.height}).`,
119
+ `Artifact: ${args.artifactPath}`,
120
+ ];
121
+ if (args.outputPath) lines.push(`Copied to: ${args.outputPath}`);
122
+ if (args.warning) lines.push(`Warning: ${args.warning}`);
123
+ return lines.join("\n");
124
+ }
125
+
126
+ export function createImageGenerationExecutor(
127
+ deps: ImageGenerationServiceDependencies = DEFAULT_SERVICE_DEPS,
128
+ ): (args: {
129
+ params: GenerateImageParams;
130
+ toolCallId: string;
131
+ signal?: AbortSignal;
132
+ ctx: ExtensionContext;
133
+ }) => Promise<ImageGenerationExecutionResult> {
134
+ return async (args) => {
135
+ const params = normalizeGenerateImageParams(args.params);
136
+ throwIfAborted(args.signal);
137
+ const { config } = deps.loadConfig();
138
+ if (!isImageGenerationEnabledForModel(args.ctx.model, config.imageGeneration)) {
139
+ throw new ImageGenerationError(
140
+ "unsupported-model",
141
+ "Image generation is not enabled for the current provider/model-id.",
142
+ );
143
+ }
144
+
145
+ const imageModel = selectImageGenerationModel({
146
+ requestedModel: params.model,
147
+ configuredModels: config.imageGeneration.models,
148
+ });
149
+
150
+ const runtimeResolution = await deps.resolveRuntime(args.ctx, {
151
+ enabled: config.imageGeneration.enabled,
152
+ responsesApis: IMAGE_GENERATION_CAPABLE_APIS,
153
+ });
154
+ if (!runtimeResolution.ok) {
155
+ throw runtimeFailure(runtimeResolution.reason, runtimeResolution.errorMessage);
156
+ }
157
+
158
+ const agentDir = deps.getAgentDir();
159
+ const explicitOutput = await deps.prepareOutput({
160
+ rawPath: params.outputPath,
161
+ agentDir,
162
+ ctx: args.ctx,
163
+ signal: args.signal,
164
+ });
165
+
166
+ const references = await deps.prepareReferences({
167
+ paths: params.referenceImagePaths,
168
+ ctx: args.ctx,
169
+ signal: args.signal,
170
+ });
171
+ let generated: ParsedGeneratedImage | undefined;
172
+ try {
173
+ throwIfAborted(args.signal);
174
+ const body = buildImageGenerationRequest({
175
+ routingModel: runtimeResolution.runtime.model,
176
+ imageModel,
177
+ params,
178
+ references,
179
+ });
180
+ const response = await deps.requestImage({
181
+ runtime: runtimeResolution.runtime,
182
+ body,
183
+ signal: args.signal,
184
+ });
185
+ if (!response.ok) {
186
+ throw new ImageGenerationError(
187
+ clientFailureCode(response.reason),
188
+ response.errorMessage,
189
+ );
190
+ }
191
+ generated = response.image;
192
+
193
+ const artifactPath = await deps.saveCanonical({
194
+ bytes: generated.bytes,
195
+ agentDir,
196
+ sessionId: args.ctx.sessionManager.getSessionId(),
197
+ imageCallId: generated.imageCallId || args.toolCallId,
198
+ });
199
+
200
+ let outputPath: string | undefined;
201
+ let warning: string | undefined;
202
+ if (explicitOutput) {
203
+ try {
204
+ await deps.copyExplicit({ bytes: generated.bytes, plan: explicitOutput });
205
+ outputPath = explicitOutput.path;
206
+ } catch (error) {
207
+ warning = sanitizeImageDiagnostic(
208
+ error instanceof Error ? error.message : error,
209
+ `The generated image was saved to the canonical artifact, but copying to ${explicitOutput.path} failed.`,
210
+ );
211
+ }
212
+ }
213
+
214
+ const edited = references.length > 0;
215
+ const details = {
216
+ artifactPath,
217
+ ...(outputPath ? { outputPath } : {}),
218
+ routingModel: `${runtimeResolution.runtime.provider}/${runtimeResolution.runtime.model}`,
219
+ imageModel,
220
+ imageCallId: generated.imageCallId,
221
+ ...(generated.responseId ? { responseId: generated.responseId } : {}),
222
+ mimeType: IMAGE_GENERATION_MIME_TYPE,
223
+ byteCount: generated.bytes.length,
224
+ width: generated.width,
225
+ height: generated.height,
226
+ edited,
227
+ referenceCount: references.length,
228
+ ...(generated.revisedPrompt ? { revisedPrompt: generated.revisedPrompt } : {}),
229
+ ...(warning ? { warning } : {}),
230
+ };
231
+ return {
232
+ details,
233
+ text: formatResultText({
234
+ artifactPath,
235
+ outputPath,
236
+ warning,
237
+ edited,
238
+ width: generated.width,
239
+ height: generated.height,
240
+ }),
241
+ };
242
+ } finally {
243
+ deps.clearReferences(references);
244
+ generated?.bytes.fill(0);
245
+ }
246
+ };
247
+ }
248
+
249
+ export const executeImageGeneration = createImageGenerationExecutor();