pi-openai-toolkit 0.17.3 → 0.19.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.
@@ -1,4 +1,7 @@
1
1
  export const IMAGE_GENERATION_TOOL_NAME = "openai_generate_image";
2
+ /** Image formats accepted from independent Images API responses. */
3
+ export const IMAGE_GENERATION_MIME_TYPES = ["image/png", "image/jpeg", "image/webp"] as const;
4
+ /** Backward-compatible default format for callers that construct PNG-only fixtures. */
2
5
  export const IMAGE_GENERATION_MIME_TYPE = "image/png" as const;
3
6
  export const IMAGE_GENERATION_CAPABLE_APIS = [
4
7
  "openai-responses",
@@ -23,15 +26,28 @@ export const IMAGE_REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
23
26
  export const MAX_IMAGE_PATH_CHARS = 4096;
24
27
  export const MAX_IMAGE_DIAGNOSTIC_CHARS = 4096;
25
28
  export const MAX_IMAGE_IDENTIFIER_CHARS = 256;
26
- /** Upper bound for a configured or requested `image_generation` model id. */
29
+ /** Upper bound for a configured or requested Images API model id. */
27
30
  export const MAX_IMAGE_MODEL_ID_CHARS = 256;
31
+ export const MAX_IMAGE_BATCH_SIZE = 10;
28
32
  export const MAX_IMAGE_DIMENSION = 100_000;
29
33
 
30
34
  export type ImageGenerationCapableApi = (typeof IMAGE_GENERATION_CAPABLE_APIS)[number];
31
35
  export type ImageGenerationSize = (typeof IMAGE_GENERATION_SIZES)[number];
32
36
  export type ImageGenerationQuality = (typeof IMAGE_GENERATION_QUALITIES)[number];
33
37
  export type ImageGenerationAction = "generate" | "edit";
34
- export type ReferenceImageMimeType = "image/png" | "image/jpeg" | "image/webp";
38
+ export type ImageGenerationMimeType = (typeof IMAGE_GENERATION_MIME_TYPES)[number];
39
+ export type ReferenceImageMimeType = ImageGenerationMimeType;
40
+
41
+ export function imageFileExtension(mimeType: ImageGenerationMimeType): "png" | "jpg" | "webp" {
42
+ switch (mimeType) {
43
+ case "image/png":
44
+ return "png";
45
+ case "image/jpeg":
46
+ return "jpg";
47
+ case "image/webp":
48
+ return "webp";
49
+ }
50
+ }
35
51
 
36
52
  export type ImageGenerationModel = {
37
53
  api?: string;
@@ -45,8 +61,10 @@ export type GenerateImageParams = {
45
61
  outputPath?: string | null;
46
62
  size?: ImageGenerationSize;
47
63
  quality?: ImageGenerationQuality;
48
- /** Optional bare image model id that must exist in `imageGeneration.models`. */
64
+ /** Optional bare image model id that must exist in the configured image model allowlist. */
49
65
  model?: string | null;
66
+ /** Number of independent image requests to run concurrently. */
67
+ batchSize?: number | null;
50
68
  };
51
69
 
52
70
  export type NormalizedGenerateImageParams = {
@@ -56,6 +74,7 @@ export type NormalizedGenerateImageParams = {
56
74
  size: ImageGenerationSize;
57
75
  quality: ImageGenerationQuality;
58
76
  action: ImageGenerationAction;
77
+ batchSize: number;
59
78
  /** Trimmed requested model id, or undefined when the caller omitted it. */
60
79
  model?: string;
61
80
  };
@@ -68,6 +87,7 @@ export type PreparedReferenceImage = {
68
87
 
69
88
  export type ParsedGeneratedImage = {
70
89
  bytes: Buffer;
90
+ mimeType: ImageGenerationMimeType;
71
91
  imageCallId: string;
72
92
  responseId?: string;
73
93
  revisedPrompt?: string;
@@ -82,7 +102,7 @@ export type ImageGenerationDetails = {
82
102
  imageModel: string;
83
103
  imageCallId: string;
84
104
  responseId?: string;
85
- mimeType: typeof IMAGE_GENERATION_MIME_TYPE;
105
+ mimeType: ImageGenerationMimeType;
86
106
  byteCount: number;
87
107
  width: number;
88
108
  height: number;
@@ -92,8 +112,25 @@ export type ImageGenerationDetails = {
92
112
  warning?: string;
93
113
  };
94
114
 
115
+ export type ImageGenerationBatchFailure = {
116
+ index: number;
117
+ code: ImageGenerationFailureCode;
118
+ message: string;
119
+ };
120
+
121
+ export type ImageGenerationBatchDetails = {
122
+ batch: true;
123
+ requestedCount: number;
124
+ succeededCount: number;
125
+ failedCount: number;
126
+ images: ImageGenerationDetails[];
127
+ failures?: ImageGenerationBatchFailure[];
128
+ };
129
+
130
+ export type ImageGenerationToolDetails = ImageGenerationDetails | ImageGenerationBatchDetails;
131
+
95
132
  export type ImageGenerationExecutionResult = {
96
- details: ImageGenerationDetails;
133
+ details: ImageGenerationToolDetails;
97
134
  text: string;
98
135
  };
99
136
 
@@ -120,6 +157,35 @@ export type ImageGenerationFailureCode =
120
157
  | "no-image"
121
158
  | "artifact-write-failed";
122
159
 
160
+ function isImageGenerationFailureCode(value: unknown): value is ImageGenerationFailureCode {
161
+ switch (value) {
162
+ case "aborted":
163
+ case "timeout":
164
+ case "disabled":
165
+ case "unsupported-model":
166
+ case "missing-runtime":
167
+ case "invalid-parameters":
168
+ case "reference-input-invalid":
169
+ case "reference-upload-approval-required":
170
+ case "reference-upload-declined":
171
+ case "output-path-invalid":
172
+ case "output-path-approval-required":
173
+ case "output-path-declined":
174
+ case "authentication":
175
+ case "rate-limit":
176
+ case "request-rejected":
177
+ case "backend-unavailable":
178
+ case "network":
179
+ case "oversized-response":
180
+ case "malformed-response":
181
+ case "no-image":
182
+ case "artifact-write-failed":
183
+ return true;
184
+ default:
185
+ return false;
186
+ }
187
+ }
188
+
123
189
  export class ImageGenerationError extends Error {
124
190
  readonly code: ImageGenerationFailureCode;
125
191
 
@@ -138,6 +204,10 @@ function isBoundedString(value: unknown, maxChars: number): value is string {
138
204
  return typeof value === "string" && value.trim().length > 0 && value.length <= maxChars;
139
205
  }
140
206
 
207
+ function isImageGenerationMimeType(value: unknown): value is ImageGenerationMimeType {
208
+ return (IMAGE_GENERATION_MIME_TYPES as readonly string[]).includes(value as string);
209
+ }
210
+
141
211
  export function isImageGenerationDetails(value: unknown): value is ImageGenerationDetails {
142
212
  if (!isRecord(value)) return false;
143
213
  return (
@@ -147,7 +217,7 @@ export function isImageGenerationDetails(value: unknown): value is ImageGenerati
147
217
  isBoundedString(value.imageModel, MAX_IMAGE_MODEL_ID_CHARS) &&
148
218
  isBoundedString(value.imageCallId, MAX_IMAGE_IDENTIFIER_CHARS) &&
149
219
  (value.responseId === undefined || isBoundedString(value.responseId, MAX_IMAGE_IDENTIFIER_CHARS)) &&
150
- value.mimeType === IMAGE_GENERATION_MIME_TYPE &&
220
+ isImageGenerationMimeType(value.mimeType) &&
151
221
  typeof value.byteCount === "number" &&
152
222
  Number.isInteger(value.byteCount) &&
153
223
  value.byteCount > 0 &&
@@ -170,6 +240,47 @@ export function isImageGenerationDetails(value: unknown): value is ImageGenerati
170
240
  );
171
241
  }
172
242
 
243
+ function isBoundedIndex(value: unknown, maximum: number): value is number {
244
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value < maximum;
245
+ }
246
+
247
+ export function isImageGenerationBatchDetails(value: unknown): value is ImageGenerationBatchDetails {
248
+ if (!isRecord(value) || value.batch !== true) return false;
249
+ if (
250
+ typeof value.requestedCount !== "number" ||
251
+ !Number.isInteger(value.requestedCount) ||
252
+ value.requestedCount < 2 ||
253
+ value.requestedCount > MAX_IMAGE_BATCH_SIZE ||
254
+ typeof value.succeededCount !== "number" ||
255
+ !Number.isInteger(value.succeededCount) ||
256
+ value.succeededCount < 1 ||
257
+ value.succeededCount > value.requestedCount ||
258
+ typeof value.failedCount !== "number" ||
259
+ !Number.isInteger(value.failedCount) ||
260
+ value.failedCount !== value.requestedCount - value.succeededCount ||
261
+ !Array.isArray(value.images) ||
262
+ value.images.length !== value.succeededCount ||
263
+ !value.images.every(isImageGenerationDetails)
264
+ ) {
265
+ return false;
266
+ }
267
+ if (value.failedCount === 0) return value.failures === undefined;
268
+ if (!Array.isArray(value.failures) || value.failures.length !== value.failedCount) return false;
269
+ const requestedCount = value.requestedCount;
270
+ const indexes = new Set<number>();
271
+ return value.failures.every((failure) => {
272
+ if (!isRecord(failure) || !isBoundedIndex(failure.index, requestedCount)) return false;
273
+ if (!isImageGenerationFailureCode(failure.code) || !isBoundedString(failure.message, MAX_IMAGE_DIAGNOSTIC_CHARS)) return false;
274
+ if (indexes.has(failure.index)) return false;
275
+ indexes.add(failure.index);
276
+ return true;
277
+ });
278
+ }
279
+
280
+ export function isImageGenerationToolDetails(value: unknown): value is ImageGenerationToolDetails {
281
+ return isImageGenerationDetails(value) || isImageGenerationBatchDetails(value);
282
+ }
283
+
173
284
  export function sanitizeImageDiagnostic(value: unknown, fallback: string): string {
174
285
  if (typeof value !== "string") return fallback;
175
286
  const normalized = value
@@ -9,7 +9,13 @@ import type {
9
9
  NativeCompactionEntry,
10
10
  NativeCompactionInputProvenance,
11
11
  } from "./types";
12
- import { serializeMessagesToResponsesInput, type ResponsesInputContentItem, type ResponsesInputItem, type ResponsesInputMessageItem } from "./serializer";
12
+ import {
13
+ serializeMessagesToResponsesInput,
14
+ serializeMessagesToResponsesInputWithLimits,
15
+ type ResponsesInputContentItem,
16
+ type ResponsesInputItem,
17
+ type ResponsesInputMessageItem,
18
+ } from "./serializer";
13
19
 
14
20
  export type NativeReplaySegments = {
15
21
  boundaryIndex: number;
@@ -369,6 +375,15 @@ export function serializeLiveTailToResponsesInput<TApi extends Api>(args: {
369
375
  });
370
376
  }
371
377
 
378
+ export async function serializeLiveTailToResponsesInputWithLimits<TApi extends Api>(args: {
379
+ model: Model<TApi>;
380
+ entries: readonly SessionEntry[];
381
+ }): Promise<ResponsesInputItem[]> {
382
+ return serializeMessagesToResponsesInputWithLimits(args.model, collectReplayMessages(args.entries), {
383
+ firstSystemMessageIsUpdate: true,
384
+ });
385
+ }
386
+
372
387
  function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
373
388
  model: Model<TApi>;
374
389
  payload: ResponsesCompatibleRequestPayload;
@@ -107,7 +107,8 @@ export function clampCodexSessionId(sessionId: string | undefined): string | und
107
107
 
108
108
  export type ResponsesHeaderOptions = {
109
109
  accept: string;
110
- contentType?: string;
110
+ /** `null` omits content-type so a multipart FormData body can set its boundary. */
111
+ contentType?: string | null;
111
112
  /** Conversation identity for Codex cache-affinity headers; clamped to 64 chars here. */
112
113
  sessionId?: string;
113
114
  };
@@ -121,8 +122,17 @@ export function buildResponsesRequestHeaders(
121
122
  runtime.codexAffinity ? filterCodexGatewayHeaders(mergedHeaders) : mergedHeaders,
122
123
  );
123
124
  headers.set("accept", options.accept);
124
- headers.set("content-type", options.contentType ?? "application/json");
125
- if (!headers.has("authorization")) {
125
+ if (options.contentType === null) {
126
+ headers.delete("content-type");
127
+ } else {
128
+ headers.set("content-type", options.contentType ?? "application/json");
129
+ }
130
+ if (runtime.api === "azure-openai-responses") {
131
+ // Azure OpenAI authenticates Responses requests with `api-key`; do not
132
+ // forward an inherited bearer token from a model/provider header.
133
+ headers.delete("authorization");
134
+ headers.set("api-key", runtime.apiKey);
135
+ } else if (!headers.has("authorization")) {
126
136
  headers.set("authorization", `Bearer ${runtime.apiKey}`);
127
137
  }
128
138