dsh-coding-subscription-oauth 0.6.3 → 0.6.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.
Files changed (127) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/CONTRIBUTING.md +13 -4
  3. package/INSTALL.md +7 -5
  4. package/README.de.md +6 -6
  5. package/README.es.md +6 -6
  6. package/README.fr.md +6 -6
  7. package/README.ja.md +6 -6
  8. package/README.ko.md +6 -6
  9. package/README.md +6 -6
  10. package/README.pt-BR.md +6 -6
  11. package/README.ru.md +6 -6
  12. package/README.zh-CN.md +6 -6
  13. package/docs/00-project-rules.md +4 -4
  14. package/docs/02-architecture.md +7 -3
  15. package/docs/02-architecture.zh-CN.md +7 -3
  16. package/lib/bin.js +1333 -569
  17. package/lib/bin.js.map +4 -4
  18. package/lib/client.js +1 -1
  19. package/lib/client.js.map +2 -2
  20. package/lib/gateway-protocol.d.ts +3 -46
  21. package/lib/gateway-protocol.d.ts.map +1 -1
  22. package/lib/gateway-routes.d.ts.map +1 -1
  23. package/lib/grok-errors.d.ts +2 -12
  24. package/lib/grok-errors.d.ts.map +1 -1
  25. package/lib/grok-imagine/client.d.ts +73 -0
  26. package/lib/grok-imagine/client.d.ts.map +1 -0
  27. package/lib/grok-imagine/index.d.ts +8 -0
  28. package/lib/grok-imagine/index.d.ts.map +1 -0
  29. package/lib/grok-imagine/net.d.ts +10 -0
  30. package/lib/grok-imagine/net.d.ts.map +1 -0
  31. package/lib/grok-imagine/parse.d.ts +20 -0
  32. package/lib/grok-imagine/parse.d.ts.map +1 -0
  33. package/lib/grok-imagine/types.d.ts +191 -0
  34. package/lib/grok-imagine/types.d.ts.map +1 -0
  35. package/lib/grok-imagine.d.ts +3 -263
  36. package/lib/grok-imagine.d.ts.map +1 -1
  37. package/lib/http-json.d.ts +2 -9
  38. package/lib/http-json.d.ts.map +1 -1
  39. package/lib/index.js +1395 -26178
  40. package/lib/index.js.map +4 -4
  41. package/lib/kimi-errors.d.ts +2 -12
  42. package/lib/kimi-errors.d.ts.map +1 -1
  43. package/lib/runtime/gateway-protocol.d.ts +47 -0
  44. package/lib/runtime/gateway-protocol.d.ts.map +1 -0
  45. package/lib/runtime/grok-errors.d.ts +13 -0
  46. package/lib/runtime/grok-errors.d.ts.map +1 -0
  47. package/lib/runtime/http-json.d.ts +10 -0
  48. package/lib/runtime/http-json.d.ts.map +1 -0
  49. package/lib/runtime/kimi-errors.d.ts +13 -0
  50. package/lib/runtime/kimi-errors.d.ts.map +1 -0
  51. package/package.json +12 -7
  52. package/scripts/release.mjs +1 -0
  53. package/src/adapter.ts +0 -348
  54. package/src/alias-adapter.ts +0 -147
  55. package/src/auth-routes.ts +0 -921
  56. package/src/auth.ts +0 -67
  57. package/src/bin.ts +0 -350
  58. package/src/capability-routes.ts +0 -279
  59. package/src/capability-runtime.ts +0 -314
  60. package/src/capability-settings.ts +0 -671
  61. package/src/capability-tools.ts +0 -685
  62. package/src/catalog.ts +0 -271
  63. package/src/client/GrokBuildSettings.tsx +0 -771
  64. package/src/client/api.ts +0 -88
  65. package/src/client/components/AboutTab.tsx +0 -30
  66. package/src/client/components/AccountsTab.tsx +0 -241
  67. package/src/client/components/Badge.tsx +0 -33
  68. package/src/client/components/CapabilitiesTab.tsx +0 -265
  69. package/src/client/components/CliPullPreview.tsx +0 -116
  70. package/src/client/components/CopyButton.tsx +0 -57
  71. package/src/client/components/GatewayTab.tsx +0 -469
  72. package/src/client/components/NoticeBanner.tsx +0 -46
  73. package/src/client/components/ProgressBar.tsx +0 -53
  74. package/src/client/components/ProviderCard.tsx +0 -606
  75. package/src/client/components/SettingsTabs.tsx +0 -75
  76. package/src/client/components/ToggleSwitch.tsx +0 -71
  77. package/src/client/constants.ts +0 -230
  78. package/src/client/display.ts +0 -61
  79. package/src/client/dshClientAdapter.ts +0 -127
  80. package/src/client/gatewaySnippets.ts +0 -37
  81. package/src/client/index.tsx +0 -156
  82. package/src/client/locales.ts +0 -540
  83. package/src/client/microStyles.ts +0 -52
  84. package/src/client/parsers.ts +0 -398
  85. package/src/client/styles.ts +0 -325
  86. package/src/client/types.ts +0 -199
  87. package/src/codex-http.ts +0 -447
  88. package/src/codex-images.ts +0 -503
  89. package/src/codex-model-capabilities.ts +0 -320
  90. package/src/codex-search.ts +0 -245
  91. package/src/codex-usage.ts +0 -263
  92. package/src/compatibility.ts +0 -55
  93. package/src/dsh-host-adapter.ts +0 -173
  94. package/src/gateway-anthropic-messages.ts +0 -84
  95. package/src/gateway-auth.ts +0 -102
  96. package/src/gateway-backend.ts +0 -274
  97. package/src/gateway-body.ts +0 -49
  98. package/src/gateway-config.ts +0 -76
  99. package/src/gateway-http.ts +0 -104
  100. package/src/gateway-openai-chat.ts +0 -124
  101. package/src/gateway-openai-responses.ts +0 -53
  102. package/src/gateway-parse.ts +0 -224
  103. package/src/gateway-protocol.ts +0 -52
  104. package/src/gateway-routes.ts +0 -158
  105. package/src/gateway.ts +0 -258
  106. package/src/grok-errors.ts +0 -24
  107. package/src/grok-imagine.ts +0 -1627
  108. package/src/grok-import.ts +0 -151
  109. package/src/http-json.ts +0 -82
  110. package/src/ids.ts +0 -59
  111. package/src/imagine-routes.ts +0 -463
  112. package/src/index.ts +0 -735
  113. package/src/invariant.ts +0 -17
  114. package/src/kimi-errors.ts +0 -26
  115. package/src/media-store.ts +0 -927
  116. package/src/oauth-import-routes.ts +0 -324
  117. package/src/oauth-providers.ts +0 -152
  118. package/src/oauth-session.ts +0 -183
  119. package/src/oauth-sources.ts +0 -1104
  120. package/src/oauth.ts +0 -620
  121. package/src/provider.ts +0 -128
  122. package/src/proxy.ts +0 -11
  123. package/src/redact.ts +0 -72
  124. package/src/session.ts +0 -218
  125. package/src/store.ts +0 -217
  126. package/src/web-origin.ts +0 -296
  127. package/src/web-routes.ts +0 -38
@@ -1,503 +0,0 @@
1
- /**
2
- * Optional Codex image generation/edits against the private ChatGPT backend.
3
- * Fixed model `gpt-image-2` with `auto` size/quality/background defaults.
4
- * Always requests `response_format: b64_json`. Only `b64_json` items are
5
- * accepted after an encoded-size precheck and strict base64/base64url decode.
6
- * Edit IDs resolve solely from canonical image attachment refs that are
7
- * visibly present in the current session's top-level message content (max 5).
8
- *
9
- * @module dsh-coding-subscription-oauth/codex-images
10
- */
11
-
12
- import type {
13
- ImageAttachmentRef,
14
- ImageMediaType,
15
- SaveImageAttachment,
16
- StoredImageAttachment,
17
- } from "@deepseek-ai/dsh-attachment";
18
- import { LlmError } from "@deepseek-ai/dsh-llm";
19
- import {
20
- type CodexAuthSession,
21
- type CodexFetch,
22
- type CodexHttpClient,
23
- createCodexHttpClient,
24
- isRecord,
25
- } from "./codex-http.ts";
26
-
27
- export const CODEX_IMAGE_GENERATION_URL = "https://chatgpt.com/backend-api/codex/images/generations";
28
- export const CODEX_IMAGE_EDIT_URL = "https://chatgpt.com/backend-api/codex/images/edits";
29
- export const CODEX_IMAGE_MODEL = "gpt-image-2";
30
- export const CODEX_IMAGE_AUTO = "auto";
31
- export const CODEX_IMAGE_RESPONSE_FORMAT = "b64_json";
32
- export const CODEX_IMAGE_MAX_REFERENCES = 5;
33
- export const CODEX_IMAGE_PROMPT_MAX_LENGTH = 4000;
34
-
35
- export const CODEX_IMAGE_CAPABLE_PROVIDERS = ["codex-oauth", "openai-codex", "codex-oauth-fast"] as const;
36
- export type CodexImageCapableProvider = (typeof CODEX_IMAGE_CAPABLE_PROVIDERS)[number];
37
-
38
- export const CODEX_IMAGE_SIZES = ["auto", "1024x1024", "1536x1024", "1024x1536"] as const;
39
- export type CodexImageSize = (typeof CODEX_IMAGE_SIZES)[number];
40
-
41
- export const CODEX_IMAGE_QUALITIES = ["auto", "low", "medium", "high"] as const;
42
- export type CodexImageQuality = (typeof CODEX_IMAGE_QUALITIES)[number];
43
-
44
- export const CODEX_IMAGE_BACKGROUNDS = ["auto", "opaque", "transparent"] as const;
45
- export type CodexImageBackground = (typeof CODEX_IMAGE_BACKGROUNDS)[number];
46
-
47
- export interface CodexImageAttachmentStore {
48
- readonly imageLimits: {
49
- readonly maxImageBytes: number;
50
- readonly maxImagesPerMessage: number;
51
- readonly maxMessageImageBytes: number;
52
- readonly mediaTypes: readonly ImageMediaType[];
53
- };
54
- validateImage(input: SaveImageAttachment): Promise<void>;
55
- saveImage(input: SaveImageAttachment): Promise<ImageAttachmentRef>;
56
- readImage(ref: ImageAttachmentRef, signal?: AbortSignal): Promise<StoredImageAttachment>;
57
- }
58
-
59
- export interface CodexDerivedMessage {
60
- readonly content?: readonly unknown[];
61
- }
62
-
63
- export interface CodexImageRoute {
64
- readonly provider?: string;
65
- readonly model?: string;
66
- readonly inputModalities?: readonly string[];
67
- }
68
-
69
- export interface CodexImageSessionContext {
70
- deriveMessages(): readonly CodexDerivedMessage[];
71
- route?: CodexImageRoute;
72
- }
73
-
74
- /**
75
- * Route gate for the image tools. `codex-capable` (default) requires a Codex
76
- * OAuth route that declares image input; `any` lets any model route call the
77
- * tools while the ChatGPT subscription backend still performs the work.
78
- */
79
- export type CodexImageRoutePolicy = "codex-capable" | "any";
80
-
81
- export interface CodexImageGenerateInput {
82
- readonly prompt: string;
83
- readonly n?: number;
84
- readonly size?: CodexImageSize;
85
- readonly quality?: CodexImageQuality;
86
- readonly background?: CodexImageBackground;
87
- }
88
-
89
- export interface CodexImageEditInput extends CodexImageGenerateInput {
90
- readonly imageIds: readonly string[];
91
- }
92
-
93
- export interface CodexImageWarning {
94
- readonly index: number;
95
- readonly code: string;
96
- readonly message: string;
97
- }
98
-
99
- export interface CodexImageResult {
100
- readonly operation: "generate" | "edit";
101
- readonly model: typeof CODEX_IMAGE_MODEL;
102
- readonly images: readonly ImageAttachmentRef[];
103
- readonly references: readonly ImageAttachmentRef[];
104
- readonly warnings: readonly CodexImageWarning[];
105
- }
106
-
107
- export interface CodexImageControllerOptions {
108
- readonly auth: CodexAuthSession;
109
- readonly attachments: CodexImageAttachmentStore;
110
- readonly session: CodexImageSessionContext;
111
- /** Route gate; defaults to `codex-capable`. Set `any` to allow non-Codex routes. */
112
- readonly routePolicy?: CodexImageRoutePolicy;
113
- readonly http?: CodexHttpClient;
114
- readonly fetchImpl?: CodexFetch;
115
- readonly originator?: string;
116
- readonly userAgent?: string;
117
- readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
118
- readonly now?: () => number;
119
- }
120
-
121
- export interface CodexImageController {
122
- generate(input: CodexImageGenerateInput, signal?: AbortSignal): Promise<CodexImageResult>;
123
- edit(input: CodexImageEditInput, signal?: AbortSignal): Promise<CodexImageResult>;
124
- }
125
-
126
- const MAX_GENERATED = 4;
127
-
128
- /** True only for a Codex OAuth route that explicitly declares image input. */
129
- export function isCodexImageCapableRoute(route: CodexImageRoute | undefined): boolean {
130
- if (route === undefined) return false;
131
- const provider = route.provider;
132
- if (provider === undefined || !(CODEX_IMAGE_CAPABLE_PROVIDERS as readonly string[]).includes(provider)) {
133
- return false;
134
- }
135
- return route.inputModalities?.includes("image") === true;
136
- }
137
-
138
- export function assertCodexImageCapableRoute(route: CodexImageRoute | undefined): void {
139
- if (!isCodexImageCapableRoute(route)) {
140
- throw new LlmError("Codex image tools require an image-capable Codex OAuth route", "UNSUPPORTED_CONTENT");
141
- }
142
- }
143
-
144
- function isImageAttachmentRef(value: unknown): value is ImageAttachmentRef {
145
- return (
146
- isRecord(value) &&
147
- typeof value["attachmentId"] === "string" &&
148
- typeof value["mediaType"] === "string" &&
149
- typeof value["bytes"] === "number" &&
150
- typeof value["width"] === "number" &&
151
- typeof value["height"] === "number"
152
- );
153
- }
154
-
155
- /**
156
- * Collect canonical image attachment refs visibly present in current session
157
- * message content. Nested tool-results and arbitrary objects are ignored.
158
- */
159
- export function collectCanonicalImageRefs(messages: readonly CodexDerivedMessage[]): Map<string, ImageAttachmentRef> {
160
- const refs = new Map<string, ImageAttachmentRef>();
161
- for (const message of messages) {
162
- const content = message.content;
163
- if (!Array.isArray(content)) continue;
164
- for (const block of content) {
165
- if (!isRecord(block) || block["type"] !== "image" || !isImageAttachmentRef(block["attachment"])) continue;
166
- const ref = block["attachment"];
167
- const id = String(ref.attachmentId);
168
- if (!refs.has(id)) refs.set(id, ref);
169
- }
170
- }
171
- return refs;
172
- }
173
-
174
- function normalizeImageId(value: string): string {
175
- return value.startsWith("image:") ? value.slice("image:".length) : value;
176
- }
177
-
178
- /**
179
- * Resolve edit IDs only against canonical refs visibly present in `deriveMessages()`.
180
- * Unknown IDs, HTTP URLs, nested-only objects, and more than
181
- * {@link CODEX_IMAGE_MAX_REFERENCES} are rejected (fail closed).
182
- */
183
- export function resolveSessionImageRefs(
184
- deriveMessages: () => readonly CodexDerivedMessage[],
185
- imageIds: readonly string[],
186
- ): ImageAttachmentRef[] {
187
- if (imageIds.length === 0) {
188
- throw new LlmError("Codex image edit requires at least one session image id", "INVALID_ARGS");
189
- }
190
- if (imageIds.length > CODEX_IMAGE_MAX_REFERENCES) {
191
- throw new LlmError(
192
- `Codex image edit accepts at most ${String(CODEX_IMAGE_MAX_REFERENCES)} session images`,
193
- "INVALID_ARGS",
194
- );
195
- }
196
- const authorized = collectCanonicalImageRefs(deriveMessages());
197
- const resolved: ImageAttachmentRef[] = [];
198
- const seen = new Set<string>();
199
- for (const raw of imageIds) {
200
- if (/^https?:\/\//iu.test(raw)) {
201
- throw new LlmError("Codex image edit does not accept HTTP(S) image URLs", "INVALID_ARGS");
202
- }
203
- const id = normalizeImageId(raw.trim());
204
- if (id.length === 0) {
205
- throw new LlmError("Codex image edit received an empty image id", "INVALID_ARGS");
206
- }
207
- const ref = authorized.get(id);
208
- if (ref === undefined) {
209
- throw new LlmError(
210
- `Image ${JSON.stringify(raw)} is not a canonical attachment in the current session`,
211
- "INVALID_ARGS",
212
- );
213
- }
214
- if (seen.has(id)) continue;
215
- seen.add(id);
216
- resolved.push(ref);
217
- }
218
- return resolved;
219
- }
220
-
221
- function ascii(data: Uint8Array, start: number, end: number): string {
222
- return String.fromCharCode(...data.slice(start, end));
223
- }
224
-
225
- export function detectImageMediaType(data: Uint8Array): ImageMediaType | undefined {
226
- if (
227
- data.length >= 8 &&
228
- data[0] === 0x89 &&
229
- data[1] === 0x50 &&
230
- data[2] === 0x4e &&
231
- data[3] === 0x47 &&
232
- data[4] === 0x0d &&
233
- data[5] === 0x0a &&
234
- data[6] === 0x1a &&
235
- data[7] === 0x0a
236
- ) {
237
- return "image/png";
238
- }
239
- if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) return "image/jpeg";
240
- if (data.length >= 12 && ascii(data, 0, 4) === "RIFF" && ascii(data, 8, 12) === "WEBP") return "image/webp";
241
- if (data.length >= 6 && (ascii(data, 0, 6) === "GIF87a" || ascii(data, 0, 6) === "GIF89a")) return "image/gif";
242
- return undefined;
243
- }
244
-
245
- function normalizeBase64Payload(value: string): string | undefined {
246
- const stripped = value.replace(/\s+/gu, "");
247
- const payload = stripped.startsWith("data:") ? (stripped.split(",", 2)[1] ?? "") : stripped;
248
- if (payload.length === 0) return undefined;
249
- if (!/^[A-Za-z0-9+/_-]+={0,2}$/u.test(payload)) return undefined;
250
- return payload;
251
- }
252
-
253
- /** Encoded-size precheck. `undefined` means the payload is not valid base64/base64url. */
254
- export function estimateDecodedBase64Bytes(value: string): number | undefined {
255
- const payload = normalizeBase64Payload(value);
256
- if (payload === undefined) return undefined;
257
- const unpadded = payload.replace(/=+$/u, "");
258
- if (unpadded.length % 4 === 1) return undefined;
259
- return Math.floor((unpadded.length * 3) / 4);
260
- }
261
-
262
- /** Decode standard base64 or base64url. Rejects invalid alphabets and length mismatches. */
263
- export function decodeImageBase64(value: string): Uint8Array | undefined {
264
- const payload = normalizeBase64Payload(value);
265
- if (payload === undefined) return undefined;
266
- const estimated = estimateDecodedBase64Bytes(payload);
267
- if (estimated === undefined || estimated === 0) return undefined;
268
- const normalized = payload.replace(/-/gu, "+").replace(/_/gu, "/");
269
- const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
270
- try {
271
- const decoded = Buffer.from(padded, "base64");
272
- if (decoded.byteLength !== estimated) return undefined;
273
- return new Uint8Array(decoded);
274
- } catch {
275
- return undefined;
276
- }
277
- }
278
-
279
- function dataUrl(stored: StoredImageAttachment): string {
280
- return `data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}`;
281
- }
282
-
283
- function extensionFor(mediaType: ImageMediaType): string {
284
- return mediaType === "image/jpeg" ? "jpg" : mediaType.slice("image/".length);
285
- }
286
-
287
- function enumValue<const T extends string>(value: T | undefined, allowed: readonly T[], fallback: T, field: string): T {
288
- if (value === undefined) return fallback;
289
- if (!allowed.includes(value)) {
290
- throw new LlmError(`${field} must be one of ${allowed.join(", ")}`, "INVALID_ARGS");
291
- }
292
- return value;
293
- }
294
-
295
- async function assertSignedIn(auth: CodexAuthSession): Promise<void> {
296
- if ((await auth.resolve()) === undefined) {
297
- throw new LlmError("Codex image tools require a signed-in Codex account", "MISSING_CREDENTIAL");
298
- }
299
- }
300
-
301
- function imageEnvelopeLimit(maxImageBytes: number, n: number): number {
302
- return Math.ceil(maxImageBytes / 3) * 4 * Math.max(1, n) + 128 * 1024;
303
- }
304
-
305
- interface PreparedImageRequest {
306
- readonly operation: "generate" | "edit";
307
- readonly n: number;
308
- readonly body: Record<string, unknown>;
309
- readonly references: readonly ImageAttachmentRef[];
310
- readonly storedReferences: readonly StoredImageAttachment[];
311
- }
312
-
313
- async function prepareRequest(
314
- options: CodexImageControllerOptions,
315
- input: CodexImageGenerateInput,
316
- imageIds: readonly string[] | undefined,
317
- signal?: AbortSignal,
318
- ): Promise<PreparedImageRequest> {
319
- if (options.routePolicy !== "any") {
320
- assertCodexImageCapableRoute(options.session.route);
321
- }
322
- const prompt = input.prompt.trim();
323
- if (prompt.length === 0) {
324
- throw new LlmError("Codex image generation requires a non-empty prompt", "INVALID_ARGS");
325
- }
326
- if (prompt.length > CODEX_IMAGE_PROMPT_MAX_LENGTH) {
327
- throw new LlmError(
328
- `Codex image prompt must be ${String(CODEX_IMAGE_PROMPT_MAX_LENGTH)} characters or fewer`,
329
- "INVALID_ARGS",
330
- );
331
- }
332
- const n = input.n === undefined ? 1 : input.n;
333
- if (!Number.isSafeInteger(n) || n < 1 || n > MAX_GENERATED) {
334
- throw new LlmError(`Codex image n must be an integer from 1 through ${String(MAX_GENERATED)}`, "INVALID_ARGS");
335
- }
336
- const size = enumValue(input.size, CODEX_IMAGE_SIZES, CODEX_IMAGE_AUTO, "size");
337
- const quality = enumValue(input.quality, CODEX_IMAGE_QUALITIES, CODEX_IMAGE_AUTO, "quality");
338
- const background = enumValue(input.background, CODEX_IMAGE_BACKGROUNDS, CODEX_IMAGE_AUTO, "background");
339
- const references =
340
- imageIds === undefined ? [] : resolveSessionImageRefs(() => options.session.deriveMessages(), imageIds);
341
- if (references.length > options.attachments.imageLimits.maxImagesPerMessage) {
342
- throw new LlmError("Codex image edit exceeds the deployment image-count limit", "INVALID_ARGS");
343
- }
344
- const storedReferences: StoredImageAttachment[] = [];
345
- let totalBytes = 0;
346
- for (const ref of references) {
347
- const stored = await options.attachments.readImage(ref, signal);
348
- if (totalBytes + stored.data.byteLength > options.attachments.imageLimits.maxMessageImageBytes) {
349
- throw new LlmError("Codex image edit exceeds the deployment total image-byte limit", "INVALID_ARGS");
350
- }
351
- totalBytes += stored.data.byteLength;
352
- storedReferences.push(stored);
353
- }
354
- const body: Record<string, unknown> = {
355
- prompt,
356
- model: CODEX_IMAGE_MODEL,
357
- n,
358
- size,
359
- quality,
360
- background,
361
- response_format: CODEX_IMAGE_RESPONSE_FORMAT,
362
- };
363
- if (storedReferences.length > 0) {
364
- body["images"] = storedReferences.map((stored) => ({ image_url: dataUrl(stored) }));
365
- }
366
- return {
367
- operation: storedReferences.length === 0 ? "generate" : "edit",
368
- n,
369
- body,
370
- references,
371
- storedReferences,
372
- };
373
- }
374
-
375
- async function persistEnvelope(
376
- options: CodexImageControllerOptions,
377
- prepared: PreparedImageRequest,
378
- payload: unknown,
379
- createdAt: number,
380
- ): Promise<CodexImageResult> {
381
- if (!isRecord(payload) || !Array.isArray(payload["data"])) {
382
- throw new LlmError("Codex image response was missing a data array", "SERVER");
383
- }
384
- const warnings: CodexImageWarning[] = [];
385
- const images: ImageAttachmentRef[] = [];
386
- const limits = options.attachments.imageLimits;
387
- const referenceBytes = prepared.storedReferences.reduce((sum, item) => sum + item.data.byteLength, 0);
388
- let acceptedBytes = referenceBytes;
389
- const maxGenerated = Math.max(0, limits.maxImagesPerMessage - prepared.references.length);
390
- for (const [index, item] of payload["data"].entries()) {
391
- if (images.length >= maxGenerated) {
392
- warnings.push({
393
- index,
394
- code: "IMAGE_POLICY_REJECTED",
395
- message: "The item exceeded the deployment image-count limit.",
396
- });
397
- continue;
398
- }
399
- if (!isRecord(item) || typeof item["b64_json"] !== "string" || item["b64_json"].length === 0) {
400
- warnings.push({ index, code: "IMAGE_DATA_MISSING", message: "Only b64_json image items are accepted." });
401
- continue;
402
- }
403
- const encoded = item["b64_json"];
404
- const estimated = estimateDecodedBase64Bytes(encoded);
405
- if (estimated === undefined || estimated > limits.maxImageBytes) {
406
- warnings.push({
407
- index,
408
- code: "IMAGE_DATA_INVALID",
409
- message: "The response item was not valid bounded base64 image data.",
410
- });
411
- continue;
412
- }
413
- const data = decodeImageBase64(encoded);
414
- if (data === undefined || data.byteLength > limits.maxImageBytes) {
415
- warnings.push({
416
- index,
417
- code: "IMAGE_DATA_INVALID",
418
- message: "The response item was not valid bounded base64 image data.",
419
- });
420
- continue;
421
- }
422
- if (acceptedBytes + data.byteLength > limits.maxMessageImageBytes) {
423
- warnings.push({
424
- index,
425
- code: "IMAGE_POLICY_REJECTED",
426
- message: "The item exceeded the deployment total image-byte limit.",
427
- });
428
- continue;
429
- }
430
- const mediaType = detectImageMediaType(data);
431
- if (mediaType === undefined || !limits.mediaTypes.includes(mediaType)) {
432
- warnings.push({
433
- index,
434
- code: "IMAGE_MEDIA_INVALID",
435
- message: "The decoded item was not a supported raster image.",
436
- });
437
- continue;
438
- }
439
- const input: SaveImageAttachment = {
440
- data,
441
- mediaType,
442
- name: `codex-image-${String(createdAt)}-${String(index + 1)}.${extensionFor(mediaType)}`,
443
- };
444
- try {
445
- await options.attachments.validateImage(input);
446
- images.push(await options.attachments.saveImage(input));
447
- acceptedBytes += data.byteLength;
448
- } catch {
449
- warnings.push({
450
- index,
451
- code: "IMAGE_POLICY_REJECTED",
452
- message: "The decoded image failed the deployment media policy.",
453
- });
454
- }
455
- }
456
- if (images.length === 0) {
457
- throw new LlmError("Codex image response contained no usable b64_json image", "EMPTY_RESPONSE");
458
- }
459
- return {
460
- operation: prepared.operation,
461
- model: CODEX_IMAGE_MODEL,
462
- images,
463
- references: prepared.references,
464
- warnings,
465
- };
466
- }
467
-
468
- /** Factory for generate/edit controllers. Parent registers tools only when the feature is enabled. */
469
- export function createCodexImageController(options: CodexImageControllerOptions): CodexImageController {
470
- const http =
471
- options.http ??
472
- createCodexHttpClient({
473
- auth: options.auth,
474
- ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
475
- ...(options.originator === undefined ? {} : { originator: options.originator }),
476
- ...(options.userAgent === undefined ? {} : { userAgent: options.userAgent }),
477
- ...(options.sleep === undefined ? {} : { sleep: options.sleep }),
478
- ...(options.now === undefined ? {} : { now: options.now }),
479
- });
480
- const now = options.now ?? Date.now;
481
-
482
- const dispatch = async (
483
- input: CodexImageGenerateInput,
484
- imageIds: readonly string[] | undefined,
485
- signal?: AbortSignal,
486
- ): Promise<CodexImageResult> => {
487
- await assertSignedIn(options.auth);
488
- const prepared = await prepareRequest(options, input, imageIds, signal);
489
- const payload = await http.requestJson({
490
- url: prepared.operation === "generate" ? CODEX_IMAGE_GENERATION_URL : CODEX_IMAGE_EDIT_URL,
491
- method: "POST",
492
- body: prepared.body,
493
- maxBytes: imageEnvelopeLimit(options.attachments.imageLimits.maxImageBytes, prepared.n),
494
- ...(signal === undefined ? {} : { signal }),
495
- });
496
- return persistEnvelope(options, prepared, payload, now());
497
- };
498
-
499
- return {
500
- generate: (input, signal) => dispatch(input, undefined, signal),
501
- edit: (input, signal) => dispatch(input, input.imageIds, signal),
502
- };
503
- }