dsh-codex-subscription 2.4.2 → 2.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/lib/index.js CHANGED
@@ -14,9 +14,9 @@ import { constants, readFileSync } from "node:fs";
14
14
  import { dirname, join, resolve } from "node:path";
15
15
  import { pathToFileURL } from "node:url";
16
16
  import { defineTool } from "@deepseek-ai/dsh-tools";
17
+ import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
17
18
  import * as dshCredentials from "@deepseek-ai/dsh-credentials";
18
19
  import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
19
- import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
20
20
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
21
21
  import z from "@deepseek-ai/schemastery";
22
22
  import { openaiCodexProvider as createOpenAICodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
@@ -166,8 +166,7 @@ const IMAGE_FEATURE_DEFAULTS = Object.freeze({
166
166
  imageViewer: true,
167
167
  imageAnnotations: true,
168
168
  imageSketch: false,
169
- imageSketchAgent: false,
170
- imageSketchAgentPreview: false
169
+ imageSketchAgent: false
171
170
  });
172
171
  function readImageFeatures(value = {}) {
173
172
  return Object.fromEntries(Object.entries(IMAGE_FEATURE_DEFAULTS).map(([key, fallback]) => [key, typeof value?.[key] === "boolean" ? value[key] : fallback]));
@@ -1980,322 +1979,913 @@ const sketchCommandArray = {
1980
1979
  })
1981
1980
  };
1982
1981
  //#endregion
1983
- //#region src/sketch-agent-tool.js
1984
- function createSketchAgentTool(bridge, attachments) {
1985
- return defineTool({
1986
- name: "codex_sketch",
1987
- description: "Edit the sketch board in this session using native editable strokes and layers. Use for @sketch requests and explicit follow-up edits to that drawing. The toolbar pen button is for manual drawing; do not require the user to open it. Start with inspect for the runId, documentId, revision and command reference. Draw with the svg op whenever possible: one SVG document per stage (paths, shapes, groups, transforms; see the reference from inspect) is far faster and richer than hand-placed strokes, and still becomes native editable strokes. Draw the shapes, hair, clothing and lettering outlines of every layer with svg; use stroke commands only for small corrections and native text. Apply atomic batches, preview between stages, and call finish to save the finished draft and release the editing lock. save is only a checkpoint. finish returns an image only when the user enables experimental preview feedback. Closing the board does not stop drawing. If the user stops drawing, do not retry. Never generates AI images, sends messages or attaches images automatically. Inspect automatically opens the board in the currently viewed session. Do not ask the user to open it first. If the session is not visible in DSH, ask them to switch to it. On timeout inspect before retrying; reuse the exact requestId only for the same request.",
1988
- parameters: {
1989
- action: {
1990
- type: "string",
1991
- required: true,
1992
- enum: [
1993
- "inspect",
1994
- "apply",
1995
- "preview",
1996
- "save",
1997
- "finish"
1998
- ]
1999
- },
2000
- runId: {
2001
- type: "string",
2002
- description: "From inspect; required for all other actions. Never reuse a stopped run."
2003
- },
2004
- documentId: {
2005
- type: "string",
2006
- description: "From inspect; required except for inspect."
2007
- },
2008
- revision: {
2009
- type: "integer",
2010
- description: "From latest response; required for apply/save/finish."
2011
- },
2012
- requestId: {
2013
- type: "string",
2014
- description: "Unique id for apply/save/finish; exact retries are deduplicated. After timeout inspect recentRequests before repeating a write."
2015
- },
2016
- commands: {
2017
- oneOf: [sketchCommandArray, { type: "string" }],
2018
- description: "Prefer a native command array. Legacy JSON string also accepted. Required for apply. Use named objects and update existing IDs; prefer Bezier start + segments (control1/control2/end) for curves, not hundreds of pen points."
2019
- },
2020
- name: {
2021
- type: "string",
2022
- description: "Draft name for save/finish."
2023
- },
2024
- offset: {
2025
- type: "integer",
2026
- description: "inspect only: object list offset, default 0. Follow nextOffset for further pages."
2027
- },
2028
- objectId: {
2029
- type: "string",
2030
- description: "inspect only: return full editable geometry for this object, in layer (defaults to active layer)."
2031
- },
2032
- layer: {
2033
- type: "integer",
2034
- description: "inspect only: layer containing objectId."
2035
- }
2036
- },
2037
- timeoutMs: 25e3,
2038
- isConcurrencySafe: () => false,
2039
- async execute(args, exec) {
2040
- const sessionId = exec.agent?.id;
2041
- if (typeof sessionId !== "string") throw Error("A session-owned sketch call is required");
2042
- const request = { ...args };
2043
- if (args.action === "apply") try {
2044
- request.commands = typeof args.commands === "string" ? JSON.parse(args.commands) : args.commands;
2045
- if (!Array.isArray(request.commands)) throw Error();
2046
- } catch {
2047
- throw Error("commands must be a native array or JSON array string");
2048
- }
2049
- const value = await bridge.request(sessionId, request, exec.signal);
2050
- if (value.png) {
2051
- if (!/^data:image\/png;base64,/.test(value.png) || value.png.length > 8 * 1024 * 1024) throw Error("Invalid sketch preview");
2052
- const image = await attachments.saveImage({
2053
- data: new Uint8Array(Buffer.from(value.png.split(",")[1], "base64")),
2054
- mediaType: "image/png",
2055
- name: "sketch-preview.png"
2056
- });
2057
- const { png, ...snapshot } = value;
2058
- return {
2059
- ...snapshot,
2060
- image
2061
- };
2062
- }
2063
- return value;
2064
- },
2065
- output: {
2066
- schema: {
2067
- type: "object",
2068
- additionalProperties: true
2069
- },
2070
- render: (_args, value) => [{
2071
- type: "text",
2072
- text: JSON.stringify({
2073
- ...value,
2074
- image: void 0
2075
- })
2076
- }, ...value.image ? [{
2077
- type: "image",
2078
- attachment: value.image
2079
- }] : []]
2080
- }
2081
- });
1982
+ //#region src/subscription-credentials.js
1983
+ async function readSubscriptionCredentials(getAuth, readCredential, signal) {
1984
+ for (let attempt = 0; attempt < 2; attempt++) {
1985
+ const auth = await getAuth({ signal });
1986
+ signal?.throwIfAborted();
1987
+ if (typeof auth?.auth?.apiKey !== "string" || !auth.auth.apiKey) break;
1988
+ const credential = await readCredential({ signal });
1989
+ signal?.throwIfAborted();
1990
+ if (credential?.type !== "oauth") break;
1991
+ if (credential.access === auth.auth.apiKey) return {
1992
+ access: credential.access,
1993
+ accountId: credential.accountId
1994
+ };
1995
+ }
1996
+ return {
1997
+ access: void 0,
1998
+ accountId: void 0
1999
+ };
2082
2000
  }
2083
- //#endregion
2084
- //#region src/sketch-codec-route.js
2085
- function registerSketchCodec(connection) {
2086
- let source;
2087
- return connection.fetch.register({
2088
- path: "/api/codex-subscription/sketch-psd-worker",
2089
- methods: ["GET"],
2090
- requestBody: "buffered",
2091
- async fetch() {
2092
- source ??= await readFile(new URL("./sketch-psd-worker.js", import.meta.url));
2093
- return new Response(source, { headers: {
2094
- "content-type": "text/javascript; charset=utf-8",
2095
- "cache-control": "no-store"
2096
- } });
2097
- }
2098
- });
2001
+ const ORIGINAL_IMAGE_CHUNK_BYTES = 4 * 1024 * 1024;
2002
+ const ORIGINAL_IMAGE_ID_PATTERN = /^img_[0-9a-f]{32}$/u;
2003
+ const positiveInteger$1 = (value) => Number.isSafeInteger(value) && value > 0;
2004
+ function decodeOriginalImageRef(value) {
2005
+ if (value === null || typeof value !== "object" || Array.isArray(value) || typeof value.assetId !== "string" || !ORIGINAL_IMAGE_ID_PATTERN.test(value.assetId) || value.mediaType !== "image/png" || !positiveInteger$1(value.bytes) || value.bytes > 48 * 1024 * 1024 || !positiveInteger$1(value.width) || !positiveInteger$1(value.height) || typeof value.name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value.name) || typeof value.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(value.sha256)) return void 0;
2006
+ return {
2007
+ assetId: value.assetId,
2008
+ mediaType: value.mediaType,
2009
+ bytes: value.bytes,
2010
+ width: value.width,
2011
+ height: value.height,
2012
+ name: value.name,
2013
+ sha256: value.sha256
2014
+ };
2099
2015
  }
2100
- //#endregion
2101
- //#region src/account-vault.js
2102
- const VERSION = 1;
2103
- const DEFAULT_LABEL = "Account 1";
2104
- const clone$1 = (value) => value === void 0 ? void 0 : structuredClone(value);
2105
- const EMAIL_MAX_LENGTH = 254;
2106
- const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
2107
- /** Keep only a bounded, display-safe email address from a trusted OAuth result. */
2108
- function normalizeAccountEmail(value) {
2109
- if (typeof value !== "string") return void 0;
2110
- const email = value.trim();
2111
- return email.length > 0 && email.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(email) ? email : void 0;
2016
+ function decodeImagePresentation(value) {
2017
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value.kind !== "codex-subscription-image" || value.schemaVersion !== 1) return void 0;
2018
+ const original = decodeOriginalImageRef(value.original);
2019
+ return original === void 0 ? void 0 : { original };
2112
2020
  }
2113
- function decodeJwtPayload(access) {
2114
- if (typeof access !== "string") return void 0;
2115
- const encoded = access.split(".")[1];
2116
- if (typeof encoded !== "string" || encoded.length === 0) return void 0;
2117
- try {
2118
- return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
2021
+ function originalImageRefsEqual(left, right) {
2022
+ const a = decodeOriginalImageRef(left);
2023
+ const b = decodeOriginalImageRef(right);
2024
+ return a !== void 0 && b !== void 0 && a.assetId === b.assetId && a.mediaType === b.mediaType && a.bytes === b.bytes && a.width === b.width && a.height === b.height && a.name === b.name && a.sha256 === b.sha256;
2025
+ }
2026
+ /** Resolve only an exact original reference copied into a DSH fork prefix. */
2027
+ function inheritedOriginalImageRef(session, assetId) {
2028
+ const parentSession = session?.header?.parentSession;
2029
+ const seedLength = Number.isSafeInteger(session?.inheritedEventCount) ? session.inheritedEventCount : session?.header?.seedLength;
2030
+ if (typeof parentSession !== "string" || parentSession.length === 0 || !Number.isSafeInteger(seedLength) || seedLength < 0 || !ORIGINAL_IMAGE_ID_PATTERN.test(assetId)) return void 0;
2031
+ let events = session?.events;
2032
+ if (!Array.isArray(events) && typeof session?.snapshotEvents === "function") try {
2033
+ events = session.snapshotEvents();
2119
2034
  } catch {
2120
2035
  return;
2121
2036
  }
2037
+ if (!Array.isArray(events)) return void 0;
2038
+ for (const event of events) {
2039
+ if (!Number.isSafeInteger(event?.seq) || event.seq < 0 || event.seq >= seedLength || event.type !== "tool/result") continue;
2040
+ const original = decodeImagePresentation(event.data?.meta)?.original;
2041
+ if (original?.assetId === assetId) return original;
2042
+ }
2122
2043
  }
2123
- function emailFromAccessToken(access) {
2124
- const payload = decodeJwtPayload(access);
2125
- return normalizeAccountEmail(payload?.["https://api.openai.com/profile"]?.email ?? payload?.email);
2044
+ //#endregion
2045
+ //#region src/version.js
2046
+ const PACKAGE_VERSION = "2.5.0";
2047
+ const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
2048
+ //#endregion
2049
+ //#region src/codex-images.js
2050
+ const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
2051
+ const CODEX_IMAGE_GENERATION_URL = "https://chatgpt.com/backend-api/codex/images/generations";
2052
+ const CODEX_IMAGE_EDIT_URL = "https://chatgpt.com/backend-api/codex/images/edits";
2053
+ const MAX_REFERENCE_IMAGES = 5;
2054
+ const RESPONSE_ENVELOPE_BYTES = 1024 * 1024;
2055
+ const IMAGE_BACKGROUNDS = /* @__PURE__ */ new Set([
2056
+ "auto",
2057
+ "transparent",
2058
+ "opaque"
2059
+ ]);
2060
+ const PNG_SIGNATURE = Buffer.from([
2061
+ 137,
2062
+ 80,
2063
+ 78,
2064
+ 71,
2065
+ 13,
2066
+ 10,
2067
+ 26,
2068
+ 10
2069
+ ]);
2070
+ const FULL_ATTACHMENT_ID = /^sha256:[0-9a-f]{64}$/u;
2071
+ const BARE_ATTACHMENT_DIGEST = /^[0-9a-f]{64}$/iu;
2072
+ const PATH_LIKE_ATTACHMENT_ID = /[\\/]/u;
2073
+ const FILE_NAME_ATTACHMENT_ID = /\.[A-Za-z0-9]{1,16}$/u;
2074
+ const record$4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
2075
+ const nonEmpty$2 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
2076
+ const NETWORK_REASONS = {
2077
+ UND_ERR_HEADERS_TIMEOUT: "the image service sent no response within 5 minutes",
2078
+ UND_ERR_BODY_TIMEOUT: "the image response stalled",
2079
+ UND_ERR_CONNECT_TIMEOUT: "the connection to the image service timed out",
2080
+ UND_ERR_SOCKET: "the connection closed unexpectedly",
2081
+ ECONNRESET: "the connection was reset",
2082
+ ETIMEDOUT: "the connection timed out",
2083
+ ENOTFOUND: "DNS lookup failed",
2084
+ ECONNREFUSED: "the connection was refused",
2085
+ EAI_AGAIN: "DNS lookup failed"
2086
+ };
2087
+ function requestFailureReason(error) {
2088
+ const code = [(error?.cause ?? error)?.code, error?.code].find((value) => typeof value === "string" && /^[A-Z][A-Z0-9_]{2,39}$/u.test(value));
2089
+ if (code !== void 0) return `: ${NETWORK_REASONS[code] ?? "network error"} (${code})`;
2090
+ if (error?.name === "TimeoutError") return ": the image request timed out";
2091
+ return "";
2126
2092
  }
2127
- /** Normalize the one non-secret account attribute that may cross the UI boundary. */
2128
- function sanitizeOAuthCredential(value) {
2129
- const credential = assertOAuthCredential$1(value);
2130
- const email = emailFromAccessToken(credential.access) ?? normalizeAccountEmail(credential.email);
2131
- if (email === void 0) {
2132
- delete credential.email;
2133
- return credential;
2093
+ function normalizeImageOptions(args) {
2094
+ const quality = nonEmpty$2(args?.quality) ?? "auto";
2095
+ const background = nonEmpty$2(args?.background) ?? "auto";
2096
+ const size = nonEmpty$2(args?.size) ?? "auto";
2097
+ validateImageQuality(resolveImageModel(args?.model), quality);
2098
+ if (!IMAGE_BACKGROUNDS.has(background)) throw new Error("background must be auto, transparent, or opaque");
2099
+ if (size !== "auto") {
2100
+ const match = /^(\d+)x(\d+)$/u.exec(size);
2101
+ const width = Number(match?.[1]);
2102
+ const height = Number(match?.[2]);
2103
+ const short = Math.min(width, height);
2104
+ const long = Math.max(width, height);
2105
+ const pixels = width * height;
2106
+ if (match === null || width % 16 !== 0 || height % 16 !== 0 || long > 3840 || long > short * 3 || pixels < 655360 || pixels > 8294400) throw new Error("size must be auto or a valid GPT Image 2 widthxheight resolution");
2134
2107
  }
2135
2108
  return {
2136
- ...credential,
2137
- email
2138
- };
2139
- }
2140
- function assertOAuthCredential$1(value) {
2141
- if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex account vault received a malformed OAuth credential");
2142
- return clone$1(value);
2109
+ quality,
2110
+ background,
2111
+ size
2112
+ };
2143
2113
  }
2144
- function parseOAuthCredential$1(value) {
2114
+ function encodedLimit(decodedBytes) {
2115
+ return Math.ceil(decodedBytes / 3) * 4;
2116
+ }
2117
+ function validBase64Body(value, end) {
2118
+ for (let index = 0; index < end; index += 1) {
2119
+ const code = value.charCodeAt(index);
2120
+ if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47)) return false;
2121
+ }
2122
+ return true;
2123
+ }
2124
+ async function readJsonWithin(response, maximumBytes) {
2125
+ const contentLength = Number(response.headers.get("content-length"));
2126
+ if (Number.isFinite(contentLength) && contentLength > maximumBytes) throw new Error("Codex image response exceeds the image size limit");
2127
+ if (response.body === null) throw new Error("Codex returned an unreadable image response");
2128
+ const reader = response.body.getReader();
2129
+ const chunks = [];
2130
+ let bytes = 0;
2131
+ while (true) {
2132
+ const { done, value } = await reader.read();
2133
+ if (done) break;
2134
+ bytes += value.byteLength;
2135
+ if (bytes > maximumBytes) {
2136
+ await reader.cancel();
2137
+ throw new Error("Codex image response exceeds the image size limit");
2138
+ }
2139
+ chunks.push(value);
2140
+ }
2141
+ const body = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), bytes).toString("utf8");
2145
2142
  try {
2146
- return assertOAuthCredential$1(JSON.parse(value));
2147
- } catch (error) {
2148
- if (error?.message === "Codex account vault received a malformed OAuth credential") throw error;
2149
- throw new Error("Codex account vault contains malformed OAuth JSON", { cause: error });
2143
+ return JSON.parse(body);
2144
+ } catch {
2145
+ throw new Error("Codex returned an unreadable image response");
2150
2146
  }
2151
2147
  }
2152
- function normalizeLabel(value) {
2153
- if (typeof value !== "string") throw new Error("Codex account label must be text");
2154
- const label = value.trim().replace(/\s+/gu, " ");
2155
- if (label.length === 0 || label.length > 48) throw new Error("Codex account label must contain 1 to 48 characters");
2156
- return label;
2148
+ /** Strictly decode one PNG returned by the subscription backend. */
2149
+ function decodeCodexPng(value, maximumBytes) {
2150
+ const encoded = nonEmpty$2(value);
2151
+ const padding = encoded?.endsWith("==") ? 2 : encoded?.endsWith("=") ? 1 : 0;
2152
+ if (encoded === void 0 || encoded.length % 4 !== 0 || !validBase64Body(encoded, encoded.length - padding)) throw new Error("Codex returned an invalid base64 PNG");
2153
+ const decodedBytes = encoded.length / 4 * 3 - padding;
2154
+ if (decodedBytes > maximumBytes) throw new Error("Codex image exceeds the image size limit");
2155
+ const data = Buffer.from(encoded, "base64");
2156
+ if (data.length !== decodedBytes || data.length < PNG_SIGNATURE.length || !data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) throw new Error("Codex returned an invalid PNG");
2157
+ return new Uint8Array(data);
2157
2158
  }
2158
- function assertVaultRecord(record) {
2159
- if (record?.kind !== "grant" || record.payload?.version !== VERSION || typeof record.payload.activeId !== "string" || !Array.isArray(record.payload.accounts) || record.payload.accounts.length === 0) throw new Error("Codex account vault contains a malformed grant record");
2160
- const ids = /* @__PURE__ */ new Set();
2161
- const accounts = record.payload.accounts.map((account) => {
2162
- if (account === null || typeof account !== "object" || typeof account.id !== "string" || account.id.length === 0 || ids.has(account.id)) throw new Error("Codex account vault contains a malformed account id");
2163
- ids.add(account.id);
2164
- return {
2165
- id: account.id,
2166
- label: normalizeLabel(account.label),
2167
- credential: sanitizeOAuthCredential(account.credential)
2168
- };
2159
+ function imageReference(value) {
2160
+ const originalDimensions = record$4(value.originalDimensions) ? {
2161
+ width: value.originalDimensions.width,
2162
+ height: value.originalDimensions.height
2163
+ } : void 0;
2164
+ return {
2165
+ attachmentId: value.attachmentId,
2166
+ mediaType: value.mediaType,
2167
+ bytes: value.bytes,
2168
+ width: value.width,
2169
+ height: value.height,
2170
+ ...value.name === void 0 ? {} : { name: value.name },
2171
+ ...originalDimensions === void 0 ? {} : { originalDimensions }
2172
+ };
2173
+ }
2174
+ function normalizeAttachmentId(value) {
2175
+ if (typeof value !== "string") return void 0;
2176
+ if (FULL_ATTACHMENT_ID.test(value)) return value;
2177
+ const bare = BARE_ATTACHMENT_DIGEST.exec(value);
2178
+ return bare === null ? void 0 : `sha256:${bare[0].toLowerCase()}`;
2179
+ }
2180
+ function invalidAttachmentId(value) {
2181
+ const attachmentId = value?.attachmentId;
2182
+ if (typeof attachmentId === "string" && (PATH_LIKE_ATTACHMENT_ID.test(attachmentId) || FILE_NAME_ATTACHMENT_ID.test(attachmentId))) throw new Error("referenceImages attachmentId is a file path or filename; call read_image on that file and retry with its complete sha256:<64 lowercase hex> attachment reference. Do not omit referenceImages or fall back to new image generation.");
2183
+ throw new Error("referenceImages attachmentId must be sha256:<64 lowercase hex> copied from an image block or read_image result. Do not omit referenceImages or fall back to new image generation.");
2184
+ }
2185
+ function referenceOf(value, attachments) {
2186
+ const attachmentId = normalizeAttachmentId(value?.attachmentId);
2187
+ if (attachmentId === void 0) invalidAttachmentId(value);
2188
+ if (!record$4(value) || !attachments.imageLimits.mediaTypes.includes(value.mediaType) || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || !Number.isSafeInteger(value.width) || value.width <= 0 || !Number.isSafeInteger(value.height) || value.height <= 0 || value.name !== void 0 && (typeof value.name !== "string" || value.name.length > 256) || value.originalDimensions !== void 0 && (!record$4(value.originalDimensions) || !Number.isSafeInteger(value.originalDimensions.width) || value.originalDimensions.width <= 0 || !Number.isSafeInteger(value.originalDimensions.height) || value.originalDimensions.height <= 0)) throw new Error("referenceImages contains an invalid image reference");
2189
+ return imageReference({
2190
+ ...value,
2191
+ attachmentId
2169
2192
  });
2170
- if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
2171
- const legacyAccountId = record.payload.legacyAccountId;
2172
- if (legacyAccountId !== void 0 && !ids.has(legacyAccountId)) throw new Error("Codex account vault legacy account is missing");
2193
+ }
2194
+ function sessionImageReferences(messages) {
2195
+ const references = /* @__PURE__ */ new Map();
2196
+ const visit = (content) => {
2197
+ if (!Array.isArray(content)) return;
2198
+ for (const block of content) if (block?.type === "image" && record$4(block.attachment)) {
2199
+ const id = normalizeAttachmentId(block.attachment.attachmentId);
2200
+ if (id !== void 0) references.set(id, block.attachment);
2201
+ } else if (block?.type === "tool-result") visit(block.content);
2202
+ };
2203
+ for (const message of messages ?? []) visit(message?.content);
2204
+ return references;
2205
+ }
2206
+ async function readSessionImage(attachmentId, attachments, signal, messages) {
2207
+ const id = normalizeAttachmentId(attachmentId);
2208
+ if (id === void 0) invalidAttachmentId({ attachmentId });
2209
+ const selected = sessionImageReferences(messages).get(id);
2210
+ if (selected === void 0) throw new Error("The selected image attachment cannot be found in the current session. Copy attachmentId from the image block of the user message; never pass a path or filename.");
2211
+ const stored = await attachments.readImage(referenceOf(selected, attachments), signal);
2173
2212
  return {
2174
- version: VERSION,
2175
- activeId: record.payload.activeId,
2176
- legacyAccountId,
2177
- accounts
2213
+ data: stored.data,
2214
+ mediaType: stored.ref.mediaType,
2215
+ name: stored.ref.name
2178
2216
  };
2179
2217
  }
2180
- const grant = (payload) => ({
2181
- kind: "grant",
2182
- payload
2183
- });
2184
- var PendingOAuthCredentialStore = class {
2185
- #credential;
2186
- async read(providerId) {
2187
- if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2188
- return clone$1(this.#credential);
2189
- }
2190
- async list() {
2191
- return this.#credential === void 0 ? [] : [{
2192
- providerId: "openai-codex",
2193
- type: "oauth"
2194
- }];
2195
- }
2196
- async modify(providerId, update) {
2197
- if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2198
- const next = await update(clone$1(this.#credential));
2199
- if (next !== void 0) this.#credential = sanitizeOAuthCredential(next);
2200
- return clone$1(this.#credential);
2201
- }
2202
- async delete(providerId) {
2203
- if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2204
- this.#credential = void 0;
2205
- }
2206
- credential() {
2207
- return clone$1(this.#credential);
2208
- }
2209
- };
2210
- /**
2211
- * Multi-account owner state stored in DSH's atomic plugin credential record.
2212
- * The old single-account reference remains as a rollback source and is kept in
2213
- * sync whenever that imported account rotates its refresh token.
2214
- */
2215
- var DshOAuthAccountVault = class {
2216
- #tail = Promise.resolve();
2217
- constructor(credentials, options) {
2218
- if (credentials === void 0 || credentials === null || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("Codex multi-account requires DSH credential records");
2219
- this.credentials = credentials;
2220
- this.key = options.key;
2221
- this.legacyRef = options.legacyRef;
2222
- this.legacyRefs = Object.freeze([...options.legacyRefs ?? []]);
2223
- this.createId = options.createId ?? randomUUID;
2224
- this.onLegacySyncFailure = options.onLegacySyncFailure ?? (() => {});
2225
- }
2226
- #enqueue(operation) {
2227
- const current = this.#tail.catch(() => void 0).then(operation);
2228
- this.#tail = current.catch(() => void 0);
2229
- return current;
2230
- }
2231
- async #legacyCredential() {
2232
- for (const ref of [this.legacyRef, ...this.legacyRefs]) {
2233
- const hit = await this.credentials.resolve(ref);
2234
- if (hit?.value === void 0 || hit.value === "") continue;
2235
- return {
2236
- ref,
2237
- credential: parseOAuthCredential$1(hit.value)
2238
- };
2239
- }
2240
- }
2241
- async #ensurePayload() {
2242
- const existing = await this.credentials.readRecord(this.key);
2243
- if (existing !== void 0) return assertVaultRecord(existing);
2244
- const legacy = await this.#legacyCredential();
2245
- if (legacy === void 0) return void 0;
2246
- const id = this.createId();
2247
- return assertVaultRecord(await this.credentials.modifyRecord(this.key, (current) => {
2248
- if (current !== void 0) return Promise.resolve(current);
2249
- return Promise.resolve(grant({
2250
- version: VERSION,
2251
- activeId: id,
2252
- legacyAccountId: id,
2253
- accounts: [{
2254
- id,
2255
- label: DEFAULT_LABEL,
2256
- credential: legacy.credential
2257
- }]
2258
- }));
2259
- }));
2260
- }
2261
- async #modifyPayload(update) {
2262
- await this.#ensurePayload();
2263
- let previousLegacy;
2264
- const payload = assertVaultRecord(await this.credentials.modifyRecord(this.key, async (current) => {
2265
- if (current === void 0) throw new Error("Codex account vault is not signed in");
2266
- const payload = assertVaultRecord(current);
2267
- previousLegacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
2268
- const next = await update(clone$1(payload));
2269
- return grant(next);
2270
- }));
2271
- const legacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
2272
- try {
2273
- if (legacy === void 0) {
2274
- if (previousLegacy !== void 0) await this.credentials.unset(this.legacyRef);
2275
- } else if (JSON.stringify(legacy) !== JSON.stringify(previousLegacy)) await this.credentials.set(this.legacyRef, JSON.stringify(legacy));
2276
- } catch {
2277
- this.onLegacySyncFailure();
2278
- }
2279
- return payload;
2280
- }
2281
- list() {
2282
- return this.#enqueue(async () => {
2283
- const payload = await this.#ensurePayload();
2284
- if (payload === void 0) return [];
2285
- return payload.accounts.map((account) => ({
2286
- id: account.id,
2287
- label: account.label,
2288
- active: account.id === payload.activeId,
2289
- expiresAt: account.credential.expires,
2290
- ...account.credential.email === void 0 ? {} : { email: account.credential.email }
2291
- }));
2292
- });
2293
- }
2294
- readActive() {
2295
- return this.#enqueue(async () => {
2296
- const payload = await this.#ensurePayload();
2297
- return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
2298
- });
2218
+ async function editImages(values, attachments, signal, messages) {
2219
+ if (!Array.isArray(values) || values.length === 0 || values.length > MAX_REFERENCE_IMAGES) throw new Error(`referenceImages must contain between 1 and ${MAX_REFERENCE_IMAGES} images`);
2220
+ const available = messages === void 0 ? void 0 : sessionImageReferences(messages);
2221
+ const references = values.map((value) => {
2222
+ const id = normalizeAttachmentId(value?.attachmentId);
2223
+ if (id === void 0) invalidAttachmentId(value);
2224
+ if (available === void 0) return referenceOf(value, attachments);
2225
+ const selected = available.get(id);
2226
+ if (selected === void 0) throw new Error("The selected image attachment cannot be found in the current session. Call read_image on the intended image and retry with its returned reference. Do not omit referenceImages or substitute another image.");
2227
+ return referenceOf(selected, attachments);
2228
+ });
2229
+ if (new Set(references.map((value) => value.attachmentId)).size !== references.length) throw new Error("referenceImages must not contain duplicates");
2230
+ const images = [];
2231
+ let totalBytes = 0;
2232
+ for (const reference of references) {
2233
+ const stored = await attachments.readImage(reference, signal);
2234
+ totalBytes += stored.data.byteLength;
2235
+ if (totalBytes > attachments.imageLimits.maxMessageImageBytes) throw new Error("referenceImages exceed the DSH message image limit");
2236
+ images.push({ image_url: `data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}` });
2237
+ }
2238
+ return images;
2239
+ }
2240
+ function imageContent(value) {
2241
+ const label = typeof value.size === "string" && value.size.length > 0 ? `Generated a ${value.size} image.` : "Generated an image.";
2242
+ return [{
2243
+ type: "text",
2244
+ text: value.localPath === void 0 ? label : `${label}\nOriginal PNG saved on the DSH host at: ${JSON.stringify(value.localPath)}. Read this file or copy it to the workspace with a .png extension; this host path is not a browser URL.`
2245
+ }, {
2246
+ type: "image",
2247
+ attachment: imageReference(value.image)
2248
+ }];
2249
+ }
2250
+ function imageOutputSchema() {
2251
+ return {
2252
+ type: "object",
2253
+ additionalProperties: false,
2254
+ properties: {
2255
+ image: {
2256
+ type: "object",
2257
+ required: true,
2258
+ additionalProperties: false,
2259
+ properties: {
2260
+ attachmentId: {
2261
+ type: "string",
2262
+ required: true
2263
+ },
2264
+ mediaType: {
2265
+ type: "string",
2266
+ enum: ["image/png"],
2267
+ required: true
2268
+ },
2269
+ bytes: {
2270
+ type: "integer",
2271
+ required: true
2272
+ },
2273
+ width: {
2274
+ type: "integer",
2275
+ required: true
2276
+ },
2277
+ height: {
2278
+ type: "integer",
2279
+ required: true
2280
+ },
2281
+ name: { type: "string" },
2282
+ originalDimensions: {
2283
+ type: "object",
2284
+ additionalProperties: false,
2285
+ properties: {
2286
+ width: {
2287
+ type: "integer",
2288
+ required: true
2289
+ },
2290
+ height: {
2291
+ type: "integer",
2292
+ required: true
2293
+ }
2294
+ }
2295
+ }
2296
+ }
2297
+ },
2298
+ original: {
2299
+ type: "object",
2300
+ required: true,
2301
+ additionalProperties: false,
2302
+ properties: {
2303
+ assetId: {
2304
+ type: "string",
2305
+ required: true
2306
+ },
2307
+ mediaType: {
2308
+ type: "string",
2309
+ enum: ["image/png"],
2310
+ required: true
2311
+ },
2312
+ bytes: {
2313
+ type: "integer",
2314
+ required: true
2315
+ },
2316
+ width: {
2317
+ type: "integer",
2318
+ required: true
2319
+ },
2320
+ height: {
2321
+ type: "integer",
2322
+ required: true
2323
+ },
2324
+ name: {
2325
+ type: "string",
2326
+ required: true
2327
+ },
2328
+ sha256: {
2329
+ type: "string",
2330
+ required: true
2331
+ }
2332
+ }
2333
+ },
2334
+ background: { type: "string" },
2335
+ requestedModel: { type: "string" },
2336
+ reportedModel: { type: "string" },
2337
+ requestedSize: { type: "string" },
2338
+ localPath: {
2339
+ type: "string",
2340
+ required: true,
2341
+ description: "Absolute path to the original PNG on the DSH host."
2342
+ },
2343
+ quality: { type: "string" },
2344
+ size: { type: "string" }
2345
+ }
2346
+ };
2347
+ }
2348
+ function responseMetadata(value) {
2349
+ const data = Array.isArray(value?.data) ? value.data[0] : void 0;
2350
+ const encoded = record$4(data) ? data.b64_json : void 0;
2351
+ if (typeof encoded !== "string") throw new Error("Codex returned no image data");
2352
+ return {
2353
+ encoded,
2354
+ background: nonEmpty$2(value.background),
2355
+ quality: nonEmpty$2(value.quality),
2356
+ size: nonEmpty$2(value.size),
2357
+ reportedModel: nonEmpty$2(value.model)
2358
+ };
2359
+ }
2360
+ /** Create the DSH-native image-generation tool backed only by the ChatGPT subscription. */
2361
+ function createCodexImageTool(options) {
2362
+ const fetchImage = options.fetch ?? fetch;
2363
+ const attachments = options.attachments;
2364
+ return defineTool({
2365
+ name: CODEX_IMAGE_TOOL_NAME,
2366
+ description: "Generate or edit images only when the user asks for image output, not when merely discussing images. Uses the signed-in Codex subscription. For a new image, omit referenceImages. For edits, copy attachmentId only from the selected session image block; the host supplies its metadata. Never call read_image for an attachmentId or attachment filename. Use read_image only for an actual local file whose reference is not already in the conversation. Never substitute paths, unrelated images, or text-only generation for an edit. For numbered annotations, include the clean source and location-reference image, preserve the requested changes and coordinates in the prompt, and remove guidance markers from the result. If the intended references cannot be identified, ask rather than guessing.",
2367
+ parameters: {
2368
+ model: {
2369
+ type: "string",
2370
+ enum: Object.keys(IMAGE_MODELS),
2371
+ description: "Optional image engine, independent of the conversation model. When omitted, uses the user image setting (initially gpt-image-2). The 2.5 identifiers are experimental subscription candidates; override only when explicitly requested. Never silently retry with another model."
2372
+ },
2373
+ prompt: {
2374
+ type: "string",
2375
+ required: true,
2376
+ description: "A complete, production-ready description of the image to generate."
2377
+ },
2378
+ size: {
2379
+ type: "string",
2380
+ description: "Optional GPT Image 2 output size. Use auto unless the user requests an exact valid widthxheight resolution."
2381
+ },
2382
+ quality: {
2383
+ type: "string",
2384
+ enum: [
2385
+ "auto",
2386
+ "low",
2387
+ "medium",
2388
+ "high",
2389
+ "xhigh",
2390
+ "max"
2391
+ ],
2392
+ description: "Optional rendering quality. Use auto unless the user requests draft speed or final quality."
2393
+ },
2394
+ background: {
2395
+ type: "string",
2396
+ enum: [
2397
+ "auto",
2398
+ "transparent",
2399
+ "opaque"
2400
+ ],
2401
+ description: "Optional background mode. Request transparent only when the user needs transparency."
2402
+ },
2403
+ referenceImages: {
2404
+ type: "array",
2405
+ description: "Optional references to 1-5 selected images. For session images provide only attachmentId; the host resolves trusted metadata. Omit only for a new image; never drop an invalid reference to bypass editing.",
2406
+ items: {
2407
+ type: "object",
2408
+ additionalProperties: false,
2409
+ properties: {
2410
+ attachmentId: {
2411
+ type: "string",
2412
+ required: true,
2413
+ description: "Copy the complete attachmentId from the image block or read_image result: sha256:<64 lowercase hex>. Never pass a workspace path, absolute path, or filename. A bare 64-character hex digest is accepted and normalized to sha256:<64 lowercase hex>."
2414
+ },
2415
+ mediaType: {
2416
+ type: "string",
2417
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2418
+ },
2419
+ bytes: {
2420
+ type: "integer",
2421
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2422
+ },
2423
+ width: {
2424
+ type: "integer",
2425
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2426
+ },
2427
+ height: {
2428
+ type: "integer",
2429
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2430
+ },
2431
+ name: { type: "string" },
2432
+ originalDimensions: {
2433
+ type: "object",
2434
+ additionalProperties: false,
2435
+ properties: {
2436
+ width: {
2437
+ type: "integer",
2438
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2439
+ },
2440
+ height: {
2441
+ type: "integer",
2442
+ ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
2443
+ }
2444
+ }
2445
+ }
2446
+ }
2447
+ }
2448
+ }
2449
+ },
2450
+ output: {
2451
+ schema: imageOutputSchema(),
2452
+ render: (_args, value) => imageContent(value),
2453
+ presentationMeta: (_args, value) => ({
2454
+ kind: "codex-subscription-image",
2455
+ schemaVersion: 1,
2456
+ original: value.original,
2457
+ ...value.requestedModel === void 0 ? {} : { requestedModel: value.requestedModel },
2458
+ ...value.reportedModel === void 0 ? {} : { reportedModel: value.reportedModel },
2459
+ ...value.requestedSize === void 0 ? {} : { requestedSize: value.requestedSize }
2460
+ })
2461
+ },
2462
+ timeoutMs: 300 * 1e3,
2463
+ isConcurrencySafe: () => false,
2464
+ async execute(args, exec) {
2465
+ assertImageOperation(options.getFeatures?.(), args.referenceImages !== void 0);
2466
+ const defaults = readImageDefaults(options.getFeatures?.());
2467
+ args = {
2468
+ ...args,
2469
+ model: args.model ?? defaults.imageModel,
2470
+ quality: args.quality ?? defaults.imageQuality
2471
+ };
2472
+ const prompt = nonEmpty$2(args.prompt);
2473
+ if (prompt === void 0) throw new Error("prompt must be a non-empty string");
2474
+ const imageOptions = normalizeImageOptions(args);
2475
+ const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, exec.signal);
2476
+ if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
2477
+ if (!attachments.imageLimits.mediaTypes.includes("image/png")) throw new Error("This DSH installation does not accept PNG image attachments");
2478
+ const maximumBytes = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
2479
+ const editing = args.referenceImages !== void 0;
2480
+ const sessionMessages = editing && typeof options.getSessionMessages === "function" ? await options.getSessionMessages(exec.agent?.id) : void 0;
2481
+ const images = editing ? await editImages(args.referenceImages, attachments, exec.signal, sessionMessages ?? (options.getSessionMessages ? [] : void 0)) : void 0;
2482
+ let response;
2483
+ assertImageOperation(options.getFeatures?.(), editing);
2484
+ try {
2485
+ response = await fetchImage(editing ? CODEX_IMAGE_EDIT_URL : CODEX_IMAGE_GENERATION_URL, {
2486
+ method: "POST",
2487
+ redirect: "error",
2488
+ headers: {
2489
+ authorization: `Bearer ${access}`,
2490
+ "chatgpt-account-id": accountId,
2491
+ accept: "application/json",
2492
+ "content-type": "application/json",
2493
+ originator: "pi",
2494
+ "x-codex-image-turn-id": String(exec.callId),
2495
+ "user-agent": USER_AGENT
2496
+ },
2497
+ body: JSON.stringify({
2498
+ ...images === void 0 ? {} : { images },
2499
+ prompt,
2500
+ background: imageOptions.background,
2501
+ model: resolveImageModel(args.model),
2502
+ quality: imageOptions.quality,
2503
+ size: imageOptions.size
2504
+ }),
2505
+ signal: exec.signal
2506
+ });
2507
+ } catch (error) {
2508
+ if (exec.signal.aborted) throw exec.signal.reason;
2509
+ throw new Error(`Codex image ${editing ? "edit" : "generation"} request failed${requestFailureReason(error)}`, { cause: error });
2510
+ }
2511
+ if (!response.ok) {
2512
+ if (response.status === 401 || response.status === 403) throw new Error("ChatGPT sign-in needs to be renewed");
2513
+ if (response.status === 429) throw new Error("Codex image generation quota is unavailable");
2514
+ throw new Error(`Codex image ${editing ? "edit" : "generation"} failed (HTTP ${response.status})`);
2515
+ }
2516
+ const metadata = responseMetadata(await readJsonWithin(response, encodedLimit(maximumBytes) + RESPONSE_ENVELOPE_BYTES));
2517
+ const data = decodeCodexPng(metadata.encoded, maximumBytes);
2518
+ const sessionId = exec.agent?.id;
2519
+ if (sessionId === void 0) throw new Error("Codex image generation requires a session-owned tool call");
2520
+ const original = await options.originalImages.save(String(sessionId), data);
2521
+ let ref;
2522
+ try {
2523
+ ref = await attachments.saveImage({
2524
+ data,
2525
+ mediaType: "image/png",
2526
+ name: "codex-generated.png"
2527
+ });
2528
+ } catch (error) {
2529
+ await options.originalImages.remove(original);
2530
+ throw error;
2531
+ }
2532
+ const result = {
2533
+ requestedModel: resolveImageModel(args.model),
2534
+ requestedSize: imageOptions.size,
2535
+ ...metadata.reportedModel === void 0 ? {} : { reportedModel: metadata.reportedModel },
2536
+ image: imageReference(ref),
2537
+ original,
2538
+ localPath: options.originalImages.originalPath(original.assetId),
2539
+ ...metadata.background === void 0 ? {} : { background: metadata.background },
2540
+ ...metadata.quality === void 0 ? {} : { quality: metadata.quality },
2541
+ size: `${original.width}x${original.height}`
2542
+ };
2543
+ if (exec.parent !== void 0) exec.deferContext(createUserMessage({
2544
+ content: imageContent(result),
2545
+ source: { kind: "codex-subscription-image" }
2546
+ }));
2547
+ return result;
2548
+ }
2549
+ });
2550
+ }
2551
+ //#endregion
2552
+ //#region src/sketch-agent-tool.js
2553
+ const MAX_REFERENCE_BYTES = 14e5;
2554
+ function createSketchAgentTool(bridge, attachments, getSessionMessages = () => []) {
2555
+ return defineTool({
2556
+ name: "codex_sketch",
2557
+ description: "Edit the sketch board in this session using native editable strokes and layers. Use for @sketch requests and explicit follow-up edits to that drawing. The toolbar pen button is for manual drawing; do not require the user to open it. Start with inspect for the runId, documentId, revision and command reference. Draw with the svg op whenever possible: one SVG document per stage (paths, shapes, groups, transforms; see the reference from inspect) is far faster and richer than hand-placed strokes, and still becomes native editable strokes. Draw the shapes, hair, clothing and lettering outlines of every layer with svg; use stroke commands only for small corrections and native text. Apply atomic batches and preview between stages. When the user attached a reference image, call reference with its attachmentId once after inspect to put it on the board as a hidden picture layer. When a reference picture is on the board, keep its layer hidden while drawing (compare shows it anyway), measure its key positions with preview grid, draw big shapes first, and after each stage check preview compare and fix the largest offset before adding detail. For repeated details such as hair strands or lace, generate the SVG with a script instead of placing each one by hand. and call finish to save the finished draft and release the editing lock. save is only a checkpoint. Closing the board does not stop drawing. If the user stops drawing, do not retry. Never generates AI images, sends messages or attaches images automatically. Inspect automatically opens the board in the currently viewed session. Do not ask the user to open it first. If the session is not visible in DSH, ask them to switch to it. On timeout inspect before retrying; reuse the exact requestId only for the same request.",
2558
+ parameters: {
2559
+ action: {
2560
+ type: "string",
2561
+ required: true,
2562
+ enum: [
2563
+ "inspect",
2564
+ "apply",
2565
+ "reference",
2566
+ "preview",
2567
+ "save",
2568
+ "finish"
2569
+ ]
2570
+ },
2571
+ runId: {
2572
+ type: "string",
2573
+ description: "From inspect; required for all other actions. Never reuse a stopped run."
2574
+ },
2575
+ documentId: {
2576
+ type: "string",
2577
+ description: "From inspect; required except for inspect."
2578
+ },
2579
+ revision: {
2580
+ type: "integer",
2581
+ description: "From latest response; required for apply/reference/save/finish."
2582
+ },
2583
+ requestId: {
2584
+ type: "string",
2585
+ description: "Unique id for apply/reference/save/finish; exact retries are deduplicated. After timeout inspect recentRequests before repeating a write."
2586
+ },
2587
+ commands: {
2588
+ oneOf: [sketchCommandArray, { type: "string" }],
2589
+ description: "Prefer a native command array. Legacy JSON string also accepted. Required for apply. Use named objects and update existing IDs; prefer Bezier start + segments (control1/control2/end) for curves, not hundreds of pen points."
2590
+ },
2591
+ name: {
2592
+ type: "string",
2593
+ description: "Draft name for save/finish."
2594
+ },
2595
+ attachmentId: {
2596
+ type: "string",
2597
+ description: "reference only: the attachmentId of an image the user attached to this session (copy it from the image block, never a path or filename). It is added to the board as a hidden picture layer, ready for preview compare."
2598
+ },
2599
+ compare: {
2600
+ type: "boolean",
2601
+ description: "preview only: blend the drawing 50/50 with the reference picture layer on the board, so offsets show as ghosting. Needs a picture layer; if there is none, ask the user to add the reference with the pictures button."
2602
+ },
2603
+ grid: {
2604
+ type: "boolean",
2605
+ description: "preview only: overlay a labelled grid every 100 canvas pixels, to read positions and sizes instead of guessing them."
2606
+ },
2607
+ offset: {
2608
+ type: "integer",
2609
+ description: "inspect only: object list offset, default 0. Follow nextOffset for further pages."
2610
+ },
2611
+ objectId: {
2612
+ type: "string",
2613
+ description: "inspect only: return full editable geometry for this object, in layer (defaults to active layer)."
2614
+ },
2615
+ layer: {
2616
+ type: "integer",
2617
+ description: "inspect only: layer containing objectId."
2618
+ }
2619
+ },
2620
+ timeoutMs: 25e3,
2621
+ isConcurrencySafe: () => false,
2622
+ async execute(args, exec) {
2623
+ const sessionId = exec.agent?.id;
2624
+ if (typeof sessionId !== "string") throw Error("A session-owned sketch call is required");
2625
+ const request = { ...args };
2626
+ delete request.attachmentId;
2627
+ if (args.action === "apply") try {
2628
+ request.commands = typeof args.commands === "string" ? JSON.parse(args.commands) : args.commands;
2629
+ if (!Array.isArray(request.commands)) throw Error();
2630
+ } catch {
2631
+ throw Error("commands must be a native array or JSON array string");
2632
+ }
2633
+ if (args.action === "reference") {
2634
+ const image = await readSessionImage(args.attachmentId, attachments, exec.signal, await getSessionMessages(sessionId));
2635
+ if (image.data.byteLength > MAX_REFERENCE_BYTES) throw Error("The reference image is too large to send to the board; ask the user to add it with the pictures button on the board");
2636
+ request.image = `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
2637
+ request.name = image.name ?? "reference";
2638
+ }
2639
+ const value = await bridge.request(sessionId, request, exec.signal);
2640
+ if (value.png) {
2641
+ if (!/^data:image\/png;base64,/.test(value.png) || value.png.length > 8 * 1024 * 1024) throw Error("Invalid sketch preview");
2642
+ const image = await attachments.saveImage({
2643
+ data: new Uint8Array(Buffer.from(value.png.split(",")[1], "base64")),
2644
+ mediaType: "image/png",
2645
+ name: "sketch-preview.png"
2646
+ });
2647
+ const { png, ...snapshot } = value;
2648
+ return {
2649
+ ...snapshot,
2650
+ image
2651
+ };
2652
+ }
2653
+ return value;
2654
+ },
2655
+ output: {
2656
+ schema: {
2657
+ type: "object",
2658
+ additionalProperties: true
2659
+ },
2660
+ render: (_args, value) => [{
2661
+ type: "text",
2662
+ text: JSON.stringify({
2663
+ ...value,
2664
+ image: void 0
2665
+ })
2666
+ }, ...value.image ? [{
2667
+ type: "image",
2668
+ attachment: value.image
2669
+ }] : []]
2670
+ }
2671
+ });
2672
+ }
2673
+ //#endregion
2674
+ //#region src/sketch-codec-route.js
2675
+ function registerSketchCodec(connection) {
2676
+ let source;
2677
+ return connection.fetch.register({
2678
+ path: "/api/codex-subscription/sketch-psd-worker",
2679
+ methods: ["GET"],
2680
+ requestBody: "buffered",
2681
+ async fetch() {
2682
+ source ??= await readFile(new URL("./sketch-psd-worker.js", import.meta.url));
2683
+ return new Response(source, { headers: {
2684
+ "content-type": "text/javascript; charset=utf-8",
2685
+ "cache-control": "no-store"
2686
+ } });
2687
+ }
2688
+ });
2689
+ }
2690
+ //#endregion
2691
+ //#region src/account-vault.js
2692
+ const VERSION = 1;
2693
+ const DEFAULT_LABEL = "Account 1";
2694
+ const clone$1 = (value) => value === void 0 ? void 0 : structuredClone(value);
2695
+ const EMAIL_MAX_LENGTH = 254;
2696
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
2697
+ /** Keep only a bounded, display-safe email address from a trusted OAuth result. */
2698
+ function normalizeAccountEmail(value) {
2699
+ if (typeof value !== "string") return void 0;
2700
+ const email = value.trim();
2701
+ return email.length > 0 && email.length <= EMAIL_MAX_LENGTH && EMAIL_PATTERN.test(email) ? email : void 0;
2702
+ }
2703
+ function decodeJwtPayload(access) {
2704
+ if (typeof access !== "string") return void 0;
2705
+ const encoded = access.split(".")[1];
2706
+ if (typeof encoded !== "string" || encoded.length === 0) return void 0;
2707
+ try {
2708
+ return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
2709
+ } catch {
2710
+ return;
2711
+ }
2712
+ }
2713
+ function emailFromAccessToken(access) {
2714
+ const payload = decodeJwtPayload(access);
2715
+ return normalizeAccountEmail(payload?.["https://api.openai.com/profile"]?.email ?? payload?.email);
2716
+ }
2717
+ /** Normalize the one non-secret account attribute that may cross the UI boundary. */
2718
+ function sanitizeOAuthCredential(value) {
2719
+ const credential = assertOAuthCredential$1(value);
2720
+ const email = emailFromAccessToken(credential.access) ?? normalizeAccountEmail(credential.email);
2721
+ if (email === void 0) {
2722
+ delete credential.email;
2723
+ return credential;
2724
+ }
2725
+ return {
2726
+ ...credential,
2727
+ email
2728
+ };
2729
+ }
2730
+ function assertOAuthCredential$1(value) {
2731
+ if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex account vault received a malformed OAuth credential");
2732
+ return clone$1(value);
2733
+ }
2734
+ function parseOAuthCredential$1(value) {
2735
+ try {
2736
+ return assertOAuthCredential$1(JSON.parse(value));
2737
+ } catch (error) {
2738
+ if (error?.message === "Codex account vault received a malformed OAuth credential") throw error;
2739
+ throw new Error("Codex account vault contains malformed OAuth JSON", { cause: error });
2740
+ }
2741
+ }
2742
+ function normalizeLabel(value) {
2743
+ if (typeof value !== "string") throw new Error("Codex account label must be text");
2744
+ const label = value.trim().replace(/\s+/gu, " ");
2745
+ if (label.length === 0 || label.length > 48) throw new Error("Codex account label must contain 1 to 48 characters");
2746
+ return label;
2747
+ }
2748
+ function assertVaultRecord(record) {
2749
+ if (record?.kind !== "grant" || record.payload?.version !== VERSION || typeof record.payload.activeId !== "string" || !Array.isArray(record.payload.accounts) || record.payload.accounts.length === 0) throw new Error("Codex account vault contains a malformed grant record");
2750
+ const ids = /* @__PURE__ */ new Set();
2751
+ const accounts = record.payload.accounts.map((account) => {
2752
+ if (account === null || typeof account !== "object" || typeof account.id !== "string" || account.id.length === 0 || ids.has(account.id)) throw new Error("Codex account vault contains a malformed account id");
2753
+ ids.add(account.id);
2754
+ return {
2755
+ id: account.id,
2756
+ label: normalizeLabel(account.label),
2757
+ credential: sanitizeOAuthCredential(account.credential)
2758
+ };
2759
+ });
2760
+ if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
2761
+ const legacyAccountId = record.payload.legacyAccountId;
2762
+ if (legacyAccountId !== void 0 && !ids.has(legacyAccountId)) throw new Error("Codex account vault legacy account is missing");
2763
+ return {
2764
+ version: VERSION,
2765
+ activeId: record.payload.activeId,
2766
+ legacyAccountId,
2767
+ accounts
2768
+ };
2769
+ }
2770
+ const grant = (payload) => ({
2771
+ kind: "grant",
2772
+ payload
2773
+ });
2774
+ var PendingOAuthCredentialStore = class {
2775
+ #credential;
2776
+ async read(providerId) {
2777
+ if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2778
+ return clone$1(this.#credential);
2779
+ }
2780
+ async list() {
2781
+ return this.#credential === void 0 ? [] : [{
2782
+ providerId: "openai-codex",
2783
+ type: "oauth"
2784
+ }];
2785
+ }
2786
+ async modify(providerId, update) {
2787
+ if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2788
+ const next = await update(clone$1(this.#credential));
2789
+ if (next !== void 0) this.#credential = sanitizeOAuthCredential(next);
2790
+ return clone$1(this.#credential);
2791
+ }
2792
+ async delete(providerId) {
2793
+ if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
2794
+ this.#credential = void 0;
2795
+ }
2796
+ credential() {
2797
+ return clone$1(this.#credential);
2798
+ }
2799
+ };
2800
+ /**
2801
+ * Multi-account owner state stored in DSH's atomic plugin credential record.
2802
+ * The old single-account reference remains as a rollback source and is kept in
2803
+ * sync whenever that imported account rotates its refresh token.
2804
+ */
2805
+ var DshOAuthAccountVault = class {
2806
+ #tail = Promise.resolve();
2807
+ constructor(credentials, options) {
2808
+ if (credentials === void 0 || credentials === null || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("Codex multi-account requires DSH credential records");
2809
+ this.credentials = credentials;
2810
+ this.key = options.key;
2811
+ this.legacyRef = options.legacyRef;
2812
+ this.legacyRefs = Object.freeze([...options.legacyRefs ?? []]);
2813
+ this.createId = options.createId ?? randomUUID;
2814
+ this.onLegacySyncFailure = options.onLegacySyncFailure ?? (() => {});
2815
+ }
2816
+ #enqueue(operation) {
2817
+ const current = this.#tail.catch(() => void 0).then(operation);
2818
+ this.#tail = current.catch(() => void 0);
2819
+ return current;
2820
+ }
2821
+ async #legacyCredential() {
2822
+ for (const ref of [this.legacyRef, ...this.legacyRefs]) {
2823
+ const hit = await this.credentials.resolve(ref);
2824
+ if (hit?.value === void 0 || hit.value === "") continue;
2825
+ return {
2826
+ ref,
2827
+ credential: parseOAuthCredential$1(hit.value)
2828
+ };
2829
+ }
2830
+ }
2831
+ async #ensurePayload() {
2832
+ const existing = await this.credentials.readRecord(this.key);
2833
+ if (existing !== void 0) return assertVaultRecord(existing);
2834
+ const legacy = await this.#legacyCredential();
2835
+ if (legacy === void 0) return void 0;
2836
+ const id = this.createId();
2837
+ return assertVaultRecord(await this.credentials.modifyRecord(this.key, (current) => {
2838
+ if (current !== void 0) return Promise.resolve(current);
2839
+ return Promise.resolve(grant({
2840
+ version: VERSION,
2841
+ activeId: id,
2842
+ legacyAccountId: id,
2843
+ accounts: [{
2844
+ id,
2845
+ label: DEFAULT_LABEL,
2846
+ credential: legacy.credential
2847
+ }]
2848
+ }));
2849
+ }));
2850
+ }
2851
+ async #modifyPayload(update) {
2852
+ await this.#ensurePayload();
2853
+ let previousLegacy;
2854
+ const payload = assertVaultRecord(await this.credentials.modifyRecord(this.key, async (current) => {
2855
+ if (current === void 0) throw new Error("Codex account vault is not signed in");
2856
+ const payload = assertVaultRecord(current);
2857
+ previousLegacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
2858
+ const next = await update(clone$1(payload));
2859
+ return grant(next);
2860
+ }));
2861
+ const legacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
2862
+ try {
2863
+ if (legacy === void 0) {
2864
+ if (previousLegacy !== void 0) await this.credentials.unset(this.legacyRef);
2865
+ } else if (JSON.stringify(legacy) !== JSON.stringify(previousLegacy)) await this.credentials.set(this.legacyRef, JSON.stringify(legacy));
2866
+ } catch {
2867
+ this.onLegacySyncFailure();
2868
+ }
2869
+ return payload;
2870
+ }
2871
+ list() {
2872
+ return this.#enqueue(async () => {
2873
+ const payload = await this.#ensurePayload();
2874
+ if (payload === void 0) return [];
2875
+ return payload.accounts.map((account) => ({
2876
+ id: account.id,
2877
+ label: account.label,
2878
+ active: account.id === payload.activeId,
2879
+ expiresAt: account.credential.expires,
2880
+ ...account.credential.email === void 0 ? {} : { email: account.credential.email }
2881
+ }));
2882
+ });
2883
+ }
2884
+ readActive() {
2885
+ return this.#enqueue(async () => {
2886
+ const payload = await this.#ensurePayload();
2887
+ return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
2888
+ });
2299
2889
  }
2300
2890
  activeId() {
2301
2891
  return this.#enqueue(async () => (await this.#ensurePayload())?.activeId);
@@ -2843,1208 +3433,652 @@ var CodexLoginCoordinator = class {
2843
3433
  ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
2844
3434
  };
2845
3435
  return;
2846
- }
2847
- } catch {}
2848
- session.view = {
2849
- id,
2850
- provider: "openai-codex",
2851
- method,
2852
- phase: "failed",
2853
- authenticated: false,
2854
- error: "Codex login failed"
2855
- };
2856
- session.hostError = error;
2857
- }).finally(publishReady);
2858
- return ready.promise;
2859
- }
2860
- read(id) {
2861
- const session = this.#sessions.get(id);
2862
- if (session === void 0) throw new Error("unknown Codex login");
2863
- return publicClone(session.view);
2864
- }
2865
- async submit({ id, value }) {
2866
- const session = this.#sessions.get(id);
2867
- if (session === void 0) throw new Error("unknown Codex login");
2868
- if (session.prompt === void 0 || session.view.phase !== "waiting_input") throw new Error("Codex login is not waiting for input");
2869
- if (typeof value !== "string" || value.trim() === "") throw new Error("Codex login input is empty");
2870
- const answer = session.prompt;
2871
- session.prompt = void 0;
2872
- session.view = {
2873
- ...session.view,
2874
- phase: session.view.authUrl === void 0 ? "starting" : "waiting_browser",
2875
- prompt: void 0
2876
- };
2877
- answer.resolve(value);
2878
- return this.read(id);
2879
- }
2880
- async cancel(id) {
2881
- const session = this.#sessions.get(id);
2882
- if (session === void 0) throw new Error("unknown Codex login");
2883
- if (!TERMINAL_PHASES.has(session.view.phase)) {
2884
- session.view = {
2885
- id,
2886
- provider: "openai-codex",
2887
- method: session.view.method,
2888
- phase: "cancelled",
2889
- authenticated: false
2890
- };
2891
- session.controller.abort(/* @__PURE__ */ new Error("Codex login cancelled"));
2892
- }
2893
- return this.read(id);
2894
- }
2895
- async logout(options) {
2896
- if (this.#activeId !== void 0) {
2897
- const active = this.#sessions.get(this.#activeId);
2898
- if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) await this.cancel(active.view.id);
2899
- }
2900
- await this.auth.logout(options);
2901
- return this.accountStatus(options);
2902
- }
2903
- async selectAccount(id) {
2904
- return publicClone(await this.auth.select(id));
2905
- }
2906
- async removeAccount(id) {
2907
- return publicClone(await this.auth.remove(id));
2908
- }
2909
- };
2910
- /** Map the loopback-only DSH Connection channel onto the coordinator. */
2911
- function createCodexRpcHandler(coordinator, options = {}) {
2912
- const openExternal = options.openExternal;
2913
- return async (endpoint, payload, signal) => {
2914
- try {
2915
- signal.throwIfAborted();
2916
- const input = asObject(payload);
2917
- if (endpoint === "status") try {
2918
- return ok(await coordinator.accountStatus({ signal }));
2919
- } catch (error) {
2920
- if (signal.aborted) throw error;
2921
- const [, message] = classifyAccountStatusError(error);
2922
- return accountStatusError(message);
2923
- }
2924
- if (endpoint === "login/start") {
2925
- const started = await coordinator.start({
2926
- method: input.method,
2927
- label: input.label
2928
- });
2929
- if (input.openExternal !== true) return ok(started);
2930
- const url = started.authUrl ?? started.deviceCode?.verificationUri;
2931
- if (typeof url !== "string" || openExternal === void 0) return ok({
2932
- ...started,
2933
- externalOpened: false
2934
- });
2935
- try {
2936
- await openExternal(url);
2937
- return ok({
2938
- ...started,
2939
- externalOpened: true
2940
- });
2941
- } catch {
2942
- return ok({
2943
- ...started,
2944
- externalOpened: false
2945
- });
2946
- }
2947
- }
2948
- if (endpoint === "login/status") return ok(coordinator.read(input.id));
2949
- if (endpoint === "login/submit") return ok(await coordinator.submit({
2950
- id: input.id,
2951
- value: input.value
2952
- }));
2953
- if (endpoint === "login/cancel") return ok(await coordinator.cancel(input.id));
2954
- if (endpoint === "logout") return ok(await coordinator.logout({ signal }));
2955
- if (endpoint === "account/select") return ok(await coordinator.selectAccount(input.id));
2956
- if (endpoint === "account/remove") return ok(await coordinator.removeAccount(input.id));
2957
- return badRequest(`unknown Codex auth endpoint: ${endpoint}`);
2958
- } catch (error) {
2959
- if (signal.aborted) throw error;
2960
- const message = error instanceof Error && /^(unknown|unsupported|a Codex|Codex login)/.test(error.message) ? error.message : "Codex request failed";
2961
- return badRequest(message);
2962
- }
2963
- };
2964
- }
2965
- //#endregion
2966
- //#region src/transport-failure.js
2967
- function normalizeTransportEvent(event, signal) {
2968
- if (signal?.aborted || event?.type !== "error" || event.reason !== "error") return event;
2969
- const message = event.error?.errorMessage;
2970
- if (typeof message !== "string" || !/^WebSocket closed (?:1006|1011|1012|1013)(?:\s|$)/.test(message)) return event;
2971
- return {
2972
- ...event,
2973
- error: {
2974
- ...event.error,
2975
- errorMessage: `Network transport failure: ${message}`
2976
- }
2977
- };
2978
- }
2979
- //#endregion
2980
- //#region src/pi-ai-runtime.js
2981
- const FAST_SERVICE_TIER = "priority";
2982
- const ULTRAFAST_SERVICE_TIER = "ultrafast";
2983
- /** Apply the chosen detail to user and tool-result images after the provider assembles the request. */
2984
- function withInputImageDetail(payload, detail) {
2985
- if (!Array.isArray(payload.input)) return payload;
2986
- let changed = false;
2987
- const input = payload.input.map((item) => {
2988
- const field = Array.isArray(item?.content) ? "content" : ["function_call_output", "custom_tool_call_output"].includes(item?.type) && Array.isArray(item.output) ? "output" : void 0;
2989
- if (field === void 0) return item;
2990
- let itemChanged = false;
2991
- const parts = item[field].map((part) => {
2992
- if (part?.type !== "input_image" || part.detail === detail) return part;
2993
- itemChanged = true;
2994
- return {
2995
- ...part,
2996
- detail
2997
- };
2998
- });
2999
- if (!itemChanged) return item;
3000
- changed = true;
3001
- return {
3002
- ...item,
3003
- [field]: parts
3004
- };
3005
- });
3006
- return changed ? {
3007
- ...payload,
3008
- input
3009
- } : payload;
3010
- }
3011
- /**
3012
- * Preserve pi-ai's native Codex OAuth provider while allowing DSH's generic
3013
- * PiAiAdapter to pass the access token resolved by the host credential store.
3014
- *
3015
- * PiAiAdapter owns a request-local Models collection backed by the same DSH
3016
- * credential store as this provider. A pure OAuth provider ignores its
3017
- * `apiKey` request override and otherwise fails before dispatch with "Provider
3018
- * is not configured". This non-interactive bridge teaches that collection how
3019
- * to consume only the already-refreshed token for this request; login, refresh,
3020
- * persistence, headers, transport, and model behavior remain owned by the
3021
- * original provider.
3022
- */
3023
- function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, resolveOutputVerbosity = () => OUTPUT_VERBOSITY_DEFAULT, resolveInputImageDetail = () => void 0, resolveContextMode = () => void 0, resolveCustomContextWindow = () => void 0, catalog, connection, compaction, runNetwork = (_area, operation) => operation() } = {}) {
3024
- const provider = createOpenAICodexProvider();
3025
- const requestToken = Object.freeze({
3026
- name: "DSH-managed Codex OAuth request token",
3027
- async resolve({ credential }) {
3028
- const token = credential?.type === "api_key" ? credential.key : void 0;
3029
- if (typeof token !== "string" || token.length === 0) return void 0;
3030
- return {
3031
- auth: { apiKey: token },
3032
- source: "DSH-managed OAuth request"
3033
- };
3034
- }
3035
- });
3036
- const modelMetadata = (model) => catalog?.metadata(model?.id);
3037
- const supportsVerbosity = (model) => modelMetadata(model)?.supportVerbosity ?? model?.id !== "gpt-5.3-codex-spark";
3038
- const withPreferences = (model, options = {}) => {
3039
- const metadata = modelMetadata(model);
3040
- const requestedVerbosity = resolveOutputVerbosity();
3041
- const textVerbosity = supportsVerbosity(model) ? requestedVerbosity === "default" ? metadata?.defaultVerbosity ?? "medium" : requestedVerbosity : void 0;
3042
- const speedMode = resolveSpeedMode();
3043
- const serviceTier = speedMode === "ultrafast" && metadata?.supportsUltrafast === true ? ULTRAFAST_SERVICE_TIER : speedMode === "fast" && (metadata?.supportsFast ?? supportsCodexFastMode(model?.id)) ? FAST_SERVICE_TIER : void 0;
3044
- const inputImageDetail = normalizeInputImageDetail(resolveInputImageDetail());
3045
- const onPayload = options.onPayload;
3046
- return {
3047
- ...options,
3048
- ...textVerbosity === void 0 ? {} : { textVerbosity },
3049
- ...serviceTier === void 0 ? {} : { serviceTier },
3050
- async onPayload(payload, requestModel) {
3051
- const preferred = {
3052
- ...payload,
3053
- ...textVerbosity === void 0 ? {} : { text: {
3054
- ...payload.text ?? {},
3055
- verbosity: textVerbosity
3056
- } },
3057
- ...serviceTier === void 0 ? {} : { service_tier: serviceTier }
3058
- };
3059
- const managed = compaction?.preparePayload(preferred, model?.contextWindow) ?? preferred;
3060
- const next = await onPayload?.(managed, requestModel);
3061
- const detailed = inputImageDetail === "auto" ? next ?? managed : withInputImageDetail(next ?? managed, inputImageDetail);
3062
- return {
3063
- ...detailed,
3064
- ...textVerbosity === void 0 ? {} : { text: {
3065
- ...detailed.text ?? {},
3066
- verbosity: textVerbosity
3067
- } },
3068
- ...serviceTier === void 0 ? {} : { service_tier: serviceTier }
3069
- };
3070
- }
3071
- };
3072
- };
3073
- const getModels = () => (catalog?.getModels() ?? provider.getModels()).map((model) => {
3074
- const maximum = modelContextMaximum(model);
3075
- const mode = resolveContextMode();
3076
- if (model.id === "gpt-5.3-codex-spark" || !["extended", "custom"].includes(mode)) return model;
3077
- if (mode === "extended") {
3078
- const contextWindow = maximum;
3079
- return {
3080
- ...model,
3081
- contextWindow
3436
+ }
3437
+ } catch {}
3438
+ session.view = {
3439
+ id,
3440
+ provider: "openai-codex",
3441
+ method,
3442
+ phase: "failed",
3443
+ authenticated: false,
3444
+ error: "Codex login failed"
3082
3445
  };
3083
- }
3084
- const requested = clampModelContext(resolveCustomContextWindow(customContextModelKey(model.id)), maximum, model.contextWindow);
3085
- return {
3086
- ...model,
3087
- contextWindow: requested
3446
+ session.hostError = error;
3447
+ }).finally(publishReady);
3448
+ return ready.promise;
3449
+ }
3450
+ read(id) {
3451
+ const session = this.#sessions.get(id);
3452
+ if (session === void 0) throw new Error("unknown Codex login");
3453
+ return publicClone(session.view);
3454
+ }
3455
+ async submit({ id, value }) {
3456
+ const session = this.#sessions.get(id);
3457
+ if (session === void 0) throw new Error("unknown Codex login");
3458
+ if (session.prompt === void 0 || session.view.phase !== "waiting_input") throw new Error("Codex login is not waiting for input");
3459
+ if (typeof value !== "string" || value.trim() === "") throw new Error("Codex login input is empty");
3460
+ const answer = session.prompt;
3461
+ session.prompt = void 0;
3462
+ session.view = {
3463
+ ...session.view,
3464
+ phase: session.view.authUrl === void 0 ? "starting" : "waiting_browser",
3465
+ prompt: void 0
3088
3466
  };
3089
- });
3090
- const networkIterable = (factory, options) => {
3091
- let iterator;
3092
- let prepared;
3093
- const step = async (method, value) => {
3094
- const request = await (prepared ??= connection?.prepare(options) ?? Promise.resolve({ options }));
3095
- const result = await runNetwork("model", () => {
3096
- iterator ??= factory(compaction?.requestOptions(request.options) ?? request.options)[Symbol.asyncIterator]();
3097
- return iterator[method]?.(value) ?? (method === "throw" ? Promise.reject(value) : Promise.resolve({
3098
- done: true,
3099
- value
3100
- }));
3101
- }, compaction?.networkOptions(request.network) ?? request.network);
3102
- return result.done ? result : {
3103
- ...result,
3104
- value: normalizeTransportEvent(result.value, request.options?.signal)
3467
+ answer.resolve(value);
3468
+ return this.read(id);
3469
+ }
3470
+ async cancel(id) {
3471
+ const session = this.#sessions.get(id);
3472
+ if (session === void 0) throw new Error("unknown Codex login");
3473
+ if (!TERMINAL_PHASES.has(session.view.phase)) {
3474
+ session.view = {
3475
+ id,
3476
+ provider: "openai-codex",
3477
+ method: session.view.method,
3478
+ phase: "cancelled",
3479
+ authenticated: false
3105
3480
  };
3106
- };
3107
- return {
3108
- [Symbol.asyncIterator]() {
3109
- return this;
3110
- },
3111
- next: (value) => step("next", value),
3112
- return: (value) => iterator ? step("return", value) : Promise.resolve({
3113
- done: true,
3114
- value
3115
- }),
3116
- throw: (error) => iterator ? step("throw", error) : Promise.reject(error)
3117
- };
3118
- };
3119
- return Object.freeze({
3120
- ...provider,
3121
- auth: Object.freeze({
3122
- ...provider.auth,
3123
- apiKey: requestToken
3124
- }),
3125
- getModels,
3126
- stream: (model, context, options) => networkIterable((prepared) => provider.stream(model, context, prepared), withPreferences(model, options)),
3127
- streamSimple: (model, context, options) => networkIterable((prepared) => provider.streamSimple(model, context, prepared), withPreferences(model, options))
3128
- });
3129
- }
3130
- Object.freeze([
3131
- "0.82.1",
3132
- "0.85.1",
3133
- "0.87.1"
3134
- ]);
3135
- //#endregion
3136
- //#region src/subscription-credentials.js
3137
- async function readSubscriptionCredentials(getAuth, readCredential, signal) {
3138
- for (let attempt = 0; attempt < 2; attempt++) {
3139
- const auth = await getAuth({ signal });
3140
- signal?.throwIfAborted();
3141
- if (typeof auth?.auth?.apiKey !== "string" || !auth.auth.apiKey) break;
3142
- const credential = await readCredential({ signal });
3143
- signal?.throwIfAborted();
3144
- if (credential?.type !== "oauth") break;
3145
- if (credential.access === auth.auth.apiKey) return {
3146
- access: credential.access,
3147
- accountId: credential.accountId
3148
- };
3481
+ session.controller.abort(/* @__PURE__ */ new Error("Codex login cancelled"));
3482
+ }
3483
+ return this.read(id);
3149
3484
  }
3150
- return {
3151
- access: void 0,
3152
- accountId: void 0
3153
- };
3154
- }
3155
- //#endregion
3156
- //#region src/version.js
3157
- const PACKAGE_VERSION = "2.4.2";
3158
- const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
3159
- //#endregion
3160
- //#region src/model-catalog.js
3161
- const CODEX_MODELS_URL = `https://chatgpt.com/backend-api/codex/models?client_version=${encodeURIComponent(PACKAGE_VERSION)}`;
3162
- const LEVELS = [
3163
- "off",
3164
- "minimal",
3165
- "low",
3166
- "medium",
3167
- "high",
3168
- "xhigh",
3169
- "max"
3170
- ];
3171
- const DEFAULT_REFRESH_TIMEOUT_MS = 1e4;
3172
- const record$4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3173
- const nonEmpty$2 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
3174
- const positiveInteger$1 = (value) => Number.isSafeInteger(value) && value > 0 ? value : void 0;
3175
- function upgradeModelId(value) {
3176
- return (typeof value.upgrade === "string" ? nonEmpty$2(value.upgrade) : record$4(value.upgrade) ? nonEmpty$2(value.upgrade.model) : void 0) ?? (record$4(value.upgrade_info) ? nonEmpty$2(value.upgrade_info.model) : void 0) ?? (record$4(value.upgradeInfo) ? nonEmpty$2(value.upgradeInfo.model) : void 0);
3177
- }
3178
- function retirementMillis(value) {
3179
- for (const source of [
3180
- value,
3181
- value.upgrade,
3182
- value.upgrade_info,
3183
- value.upgradeInfo
3184
- ]) {
3185
- if (!record$4(source)) continue;
3186
- for (const raw of [source.retirement_at, source.retirementAt]) {
3187
- const seconds = positiveInteger$1(raw);
3188
- if (seconds !== void 0 && Number.isSafeInteger(seconds * 1e3)) return seconds * 1e3;
3189
- if (typeof raw === "string" && /^\d{4}-\d{2}-\d{2}T[\d:.]+(?:Z|[+-]\d{2}:\d{2})$/u.test(raw)) {
3190
- const parsed = Date.parse(raw);
3191
- if (Number.isFinite(parsed) && parsed > 0) return parsed;
3192
- }
3485
+ async logout(options) {
3486
+ if (this.#activeId !== void 0) {
3487
+ const active = this.#sessions.get(this.#activeId);
3488
+ if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) await this.cancel(active.view.id);
3193
3489
  }
3490
+ await this.auth.logout(options);
3491
+ return this.accountStatus(options);
3194
3492
  }
3195
- }
3196
- function retirementNotice(value, now = Date.now(), targetDisplayName) {
3197
- if (!record$4(value) || !Number.isFinite(now)) return void 0;
3198
- const at = retirementMillis(value);
3199
- if (at === void 0 || at <= now) return void 0;
3200
- const date = new Date(at);
3201
- if (!Number.isFinite(date.getTime())) return void 0;
3202
- const dateText = `${String(date.getUTCFullYear()).padStart(4, "0")}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
3203
- const upgradeTo = upgradeModelId(value);
3204
- const upgradeName = upgradeTo === void 0 ? void 0 : nonEmpty$2(targetDisplayName) ?? upgradeTo;
3205
- return {
3206
- text: `Retires on ${dateText} (UTC).${upgradeName === void 0 ? "" : ` Switch to ${upgradeName} to keep working.`}`,
3207
- at,
3208
- upgradeTo,
3209
- dateText
3210
- };
3211
- }
3212
- function reasoningMap(levels) {
3213
- const supported = new Set((Array.isArray(levels) ? levels : []).map((level) => nonEmpty$2(record$4(level) ? level.effort : void 0)).filter(Boolean));
3214
- const map = Object.fromEntries(LEVELS.map((level) => [level, null]));
3215
- if (supported.has("none")) map.off = "none";
3216
- for (const level of LEVELS.slice(1)) if (supported.has(level)) map[level] = level;
3217
- return map;
3218
- }
3219
- const capabilityNames = (values) => [...new Set(values.filter((value) => typeof value === "string" && /^[a-z][a-z0-9_-]{0,31}$/u.test(value)))].sort().slice(0, 16);
3220
- const speedTierIds = (value) => [...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []];
3221
- function unsupportedCapabilities(value) {
3222
- const reasoning = capabilityNames((value.supported_reasoning_levels ?? []).map((item) => item?.effort)).filter((level) => !["none", ...LEVELS.slice(1)].includes(level));
3223
- const inputs = capabilityNames(Array.isArray(value.input_modalities) ? value.input_modalities : []).filter((input) => !["text", "image"].includes(input));
3224
- const speeds = capabilityNames([...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []]).filter((tier) => ![
3225
- "auto",
3226
- "default",
3227
- "standard",
3228
- "fast",
3229
- "priority",
3230
- "ultrafast"
3231
- ].includes(tier));
3232
- return {
3233
- ...reasoning.length ? { reasoning } : {},
3234
- ...inputs.length ? { inputs } : {},
3235
- ...speeds.length ? { speeds } : {}
3236
- };
3237
- }
3238
- function visibleModel(value, displayNames, now) {
3239
- if (!record$4(value)) return void 0;
3240
- const id = nonEmpty$2(value.slug);
3241
- const reserve = id === "gpt-reserve" && ["list", "hide"].includes(value.visibility);
3242
- if (id === void 0 || value.visibility !== "list" && !reserve) return void 0;
3243
- const supported = Array.isArray(value.supported_reasoning_levels) ? value.supported_reasoning_levels : [];
3244
- const input = Array.isArray(value.input_modalities) ? value.input_modalities.filter((item) => ["text", "image"].includes(item)) : ["text", "image"];
3245
- const unsupported = unsupportedCapabilities({
3246
- ...value,
3247
- supported_reasoning_levels: supported
3248
- });
3249
- const upgradeTo = upgradeModelId(value);
3250
- const retirement = retirementNotice(value, now, displayNames.get(upgradeTo));
3251
- const notice = retirement?.text;
3252
- const description = nonEmpty$2(value.description);
3253
- return {
3254
- ...Object.keys(unsupported).length ? { unsupported } : {},
3255
- id,
3256
- name: `${reserve ? "GPT-Reserve (Experimental)" : nonEmpty$2(value.display_name) ?? id}${retirement === void 0 ? "" : ` (retires ${retirement.dateText})`}`,
3257
- description: notice === void 0 ? description : description === void 0 ? notice : `${description}\n${notice}`,
3258
- ...retirement === void 0 ? {} : { retirement: {
3259
- at: retirement.at,
3260
- upgradeTo: retirement.upgradeTo
3261
- } },
3262
- priority: Number.isFinite(value.priority) ? value.priority : 0,
3263
- input: input.length > 0 ? input : ["text"],
3264
- contextWindow: positiveInteger$1(value.context_window) ?? positiveInteger$1(value.max_context_window),
3265
- ...positiveInteger$1(value.max_context_window) === void 0 ? {} : { maxContextWindow: value.max_context_window },
3266
- reasoning: supported.length > 0,
3267
- thinkingLevelMap: reasoningMap(supported),
3268
- supportVerbosity: value.support_verbosity === true,
3269
- defaultVerbosity: [
3270
- "low",
3271
- "medium",
3272
- "high"
3273
- ].includes(value.default_verbosity) ? value.default_verbosity : void 0,
3274
- supportsFast: speedTierIds(value).some((tier) => tier === "fast" || tier === "priority"),
3275
- supportsUltrafast: speedTierIds(value).includes("ultrafast")
3493
+ async selectAccount(id) {
3494
+ return publicClone(await this.auth.select(id));
3495
+ }
3496
+ async removeAccount(id) {
3497
+ return publicClone(await this.auth.remove(id));
3498
+ }
3499
+ };
3500
+ /** Map the loopback-only DSH Connection channel onto the coordinator. */
3501
+ function createCodexRpcHandler(coordinator, options = {}) {
3502
+ const openExternal = options.openExternal;
3503
+ return async (endpoint, payload, signal) => {
3504
+ try {
3505
+ signal.throwIfAborted();
3506
+ const input = asObject(payload);
3507
+ if (endpoint === "status") try {
3508
+ return ok(await coordinator.accountStatus({ signal }));
3509
+ } catch (error) {
3510
+ if (signal.aborted) throw error;
3511
+ const [, message] = classifyAccountStatusError(error);
3512
+ return accountStatusError(message);
3513
+ }
3514
+ if (endpoint === "login/start") {
3515
+ const started = await coordinator.start({
3516
+ method: input.method,
3517
+ label: input.label
3518
+ });
3519
+ if (input.openExternal !== true) return ok(started);
3520
+ const url = started.authUrl ?? started.deviceCode?.verificationUri;
3521
+ if (typeof url !== "string" || openExternal === void 0) return ok({
3522
+ ...started,
3523
+ externalOpened: false
3524
+ });
3525
+ try {
3526
+ await openExternal(url);
3527
+ return ok({
3528
+ ...started,
3529
+ externalOpened: true
3530
+ });
3531
+ } catch {
3532
+ return ok({
3533
+ ...started,
3534
+ externalOpened: false
3535
+ });
3536
+ }
3537
+ }
3538
+ if (endpoint === "login/status") return ok(coordinator.read(input.id));
3539
+ if (endpoint === "login/submit") return ok(await coordinator.submit({
3540
+ id: input.id,
3541
+ value: input.value
3542
+ }));
3543
+ if (endpoint === "login/cancel") return ok(await coordinator.cancel(input.id));
3544
+ if (endpoint === "logout") return ok(await coordinator.logout({ signal }));
3545
+ if (endpoint === "account/select") return ok(await coordinator.selectAccount(input.id));
3546
+ if (endpoint === "account/remove") return ok(await coordinator.removeAccount(input.id));
3547
+ return badRequest(`unknown Codex auth endpoint: ${endpoint}`);
3548
+ } catch (error) {
3549
+ if (signal.aborted) throw error;
3550
+ const message = error instanceof Error && /^(unknown|unsupported|a Codex|Codex login)/.test(error.message) ? error.message : "Codex request failed";
3551
+ return badRequest(message);
3552
+ }
3276
3553
  };
3277
3554
  }
3278
- function parseOfficialModelCatalog(value, now = Date.now()) {
3279
- if (!record$4(value) || !Array.isArray(value.models)) throw new Error("Codex returned a malformed model catalog");
3280
- const displayNames = /* @__PURE__ */ new Map();
3281
- for (const model of value.models) {
3282
- if (!record$4(model)) continue;
3283
- const id = nonEmpty$2(model.slug);
3284
- const name = nonEmpty$2(model.display_name);
3285
- if (id !== void 0 && name !== void 0 && !displayNames.has(id)) displayNames.set(id, name);
3286
- }
3287
- const seen = /* @__PURE__ */ new Set();
3288
- return value.models.map((model) => visibleModel(model, displayNames, now)).filter((model) => model !== void 0 && !seen.has(model.id) && seen.add(model.id)).sort((left, right) => Number(left.id === "gpt-reserve") - Number(right.id === "gpt-reserve") || right.priority - left.priority);
3289
- }
3290
- function mergeModel(baseModels, remote) {
3291
- const base = baseModels.find((model) => model.id === remote.id) ?? baseModels.find((model) => model.id !== "gpt-5.3-codex-spark") ?? baseModels[0];
3292
- if (base === void 0) return void 0;
3555
+ //#endregion
3556
+ //#region src/transport-failure.js
3557
+ function normalizeTransportEvent(event, signal) {
3558
+ if (signal?.aborted || event?.type !== "error" || event.reason !== "error") return event;
3559
+ const message = event.error?.errorMessage;
3560
+ if (typeof message !== "string" || !/^WebSocket closed (?:1006|1011|1012|1013)(?:\s|$)/.test(message)) return event;
3293
3561
  return {
3294
- ...base,
3295
- id: remote.id,
3296
- name: remote.name,
3297
- ...remote.retirement === void 0 || remote.description === void 0 ? {} : { description: remote.description },
3298
- input: remote.input,
3299
- reasoning: remote.reasoning,
3300
- thinkingLevelMap: remote.thinkingLevelMap,
3301
- ...remote.contextWindow === void 0 ? {} : { contextWindow: remote.contextWindow },
3302
- ...remote.maxContextWindow === void 0 ? {} : { maxContextWindow: remote.maxContextWindow },
3303
- ...base.id === remote.id ? {} : { cost: {
3304
- input: 0,
3305
- output: 0,
3306
- cacheRead: 0,
3307
- cacheWrite: 0
3308
- } }
3562
+ ...event,
3563
+ error: {
3564
+ ...event.error,
3565
+ errorMessage: `Network transport failure: ${message}`
3566
+ }
3309
3567
  };
3310
3568
  }
3311
- function createOfficialModelCatalog(options = {}) {
3312
- const fetchCatalog = options.fetch ?? fetch;
3313
- const getNow = options.now ?? Date.now;
3314
- const scheduleTimeout = options.setTimeout ?? setTimeout;
3315
- const cancelTimeout = options.clearTimeout ?? clearTimeout;
3316
- const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : DEFAULT_REFRESH_TIMEOUT_MS;
3317
- let models;
3318
- let metadata = /* @__PURE__ */ new Map();
3319
- let etag;
3320
- let rawCatalog;
3321
- let revision = 0;
3322
- let refreshing;
3323
- let generation = 0;
3324
- let refreshStatus = "idle";
3325
- const refresh = ({ signal } = {}) => {
3326
- if (signal?.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3327
- if (refreshing?.generation === generation) return refreshing.promise;
3328
- const currentGeneration = generation;
3329
- refreshStatus = "refreshing";
3330
- let outcome = "idle";
3331
- const controller = new AbortController();
3332
- const abort = () => {
3333
- if (!controller.signal.aborted) controller.abort(signal?.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3334
- };
3335
- signal?.addEventListener("abort", abort, { once: true });
3336
- const requestSignal = controller.signal;
3337
- let timer;
3338
- const timeoutError = /* @__PURE__ */ new Error("Codex model catalog refresh timed out");
3339
- const work = (async () => {
3340
- const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, requestSignal);
3341
- if (currentGeneration !== generation || requestSignal.aborted) return false;
3342
- if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) return false;
3343
- const headers = {
3344
- authorization: `Bearer ${access}`,
3345
- "chatgpt-account-id": accountId,
3346
- accept: "application/json",
3347
- originator: "pi",
3348
- "user-agent": USER_AGENT,
3349
- ...etag === void 0 ? {} : { "if-none-match": etag }
3569
+ //#endregion
3570
+ //#region src/pi-ai-runtime.js
3571
+ const FAST_SERVICE_TIER = "priority";
3572
+ const ULTRAFAST_SERVICE_TIER = "ultrafast";
3573
+ /** Apply the chosen detail to user and tool-result images after the provider assembles the request. */
3574
+ function withInputImageDetail(payload, detail) {
3575
+ if (!Array.isArray(payload.input)) return payload;
3576
+ let changed = false;
3577
+ const input = payload.input.map((item) => {
3578
+ const field = Array.isArray(item?.content) ? "content" : ["function_call_output", "custom_tool_call_output"].includes(item?.type) && Array.isArray(item.output) ? "output" : void 0;
3579
+ if (field === void 0) return item;
3580
+ let itemChanged = false;
3581
+ const parts = item[field].map((part) => {
3582
+ if (part?.type !== "input_image" || part.detail === detail) return part;
3583
+ itemChanged = true;
3584
+ return {
3585
+ ...part,
3586
+ detail
3350
3587
  };
3351
- const response = await fetchCatalog(CODEX_MODELS_URL, {
3352
- method: "GET",
3353
- redirect: "error",
3354
- headers,
3355
- signal: requestSignal
3356
- });
3357
- if (currentGeneration !== generation || requestSignal.aborted) return false;
3358
- if (response.status === 304) {
3359
- outcome = "ok";
3360
- if (rawCatalog === void 0) return false;
3361
- const remote = parseOfficialModelCatalog(rawCatalog, getNow());
3362
- const next = remote.map((model) => mergeModel(options.baseModels(), model)).filter(Boolean);
3363
- if (next.length === 0 || JSON.stringify(next) === JSON.stringify(models)) return false;
3364
- models = next;
3365
- metadata = new Map(remote.map((model) => [model.id, model]));
3366
- revision += 1;
3367
- options.onUpdated?.();
3368
- return true;
3369
- }
3370
- if (!response.ok) throw new Error(`Codex model catalog failed (HTTP ${response.status})`);
3371
- const raw = await response.json();
3372
- const remote = parseOfficialModelCatalog(raw, getNow());
3373
- if (currentGeneration !== generation || requestSignal.aborted) return false;
3374
- if (remote.length === 0) throw new Error("Codex returned an empty model catalog");
3375
- const baseModels = options.baseModels();
3376
- const next = remote.map((model) => mergeModel(baseModels, model)).filter(Boolean);
3377
- if (next.length === 0) throw new Error("Codex model catalog has no compatible models");
3378
- if (currentGeneration !== generation || requestSignal.aborted) return false;
3379
- models = next;
3380
- rawCatalog = raw;
3381
- metadata = new Map(remote.map((model) => [model.id, model]));
3382
- etag = nonEmpty$2(response.headers.get("etag")) ?? etag;
3383
- revision += 1;
3384
- outcome = "ok";
3385
- options.onUpdated?.();
3386
- return true;
3387
- })();
3388
- let rejectAborted;
3389
- const abortPromise = new Promise((_, reject) => {
3390
- rejectAborted = () => reject(requestSignal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3391
- if (requestSignal.aborted) rejectAborted();
3392
- else requestSignal.addEventListener("abort", rejectAborted, { once: true });
3393
3588
  });
3394
- timer = scheduleTimeout(() => controller.abort(timeoutError), timeoutMs);
3395
- timer.unref?.();
3396
- const promise = Promise.race([work, abortPromise]).catch((error) => {
3397
- outcome = "failed";
3398
- throw error;
3399
- }).finally(() => {
3400
- cancelTimeout(timer);
3401
- signal?.removeEventListener("abort", abort);
3402
- requestSignal.removeEventListener("abort", rejectAborted);
3403
- if (refreshing?.promise === promise) {
3404
- refreshing = void 0;
3405
- refreshStatus = outcome;
3589
+ if (!itemChanged) return item;
3590
+ changed = true;
3591
+ return {
3592
+ ...item,
3593
+ [field]: parts
3594
+ };
3595
+ });
3596
+ return changed ? {
3597
+ ...payload,
3598
+ input
3599
+ } : payload;
3600
+ }
3601
+ /**
3602
+ * Preserve pi-ai's native Codex OAuth provider while allowing DSH's generic
3603
+ * PiAiAdapter to pass the access token resolved by the host credential store.
3604
+ *
3605
+ * PiAiAdapter owns a request-local Models collection backed by the same DSH
3606
+ * credential store as this provider. A pure OAuth provider ignores its
3607
+ * `apiKey` request override and otherwise fails before dispatch with "Provider
3608
+ * is not configured". This non-interactive bridge teaches that collection how
3609
+ * to consume only the already-refreshed token for this request; login, refresh,
3610
+ * persistence, headers, transport, and model behavior remain owned by the
3611
+ * original provider.
3612
+ */
3613
+ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, resolveOutputVerbosity = () => OUTPUT_VERBOSITY_DEFAULT, resolveInputImageDetail = () => void 0, resolveContextMode = () => void 0, resolveCustomContextWindow = () => void 0, catalog, connection, compaction, runNetwork = (_area, operation) => operation() } = {}) {
3614
+ const provider = createOpenAICodexProvider();
3615
+ const requestToken = Object.freeze({
3616
+ name: "DSH-managed Codex OAuth request token",
3617
+ async resolve({ credential }) {
3618
+ const token = credential?.type === "api_key" ? credential.key : void 0;
3619
+ if (typeof token !== "string" || token.length === 0) return void 0;
3620
+ return {
3621
+ auth: { apiKey: token },
3622
+ source: "DSH-managed OAuth request"
3623
+ };
3624
+ }
3625
+ });
3626
+ const modelMetadata = (model) => catalog?.metadata(model?.id);
3627
+ const supportsVerbosity = (model) => modelMetadata(model)?.supportVerbosity ?? model?.id !== "gpt-5.3-codex-spark";
3628
+ const withPreferences = (model, options = {}) => {
3629
+ const metadata = modelMetadata(model);
3630
+ const requestedVerbosity = resolveOutputVerbosity();
3631
+ const textVerbosity = supportsVerbosity(model) ? requestedVerbosity === "default" ? metadata?.defaultVerbosity ?? "medium" : requestedVerbosity : void 0;
3632
+ const speedMode = resolveSpeedMode();
3633
+ const serviceTier = speedMode === "ultrafast" && metadata?.supportsUltrafast === true ? ULTRAFAST_SERVICE_TIER : speedMode === "fast" && (metadata?.supportsFast ?? supportsCodexFastMode(model?.id)) ? FAST_SERVICE_TIER : void 0;
3634
+ const inputImageDetail = normalizeInputImageDetail(resolveInputImageDetail());
3635
+ const onPayload = options.onPayload;
3636
+ return {
3637
+ ...options,
3638
+ ...textVerbosity === void 0 ? {} : { textVerbosity },
3639
+ ...serviceTier === void 0 ? {} : { serviceTier },
3640
+ async onPayload(payload, requestModel) {
3641
+ const preferred = {
3642
+ ...payload,
3643
+ ...textVerbosity === void 0 ? {} : { text: {
3644
+ ...payload.text ?? {},
3645
+ verbosity: textVerbosity
3646
+ } },
3647
+ ...serviceTier === void 0 ? {} : { service_tier: serviceTier }
3648
+ };
3649
+ const managed = compaction?.preparePayload(preferred, model?.contextWindow) ?? preferred;
3650
+ const next = await onPayload?.(managed, requestModel);
3651
+ const detailed = inputImageDetail === "auto" ? next ?? managed : withInputImageDetail(next ?? managed, inputImageDetail);
3652
+ return {
3653
+ ...detailed,
3654
+ ...textVerbosity === void 0 ? {} : { text: {
3655
+ ...detailed.text ?? {},
3656
+ verbosity: textVerbosity
3657
+ } },
3658
+ ...serviceTier === void 0 ? {} : { service_tier: serviceTier }
3659
+ };
3406
3660
  }
3407
- });
3408
- refreshing = {
3409
- generation: currentGeneration,
3410
- promise,
3411
- cancel: () => controller.abort()
3412
3661
  };
3413
- return promise;
3662
+ };
3663
+ const getModels = () => (catalog?.getModels() ?? provider.getModels()).map((model) => {
3664
+ const maximum = modelContextMaximum(model);
3665
+ const mode = resolveContextMode();
3666
+ if (model.id === "gpt-5.3-codex-spark" || !["extended", "custom"].includes(mode)) return model;
3667
+ if (mode === "extended") {
3668
+ const contextWindow = maximum;
3669
+ return {
3670
+ ...model,
3671
+ contextWindow
3672
+ };
3673
+ }
3674
+ const requested = clampModelContext(resolveCustomContextWindow(customContextModelKey(model.id)), maximum, model.contextWindow);
3675
+ return {
3676
+ ...model,
3677
+ contextWindow: requested
3678
+ };
3679
+ });
3680
+ const networkIterable = (factory, options) => {
3681
+ let iterator;
3682
+ let prepared;
3683
+ const step = async (method, value) => {
3684
+ const request = await (prepared ??= connection?.prepare(options) ?? Promise.resolve({ options }));
3685
+ const result = await runNetwork("model", () => {
3686
+ iterator ??= factory(compaction?.requestOptions(request.options) ?? request.options)[Symbol.asyncIterator]();
3687
+ return iterator[method]?.(value) ?? (method === "throw" ? Promise.reject(value) : Promise.resolve({
3688
+ done: true,
3689
+ value
3690
+ }));
3691
+ }, compaction?.networkOptions(request.network) ?? request.network);
3692
+ return result.done ? result : {
3693
+ ...result,
3694
+ value: normalizeTransportEvent(result.value, request.options?.signal)
3695
+ };
3696
+ };
3697
+ return {
3698
+ [Symbol.asyncIterator]() {
3699
+ return this;
3700
+ },
3701
+ next: (value) => step("next", value),
3702
+ return: (value) => iterator ? step("return", value) : Promise.resolve({
3703
+ done: true,
3704
+ value
3705
+ }),
3706
+ throw: (error) => iterator ? step("throw", error) : Promise.reject(error)
3707
+ };
3414
3708
  };
3415
3709
  return Object.freeze({
3416
- refresh,
3417
- getModels: () => models ?? [],
3418
- metadata: (modelId) => metadata.get(modelId),
3419
- revision: () => revision,
3420
- capabilityGaps: () => [...metadata.values()].filter((model) => model.unsupported && /^[a-z][a-z0-9._-]{0,79}$/u.test(model.id)).slice(0, 20).map((model) => ({
3421
- model: model.id,
3422
- ...structuredClone(model.unsupported)
3423
- })),
3424
- status: () => ({
3425
- source: models === void 0 ? "unavailable" : "online",
3426
- refresh: refreshStatus
3710
+ ...provider,
3711
+ auth: Object.freeze({
3712
+ ...provider.auth,
3713
+ apiKey: requestToken
3427
3714
  }),
3428
- clear() {
3429
- generation += 1;
3430
- const flight = refreshing;
3431
- refreshing = void 0;
3432
- flight?.cancel();
3433
- models = void 0;
3434
- rawCatalog = void 0;
3435
- metadata = /* @__PURE__ */ new Map();
3436
- etag = void 0;
3437
- refreshStatus = "idle";
3438
- revision += 1;
3439
- options.onUpdated?.();
3440
- }
3715
+ getModels,
3716
+ stream: (model, context, options) => networkIterable((prepared) => provider.stream(model, context, prepared), withPreferences(model, options)),
3717
+ streamSimple: (model, context, options) => networkIterable((prepared) => provider.streamSimple(model, context, prepared), withPreferences(model, options))
3441
3718
  });
3442
3719
  }
3720
+ Object.freeze([
3721
+ "0.82.1",
3722
+ "0.85.1",
3723
+ "0.87.1"
3724
+ ]);
3443
3725
  //#endregion
3444
- //#region src/codex-search.js
3445
- const CODEX_SEARCH_PROVIDER_ID = "codex-subscription";
3446
- const CODEX_AUTO_SEARCH_PROVIDER_ID = "codex-subscription-auto";
3447
- const CODEX_SEARCH_URL = "https://chatgpt.com/backend-api/codex/alpha/search";
3448
- const DEFAULT_MODEL = "gpt-5.6-luna";
3449
- const MAX_OUTPUT_TOKENS = 4096;
3450
- const MAX_SOURCE_DATE = 64;
3726
+ //#region src/model-catalog.js
3727
+ const CODEX_MODELS_URL = `https://chatgpt.com/backend-api/codex/models?client_version=${encodeURIComponent(PACKAGE_VERSION)}`;
3728
+ const LEVELS = [
3729
+ "off",
3730
+ "minimal",
3731
+ "low",
3732
+ "medium",
3733
+ "high",
3734
+ "xhigh",
3735
+ "max"
3736
+ ];
3737
+ const DEFAULT_REFRESH_TIMEOUT_MS = 1e4;
3451
3738
  const record$3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3452
- const nonEmpty$1 = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
3453
- const displayText = (value) => {
3454
- const text = nonEmpty$1(value)?.replace(/\s+/gu, " ").trim();
3455
- if (text === void 0 || text.length === 0) return void 0;
3456
- return text;
3457
- };
3458
- const boundedDisplayText = (value, maximum) => {
3459
- const text = displayText(value);
3460
- if (text === void 0 || text.length <= maximum) return text;
3461
- return `${text.slice(0, maximum - 1)}…`;
3462
- };
3463
- function sourceOf(value) {
3464
- if (!record$3(value)) return void 0;
3465
- const url = nonEmpty$1(value.url);
3466
- if (url === void 0) return void 0;
3467
- let parsed;
3468
- try {
3469
- parsed = new URL(url);
3470
- } catch {
3471
- return;
3472
- }
3473
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
3474
- const title = displayText(value.title) ?? parsed.hostname;
3475
- const snippet = displayText(value.snippet);
3476
- const publishedAt = boundedDisplayText(value.published_at, MAX_SOURCE_DATE) ?? boundedDisplayText(value.publishedAt, MAX_SOURCE_DATE);
3477
- return {
3478
- url,
3479
- title,
3480
- ...snippet === void 0 ? {} : { snippet },
3481
- ...publishedAt === void 0 ? {} : { publishedAt }
3482
- };
3739
+ const nonEmpty$1 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
3740
+ const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0 ? value : void 0;
3741
+ function upgradeModelId(value) {
3742
+ return (typeof value.upgrade === "string" ? nonEmpty$1(value.upgrade) : record$3(value.upgrade) ? nonEmpty$1(value.upgrade.model) : void 0) ?? (record$3(value.upgrade_info) ? nonEmpty$1(value.upgrade_info.model) : void 0) ?? (record$3(value.upgradeInfo) ? nonEmpty$1(value.upgradeInfo.model) : void 0);
3483
3743
  }
3484
- function parseSearchResponse(value) {
3485
- if (!record$3(value) || !Array.isArray(value.results)) throw new Error("Codex returned a malformed search response");
3486
- const seen = /* @__PURE__ */ new Set();
3487
- const sources = [];
3488
- for (const result of value.results ?? []) {
3489
- const source = sourceOf(result);
3490
- if (source === void 0 || seen.has(source.url)) continue;
3491
- seen.add(source.url);
3492
- sources.push(source);
3744
+ function retirementMillis(value) {
3745
+ for (const source of [
3746
+ value,
3747
+ value.upgrade,
3748
+ value.upgrade_info,
3749
+ value.upgradeInfo
3750
+ ]) {
3751
+ if (!record$3(source)) continue;
3752
+ for (const raw of [source.retirement_at, source.retirementAt]) {
3753
+ const seconds = positiveInteger(raw);
3754
+ if (seconds !== void 0 && Number.isSafeInteger(seconds * 1e3)) return seconds * 1e3;
3755
+ if (typeof raw === "string" && /^\d{4}-\d{2}-\d{2}T[\d:.]+(?:Z|[+-]\d{2}:\d{2})$/u.test(raw)) {
3756
+ const parsed = Date.parse(raw);
3757
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
3758
+ }
3759
+ }
3493
3760
  }
3761
+ }
3762
+ function retirementNotice(value, now = Date.now(), targetDisplayName) {
3763
+ if (!record$3(value) || !Number.isFinite(now)) return void 0;
3764
+ const at = retirementMillis(value);
3765
+ if (at === void 0 || at <= now) return void 0;
3766
+ const date = new Date(at);
3767
+ if (!Number.isFinite(date.getTime())) return void 0;
3768
+ const dateText = `${String(date.getUTCFullYear()).padStart(4, "0")}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
3769
+ const upgradeTo = upgradeModelId(value);
3770
+ const upgradeName = upgradeTo === void 0 ? void 0 : nonEmpty$1(targetDisplayName) ?? upgradeTo;
3494
3771
  return {
3495
- sources,
3496
- truncated: false
3772
+ text: `Retires on ${dateText} (UTC).${upgradeName === void 0 ? "" : ` Switch to ${upgradeName} to keep working.`}`,
3773
+ at,
3774
+ upgradeTo,
3775
+ dateText
3497
3776
  };
3498
3777
  }
3499
- /** Create the DSH web provider backed only by the ChatGPT subscription search endpoint. */
3500
- function createCodexSearchProvider(options) {
3501
- const fetchSearch = options.fetch ?? fetch;
3502
- return Object.freeze({
3503
- id: CODEX_SEARCH_PROVIDER_ID,
3504
- available: () => true,
3505
- async search(request, signal) {
3506
- signal?.throwIfAborted();
3507
- const preferences = readCapabilitySettings(options.resolvePreferences?.());
3508
- if (preferences.searchMode === "disabled") throw new WebError("Codex search is disabled in subscription settings", "WEB_PROVIDER_UNAVAILABLE");
3509
- const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, signal);
3510
- if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new WebError("ChatGPT subscription is not signed in", "WEB_PROVIDER_CREDENTIAL_MISSING");
3511
- const model = nonEmpty$1(options.resolveModel?.()) ?? DEFAULT_MODEL;
3512
- const id = nonEmpty$1(options.resolveSessionId?.()) ?? randomUUID();
3513
- let response;
3514
- try {
3515
- response = await fetchSearch(CODEX_SEARCH_URL, {
3516
- method: "POST",
3517
- redirect: "error",
3518
- headers: {
3519
- authorization: `Bearer ${access}`,
3520
- "chatgpt-account-id": accountId,
3521
- accept: "application/json",
3522
- "content-type": "application/json",
3523
- originator: "pi",
3524
- "user-agent": USER_AGENT
3525
- },
3526
- body: JSON.stringify({
3527
- id,
3528
- model,
3529
- input: request.query,
3530
- commands: {
3531
- search_query: [{ q: request.query }],
3532
- response_length: "short"
3533
- },
3534
- settings: {
3535
- allowed_callers: ["direct"],
3536
- external_web_access: preferences.searchMode === "live"
3537
- },
3538
- max_output_tokens: MAX_OUTPUT_TOKENS
3539
- }),
3540
- signal
3541
- });
3542
- } catch (error) {
3543
- if (signal?.aborted || error?.name === "AbortError") throw new WebError("Codex search aborted", "WEB_ABORTED", { cause: error });
3544
- throw new WebError("Codex search request failed", "WEB_PROVIDER_ERROR", { cause: error });
3545
- }
3546
- if (!response.ok) throw response.status === 401 || response.status === 403 ? new WebError("ChatGPT sign-in needs to be renewed", "WEB_PROVIDER_CREDENTIAL_MISSING") : new WebError(`Codex search request failed (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
3547
- let value;
3548
- try {
3549
- value = await response.json();
3550
- } catch (error) {
3551
- throw new WebError("Codex returned an unreadable search response", "WEB_PROVIDER_ERROR", { cause: error });
3552
- }
3553
- try {
3554
- const result = parseSearchResponse(value);
3555
- if (preferences.searchDomains.length > 0) result.sources = result.sources.filter((source) => {
3556
- const hostname = new URL(source.url).hostname.toLowerCase();
3557
- return preferences.searchDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
3558
- });
3559
- return result;
3560
- } catch (error) {
3561
- throw new WebError("Codex returned a malformed search response", "WEB_PROVIDER_ERROR", { cause: error });
3562
- }
3563
- }
3564
- });
3565
- }
3566
- /** Route each request by its initiating model without changing the user's explicit overrides. */
3567
- function createCodexAutoSearchProvider(options) {
3568
- return Object.freeze({
3569
- id: CODEX_AUTO_SEARCH_PROVIDER_ID,
3570
- available: () => true,
3571
- async search(request, signal) {
3572
- if (options.resolveModelProvider?.() === "openai-codex") return options.codex.search(request, signal);
3573
- const provider = options.resolveDshProvider?.();
3574
- if (provider === void 0 || provider.id === "codex-subscription-auto" || provider.id === "codex-subscription" || provider.available() !== true) throw new WebError("DSH default search is unavailable", "WEB_PROVIDER_UNAVAILABLE");
3575
- return provider.search(request, signal);
3576
- }
3577
- });
3778
+ function reasoningMap(levels) {
3779
+ const supported = new Set((Array.isArray(levels) ? levels : []).map((level) => nonEmpty$1(record$3(level) ? level.effort : void 0)).filter(Boolean));
3780
+ const map = Object.fromEntries(LEVELS.map((level) => [level, null]));
3781
+ if (supported.has("none")) map.off = "none";
3782
+ for (const level of LEVELS.slice(1)) if (supported.has(level)) map[level] = level;
3783
+ return map;
3578
3784
  }
3579
- const ORIGINAL_IMAGE_CHUNK_BYTES = 4 * 1024 * 1024;
3580
- const ORIGINAL_IMAGE_ID_PATTERN = /^img_[0-9a-f]{32}$/u;
3581
- const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
3582
- function decodeOriginalImageRef(value) {
3583
- if (value === null || typeof value !== "object" || Array.isArray(value) || typeof value.assetId !== "string" || !ORIGINAL_IMAGE_ID_PATTERN.test(value.assetId) || value.mediaType !== "image/png" || !positiveInteger(value.bytes) || value.bytes > 48 * 1024 * 1024 || !positiveInteger(value.width) || !positiveInteger(value.height) || typeof value.name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value.name) || typeof value.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(value.sha256)) return void 0;
3785
+ const capabilityNames = (values) => [...new Set(values.filter((value) => typeof value === "string" && /^[a-z][a-z0-9_-]{0,31}$/u.test(value)))].sort().slice(0, 16);
3786
+ const speedTierIds = (value) => [...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []];
3787
+ function unsupportedCapabilities(value) {
3788
+ const reasoning = capabilityNames((value.supported_reasoning_levels ?? []).map((item) => item?.effort)).filter((level) => !["none", ...LEVELS.slice(1)].includes(level));
3789
+ const inputs = capabilityNames(Array.isArray(value.input_modalities) ? value.input_modalities : []).filter((input) => !["text", "image"].includes(input));
3790
+ const speeds = capabilityNames([...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []]).filter((tier) => ![
3791
+ "auto",
3792
+ "default",
3793
+ "standard",
3794
+ "fast",
3795
+ "priority",
3796
+ "ultrafast"
3797
+ ].includes(tier));
3584
3798
  return {
3585
- assetId: value.assetId,
3586
- mediaType: value.mediaType,
3587
- bytes: value.bytes,
3588
- width: value.width,
3589
- height: value.height,
3590
- name: value.name,
3591
- sha256: value.sha256
3799
+ ...reasoning.length ? { reasoning } : {},
3800
+ ...inputs.length ? { inputs } : {},
3801
+ ...speeds.length ? { speeds } : {}
3592
3802
  };
3593
3803
  }
3594
- function decodeImagePresentation(value) {
3595
- if (value === null || typeof value !== "object" || Array.isArray(value) || value.kind !== "codex-subscription-image" || value.schemaVersion !== 1) return void 0;
3596
- const original = decodeOriginalImageRef(value.original);
3597
- return original === void 0 ? void 0 : { original };
3598
- }
3599
- function originalImageRefsEqual(left, right) {
3600
- const a = decodeOriginalImageRef(left);
3601
- const b = decodeOriginalImageRef(right);
3602
- return a !== void 0 && b !== void 0 && a.assetId === b.assetId && a.mediaType === b.mediaType && a.bytes === b.bytes && a.width === b.width && a.height === b.height && a.name === b.name && a.sha256 === b.sha256;
3804
+ function visibleModel(value, displayNames, now) {
3805
+ if (!record$3(value)) return void 0;
3806
+ const id = nonEmpty$1(value.slug);
3807
+ const reserve = id === "gpt-reserve" && ["list", "hide"].includes(value.visibility);
3808
+ if (id === void 0 || value.visibility !== "list" && !reserve) return void 0;
3809
+ const supported = Array.isArray(value.supported_reasoning_levels) ? value.supported_reasoning_levels : [];
3810
+ const input = Array.isArray(value.input_modalities) ? value.input_modalities.filter((item) => ["text", "image"].includes(item)) : ["text", "image"];
3811
+ const unsupported = unsupportedCapabilities({
3812
+ ...value,
3813
+ supported_reasoning_levels: supported
3814
+ });
3815
+ const upgradeTo = upgradeModelId(value);
3816
+ const retirement = retirementNotice(value, now, displayNames.get(upgradeTo));
3817
+ const notice = retirement?.text;
3818
+ const description = nonEmpty$1(value.description);
3819
+ return {
3820
+ ...Object.keys(unsupported).length ? { unsupported } : {},
3821
+ id,
3822
+ name: `${reserve ? "GPT-Reserve (Experimental)" : nonEmpty$1(value.display_name) ?? id}${retirement === void 0 ? "" : ` (retires ${retirement.dateText})`}`,
3823
+ description: notice === void 0 ? description : description === void 0 ? notice : `${description}\n${notice}`,
3824
+ ...retirement === void 0 ? {} : { retirement: {
3825
+ at: retirement.at,
3826
+ upgradeTo: retirement.upgradeTo
3827
+ } },
3828
+ priority: Number.isFinite(value.priority) ? value.priority : 0,
3829
+ input: input.length > 0 ? input : ["text"],
3830
+ contextWindow: positiveInteger(value.context_window) ?? positiveInteger(value.max_context_window),
3831
+ ...positiveInteger(value.max_context_window) === void 0 ? {} : { maxContextWindow: value.max_context_window },
3832
+ reasoning: supported.length > 0,
3833
+ thinkingLevelMap: reasoningMap(supported),
3834
+ supportVerbosity: value.support_verbosity === true,
3835
+ defaultVerbosity: [
3836
+ "low",
3837
+ "medium",
3838
+ "high"
3839
+ ].includes(value.default_verbosity) ? value.default_verbosity : void 0,
3840
+ supportsFast: speedTierIds(value).some((tier) => tier === "fast" || tier === "priority"),
3841
+ supportsUltrafast: speedTierIds(value).includes("ultrafast")
3842
+ };
3603
3843
  }
3604
- /** Resolve only an exact original reference copied into a DSH fork prefix. */
3605
- function inheritedOriginalImageRef(session, assetId) {
3606
- const parentSession = session?.header?.parentSession;
3607
- const seedLength = Number.isSafeInteger(session?.inheritedEventCount) ? session.inheritedEventCount : session?.header?.seedLength;
3608
- if (typeof parentSession !== "string" || parentSession.length === 0 || !Number.isSafeInteger(seedLength) || seedLength < 0 || !ORIGINAL_IMAGE_ID_PATTERN.test(assetId)) return void 0;
3609
- let events = session?.events;
3610
- if (!Array.isArray(events) && typeof session?.snapshotEvents === "function") try {
3611
- events = session.snapshotEvents();
3612
- } catch {
3613
- return;
3614
- }
3615
- if (!Array.isArray(events)) return void 0;
3616
- for (const event of events) {
3617
- if (!Number.isSafeInteger(event?.seq) || event.seq < 0 || event.seq >= seedLength || event.type !== "tool/result") continue;
3618
- const original = decodeImagePresentation(event.data?.meta)?.original;
3619
- if (original?.assetId === assetId) return original;
3844
+ function parseOfficialModelCatalog(value, now = Date.now()) {
3845
+ if (!record$3(value) || !Array.isArray(value.models)) throw new Error("Codex returned a malformed model catalog");
3846
+ const displayNames = /* @__PURE__ */ new Map();
3847
+ for (const model of value.models) {
3848
+ if (!record$3(model)) continue;
3849
+ const id = nonEmpty$1(model.slug);
3850
+ const name = nonEmpty$1(model.display_name);
3851
+ if (id !== void 0 && name !== void 0 && !displayNames.has(id)) displayNames.set(id, name);
3620
3852
  }
3853
+ const seen = /* @__PURE__ */ new Set();
3854
+ return value.models.map((model) => visibleModel(model, displayNames, now)).filter((model) => model !== void 0 && !seen.has(model.id) && seen.add(model.id)).sort((left, right) => Number(left.id === "gpt-reserve") - Number(right.id === "gpt-reserve") || right.priority - left.priority);
3621
3855
  }
3622
- //#endregion
3623
- //#region src/codex-images.js
3624
- const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
3625
- const CODEX_IMAGE_GENERATION_URL = "https://chatgpt.com/backend-api/codex/images/generations";
3626
- const CODEX_IMAGE_EDIT_URL = "https://chatgpt.com/backend-api/codex/images/edits";
3627
- const MAX_REFERENCE_IMAGES = 5;
3628
- const RESPONSE_ENVELOPE_BYTES = 1024 * 1024;
3629
- const IMAGE_BACKGROUNDS = /* @__PURE__ */ new Set([
3630
- "auto",
3631
- "transparent",
3632
- "opaque"
3633
- ]);
3634
- const PNG_SIGNATURE = Buffer.from([
3635
- 137,
3636
- 80,
3637
- 78,
3638
- 71,
3639
- 13,
3640
- 10,
3641
- 26,
3642
- 10
3643
- ]);
3644
- const FULL_ATTACHMENT_ID = /^sha256:[0-9a-f]{64}$/u;
3645
- const BARE_ATTACHMENT_DIGEST = /^[0-9a-f]{64}$/iu;
3646
- const PATH_LIKE_ATTACHMENT_ID = /[\\/]/u;
3647
- const FILE_NAME_ATTACHMENT_ID = /\.[A-Za-z0-9]{1,16}$/u;
3648
- const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3649
- const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
3650
- const NETWORK_REASONS = {
3651
- UND_ERR_HEADERS_TIMEOUT: "the image service sent no response within 5 minutes",
3652
- UND_ERR_BODY_TIMEOUT: "the image response stalled",
3653
- UND_ERR_CONNECT_TIMEOUT: "the connection to the image service timed out",
3654
- UND_ERR_SOCKET: "the connection closed unexpectedly",
3655
- ECONNRESET: "the connection was reset",
3656
- ETIMEDOUT: "the connection timed out",
3657
- ENOTFOUND: "DNS lookup failed",
3658
- ECONNREFUSED: "the connection was refused",
3659
- EAI_AGAIN: "DNS lookup failed"
3660
- };
3661
- function requestFailureReason(error) {
3662
- const code = [(error?.cause ?? error)?.code, error?.code].find((value) => typeof value === "string" && /^[A-Z][A-Z0-9_]{2,39}$/u.test(value));
3663
- if (code !== void 0) return `: ${NETWORK_REASONS[code] ?? "network error"} (${code})`;
3664
- if (error?.name === "TimeoutError") return ": the image request timed out";
3665
- return "";
3666
- }
3667
- function normalizeImageOptions(args) {
3668
- const quality = nonEmpty(args?.quality) ?? "auto";
3669
- const background = nonEmpty(args?.background) ?? "auto";
3670
- const size = nonEmpty(args?.size) ?? "auto";
3671
- validateImageQuality(resolveImageModel(args?.model), quality);
3672
- if (!IMAGE_BACKGROUNDS.has(background)) throw new Error("background must be auto, transparent, or opaque");
3673
- if (size !== "auto") {
3674
- const match = /^(\d+)x(\d+)$/u.exec(size);
3675
- const width = Number(match?.[1]);
3676
- const height = Number(match?.[2]);
3677
- const short = Math.min(width, height);
3678
- const long = Math.max(width, height);
3679
- const pixels = width * height;
3680
- if (match === null || width % 16 !== 0 || height % 16 !== 0 || long > 3840 || long > short * 3 || pixels < 655360 || pixels > 8294400) throw new Error("size must be auto or a valid GPT Image 2 widthxheight resolution");
3681
- }
3856
+ function mergeModel(baseModels, remote) {
3857
+ const base = baseModels.find((model) => model.id === remote.id) ?? baseModels.find((model) => model.id !== "gpt-5.3-codex-spark") ?? baseModels[0];
3858
+ if (base === void 0) return void 0;
3682
3859
  return {
3683
- quality,
3684
- background,
3685
- size
3860
+ ...base,
3861
+ id: remote.id,
3862
+ name: remote.name,
3863
+ ...remote.retirement === void 0 || remote.description === void 0 ? {} : { description: remote.description },
3864
+ input: remote.input,
3865
+ reasoning: remote.reasoning,
3866
+ thinkingLevelMap: remote.thinkingLevelMap,
3867
+ ...remote.contextWindow === void 0 ? {} : { contextWindow: remote.contextWindow },
3868
+ ...remote.maxContextWindow === void 0 ? {} : { maxContextWindow: remote.maxContextWindow },
3869
+ ...base.id === remote.id ? {} : { cost: {
3870
+ input: 0,
3871
+ output: 0,
3872
+ cacheRead: 0,
3873
+ cacheWrite: 0
3874
+ } }
3875
+ };
3876
+ }
3877
+ function createOfficialModelCatalog(options = {}) {
3878
+ const fetchCatalog = options.fetch ?? fetch;
3879
+ const getNow = options.now ?? Date.now;
3880
+ const scheduleTimeout = options.setTimeout ?? setTimeout;
3881
+ const cancelTimeout = options.clearTimeout ?? clearTimeout;
3882
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : DEFAULT_REFRESH_TIMEOUT_MS;
3883
+ let models;
3884
+ let metadata = /* @__PURE__ */ new Map();
3885
+ let etag;
3886
+ let rawCatalog;
3887
+ let revision = 0;
3888
+ let refreshing;
3889
+ let generation = 0;
3890
+ let refreshStatus = "idle";
3891
+ const refresh = ({ signal } = {}) => {
3892
+ if (signal?.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3893
+ if (refreshing?.generation === generation) return refreshing.promise;
3894
+ const currentGeneration = generation;
3895
+ refreshStatus = "refreshing";
3896
+ let outcome = "idle";
3897
+ const controller = new AbortController();
3898
+ const abort = () => {
3899
+ if (!controller.signal.aborted) controller.abort(signal?.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3900
+ };
3901
+ signal?.addEventListener("abort", abort, { once: true });
3902
+ const requestSignal = controller.signal;
3903
+ let timer;
3904
+ const timeoutError = /* @__PURE__ */ new Error("Codex model catalog refresh timed out");
3905
+ const work = (async () => {
3906
+ const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, requestSignal);
3907
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
3908
+ if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) return false;
3909
+ const headers = {
3910
+ authorization: `Bearer ${access}`,
3911
+ "chatgpt-account-id": accountId,
3912
+ accept: "application/json",
3913
+ originator: "pi",
3914
+ "user-agent": USER_AGENT,
3915
+ ...etag === void 0 ? {} : { "if-none-match": etag }
3916
+ };
3917
+ const response = await fetchCatalog(CODEX_MODELS_URL, {
3918
+ method: "GET",
3919
+ redirect: "error",
3920
+ headers,
3921
+ signal: requestSignal
3922
+ });
3923
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
3924
+ if (response.status === 304) {
3925
+ outcome = "ok";
3926
+ if (rawCatalog === void 0) return false;
3927
+ const remote = parseOfficialModelCatalog(rawCatalog, getNow());
3928
+ const next = remote.map((model) => mergeModel(options.baseModels(), model)).filter(Boolean);
3929
+ if (next.length === 0 || JSON.stringify(next) === JSON.stringify(models)) return false;
3930
+ models = next;
3931
+ metadata = new Map(remote.map((model) => [model.id, model]));
3932
+ revision += 1;
3933
+ options.onUpdated?.();
3934
+ return true;
3935
+ }
3936
+ if (!response.ok) throw new Error(`Codex model catalog failed (HTTP ${response.status})`);
3937
+ const raw = await response.json();
3938
+ const remote = parseOfficialModelCatalog(raw, getNow());
3939
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
3940
+ if (remote.length === 0) throw new Error("Codex returned an empty model catalog");
3941
+ const baseModels = options.baseModels();
3942
+ const next = remote.map((model) => mergeModel(baseModels, model)).filter(Boolean);
3943
+ if (next.length === 0) throw new Error("Codex model catalog has no compatible models");
3944
+ if (currentGeneration !== generation || requestSignal.aborted) return false;
3945
+ models = next;
3946
+ rawCatalog = raw;
3947
+ metadata = new Map(remote.map((model) => [model.id, model]));
3948
+ etag = nonEmpty$1(response.headers.get("etag")) ?? etag;
3949
+ revision += 1;
3950
+ outcome = "ok";
3951
+ options.onUpdated?.();
3952
+ return true;
3953
+ })();
3954
+ let rejectAborted;
3955
+ const abortPromise = new Promise((_, reject) => {
3956
+ rejectAborted = () => reject(requestSignal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
3957
+ if (requestSignal.aborted) rejectAborted();
3958
+ else requestSignal.addEventListener("abort", rejectAborted, { once: true });
3959
+ });
3960
+ timer = scheduleTimeout(() => controller.abort(timeoutError), timeoutMs);
3961
+ timer.unref?.();
3962
+ const promise = Promise.race([work, abortPromise]).catch((error) => {
3963
+ outcome = "failed";
3964
+ throw error;
3965
+ }).finally(() => {
3966
+ cancelTimeout(timer);
3967
+ signal?.removeEventListener("abort", abort);
3968
+ requestSignal.removeEventListener("abort", rejectAborted);
3969
+ if (refreshing?.promise === promise) {
3970
+ refreshing = void 0;
3971
+ refreshStatus = outcome;
3972
+ }
3973
+ });
3974
+ refreshing = {
3975
+ generation: currentGeneration,
3976
+ promise,
3977
+ cancel: () => controller.abort()
3978
+ };
3979
+ return promise;
3686
3980
  };
3687
- }
3688
- function encodedLimit(decodedBytes) {
3689
- return Math.ceil(decodedBytes / 3) * 4;
3690
- }
3691
- function validBase64Body(value, end) {
3692
- for (let index = 0; index < end; index += 1) {
3693
- const code = value.charCodeAt(index);
3694
- if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47)) return false;
3695
- }
3696
- return true;
3697
- }
3698
- async function readJsonWithin(response, maximumBytes) {
3699
- const contentLength = Number(response.headers.get("content-length"));
3700
- if (Number.isFinite(contentLength) && contentLength > maximumBytes) throw new Error("Codex image response exceeds the image size limit");
3701
- if (response.body === null) throw new Error("Codex returned an unreadable image response");
3702
- const reader = response.body.getReader();
3703
- const chunks = [];
3704
- let bytes = 0;
3705
- while (true) {
3706
- const { done, value } = await reader.read();
3707
- if (done) break;
3708
- bytes += value.byteLength;
3709
- if (bytes > maximumBytes) {
3710
- await reader.cancel();
3711
- throw new Error("Codex image response exceeds the image size limit");
3981
+ return Object.freeze({
3982
+ refresh,
3983
+ getModels: () => models ?? [],
3984
+ metadata: (modelId) => metadata.get(modelId),
3985
+ revision: () => revision,
3986
+ capabilityGaps: () => [...metadata.values()].filter((model) => model.unsupported && /^[a-z][a-z0-9._-]{0,79}$/u.test(model.id)).slice(0, 20).map((model) => ({
3987
+ model: model.id,
3988
+ ...structuredClone(model.unsupported)
3989
+ })),
3990
+ status: () => ({
3991
+ source: models === void 0 ? "unavailable" : "online",
3992
+ refresh: refreshStatus
3993
+ }),
3994
+ clear() {
3995
+ generation += 1;
3996
+ const flight = refreshing;
3997
+ refreshing = void 0;
3998
+ flight?.cancel();
3999
+ models = void 0;
4000
+ rawCatalog = void 0;
4001
+ metadata = /* @__PURE__ */ new Map();
4002
+ etag = void 0;
4003
+ refreshStatus = "idle";
4004
+ revision += 1;
4005
+ options.onUpdated?.();
3712
4006
  }
3713
- chunks.push(value);
3714
- }
3715
- const body = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), bytes).toString("utf8");
3716
- try {
3717
- return JSON.parse(body);
3718
- } catch {
3719
- throw new Error("Codex returned an unreadable image response");
3720
- }
3721
- }
3722
- /** Strictly decode one PNG returned by the subscription backend. */
3723
- function decodeCodexPng(value, maximumBytes) {
3724
- const encoded = nonEmpty(value);
3725
- const padding = encoded?.endsWith("==") ? 2 : encoded?.endsWith("=") ? 1 : 0;
3726
- if (encoded === void 0 || encoded.length % 4 !== 0 || !validBase64Body(encoded, encoded.length - padding)) throw new Error("Codex returned an invalid base64 PNG");
3727
- const decodedBytes = encoded.length / 4 * 3 - padding;
3728
- if (decodedBytes > maximumBytes) throw new Error("Codex image exceeds the image size limit");
3729
- const data = Buffer.from(encoded, "base64");
3730
- if (data.length !== decodedBytes || data.length < PNG_SIGNATURE.length || !data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) throw new Error("Codex returned an invalid PNG");
3731
- return new Uint8Array(data);
3732
- }
3733
- function imageReference(value) {
3734
- const originalDimensions = record$2(value.originalDimensions) ? {
3735
- width: value.originalDimensions.width,
3736
- height: value.originalDimensions.height
3737
- } : void 0;
3738
- return {
3739
- attachmentId: value.attachmentId,
3740
- mediaType: value.mediaType,
3741
- bytes: value.bytes,
3742
- width: value.width,
3743
- height: value.height,
3744
- ...value.name === void 0 ? {} : { name: value.name },
3745
- ...originalDimensions === void 0 ? {} : { originalDimensions }
3746
- };
3747
- }
3748
- function normalizeAttachmentId(value) {
3749
- if (typeof value !== "string") return void 0;
3750
- if (FULL_ATTACHMENT_ID.test(value)) return value;
3751
- const bare = BARE_ATTACHMENT_DIGEST.exec(value);
3752
- return bare === null ? void 0 : `sha256:${bare[0].toLowerCase()}`;
3753
- }
3754
- function invalidAttachmentId(value) {
3755
- const attachmentId = value?.attachmentId;
3756
- if (typeof attachmentId === "string" && (PATH_LIKE_ATTACHMENT_ID.test(attachmentId) || FILE_NAME_ATTACHMENT_ID.test(attachmentId))) throw new Error("referenceImages attachmentId is a file path or filename; call read_image on that file and retry with its complete sha256:<64 lowercase hex> attachment reference. Do not omit referenceImages or fall back to new image generation.");
3757
- throw new Error("referenceImages attachmentId must be sha256:<64 lowercase hex> copied from an image block or read_image result. Do not omit referenceImages or fall back to new image generation.");
3758
- }
3759
- function referenceOf(value, attachments) {
3760
- const attachmentId = normalizeAttachmentId(value?.attachmentId);
3761
- if (attachmentId === void 0) invalidAttachmentId(value);
3762
- if (!record$2(value) || !attachments.imageLimits.mediaTypes.includes(value.mediaType) || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || !Number.isSafeInteger(value.width) || value.width <= 0 || !Number.isSafeInteger(value.height) || value.height <= 0 || value.name !== void 0 && (typeof value.name !== "string" || value.name.length > 256) || value.originalDimensions !== void 0 && (!record$2(value.originalDimensions) || !Number.isSafeInteger(value.originalDimensions.width) || value.originalDimensions.width <= 0 || !Number.isSafeInteger(value.originalDimensions.height) || value.originalDimensions.height <= 0)) throw new Error("referenceImages contains an invalid image reference");
3763
- return imageReference({
3764
- ...value,
3765
- attachmentId
3766
- });
3767
- }
3768
- function sessionImageReferences(messages) {
3769
- const references = /* @__PURE__ */ new Map();
3770
- const visit = (content) => {
3771
- if (!Array.isArray(content)) return;
3772
- for (const block of content) if (block?.type === "image" && record$2(block.attachment)) {
3773
- const id = normalizeAttachmentId(block.attachment.attachmentId);
3774
- if (id !== void 0) references.set(id, block.attachment);
3775
- } else if (block?.type === "tool-result") visit(block.content);
3776
- };
3777
- for (const message of messages ?? []) visit(message?.content);
3778
- return references;
3779
- }
3780
- async function editImages(values, attachments, signal, messages) {
3781
- if (!Array.isArray(values) || values.length === 0 || values.length > MAX_REFERENCE_IMAGES) throw new Error(`referenceImages must contain between 1 and ${MAX_REFERENCE_IMAGES} images`);
3782
- const available = messages === void 0 ? void 0 : sessionImageReferences(messages);
3783
- const references = values.map((value) => {
3784
- const id = normalizeAttachmentId(value?.attachmentId);
3785
- if (id === void 0) invalidAttachmentId(value);
3786
- if (available === void 0) return referenceOf(value, attachments);
3787
- const selected = available.get(id);
3788
- if (selected === void 0) throw new Error("The selected image attachment cannot be found in the current session. Call read_image on the intended image and retry with its returned reference. Do not omit referenceImages or substitute another image.");
3789
- return referenceOf(selected, attachments);
3790
4007
  });
3791
- if (new Set(references.map((value) => value.attachmentId)).size !== references.length) throw new Error("referenceImages must not contain duplicates");
3792
- const images = [];
3793
- let totalBytes = 0;
3794
- for (const reference of references) {
3795
- const stored = await attachments.readImage(reference, signal);
3796
- totalBytes += stored.data.byteLength;
3797
- if (totalBytes > attachments.imageLimits.maxMessageImageBytes) throw new Error("referenceImages exceed the DSH message image limit");
3798
- images.push({ image_url: `data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}` });
3799
- }
3800
- return images;
3801
- }
3802
- function imageContent(value) {
3803
- const label = typeof value.size === "string" && value.size.length > 0 ? `Generated a ${value.size} image.` : "Generated an image.";
3804
- return [{
3805
- type: "text",
3806
- text: value.localPath === void 0 ? label : `${label}\nOriginal PNG saved on the DSH host at: ${JSON.stringify(value.localPath)}. Read this file or copy it to the workspace with a .png extension; this host path is not a browser URL.`
3807
- }, {
3808
- type: "image",
3809
- attachment: imageReference(value.image)
3810
- }];
3811
4008
  }
3812
- function imageOutputSchema() {
3813
- return {
3814
- type: "object",
3815
- additionalProperties: false,
3816
- properties: {
3817
- image: {
3818
- type: "object",
3819
- required: true,
3820
- additionalProperties: false,
3821
- properties: {
3822
- attachmentId: {
3823
- type: "string",
3824
- required: true
3825
- },
3826
- mediaType: {
3827
- type: "string",
3828
- enum: ["image/png"],
3829
- required: true
3830
- },
3831
- bytes: {
3832
- type: "integer",
3833
- required: true
3834
- },
3835
- width: {
3836
- type: "integer",
3837
- required: true
3838
- },
3839
- height: {
3840
- type: "integer",
3841
- required: true
3842
- },
3843
- name: { type: "string" },
3844
- originalDimensions: {
3845
- type: "object",
3846
- additionalProperties: false,
3847
- properties: {
3848
- width: {
3849
- type: "integer",
3850
- required: true
3851
- },
3852
- height: {
3853
- type: "integer",
3854
- required: true
3855
- }
3856
- }
3857
- }
3858
- }
3859
- },
3860
- original: {
3861
- type: "object",
3862
- required: true,
3863
- additionalProperties: false,
3864
- properties: {
3865
- assetId: {
3866
- type: "string",
3867
- required: true
3868
- },
3869
- mediaType: {
3870
- type: "string",
3871
- enum: ["image/png"],
3872
- required: true
3873
- },
3874
- bytes: {
3875
- type: "integer",
3876
- required: true
3877
- },
3878
- width: {
3879
- type: "integer",
3880
- required: true
3881
- },
3882
- height: {
3883
- type: "integer",
3884
- required: true
3885
- },
3886
- name: {
3887
- type: "string",
3888
- required: true
3889
- },
3890
- sha256: {
3891
- type: "string",
3892
- required: true
3893
- }
3894
- }
3895
- },
3896
- background: { type: "string" },
3897
- requestedModel: { type: "string" },
3898
- reportedModel: { type: "string" },
3899
- requestedSize: { type: "string" },
3900
- localPath: {
3901
- type: "string",
3902
- required: true,
3903
- description: "Absolute path to the original PNG on the DSH host."
3904
- },
3905
- quality: { type: "string" },
3906
- size: { type: "string" }
3907
- }
4009
+ //#endregion
4010
+ //#region src/codex-search.js
4011
+ const CODEX_SEARCH_PROVIDER_ID = "codex-subscription";
4012
+ const CODEX_AUTO_SEARCH_PROVIDER_ID = "codex-subscription-auto";
4013
+ const CODEX_SEARCH_URL = "https://chatgpt.com/backend-api/codex/alpha/search";
4014
+ const DEFAULT_MODEL = "gpt-5.6-luna";
4015
+ const MAX_OUTPUT_TOKENS = 4096;
4016
+ const MAX_SOURCE_DATE = 64;
4017
+ const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
4018
+ const nonEmpty = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
4019
+ const displayText = (value) => {
4020
+ const text = nonEmpty(value)?.replace(/\s+/gu, " ").trim();
4021
+ if (text === void 0 || text.length === 0) return void 0;
4022
+ return text;
4023
+ };
4024
+ const boundedDisplayText = (value, maximum) => {
4025
+ const text = displayText(value);
4026
+ if (text === void 0 || text.length <= maximum) return text;
4027
+ return `${text.slice(0, maximum - 1)}…`;
4028
+ };
4029
+ function sourceOf(value) {
4030
+ if (!record$2(value)) return void 0;
4031
+ const url = nonEmpty(value.url);
4032
+ if (url === void 0) return void 0;
4033
+ let parsed;
4034
+ try {
4035
+ parsed = new URL(url);
4036
+ } catch {
4037
+ return;
4038
+ }
4039
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
4040
+ const title = displayText(value.title) ?? parsed.hostname;
4041
+ const snippet = displayText(value.snippet);
4042
+ const publishedAt = boundedDisplayText(value.published_at, MAX_SOURCE_DATE) ?? boundedDisplayText(value.publishedAt, MAX_SOURCE_DATE);
4043
+ return {
4044
+ url,
4045
+ title,
4046
+ ...snippet === void 0 ? {} : { snippet },
4047
+ ...publishedAt === void 0 ? {} : { publishedAt }
3908
4048
  };
3909
4049
  }
3910
- function responseMetadata(value) {
3911
- const data = Array.isArray(value?.data) ? value.data[0] : void 0;
3912
- const encoded = record$2(data) ? data.b64_json : void 0;
3913
- if (typeof encoded !== "string") throw new Error("Codex returned no image data");
4050
+ function parseSearchResponse(value) {
4051
+ if (!record$2(value) || !Array.isArray(value.results)) throw new Error("Codex returned a malformed search response");
4052
+ const seen = /* @__PURE__ */ new Set();
4053
+ const sources = [];
4054
+ for (const result of value.results ?? []) {
4055
+ const source = sourceOf(result);
4056
+ if (source === void 0 || seen.has(source.url)) continue;
4057
+ seen.add(source.url);
4058
+ sources.push(source);
4059
+ }
3914
4060
  return {
3915
- encoded,
3916
- background: nonEmpty(value.background),
3917
- quality: nonEmpty(value.quality),
3918
- size: nonEmpty(value.size),
3919
- reportedModel: nonEmpty(value.model)
4061
+ sources,
4062
+ truncated: false
3920
4063
  };
3921
4064
  }
3922
- /** Create the DSH-native image-generation tool backed only by the ChatGPT subscription. */
3923
- function createCodexImageTool(options) {
3924
- const fetchImage = options.fetch ?? fetch;
3925
- const attachments = options.attachments;
3926
- return defineTool({
3927
- name: CODEX_IMAGE_TOOL_NAME,
3928
- description: "Generate or edit images only when the user asks for image output, not when merely discussing images. Uses the signed-in Codex subscription. For a new image, omit referenceImages. For edits, copy attachmentId only from the selected session image block; the host supplies its metadata. Never call read_image for an attachmentId or attachment filename. Use read_image only for an actual local file whose reference is not already in the conversation. Never substitute paths, unrelated images, or text-only generation for an edit. For numbered annotations, include the clean source and location-reference image, preserve the requested changes and coordinates in the prompt, and remove guidance markers from the result. If the intended references cannot be identified, ask rather than guessing.",
3929
- parameters: {
3930
- model: {
3931
- type: "string",
3932
- enum: Object.keys(IMAGE_MODELS),
3933
- description: "Optional image engine, independent of the conversation model. When omitted, uses the user image setting (initially gpt-image-2). The 2.5 identifiers are experimental subscription candidates; override only when explicitly requested. Never silently retry with another model."
3934
- },
3935
- prompt: {
3936
- type: "string",
3937
- required: true,
3938
- description: "A complete, production-ready description of the image to generate."
3939
- },
3940
- size: {
3941
- type: "string",
3942
- description: "Optional GPT Image 2 output size. Use auto unless the user requests an exact valid widthxheight resolution."
3943
- },
3944
- quality: {
3945
- type: "string",
3946
- enum: [
3947
- "auto",
3948
- "low",
3949
- "medium",
3950
- "high",
3951
- "xhigh",
3952
- "max"
3953
- ],
3954
- description: "Optional rendering quality. Use auto unless the user requests draft speed or final quality."
3955
- },
3956
- background: {
3957
- type: "string",
3958
- enum: [
3959
- "auto",
3960
- "transparent",
3961
- "opaque"
3962
- ],
3963
- description: "Optional background mode. Request transparent only when the user needs transparency."
3964
- },
3965
- referenceImages: {
3966
- type: "array",
3967
- description: "Optional references to 1-5 selected images. For session images provide only attachmentId; the host resolves trusted metadata. Omit only for a new image; never drop an invalid reference to bypass editing.",
3968
- items: {
3969
- type: "object",
3970
- additionalProperties: false,
3971
- properties: {
3972
- attachmentId: {
3973
- type: "string",
3974
- required: true,
3975
- description: "Copy the complete attachmentId from the image block or read_image result: sha256:<64 lowercase hex>. Never pass a workspace path, absolute path, or filename. A bare 64-character hex digest is accepted and normalized to sha256:<64 lowercase hex>."
3976
- },
3977
- mediaType: {
3978
- type: "string",
3979
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3980
- },
3981
- bytes: {
3982
- type: "integer",
3983
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3984
- },
3985
- width: {
3986
- type: "integer",
3987
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3988
- },
3989
- height: {
3990
- type: "integer",
3991
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
3992
- },
3993
- name: { type: "string" },
3994
- originalDimensions: {
3995
- type: "object",
3996
- additionalProperties: false,
3997
- properties: {
3998
- width: {
3999
- type: "integer",
4000
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
4001
- },
4002
- height: {
4003
- type: "integer",
4004
- ...typeof options.getSessionMessages !== "function" ? { required: true } : {}
4005
- }
4006
- }
4007
- }
4008
- }
4009
- }
4010
- }
4011
- },
4012
- output: {
4013
- schema: imageOutputSchema(),
4014
- render: (_args, value) => imageContent(value),
4015
- presentationMeta: (_args, value) => ({
4016
- kind: "codex-subscription-image",
4017
- schemaVersion: 1,
4018
- original: value.original,
4019
- ...value.requestedModel === void 0 ? {} : { requestedModel: value.requestedModel },
4020
- ...value.reportedModel === void 0 ? {} : { reportedModel: value.reportedModel },
4021
- ...value.requestedSize === void 0 ? {} : { requestedSize: value.requestedSize }
4022
- })
4023
- },
4024
- timeoutMs: 300 * 1e3,
4025
- isConcurrencySafe: () => false,
4026
- async execute(args, exec) {
4027
- assertImageOperation(options.getFeatures?.(), args.referenceImages !== void 0);
4028
- const defaults = readImageDefaults(options.getFeatures?.());
4029
- args = {
4030
- ...args,
4031
- model: args.model ?? defaults.imageModel,
4032
- quality: args.quality ?? defaults.imageQuality
4033
- };
4034
- const prompt = nonEmpty(args.prompt);
4035
- if (prompt === void 0) throw new Error("prompt must be a non-empty string");
4036
- const imageOptions = normalizeImageOptions(args);
4037
- const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, exec.signal);
4038
- if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new Error("ChatGPT subscription is not signed in");
4039
- if (!attachments.imageLimits.mediaTypes.includes("image/png")) throw new Error("This DSH installation does not accept PNG image attachments");
4040
- const maximumBytes = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
4041
- const editing = args.referenceImages !== void 0;
4042
- const sessionMessages = editing && typeof options.getSessionMessages === "function" ? await options.getSessionMessages(exec.agent?.id) : void 0;
4043
- const images = editing ? await editImages(args.referenceImages, attachments, exec.signal, sessionMessages ?? (options.getSessionMessages ? [] : void 0)) : void 0;
4065
+ /** Create the DSH web provider backed only by the ChatGPT subscription search endpoint. */
4066
+ function createCodexSearchProvider(options) {
4067
+ const fetchSearch = options.fetch ?? fetch;
4068
+ return Object.freeze({
4069
+ id: CODEX_SEARCH_PROVIDER_ID,
4070
+ available: () => true,
4071
+ async search(request, signal) {
4072
+ signal?.throwIfAborted();
4073
+ const preferences = readCapabilitySettings(options.resolvePreferences?.());
4074
+ if (preferences.searchMode === "disabled") throw new WebError("Codex search is disabled in subscription settings", "WEB_PROVIDER_UNAVAILABLE");
4075
+ const { access, accountId } = await readSubscriptionCredentials(options.getAuth, options.readCredential, signal);
4076
+ if (typeof access !== "string" || access.length === 0 || typeof accountId !== "string" || accountId.length === 0) throw new WebError("ChatGPT subscription is not signed in", "WEB_PROVIDER_CREDENTIAL_MISSING");
4077
+ const model = nonEmpty(options.resolveModel?.()) ?? DEFAULT_MODEL;
4078
+ const id = nonEmpty(options.resolveSessionId?.()) ?? randomUUID();
4044
4079
  let response;
4045
- assertImageOperation(options.getFeatures?.(), editing);
4046
4080
  try {
4047
- response = await fetchImage(editing ? CODEX_IMAGE_EDIT_URL : CODEX_IMAGE_GENERATION_URL, {
4081
+ response = await fetchSearch(CODEX_SEARCH_URL, {
4048
4082
  method: "POST",
4049
4083
  redirect: "error",
4050
4084
  headers: {
@@ -4053,60 +4087,58 @@ function createCodexImageTool(options) {
4053
4087
  accept: "application/json",
4054
4088
  "content-type": "application/json",
4055
4089
  originator: "pi",
4056
- "x-codex-image-turn-id": String(exec.callId),
4057
4090
  "user-agent": USER_AGENT
4058
4091
  },
4059
4092
  body: JSON.stringify({
4060
- ...images === void 0 ? {} : { images },
4061
- prompt,
4062
- background: imageOptions.background,
4063
- model: resolveImageModel(args.model),
4064
- quality: imageOptions.quality,
4065
- size: imageOptions.size
4093
+ id,
4094
+ model,
4095
+ input: request.query,
4096
+ commands: {
4097
+ search_query: [{ q: request.query }],
4098
+ response_length: "short"
4099
+ },
4100
+ settings: {
4101
+ allowed_callers: ["direct"],
4102
+ external_web_access: preferences.searchMode === "live"
4103
+ },
4104
+ max_output_tokens: MAX_OUTPUT_TOKENS
4066
4105
  }),
4067
- signal: exec.signal
4106
+ signal
4068
4107
  });
4069
4108
  } catch (error) {
4070
- if (exec.signal.aborted) throw exec.signal.reason;
4071
- throw new Error(`Codex image ${editing ? "edit" : "generation"} request failed${requestFailureReason(error)}`, { cause: error });
4109
+ if (signal?.aborted || error?.name === "AbortError") throw new WebError("Codex search aborted", "WEB_ABORTED", { cause: error });
4110
+ throw new WebError("Codex search request failed", "WEB_PROVIDER_ERROR", { cause: error });
4072
4111
  }
4073
- if (!response.ok) {
4074
- if (response.status === 401 || response.status === 403) throw new Error("ChatGPT sign-in needs to be renewed");
4075
- if (response.status === 429) throw new Error("Codex image generation quota is unavailable");
4076
- throw new Error(`Codex image ${editing ? "edit" : "generation"} failed (HTTP ${response.status})`);
4112
+ if (!response.ok) throw response.status === 401 || response.status === 403 ? new WebError("ChatGPT sign-in needs to be renewed", "WEB_PROVIDER_CREDENTIAL_MISSING") : new WebError(`Codex search request failed (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
4113
+ let value;
4114
+ try {
4115
+ value = await response.json();
4116
+ } catch (error) {
4117
+ throw new WebError("Codex returned an unreadable search response", "WEB_PROVIDER_ERROR", { cause: error });
4077
4118
  }
4078
- const metadata = responseMetadata(await readJsonWithin(response, encodedLimit(maximumBytes) + RESPONSE_ENVELOPE_BYTES));
4079
- const data = decodeCodexPng(metadata.encoded, maximumBytes);
4080
- const sessionId = exec.agent?.id;
4081
- if (sessionId === void 0) throw new Error("Codex image generation requires a session-owned tool call");
4082
- const original = await options.originalImages.save(String(sessionId), data);
4083
- let ref;
4084
4119
  try {
4085
- ref = await attachments.saveImage({
4086
- data,
4087
- mediaType: "image/png",
4088
- name: "codex-generated.png"
4120
+ const result = parseSearchResponse(value);
4121
+ if (preferences.searchDomains.length > 0) result.sources = result.sources.filter((source) => {
4122
+ const hostname = new URL(source.url).hostname.toLowerCase();
4123
+ return preferences.searchDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
4089
4124
  });
4125
+ return result;
4090
4126
  } catch (error) {
4091
- await options.originalImages.remove(original);
4092
- throw error;
4127
+ throw new WebError("Codex returned a malformed search response", "WEB_PROVIDER_ERROR", { cause: error });
4093
4128
  }
4094
- const result = {
4095
- requestedModel: resolveImageModel(args.model),
4096
- requestedSize: imageOptions.size,
4097
- ...metadata.reportedModel === void 0 ? {} : { reportedModel: metadata.reportedModel },
4098
- image: imageReference(ref),
4099
- original,
4100
- localPath: options.originalImages.originalPath(original.assetId),
4101
- ...metadata.background === void 0 ? {} : { background: metadata.background },
4102
- ...metadata.quality === void 0 ? {} : { quality: metadata.quality },
4103
- size: `${original.width}x${original.height}`
4104
- };
4105
- if (exec.parent !== void 0) exec.deferContext(createUserMessage({
4106
- content: imageContent(result),
4107
- source: { kind: "codex-subscription-image" }
4108
- }));
4109
- return result;
4129
+ }
4130
+ });
4131
+ }
4132
+ /** Route each request by its initiating model without changing the user's explicit overrides. */
4133
+ function createCodexAutoSearchProvider(options) {
4134
+ return Object.freeze({
4135
+ id: CODEX_AUTO_SEARCH_PROVIDER_ID,
4136
+ available: () => true,
4137
+ async search(request, signal) {
4138
+ if (options.resolveModelProvider?.() === "openai-codex") return options.codex.search(request, signal);
4139
+ const provider = options.resolveDshProvider?.();
4140
+ if (provider === void 0 || provider.id === "codex-subscription-auto" || provider.id === "codex-subscription" || provider.available() !== true) throw new WebError("DSH default search is unavailable", "WEB_PROVIDER_UNAVAILABLE");
4141
+ return provider.search(request, signal);
4110
4142
  }
4111
4143
  });
4112
4144
  }
@@ -6478,7 +6510,7 @@ function apply(ctx, config = {}) {
6478
6510
  let dispose;
6479
6511
  const sync = () => {
6480
6512
  const value = settings.get();
6481
- if (value.imageSketchAgent && value.imageSketch && value.imageEditing) dispose ??= ctx.tools.register(createSketchAgentTool(sketchBridge, ctx.attachments));
6513
+ if (value.imageSketchAgent && value.imageSketch && value.imageEditing) dispose ??= ctx.tools.register(createSketchAgentTool(sketchBridge, ctx.attachments, (sessionId) => ctx.get?.("sessions")?.get?.(sessionId)?.deriveMessages?.() ?? []));
6482
6514
  else {
6483
6515
  dispose?.();
6484
6516
  dispose = void 0;