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