pi-openai-toolkit 0.17.2 → 0.18.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.
@@ -23,8 +23,9 @@ export const IMAGE_REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
23
23
  export const MAX_IMAGE_PATH_CHARS = 4096;
24
24
  export const MAX_IMAGE_DIAGNOSTIC_CHARS = 4096;
25
25
  export const MAX_IMAGE_IDENTIFIER_CHARS = 256;
26
- /** Upper bound for a configured or requested `image_generation` model id. */
26
+ /** Upper bound for a configured or requested Images API model id. */
27
27
  export const MAX_IMAGE_MODEL_ID_CHARS = 256;
28
+ export const MAX_IMAGE_BATCH_SIZE = 10;
28
29
  export const MAX_IMAGE_DIMENSION = 100_000;
29
30
 
30
31
  export type ImageGenerationCapableApi = (typeof IMAGE_GENERATION_CAPABLE_APIS)[number];
@@ -45,8 +46,10 @@ export type GenerateImageParams = {
45
46
  outputPath?: string | null;
46
47
  size?: ImageGenerationSize;
47
48
  quality?: ImageGenerationQuality;
48
- /** Optional bare image model id that must exist in `imageGeneration.models`. */
49
+ /** Optional bare image model id that must exist in the configured image model allowlist. */
49
50
  model?: string | null;
51
+ /** Number of independent image requests to run concurrently. */
52
+ batchSize?: number | null;
50
53
  };
51
54
 
52
55
  export type NormalizedGenerateImageParams = {
@@ -56,6 +59,7 @@ export type NormalizedGenerateImageParams = {
56
59
  size: ImageGenerationSize;
57
60
  quality: ImageGenerationQuality;
58
61
  action: ImageGenerationAction;
62
+ batchSize: number;
59
63
  /** Trimmed requested model id, or undefined when the caller omitted it. */
60
64
  model?: string;
61
65
  };
@@ -92,8 +96,25 @@ export type ImageGenerationDetails = {
92
96
  warning?: string;
93
97
  };
94
98
 
99
+ export type ImageGenerationBatchFailure = {
100
+ index: number;
101
+ code: ImageGenerationFailureCode;
102
+ message: string;
103
+ };
104
+
105
+ export type ImageGenerationBatchDetails = {
106
+ batch: true;
107
+ requestedCount: number;
108
+ succeededCount: number;
109
+ failedCount: number;
110
+ images: ImageGenerationDetails[];
111
+ failures?: ImageGenerationBatchFailure[];
112
+ };
113
+
114
+ export type ImageGenerationToolDetails = ImageGenerationDetails | ImageGenerationBatchDetails;
115
+
95
116
  export type ImageGenerationExecutionResult = {
96
- details: ImageGenerationDetails;
117
+ details: ImageGenerationToolDetails;
97
118
  text: string;
98
119
  };
99
120
 
@@ -120,6 +141,35 @@ export type ImageGenerationFailureCode =
120
141
  | "no-image"
121
142
  | "artifact-write-failed";
122
143
 
144
+ function isImageGenerationFailureCode(value: unknown): value is ImageGenerationFailureCode {
145
+ switch (value) {
146
+ case "aborted":
147
+ case "timeout":
148
+ case "disabled":
149
+ case "unsupported-model":
150
+ case "missing-runtime":
151
+ case "invalid-parameters":
152
+ case "reference-input-invalid":
153
+ case "reference-upload-approval-required":
154
+ case "reference-upload-declined":
155
+ case "output-path-invalid":
156
+ case "output-path-approval-required":
157
+ case "output-path-declined":
158
+ case "authentication":
159
+ case "rate-limit":
160
+ case "request-rejected":
161
+ case "backend-unavailable":
162
+ case "network":
163
+ case "oversized-response":
164
+ case "malformed-response":
165
+ case "no-image":
166
+ case "artifact-write-failed":
167
+ return true;
168
+ default:
169
+ return false;
170
+ }
171
+ }
172
+
123
173
  export class ImageGenerationError extends Error {
124
174
  readonly code: ImageGenerationFailureCode;
125
175
 
@@ -170,6 +220,47 @@ export function isImageGenerationDetails(value: unknown): value is ImageGenerati
170
220
  );
171
221
  }
172
222
 
223
+ function isBoundedIndex(value: unknown, maximum: number): value is number {
224
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value < maximum;
225
+ }
226
+
227
+ export function isImageGenerationBatchDetails(value: unknown): value is ImageGenerationBatchDetails {
228
+ if (!isRecord(value) || value.batch !== true) return false;
229
+ if (
230
+ typeof value.requestedCount !== "number" ||
231
+ !Number.isInteger(value.requestedCount) ||
232
+ value.requestedCount < 2 ||
233
+ value.requestedCount > MAX_IMAGE_BATCH_SIZE ||
234
+ typeof value.succeededCount !== "number" ||
235
+ !Number.isInteger(value.succeededCount) ||
236
+ value.succeededCount < 1 ||
237
+ value.succeededCount > value.requestedCount ||
238
+ typeof value.failedCount !== "number" ||
239
+ !Number.isInteger(value.failedCount) ||
240
+ value.failedCount !== value.requestedCount - value.succeededCount ||
241
+ !Array.isArray(value.images) ||
242
+ value.images.length !== value.succeededCount ||
243
+ !value.images.every(isImageGenerationDetails)
244
+ ) {
245
+ return false;
246
+ }
247
+ if (value.failedCount === 0) return value.failures === undefined;
248
+ if (!Array.isArray(value.failures) || value.failures.length !== value.failedCount) return false;
249
+ const requestedCount = value.requestedCount;
250
+ const indexes = new Set<number>();
251
+ return value.failures.every((failure) => {
252
+ if (!isRecord(failure) || !isBoundedIndex(failure.index, requestedCount)) return false;
253
+ if (!isImageGenerationFailureCode(failure.code) || !isBoundedString(failure.message, MAX_IMAGE_DIAGNOSTIC_CHARS)) return false;
254
+ if (indexes.has(failure.index)) return false;
255
+ indexes.add(failure.index);
256
+ return true;
257
+ });
258
+ }
259
+
260
+ export function isImageGenerationToolDetails(value: unknown): value is ImageGenerationToolDetails {
261
+ return isImageGenerationDetails(value) || isImageGenerationBatchDetails(value);
262
+ }
263
+
173
264
  export function sanitizeImageDiagnostic(value: unknown, fallback: string): string {
174
265
  if (typeof value !== "string") return fallback;
175
266
  const normalized = value
@@ -90,11 +90,9 @@ export function buildCodexCliUserAgent(version: string): string {
90
90
  }
91
91
 
92
92
  /**
93
- * The Codex backend version-gates model availability on the `version` header
94
- * (the pinned @openai/codex client version). `gpt-6-astra` requires >= 0.153.0;
95
- * an older or absent pin makes the SKU invisible to discovery and rejects
96
- * requests that name it. Keep this in sync with the newest gate Oh My Pi pins
97
- * in @oh-my-pi/pi-wire/codex (`CODEX_CLIENT_VERSION`).
93
+ * Codex client identity used by synthetic gateway requests in their User-Agent.
94
+ * Native live and context-management requests do not generate a `version`
95
+ * header; explicitly configured provider values remain caller-owned.
98
96
  */
99
97
  export const CODEX_CLIENT_VERSION = "0.153.0";
100
98
 
@@ -109,7 +107,8 @@ export function clampCodexSessionId(sessionId: string | undefined): string | und
109
107
 
110
108
  export type ResponsesHeaderOptions = {
111
109
  accept: string;
112
- contentType?: string;
110
+ /** `null` omits content-type so a multipart FormData body can set its boundary. */
111
+ contentType?: string | null;
113
112
  /** Conversation identity for Codex cache-affinity headers; clamped to 64 chars here. */
114
113
  sessionId?: string;
115
114
  };
@@ -123,7 +122,11 @@ export function buildResponsesRequestHeaders(
123
122
  runtime.codexAffinity ? filterCodexGatewayHeaders(mergedHeaders) : mergedHeaders,
124
123
  );
125
124
  headers.set("accept", options.accept);
126
- headers.set("content-type", options.contentType ?? "application/json");
125
+ if (options.contentType === null) {
126
+ headers.delete("content-type");
127
+ } else {
128
+ headers.set("content-type", options.contentType ?? "application/json");
129
+ }
127
130
  if (!headers.has("authorization")) {
128
131
  headers.set("authorization", `Bearer ${runtime.apiKey}`);
129
132
  }
@@ -148,8 +151,6 @@ export function buildResponsesRequestHeaders(
148
151
  headers.set("originator", "pi");
149
152
  headers.set("user-agent", buildCodexUserAgent());
150
153
  headers.set("openai-beta", "responses=experimental");
151
- // Preserve the existing native Codex compatibility pin.
152
- headers.set("version", CODEX_CLIENT_VERSION);
153
154
  }
154
155
  // Gateways own their version policy. Explicit versions still pass through
155
156
  // the header merge and allowlist, but Toolkit does not generate one.
package/src/types.ts CHANGED
@@ -151,9 +151,9 @@ export type WebSearchConfig = {
151
151
  export type ImageGenerationConfig = {
152
152
  enabled: boolean;
153
153
  /**
154
- * Bare `image_generation` tool model ids this toolkit may send, without a `provider/` prefix.
155
- * The first entry is the default; `openai_generate_image`'s optional `model` argument may
156
- * select any other entry in this order-preserving list.
154
+ * Bare Images API model ids this toolkit may send, without a `provider/` prefix.
155
+ * The first legacy entry is the default; `openai_generate_image`'s optional `model` argument
156
+ * may select any other entry in this order-preserving list.
157
157
  */
158
158
  models: string[];
159
159
  };