@demicodes/provider-codex 0.1.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/dist/index.mjs ADDED
@@ -0,0 +1,1279 @@
1
+ import { delay, errorMessage, isRecord, nonEmptyString, numberOrNull, numberOrZero, parseJsonObject, parseJsonOrString, shortHash, stringOrNull } from "@demicodes/utils";
2
+ import { Buffer } from "node:buffer";
3
+ import { chmod, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { applyModelPolicy, clampPromptCacheKey, defineProvider, httpErrorCode } from "@demicodes/provider";
7
+ //#region src/auth.ts
8
+ async function codexAuthStatus(options = {}) {
9
+ return new FileCodexAuthStore(options).status();
10
+ }
11
+ const TOKEN_REFRESH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
12
+ const TOKEN_REFRESH_URL = "https://auth.openai.com/oauth/token";
13
+ const REFRESH_EXPIRY_SKEW_MS = 300 * 1e3;
14
+ const REFRESH_STALENESS_MS = 11520 * 60 * 1e3;
15
+ var FileCodexAuthStore = class {
16
+ codexHome;
17
+ authFile;
18
+ refreshImpl;
19
+ now;
20
+ lockRetryDelayMs;
21
+ lockTimeoutMs;
22
+ constructor(options = {}) {
23
+ this.codexHome = options.codexHome ?? defaultCodexHome();
24
+ this.authFile = join(this.codexHome, "auth.json");
25
+ this.refreshImpl = options.refresh ?? refreshCodexToken;
26
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
27
+ this.lockRetryDelayMs = options.lockRetryDelayMs ?? 25;
28
+ this.lockTimeoutMs = options.lockTimeoutMs ?? 5e3;
29
+ }
30
+ async status() {
31
+ try {
32
+ const auth = await this.resolveAuth();
33
+ if (auth.kind === "chatgpt") return {
34
+ status: "authenticated",
35
+ accountLabel: auth.email ?? auth.accountId
36
+ };
37
+ if (auth.kind === "apiKey") return {
38
+ status: "authenticated",
39
+ accountLabel: "OPENAI_API_KEY"
40
+ };
41
+ if (auth.kind === "personalAccessToken") return {
42
+ status: "authenticated",
43
+ accountLabel: auth.accountId ?? "personal access token"
44
+ };
45
+ return {
46
+ status: "authenticated",
47
+ accountLabel: auth.accountId
48
+ };
49
+ } catch (error) {
50
+ if (error instanceof CodexAuthError && error.code === "auth_missing") return {
51
+ status: "unauthenticated",
52
+ message: error.message
53
+ };
54
+ if (error instanceof CodexAuthError && error.code === "auth_unsupported") return {
55
+ status: "error",
56
+ message: error.message
57
+ };
58
+ return {
59
+ status: "error",
60
+ message: redactSecretText(error instanceof Error ? error.message : String(error))
61
+ };
62
+ }
63
+ }
64
+ async resolveAuth(options = {}) {
65
+ const auth = await this.readAuthFile();
66
+ const mode = resolvedAuthMode(auth);
67
+ if (mode === "bedrockApiKey") throw new CodexAuthError("auth_unsupported", "Codex provider does not support Bedrock auth");
68
+ if (mode === "apiKey") {
69
+ const key = nonEmptyString(auth.OPENAI_API_KEY);
70
+ if (!key) throw new CodexAuthError("auth_missing", `No OPENAI_API_KEY found in ${this.authFile}`);
71
+ return {
72
+ kind: "apiKey",
73
+ mode,
74
+ apiKey: key,
75
+ authFile: this.authFile
76
+ };
77
+ }
78
+ if (mode === "personalAccessToken") {
79
+ const token = nonEmptyString(auth.personal_access_token);
80
+ if (!token) throw new CodexAuthError("auth_missing", `No personal access token found in ${this.authFile}`);
81
+ return {
82
+ kind: "personalAccessToken",
83
+ mode,
84
+ accessToken: token,
85
+ accountId: parseChatGptClaims(token).accountId,
86
+ authFile: this.authFile
87
+ };
88
+ }
89
+ if (mode === "agentIdentity") return resolveAgentIdentity(auth, this.authFile);
90
+ const tokens = auth.tokens;
91
+ if (!tokens || typeof tokens !== "object") throw new CodexAuthError("auth_missing", `No ChatGPT tokens found in ${this.authFile}`);
92
+ const accessToken = nonEmptyString(tokens.access_token);
93
+ if (!accessToken) throw new CodexAuthError("auth_missing", `No ChatGPT access token found in ${this.authFile}`);
94
+ const refreshToken = nonEmptyString(tokens.refresh_token) ?? null;
95
+ const claims = parseChatGptClaims(accessToken);
96
+ const idClaims = parseIdTokenClaims(tokens.id_token);
97
+ const accountId = nonEmptyString(tokens.account_id) ?? claims.accountId ?? idClaims.accountId;
98
+ if (!accountId) throw new CodexAuthError("auth_missing", `No ChatGPT account id found in ${this.authFile}`);
99
+ const expiresAt = parseJwtExpiration(accessToken);
100
+ const lastRefresh = parseDate(auth.last_refresh);
101
+ if ((options.forceRefresh === true || expiresWithin(expiresAt, this.now(), REFRESH_EXPIRY_SKEW_MS) || olderThan(lastRefresh, this.now(), REFRESH_STALENESS_MS)) && refreshToken) return this.refreshAndResolve(auth, refreshToken);
102
+ return {
103
+ kind: "chatgpt",
104
+ mode,
105
+ accessToken,
106
+ refreshToken,
107
+ accountId,
108
+ email: idClaims.email ?? claims.email,
109
+ isFedrampAccount: idClaims.isFedrampAccount || claims.isFedrampAccount,
110
+ expiresAt,
111
+ authFile: this.authFile
112
+ };
113
+ }
114
+ async refreshAndResolve(auth, refreshToken) {
115
+ return this.withAuthFileLock(async () => {
116
+ const latest = await this.readAuthFile();
117
+ const latestTokens = latest.tokens;
118
+ const latestRefreshToken = latestTokens && typeof latestTokens === "object" ? nonEmptyString(latestTokens.refresh_token) ?? refreshToken : refreshToken;
119
+ const response = await this.refreshImpl(latestRefreshToken);
120
+ const nextTokens = {
121
+ ...latestTokens && typeof latestTokens === "object" ? latestTokens : {},
122
+ ...response.id_token ? { id_token: response.id_token } : {},
123
+ ...response.access_token ? { access_token: response.access_token } : {},
124
+ ...response.refresh_token ? { refresh_token: response.refresh_token } : {}
125
+ };
126
+ const nextAuth = {
127
+ ...latest,
128
+ auth_mode: latest.auth_mode ?? auth.auth_mode ?? "chatgpt",
129
+ tokens: nextTokens,
130
+ last_refresh: this.now().toISOString()
131
+ };
132
+ await writeAuthJsonAtomic(this.authFile, nextAuth);
133
+ return resolveChatGptAuthFromFile(nextAuth, this.authFile);
134
+ });
135
+ }
136
+ async readAuthFile() {
137
+ try {
138
+ return JSON.parse(await readFile(this.authFile, "utf8"));
139
+ } catch (error) {
140
+ if (isNodeError(error) && error.code === "ENOENT") throw new CodexAuthError("auth_missing", `Codex auth file not found: ${this.authFile}`);
141
+ throw new CodexAuthError("auth_invalid", `Failed to read Codex auth file ${this.authFile}: ${redactSecretText(errorMessage(error))}`);
142
+ }
143
+ }
144
+ async withAuthFileLock(fn) {
145
+ const lockFile = `${this.authFile}.lock`;
146
+ await mkdir(dirname(this.authFile), { recursive: true });
147
+ const started = Date.now();
148
+ let handle = null;
149
+ while (!handle) try {
150
+ handle = await open(lockFile, "wx", 384);
151
+ } catch (error) {
152
+ if (!isNodeError(error) || error.code !== "EEXIST" || Date.now() - started > this.lockTimeoutMs) throw new CodexAuthError("auth_lock_failed", `Failed to lock Codex auth file: ${redactSecretText(errorMessage(error))}`);
153
+ await delay(this.lockRetryDelayMs);
154
+ }
155
+ try {
156
+ return await fn();
157
+ } finally {
158
+ await handle.close().catch(() => void 0);
159
+ await rm(lockFile, { force: true }).catch(() => void 0);
160
+ }
161
+ }
162
+ };
163
+ var CodexAuthError = class extends Error {
164
+ code;
165
+ constructor(code, message) {
166
+ super(message);
167
+ this.code = code;
168
+ this.name = "CodexAuthError";
169
+ }
170
+ };
171
+ function defaultCodexHome() {
172
+ return process.env.CODEX_HOME && process.env.CODEX_HOME.trim() ? process.env.CODEX_HOME : join(homedir(), ".codex");
173
+ }
174
+ function resolvedAuthMode(auth) {
175
+ if (auth.auth_mode) return auth.auth_mode;
176
+ if (auth.personal_access_token) return "personalAccessToken";
177
+ if (auth.bedrock_api_key) return "bedrockApiKey";
178
+ if (auth.OPENAI_API_KEY) return "apiKey";
179
+ if (auth.agent_identity) return "agentIdentity";
180
+ return "chatgpt";
181
+ }
182
+ function parseJwtExpiration(jwt) {
183
+ const exp = decodeJwtPayload(jwt)?.exp;
184
+ return typeof exp === "number" ? /* @__PURE__ */ new Date(exp * 1e3) : null;
185
+ }
186
+ function parseChatGptClaims(jwt) {
187
+ const payload = decodeJwtPayload(jwt);
188
+ const auth = isRecord(payload?.["https://api.openai.com/auth"]) ? payload["https://api.openai.com/auth"] : null;
189
+ const profile = isRecord(payload?.["https://api.openai.com/profile"]) ? payload["https://api.openai.com/profile"] : null;
190
+ return {
191
+ accountId: stringOrNull(auth?.chatgpt_account_id),
192
+ email: stringOrNull(payload?.email) ?? stringOrNull(profile?.email),
193
+ isFedrampAccount: auth?.chatgpt_account_is_fedramp === true
194
+ };
195
+ }
196
+ function parseIdTokenClaims(idToken) {
197
+ if (typeof idToken === "string") return parseChatGptClaims(idToken);
198
+ if (!isRecord(idToken)) return {
199
+ accountId: null,
200
+ email: null,
201
+ isFedrampAccount: false
202
+ };
203
+ return {
204
+ accountId: stringOrNull(idToken.chatgpt_account_id),
205
+ email: stringOrNull(idToken.email),
206
+ isFedrampAccount: idToken.chatgpt_account_is_fedramp === true
207
+ };
208
+ }
209
+ async function refreshCodexToken(refreshToken, signal) {
210
+ const response = await fetch(TOKEN_REFRESH_URL, {
211
+ method: "POST",
212
+ headers: { "content-type": "application/json" },
213
+ body: JSON.stringify({
214
+ client_id: process.env.CODEX_APP_SERVER_LOGIN_CLIENT_ID || TOKEN_REFRESH_CLIENT_ID,
215
+ grant_type: "refresh_token",
216
+ refresh_token: refreshToken
217
+ }),
218
+ signal
219
+ });
220
+ if (!response.ok) throw new CodexAuthError("auth_refresh_failed", `Codex token refresh failed with HTTP ${response.status}`);
221
+ return await response.json();
222
+ }
223
+ function redactSecretText(text) {
224
+ return text.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/g, "Bearer [REDACTED]").replace(/(access_token|refresh_token|id_token|OPENAI_API_KEY)["'=:\s]+[A-Za-z0-9._~+/=-]+/gi, "$1=[REDACTED]");
225
+ }
226
+ function resolveChatGptAuthFromFile(auth, authFile) {
227
+ const tokens = auth.tokens;
228
+ if (!tokens || typeof tokens !== "object") throw new CodexAuthError("auth_missing", `No ChatGPT tokens found in ${authFile}`);
229
+ const accessToken = nonEmptyString(tokens.access_token);
230
+ if (!accessToken) throw new CodexAuthError("auth_missing", `No ChatGPT access token found in ${authFile}`);
231
+ const claims = parseChatGptClaims(accessToken);
232
+ const idClaims = parseIdTokenClaims(tokens.id_token);
233
+ const accountId = nonEmptyString(tokens.account_id) ?? claims.accountId ?? idClaims.accountId;
234
+ if (!accountId) throw new CodexAuthError("auth_missing", `No ChatGPT account id found in ${authFile}`);
235
+ return {
236
+ kind: "chatgpt",
237
+ mode: resolvedAuthMode(auth) === "chatgptAuthTokens" ? "chatgptAuthTokens" : "chatgpt",
238
+ accessToken,
239
+ refreshToken: nonEmptyString(tokens.refresh_token) ?? null,
240
+ accountId,
241
+ email: idClaims.email ?? claims.email,
242
+ isFedrampAccount: idClaims.isFedrampAccount || claims.isFedrampAccount,
243
+ expiresAt: parseJwtExpiration(accessToken),
244
+ authFile
245
+ };
246
+ }
247
+ function resolveAgentIdentity(auth, authFile) {
248
+ const value = auth.agent_identity;
249
+ if (!isRecord(value)) throw new CodexAuthError("auth_unsupported", "Codex agent identity auth requires an auth record");
250
+ const authorization = nonEmptyString(value.authorization);
251
+ const accountId = nonEmptyString(value.account_id);
252
+ if (!authorization || !accountId) throw new CodexAuthError("auth_unsupported", "Codex agent identity auth requires an authorization header generated by official Codex");
253
+ return {
254
+ kind: "agentIdentity",
255
+ mode: "agentIdentity",
256
+ authorization,
257
+ accountId,
258
+ isFedrampAccount: value.chatgpt_account_is_fedramp === true,
259
+ authFile
260
+ };
261
+ }
262
+ async function writeAuthJsonAtomic(authFile, auth) {
263
+ await mkdir(dirname(authFile), { recursive: true });
264
+ const temp = `${authFile}.${process.pid}.${Date.now()}.tmp`;
265
+ await writeFile(temp, `${JSON.stringify(auth, null, 2)}\n`, { mode: 384 });
266
+ await chmod(temp, 384);
267
+ await rename(temp, authFile);
268
+ }
269
+ function decodeJwtPayload(jwt) {
270
+ const parts = jwt.split(".");
271
+ if (parts.length !== 3 || !parts[1]) return null;
272
+ try {
273
+ const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
274
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
275
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+ function expiresWithin(expiresAt, now, skewMs) {
281
+ return expiresAt !== null && expiresAt.getTime() - now.getTime() <= skewMs;
282
+ }
283
+ function olderThan(value, now, ageMs) {
284
+ return value !== null && now.getTime() - value.getTime() >= ageMs;
285
+ }
286
+ function parseDate(value) {
287
+ if (typeof value !== "string" || !value) return null;
288
+ const ms = Date.parse(value);
289
+ return Number.isFinite(ms) ? new Date(ms) : null;
290
+ }
291
+ function isNodeError(error) {
292
+ return error instanceof Error && "code" in error;
293
+ }
294
+ //#endregion
295
+ //#region src/models.ts
296
+ const DEFAULT_CHATGPT_CODEX_BASE_URL$1 = "https://chatgpt.com/backend-api";
297
+ const DEFAULT_CODEX_MODEL_CATALOG_CLIENT_VERSION = "0.130.0";
298
+ const CODEX_MODEL_CACHE_TTL_MS = 900 * 1e3;
299
+ const codexCatalogCache = /* @__PURE__ */ new Map();
300
+ async function listCodexModels(options = {}) {
301
+ const fetchImpl = options.fetch ?? fetch;
302
+ const nowDate = (options.now ?? (() => /* @__PURE__ */ new Date()))();
303
+ const authStore = options.authStore ?? new FileCodexAuthStore({ codexHome: options.codexHome });
304
+ const clientVersion = options.clientVersion ?? DEFAULT_CODEX_MODEL_CATALOG_CLIENT_VERSION;
305
+ const auth = await authStore.resolveAuth();
306
+ assertCodexBackendModelCatalogAuth(auth);
307
+ const cacheKey = codexModelCatalogCacheKey(auth, options.baseUrl, clientVersion);
308
+ const cached = codexCatalogCache.get(cacheKey);
309
+ if (cached && nowDate.getTime() - cached.fetchedAtMs < CODEX_MODEL_CACHE_TTL_MS) return cloneModelList(markModelListCache(cached.list, false));
310
+ try {
311
+ const list = await requestCodexModels({
312
+ auth,
313
+ clientVersion,
314
+ fetch: fetchImpl,
315
+ now: nowDate,
316
+ baseUrl: options.baseUrl,
317
+ headers: options.headers,
318
+ userAgent: options.userAgent
319
+ });
320
+ codexCatalogCache.set(cacheKey, {
321
+ fetchedAtMs: nowDate.getTime(),
322
+ list
323
+ });
324
+ return cloneModelList(list);
325
+ } catch (error) {
326
+ if (isUnauthorized(error)) {
327
+ const refreshed = await authStore.resolveAuth({ forceRefresh: true });
328
+ assertCodexBackendModelCatalogAuth(refreshed);
329
+ const list = await requestCodexModels({
330
+ auth: refreshed,
331
+ clientVersion,
332
+ fetch: fetchImpl,
333
+ now: nowDate,
334
+ baseUrl: options.baseUrl,
335
+ headers: options.headers,
336
+ userAgent: options.userAgent
337
+ });
338
+ codexCatalogCache.set(codexModelCatalogCacheKey(refreshed, options.baseUrl, clientVersion), {
339
+ fetchedAtMs: nowDate.getTime(),
340
+ list
341
+ });
342
+ return cloneModelList(list);
343
+ }
344
+ if (cached && !isAuthCatalogError(error)) {
345
+ const stale = markModelListCache(cached.list, true);
346
+ return {
347
+ ...cloneModelList(stale),
348
+ warnings: [...stale.warnings, `Using stale Codex model catalog: ${errorMessage(error)}`]
349
+ };
350
+ }
351
+ throw error;
352
+ }
353
+ }
354
+ function codexBackendModelsToModelList(value, options = {}) {
355
+ if (!isRecord(value) || !Array.isArray(value.models)) throw new Error("Codex models response does not contain a models array");
356
+ const sourceFetchedAt = options.sourceFetchedAt ?? (/* @__PURE__ */ new Date()).toISOString();
357
+ const warnings = [...options.warnings ?? []];
358
+ const models = [];
359
+ for (const raw of value.models) {
360
+ if (!isRecord(raw)) {
361
+ warnings.push("Skipped Codex model with invalid metadata");
362
+ continue;
363
+ }
364
+ const id = nonEmptyString(raw.slug);
365
+ if (!id) {
366
+ warnings.push("Skipped Codex model without slug");
367
+ continue;
368
+ }
369
+ if (nonEmptyString(raw.visibility) === "hide") continue;
370
+ models.push(codexModelFromBackendEntry(id, raw, sourceFetchedAt, options.stale === true));
371
+ }
372
+ return {
373
+ providerId: "codex",
374
+ models,
375
+ defaultModelId: null,
376
+ warnings,
377
+ sourceFetchedAt,
378
+ stale: options.stale === true
379
+ };
380
+ }
381
+ async function requestCodexModels(options) {
382
+ const response = await options.fetch(codexModelsUrl(options.baseUrl ?? DEFAULT_CHATGPT_CODEX_BASE_URL$1, options.clientVersion), { headers: buildCodexModelCatalogHeaders(options.auth, options.headers, options.userAgent) });
383
+ if (!response.ok) throw new CodexModelCatalogHttpError(response.status, `Codex models request failed with HTTP ${response.status}`);
384
+ return codexBackendModelsToModelList(await response.json(), {
385
+ sourceFetchedAt: options.now.toISOString(),
386
+ stale: false
387
+ });
388
+ }
389
+ function codexModelFromBackendEntry(id, raw, sourceFetchedAt, stale) {
390
+ const supportedReasoningEfforts = supportedReasoningLevels(raw.supported_reasoning_levels);
391
+ const tiers = serviceTiers(raw.service_tiers);
392
+ return {
393
+ providerId: "codex",
394
+ id,
395
+ displayName: nonEmptyString(raw.display_name) ?? id,
396
+ description: nonEmptyString(raw.description),
397
+ contextWindow: numberOrNull(raw.context_window),
398
+ outputLimit: null,
399
+ supportsTools: supportsCodexTools(raw),
400
+ supportsAttachments: Array.isArray(raw.input_modalities) ? raw.input_modalities.includes("image") : null,
401
+ supportsReasoning: supportedReasoningEfforts ? supportedReasoningEfforts.length > 0 : null,
402
+ supportedThinkingEfforts: supportedReasoningEfforts,
403
+ defaultThinkingEffort: null,
404
+ serviceTiers: tiers,
405
+ defaultServiceTierId: null,
406
+ sourceFetchedAt,
407
+ stale
408
+ };
409
+ }
410
+ function buildCodexModelCatalogHeaders(auth, configuredHeaders, userAgent) {
411
+ const headers = new Headers(configuredHeaders);
412
+ if (auth.kind === "agentIdentity") headers.set("Authorization", auth.authorization);
413
+ else headers.set("Authorization", `Bearer ${auth.accessToken}`);
414
+ if (auth.accountId) headers.set("ChatGPT-Account-ID", auth.accountId);
415
+ if ("isFedrampAccount" in auth && auth.isFedrampAccount) headers.set("X-OpenAI-Fedramp", "true");
416
+ headers.set("accept", "application/json");
417
+ headers.set("User-Agent", userAgent ?? defaultModelCatalogUserAgent());
418
+ return headers;
419
+ }
420
+ function codexModelsUrl(baseUrl, clientVersion) {
421
+ const normalized = baseUrl.replace(/\/+$/, "");
422
+ return `${normalized.endsWith("/codex/models") ? normalized : normalized.endsWith("/codex") ? `${normalized}/models` : `${normalized}/codex/models`}?client_version=${encodeURIComponent(clientVersion)}`;
423
+ }
424
+ function assertCodexBackendModelCatalogAuth(auth) {
425
+ if (auth.kind === "apiKey") throw new CodexAuthError("auth_unsupported", "Codex backend model catalog requires official Codex ChatGPT auth, not OPENAI_API_KEY");
426
+ }
427
+ function codexModelCatalogCacheKey(auth, baseUrl, clientVersion) {
428
+ const account = "accountId" in auth ? auth.accountId ?? "" : "";
429
+ return [
430
+ auth.kind,
431
+ auth.mode,
432
+ account,
433
+ baseUrl ?? DEFAULT_CHATGPT_CODEX_BASE_URL$1,
434
+ clientVersion
435
+ ].join("\0");
436
+ }
437
+ function supportedReasoningLevels(value) {
438
+ if (!Array.isArray(value)) return null;
439
+ const efforts = value.map((level) => isRecord(level) ? nonEmptyString(level.effort) : void 0).filter((effort) => effort !== void 0);
440
+ return efforts.length > 0 ? efforts : [];
441
+ }
442
+ function serviceTiers(value) {
443
+ if (!Array.isArray(value)) return null;
444
+ const tiers = value.flatMap((tier) => {
445
+ if (!isRecord(tier)) return [];
446
+ const id = nonEmptyString(tier.id);
447
+ if (!id) return [];
448
+ return [{
449
+ id,
450
+ label: nonEmptyString(tier.name) ?? id,
451
+ ...nonEmptyString(tier.description) ? { description: nonEmptyString(tier.description) } : {}
452
+ }];
453
+ });
454
+ return tiers.length > 0 ? tiers : [];
455
+ }
456
+ function supportsCodexTools(raw) {
457
+ if (typeof raw.tool_mode === "string" && raw.tool_mode.length > 0) return true;
458
+ if (Array.isArray(raw.experimental_supported_tools)) return raw.experimental_supported_tools.length > 0;
459
+ if (raw.apply_patch_tool_type !== void 0 || raw.web_search_tool_type !== void 0) return true;
460
+ return null;
461
+ }
462
+ function markModelListCache(list, stale) {
463
+ return {
464
+ ...list,
465
+ stale,
466
+ models: list.models.map((model) => ({
467
+ ...model,
468
+ stale
469
+ }))
470
+ };
471
+ }
472
+ function cloneModelList(list) {
473
+ return {
474
+ ...list,
475
+ warnings: [...list.warnings],
476
+ models: list.models.map((model) => ({
477
+ ...model,
478
+ ...model.cost ? { cost: { ...model.cost } } : {},
479
+ supportedThinkingEfforts: model.supportedThinkingEfforts ? [...model.supportedThinkingEfforts] : null,
480
+ serviceTiers: model.serviceTiers ? model.serviceTiers.map((tier) => ({ ...tier })) : model.serviceTiers
481
+ }))
482
+ };
483
+ }
484
+ function isUnauthorized(error) {
485
+ return error instanceof CodexModelCatalogHttpError && error.status === 401;
486
+ }
487
+ function isAuthCatalogError(error) {
488
+ if (error instanceof CodexModelCatalogHttpError && (error.status === 401 || error.status === 403)) return true;
489
+ return error instanceof CodexAuthError;
490
+ }
491
+ var CodexModelCatalogHttpError = class extends Error {
492
+ status;
493
+ constructor(status, message) {
494
+ super(redactSecretText(message));
495
+ this.status = status;
496
+ this.name = "CodexModelCatalogHttpError";
497
+ }
498
+ };
499
+ function defaultModelCatalogUserAgent() {
500
+ return `demi-provider-codex/0.0.0`;
501
+ }
502
+ //#endregion
503
+ //#region src/responses.ts
504
+ function buildCodexResponsesRequestBody(request) {
505
+ const body = {
506
+ model: request.modelId,
507
+ instructions: request.systemPrompt,
508
+ input: request.items.flatMap((item, index) => inferenceItemToResponsesInput(item, index)),
509
+ tools: request.tools.map(toolToResponsesTool),
510
+ tool_choice: "auto",
511
+ parallel_tool_calls: true,
512
+ store: false,
513
+ stream: true,
514
+ include: ["reasoning.encrypted_content"],
515
+ prompt_cache_key: clampPromptCacheKey(request.sessionId),
516
+ text: { verbosity: "low" }
517
+ };
518
+ const reasoning = thinkingToReasoning(request.thinking);
519
+ if (reasoning) body.reasoning = reasoning;
520
+ if (request.serviceTierId) body.service_tier = request.serviceTierId;
521
+ return body;
522
+ }
523
+ async function* mapCodexResponseEvents(events) {
524
+ const state = {
525
+ currentReasoning: null,
526
+ currentFunctionCall: null,
527
+ functionArguments: /* @__PURE__ */ new Map(),
528
+ reasoningDeltaSeen: false,
529
+ textDeltaSeen: false
530
+ };
531
+ for await (const event of events) yield* mapCodexResponseEvent(event, state);
532
+ }
533
+ function* mapCodexResponseEvent(event, state = newStreamState()) {
534
+ switch (event.type) {
535
+ case "response.output_item.added": {
536
+ const item = event.item;
537
+ if (isReasoningItem(item)) {
538
+ state.currentReasoning = item;
539
+ yield { type: "thinking_start" };
540
+ } else if (isFunctionCallItem(item)) {
541
+ state.currentFunctionCall = item;
542
+ if (item.id) state.functionArguments.set(item.id, item.arguments ?? "");
543
+ }
544
+ return;
545
+ }
546
+ case "response.reasoning_summary_text.delta":
547
+ case "response.reasoning_text.delta":
548
+ if (typeof event.delta === "string") {
549
+ state.reasoningDeltaSeen = true;
550
+ yield {
551
+ type: "thinking_delta",
552
+ text: event.delta
553
+ };
554
+ }
555
+ return;
556
+ case "response.output_text.delta":
557
+ if (typeof event.delta === "string") {
558
+ state.textDeltaSeen = true;
559
+ yield {
560
+ type: "text_delta",
561
+ text: event.delta
562
+ };
563
+ }
564
+ return;
565
+ case "response.function_call_arguments.delta": {
566
+ const key = event.item_id ?? state.currentFunctionCall?.id;
567
+ if (key && typeof event.delta === "string") state.functionArguments.set(key, `${state.functionArguments.get(key) ?? ""}${event.delta}`);
568
+ return;
569
+ }
570
+ case "response.function_call_arguments.done": {
571
+ const key = event.item_id ?? state.currentFunctionCall?.id;
572
+ if (key && typeof event.arguments === "string") state.functionArguments.set(key, event.arguments);
573
+ return;
574
+ }
575
+ case "response.output_item.done": {
576
+ const item = event.item;
577
+ if (isReasoningItem(item)) {
578
+ if (!state.reasoningDeltaSeen) {
579
+ const text = reasoningText(item);
580
+ if (text) yield {
581
+ type: "thinking_delta",
582
+ text
583
+ };
584
+ }
585
+ yield {
586
+ type: "thinking_signature",
587
+ signature: JSON.stringify(item)
588
+ };
589
+ if (state.currentReasoning === item) state.currentReasoning = null;
590
+ state.reasoningDeltaSeen = false;
591
+ } else if (isMessageItem(item)) {
592
+ if (!state.textDeltaSeen) {
593
+ const text = messageText(item);
594
+ if (text) yield {
595
+ type: "text_delta",
596
+ text
597
+ };
598
+ }
599
+ state.textDeltaSeen = false;
600
+ } else if (isFunctionCallItem(item)) {
601
+ const itemId = item.id ?? event.item_id;
602
+ const callId = item.call_id ?? event.call_id;
603
+ if (itemId && callId && item.name) {
604
+ const rawArgs = state.functionArguments.get(itemId) ?? item.arguments ?? "{}";
605
+ yield {
606
+ type: "tool_call_requested",
607
+ toolUseId: `${callId}|${itemId}`,
608
+ toolName: item.name,
609
+ input: parseJsonOrString(rawArgs)
610
+ };
611
+ }
612
+ if (itemId) state.functionArguments.delete(itemId);
613
+ if (state.currentFunctionCall === item) state.currentFunctionCall = null;
614
+ }
615
+ return;
616
+ }
617
+ case "response.completed":
618
+ yield {
619
+ type: "response",
620
+ usage: usageFromResponse(event.response)
621
+ };
622
+ return;
623
+ case "response.failed":
624
+ yield errorEventFromFailedResponse(event.response);
625
+ return;
626
+ case "response.incomplete":
627
+ yield {
628
+ type: "error",
629
+ message: `Incomplete response returned, reason: ${incompleteReason(event.response)}`,
630
+ code: incompleteReason(event.response) === "max_output_tokens" ? "context_length_exceeded" : "incomplete"
631
+ };
632
+ return;
633
+ case "error": {
634
+ const nested = isRecord(event.error) ? event.error : null;
635
+ yield {
636
+ type: "error",
637
+ message: event.message ?? stringOrNull(nested?.message) ?? "Codex stream error",
638
+ code: event.code ?? stringOrNull(nested?.code) ?? stringOrNull(nested?.type) ?? null
639
+ };
640
+ return;
641
+ }
642
+ }
643
+ }
644
+ function splitCodexToolUseId(toolUseId) {
645
+ const [callId, itemId] = toolUseId.split("|", 2);
646
+ return {
647
+ callId,
648
+ itemId
649
+ };
650
+ }
651
+ function usageFromResponse(response) {
652
+ const usage = isRecord(response) && isRecord(response.usage) ? response.usage : {};
653
+ const inputTokens = numberOrZero(usage.input_tokens);
654
+ const cachedTokens = isRecord(usage.input_tokens_details) ? numberOrZero(usage.input_tokens_details.cached_tokens) : 0;
655
+ return {
656
+ inputTokens: Math.max(0, inputTokens - cachedTokens),
657
+ outputTokens: numberOrZero(usage.output_tokens),
658
+ cacheReadTokens: cachedTokens,
659
+ cacheWriteTokens: 0
660
+ };
661
+ }
662
+ function inferenceItemToResponsesInput(item, index) {
663
+ switch (item.type) {
664
+ case "user_message":
665
+ case "user_steer": return [{
666
+ role: "user",
667
+ content: userContentToResponses(item.content)
668
+ }];
669
+ case "assistant_text": return [{
670
+ type: "message",
671
+ role: "assistant",
672
+ content: [{
673
+ type: "output_text",
674
+ text: item.text,
675
+ annotations: []
676
+ }],
677
+ id: `msg_${shortHash(`${index}:${item.modelId}:${item.text}`)}`,
678
+ status: "completed"
679
+ }];
680
+ case "assistant_thinking": {
681
+ const reasoning = parseReasoningSignature(item.signature);
682
+ return reasoning ? [reasoning] : [];
683
+ }
684
+ case "assistant_redacted_thinking": return [];
685
+ case "tool_use": {
686
+ const { callId, itemId } = splitCodexToolUseId(item.toolUseId);
687
+ return [{
688
+ type: "function_call",
689
+ id: itemId,
690
+ call_id: callId,
691
+ name: item.toolName,
692
+ arguments: stringifyArguments(item.input)
693
+ }];
694
+ }
695
+ case "tool_result": {
696
+ const { callId } = splitCodexToolUseId(item.toolUseId);
697
+ return [{
698
+ type: "function_call_output",
699
+ call_id: callId,
700
+ output: toolResultToResponsesOutput(item.output)
701
+ }];
702
+ }
703
+ }
704
+ }
705
+ function userContentToResponses(content) {
706
+ return content.flatMap((block) => {
707
+ if (block.type === "text") return [{
708
+ type: "input_text",
709
+ text: block.text
710
+ }];
711
+ if (block.type === "reference") return [{
712
+ type: "input_text",
713
+ text: block.reference
714
+ }];
715
+ if (block.type === "document") return [{
716
+ type: "input_text",
717
+ text: `[document:${block.source.fileName} ${block.source.mediaType}]`
718
+ }];
719
+ if (block.source.type === "url") return [{
720
+ type: "input_image",
721
+ image_url: block.source.url,
722
+ detail: "auto"
723
+ }];
724
+ const base64 = Buffer.from(block.source.data.buffer, block.source.data.byteOffset, block.source.data.byteLength).toString("base64");
725
+ return [{
726
+ type: "input_image",
727
+ image_url: `data:${block.source.mediaType};base64,${base64}`,
728
+ detail: "auto"
729
+ }];
730
+ });
731
+ }
732
+ function toolResultToResponsesOutput(output) {
733
+ const images = output.filter((block) => block.type === "image");
734
+ const text = output.filter((block) => block.type === "text").map((block) => block.text).join("\n");
735
+ if (images.length === 0) return text;
736
+ const content = [];
737
+ if (text) content.push({
738
+ type: "input_text",
739
+ text
740
+ });
741
+ for (const image of images) content.push({
742
+ type: "input_image",
743
+ image_url: `data:${image.source.mediaType};base64,${image.source.data}`,
744
+ detail: "auto"
745
+ });
746
+ return content;
747
+ }
748
+ function toolToResponsesTool(tool) {
749
+ return {
750
+ type: "function",
751
+ name: tool.name,
752
+ description: tool.description,
753
+ parameters: tool.inputSchema,
754
+ strict: null
755
+ };
756
+ }
757
+ function thinkingToReasoning(thinking) {
758
+ if (!thinking || thinking.type === "disabled" || thinking.type === "budget") return void 0;
759
+ if (thinking.type === "adaptive") return {
760
+ effort: thinking.effort,
761
+ summary: "auto"
762
+ };
763
+ if (thinking.effort === "none") return { effort: "none" };
764
+ return {
765
+ effort: thinking.effort,
766
+ summary: thinking.summary && thinking.summary !== "off" ? thinking.summary : "auto"
767
+ };
768
+ }
769
+ function parseReasoningSignature(signature) {
770
+ if (!signature) return null;
771
+ try {
772
+ const parsed = JSON.parse(signature);
773
+ return isReasoningItem(parsed) ? parsed : null;
774
+ } catch {
775
+ return null;
776
+ }
777
+ }
778
+ function stringifyArguments(input) {
779
+ return typeof input === "string" ? input : JSON.stringify(input ?? {});
780
+ }
781
+ function messageText(item) {
782
+ return item.content?.map((part) => part.type === "output_text" ? part.text : part.type === "refusal" ? part.refusal : "").join("") ?? "";
783
+ }
784
+ function reasoningText(item) {
785
+ const summary = item.summary?.map((part) => part.text).join("\n\n") ?? "";
786
+ const content = item.content?.map((part) => part.text).join("\n\n") ?? "";
787
+ return summary || content;
788
+ }
789
+ function errorEventFromFailedResponse(response) {
790
+ const error = isRecord(response) && isRecord(response.error) ? response.error : null;
791
+ const message = stringOrNull(error?.message) ?? "Codex response failed";
792
+ return {
793
+ type: "error",
794
+ message,
795
+ code: normalizeErrorCode(stringOrNull(error?.code) ?? stringOrNull(error?.type), message)
796
+ };
797
+ }
798
+ function normalizeErrorCode(code, message) {
799
+ const value = `${code ?? ""} ${message}`.toLowerCase();
800
+ if (/context|maximum context|too long|max.*token/.test(value)) return "context_length_exceeded";
801
+ if (/rate|quota|usage|limit|billing|balance/.test(value)) return "rate_limit";
802
+ if (/unauth|auth|expired|invalid.*token/.test(value)) return "auth_expired";
803
+ if (/overload|unavailable|timeout/.test(value)) return "overloaded";
804
+ return code;
805
+ }
806
+ function incompleteReason(response) {
807
+ if (isRecord(response) && isRecord(response.incomplete_details) && typeof response.incomplete_details.reason === "string") return response.incomplete_details.reason;
808
+ return "unknown";
809
+ }
810
+ function newStreamState() {
811
+ return {
812
+ currentReasoning: null,
813
+ currentFunctionCall: null,
814
+ functionArguments: /* @__PURE__ */ new Map(),
815
+ reasoningDeltaSeen: false,
816
+ textDeltaSeen: false
817
+ };
818
+ }
819
+ function isReasoningItem(item) {
820
+ return isRecord(item) && item.type === "reasoning";
821
+ }
822
+ function isMessageItem(item) {
823
+ return isRecord(item) && item.type === "message";
824
+ }
825
+ function isFunctionCallItem(item) {
826
+ return isRecord(item) && item.type === "function_call";
827
+ }
828
+ //#endregion
829
+ //#region src/sse.ts
830
+ async function* parseSseResponseStream(body) {
831
+ const reader = body.getReader();
832
+ const decoder = new TextDecoder();
833
+ let buffer = "";
834
+ try {
835
+ while (true) {
836
+ const { done, value } = await reader.read();
837
+ if (done) break;
838
+ buffer += decoder.decode(value, { stream: true });
839
+ const chunks = buffer.split(/\r?\n\r?\n/);
840
+ buffer = chunks.pop() ?? "";
841
+ for (const chunk of chunks) {
842
+ const event = parseSseChunk(chunk);
843
+ if (event) yield event;
844
+ }
845
+ }
846
+ buffer += decoder.decode();
847
+ const final = parseSseChunk(buffer);
848
+ if (final) yield final;
849
+ } finally {
850
+ reader.releaseLock();
851
+ }
852
+ }
853
+ function parseSseChunk(chunk) {
854
+ const data = chunk.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n").trim();
855
+ if (!data || data === "[DONE]") return null;
856
+ return JSON.parse(data);
857
+ }
858
+ //#endregion
859
+ //#region src/transport.ts
860
+ var FetchCodexResponsesTransport = class {
861
+ fetchImpl;
862
+ constructor(options = {}) {
863
+ this.fetchImpl = options.fetch ?? fetch;
864
+ }
865
+ async *stream(request) {
866
+ const headerTimeout = createAbortTimeout(request.headerTimeoutMs, "Codex SSE response headers timed out");
867
+ const combined = combineAbortSignals([request.signal, headerTimeout.signal]);
868
+ let response;
869
+ try {
870
+ response = await this.fetchImpl(request.url, {
871
+ method: "POST",
872
+ headers: request.headers,
873
+ body: JSON.stringify(request.body),
874
+ signal: combined.signal
875
+ });
876
+ } finally {
877
+ headerTimeout.clear();
878
+ combined.cleanup();
879
+ }
880
+ if (!response.ok) throw new CodexHttpError(response.status, response.statusText, await response.text());
881
+ if (!response.body) throw new Error("Codex response did not include a body");
882
+ yield* parseSseResponseStream(response.body);
883
+ }
884
+ };
885
+ var WebSocketCodexResponsesTransport = class {
886
+ WebSocketCtor;
887
+ constructor(options = {}) {
888
+ this.WebSocketCtor = options.WebSocket === void 0 ? defaultWebSocketConstructor() : options.WebSocket;
889
+ }
890
+ async *stream(request) {
891
+ const socket = await connectWebSocket(this.WebSocketCtor, request.websocketUrl, request.headers, request.signal, request.websocketConnectTimeoutMs);
892
+ const queue = [];
893
+ const waiters = [];
894
+ let idleTimer = null;
895
+ let finished = false;
896
+ const wake = () => {
897
+ for (const waiter of waiters.splice(0)) waiter();
898
+ };
899
+ const push = (value) => {
900
+ queue.push(value);
901
+ wake();
902
+ };
903
+ const finish = () => {
904
+ if (finished) return;
905
+ finished = true;
906
+ push(null);
907
+ };
908
+ const armIdleTimer = () => {
909
+ if (!request.streamIdleTimeoutMs) return;
910
+ if (idleTimer) clearTimeout(idleTimer);
911
+ idleTimer = setTimeout(() => {
912
+ push(/* @__PURE__ */ new Error(`Codex WebSocket stream idled for ${request.streamIdleTimeoutMs}ms`));
913
+ socket.close(1e3, "idle_timeout");
914
+ }, request.streamIdleTimeoutMs);
915
+ };
916
+ const cleanup = () => {
917
+ if (idleTimer) clearTimeout(idleTimer);
918
+ socket.removeEventListener("message", onMessage);
919
+ socket.removeEventListener("error", onError);
920
+ socket.removeEventListener("close", onClose);
921
+ request.signal.removeEventListener("abort", onAbort);
922
+ };
923
+ const onMessage = (event) => {
924
+ armIdleTimer();
925
+ try {
926
+ const parsed = parseWebSocketMessage(event.data);
927
+ if (parsed) {
928
+ push(parsed);
929
+ if (isTerminalResponseEvent(parsed)) {
930
+ finish();
931
+ socket.close(1e3, "response_done");
932
+ }
933
+ }
934
+ } catch (error) {
935
+ push(error instanceof Error ? error : new Error(String(error)));
936
+ }
937
+ };
938
+ const onError = () => push(/* @__PURE__ */ new Error("Codex WebSocket error"));
939
+ const onClose = () => finish();
940
+ const onAbort = () => {
941
+ socket.close(1e3, "aborted");
942
+ push(new DOMException("Aborted", "AbortError"));
943
+ };
944
+ socket.addEventListener("message", onMessage);
945
+ socket.addEventListener("error", onError);
946
+ socket.addEventListener("close", onClose);
947
+ request.signal.addEventListener("abort", onAbort, { once: true });
948
+ try {
949
+ socket.send(JSON.stringify({
950
+ type: "response.create",
951
+ ...request.body
952
+ }));
953
+ armIdleTimer();
954
+ while (true) {
955
+ if (queue.length === 0) await new Promise((resolve) => waiters.push(resolve));
956
+ const next = queue.shift();
957
+ if (next === void 0) continue;
958
+ if (next === null) return;
959
+ if (next instanceof Error) throw next;
960
+ yield next;
961
+ }
962
+ } finally {
963
+ cleanup();
964
+ socket.close(1e3, "done");
965
+ }
966
+ }
967
+ };
968
+ var AutoCodexResponsesTransport = class {
969
+ websocket;
970
+ sse;
971
+ constructor(websocket, sse) {
972
+ this.websocket = websocket;
973
+ this.sse = sse;
974
+ }
975
+ async *stream(request) {
976
+ let started = false;
977
+ try {
978
+ for await (const event of this.websocket.stream(request)) {
979
+ started = true;
980
+ yield event;
981
+ }
982
+ return;
983
+ } catch (error) {
984
+ if (started) throw error;
985
+ }
986
+ yield* this.sse.stream(request);
987
+ }
988
+ };
989
+ var CodexHttpError = class extends Error {
990
+ status;
991
+ responseText;
992
+ constructor(status, statusText, responseText) {
993
+ super(codexHttpErrorMessage(status, statusText, responseText));
994
+ this.status = status;
995
+ this.responseText = responseText;
996
+ this.name = "CodexHttpError";
997
+ }
998
+ };
999
+ function codexResponsesUrl(baseUrl) {
1000
+ const normalized = baseUrl.replace(/\/+$/, "");
1001
+ if (normalized.endsWith("/responses")) return normalized;
1002
+ if (normalized.endsWith("/codex")) return `${normalized}/responses`;
1003
+ return `${normalized}/codex/responses`;
1004
+ }
1005
+ function codexWebSocketUrl(responsesUrl) {
1006
+ return responsesUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
1007
+ }
1008
+ function createCodexTransport(mode, options = {}) {
1009
+ const sse = new FetchCodexResponsesTransport(options);
1010
+ if (mode === "sse") return sse;
1011
+ const websocket = new WebSocketCodexResponsesTransport(options);
1012
+ if (mode === "websocket") return websocket;
1013
+ return new AutoCodexResponsesTransport(websocket, sse);
1014
+ }
1015
+ function createAbortTimeout(ms, message) {
1016
+ const controller = new AbortController();
1017
+ if (!ms || ms <= 0) return {
1018
+ signal: controller.signal,
1019
+ clear: () => void 0
1020
+ };
1021
+ const timeout = setTimeout(() => controller.abort(/* @__PURE__ */ new Error(`${message} after ${ms}ms`)), ms);
1022
+ return {
1023
+ signal: controller.signal,
1024
+ clear: () => clearTimeout(timeout)
1025
+ };
1026
+ }
1027
+ function combineAbortSignals(signals) {
1028
+ const controller = new AbortController();
1029
+ const listeners = [];
1030
+ for (const signal of signals) {
1031
+ const listener = () => controller.abort(signal.reason);
1032
+ listeners.push(() => signal.removeEventListener("abort", listener));
1033
+ if (signal.aborted) controller.abort(signal.reason);
1034
+ else signal.addEventListener("abort", listener, { once: true });
1035
+ }
1036
+ return {
1037
+ signal: controller.signal,
1038
+ cleanup: () => {
1039
+ for (const cleanup of listeners) cleanup();
1040
+ }
1041
+ };
1042
+ }
1043
+ function connectWebSocket(WebSocketCtor, url, headers, signal, timeoutMs = 1e4) {
1044
+ if (!WebSocketCtor) return Promise.reject(/* @__PURE__ */ new Error("WebSocket transport is not available in this runtime"));
1045
+ const headerRecord = headersToRecord(headers);
1046
+ delete headerRecord.accept;
1047
+ delete headerRecord["content-type"];
1048
+ headerRecord["openai-beta"] = "responses_websockets=2026-02-06";
1049
+ return new Promise((resolve, reject) => {
1050
+ let socket;
1051
+ let settled = false;
1052
+ const timeout = setTimeout(() => fail(/* @__PURE__ */ new Error(`Codex WebSocket connect timed out after ${timeoutMs}ms`)), timeoutMs);
1053
+ const cleanup = () => {
1054
+ clearTimeout(timeout);
1055
+ socket?.removeEventListener("open", onOpen);
1056
+ socket?.removeEventListener("error", onError);
1057
+ socket?.removeEventListener("close", onClose);
1058
+ signal.removeEventListener("abort", onAbort);
1059
+ };
1060
+ const fail = (error) => {
1061
+ if (settled) return;
1062
+ settled = true;
1063
+ cleanup();
1064
+ try {
1065
+ socket?.close(1e3, "connect_failed");
1066
+ } catch {}
1067
+ reject(error);
1068
+ };
1069
+ const onOpen = () => {
1070
+ if (settled) return;
1071
+ settled = true;
1072
+ cleanup();
1073
+ resolve(socket);
1074
+ };
1075
+ const onError = () => fail(/* @__PURE__ */ new Error("Codex WebSocket connect error"));
1076
+ const onClose = () => fail(/* @__PURE__ */ new Error("Codex WebSocket closed before opening"));
1077
+ const onAbort = () => fail(new DOMException("Aborted", "AbortError"));
1078
+ try {
1079
+ socket = new WebSocketCtor(url, { headers: headerRecord });
1080
+ } catch (error) {
1081
+ clearTimeout(timeout);
1082
+ reject(error instanceof Error ? error : new Error(String(error)));
1083
+ return;
1084
+ }
1085
+ socket.addEventListener("open", onOpen);
1086
+ socket.addEventListener("error", onError);
1087
+ socket.addEventListener("close", onClose);
1088
+ signal.addEventListener("abort", onAbort, { once: true });
1089
+ });
1090
+ }
1091
+ function parseWebSocketMessage(data) {
1092
+ if (typeof data === "string") return parseWebSocketJson(data);
1093
+ if (data instanceof ArrayBuffer) return parseWebSocketJson(new TextDecoder().decode(data));
1094
+ if (data instanceof Uint8Array) return parseWebSocketJson(new TextDecoder().decode(data));
1095
+ return null;
1096
+ }
1097
+ function parseWebSocketJson(text) {
1098
+ const parsed = JSON.parse(text);
1099
+ if (parsed.type === "response.done") return {
1100
+ type: "response.completed",
1101
+ response: parsed.response
1102
+ };
1103
+ if ("event" in parsed && isRecord(parsed.event)) return parsed.event;
1104
+ return parsed;
1105
+ }
1106
+ function isTerminalResponseEvent(event) {
1107
+ return event.type === "response.completed" || event.type === "response.failed" || event.type === "response.incomplete" || event.type === "error";
1108
+ }
1109
+ function headersToRecord(headers) {
1110
+ const out = {};
1111
+ headers.forEach((value, key) => {
1112
+ out[key] = value;
1113
+ });
1114
+ return out;
1115
+ }
1116
+ function codexHttpErrorMessage(status, statusText, responseText) {
1117
+ const body = parseJsonObject(responseText);
1118
+ const error = isRecord(body?.error) ? body.error : null;
1119
+ return typeof error?.message === "string" ? error.message : responseText || statusText || `HTTP ${status}`;
1120
+ }
1121
+ function defaultWebSocketConstructor() {
1122
+ return typeof globalThis.WebSocket === "function" ? globalThis.WebSocket : null;
1123
+ }
1124
+ //#endregion
1125
+ //#region src/provider.ts
1126
+ const DEFAULT_CHATGPT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
1127
+ const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
1128
+ const DEFAULT_MAX_RETRIES = 2;
1129
+ const DEFAULT_RETRY_BASE_DELAY_MS = 250;
1130
+ const DEFAULT_SSE_HEADER_TIMEOUT_MS = 2e4;
1131
+ const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 1e4;
1132
+ var CodexProvider = class {
1133
+ config;
1134
+ authStore;
1135
+ transport;
1136
+ constructor(options = {}) {
1137
+ this.config = {
1138
+ codexHome: options.codexHome,
1139
+ baseUrl: options.baseUrl,
1140
+ transport: options.transport ?? "auto",
1141
+ headers: options.headers,
1142
+ userAgent: options.userAgent ?? defaultUserAgent(),
1143
+ maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
1144
+ retryBaseDelayMs: options.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS,
1145
+ headerTimeoutMs: options.headerTimeoutMs ?? DEFAULT_SSE_HEADER_TIMEOUT_MS,
1146
+ websocketConnectTimeoutMs: options.websocketConnectTimeoutMs ?? DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS,
1147
+ streamIdleTimeoutMs: options.streamIdleTimeoutMs ?? 0,
1148
+ clientVersion: options.clientVersion
1149
+ };
1150
+ this.authStore = options.authStore ?? new FileCodexAuthStore({ codexHome: options.codexHome });
1151
+ this.transport = options.transportImpl ?? createCodexTransport(this.config.transport);
1152
+ }
1153
+ async *run(request) {
1154
+ const body = buildCodexResponsesRequestBody(request);
1155
+ let forceRefresh = false;
1156
+ let retryAttempt = 0;
1157
+ while (retryAttempt <= this.config.maxRetries) try {
1158
+ const auth = await this.authStore.resolveAuth({ forceRefresh });
1159
+ const url = responsesUrlForAuth(auth, this.config.baseUrl);
1160
+ const headers = buildCodexHeaders(auth, request, this.config);
1161
+ yield* mapCodexResponseEvents(this.transport.stream({
1162
+ url,
1163
+ websocketUrl: codexWebSocketUrl(url),
1164
+ headers,
1165
+ body,
1166
+ signal: request.cancel,
1167
+ headerTimeoutMs: this.config.headerTimeoutMs,
1168
+ websocketConnectTimeoutMs: this.config.websocketConnectTimeoutMs,
1169
+ streamIdleTimeoutMs: this.config.streamIdleTimeoutMs || void 0
1170
+ }));
1171
+ return;
1172
+ } catch (error) {
1173
+ if (request.cancel.aborted) {
1174
+ yield { type: "abort" };
1175
+ return;
1176
+ }
1177
+ if (error instanceof CodexHttpError && error.status === 401 && !forceRefresh) {
1178
+ forceRefresh = true;
1179
+ continue;
1180
+ }
1181
+ if (error instanceof CodexHttpError && isRetryableHttpStatus(error.status) && retryAttempt < this.config.maxRetries) {
1182
+ await sleep(this.config.retryBaseDelayMs * 2 ** retryAttempt, request.cancel);
1183
+ retryAttempt += 1;
1184
+ continue;
1185
+ }
1186
+ yield providerErrorFromUnknown(error);
1187
+ return;
1188
+ }
1189
+ }
1190
+ };
1191
+ function createCodexProvider(options = {}) {
1192
+ const id = options.id ?? "codex";
1193
+ const displayName = options.displayName ?? "Codex";
1194
+ const runtimeOptions = {
1195
+ codexHome: options.codexHome,
1196
+ baseUrl: options.baseUrl,
1197
+ transport: options.transport,
1198
+ headers: options.headers,
1199
+ userAgent: options.userAgent,
1200
+ maxRetries: options.maxRetries,
1201
+ retryBaseDelayMs: options.retryBaseDelayMs,
1202
+ headerTimeoutMs: options.headerTimeoutMs,
1203
+ websocketConnectTimeoutMs: options.websocketConnectTimeoutMs,
1204
+ streamIdleTimeoutMs: options.streamIdleTimeoutMs,
1205
+ clientVersion: options.clientVersion
1206
+ };
1207
+ return defineProvider({
1208
+ id,
1209
+ displayName,
1210
+ auth: { status: () => new FileCodexAuthStore({ codexHome: options.codexHome }).status() },
1211
+ state: () => ({
1212
+ status: "ready",
1213
+ message: "Uses official Codex auth storage"
1214
+ }),
1215
+ listModels: async () => {
1216
+ return applyModelPolicy(await listCodexModels(runtimeOptions), id, options.models);
1217
+ },
1218
+ createRuntime: () => new CodexProvider(runtimeOptions)
1219
+ });
1220
+ }
1221
+ function buildCodexHeaders(auth, request, config) {
1222
+ const headers = new Headers(config.headers);
1223
+ if (auth.kind === "agentIdentity") headers.set("Authorization", auth.authorization);
1224
+ else headers.set("Authorization", `Bearer ${auth.kind === "apiKey" ? auth.apiKey : auth.accessToken}`);
1225
+ if (auth.kind !== "apiKey" && auth.accountId) headers.set("ChatGPT-Account-ID", auth.accountId);
1226
+ if (auth.kind !== "apiKey" && "isFedrampAccount" in auth && auth.isFedrampAccount) headers.set("X-OpenAI-Fedramp", "true");
1227
+ headers.set("User-Agent", config.userAgent ?? defaultUserAgent());
1228
+ headers.set("OpenAI-Beta", "responses=experimental");
1229
+ headers.set("accept", "text/event-stream");
1230
+ headers.set("content-type", "application/json");
1231
+ const sessionId = clampPromptCacheKey(request.sessionId);
1232
+ headers.set("session-id", sessionId);
1233
+ headers.set("thread-id", sessionId);
1234
+ headers.set("x-client-request-id", request.requestId);
1235
+ return headers;
1236
+ }
1237
+ function responsesUrlForAuth(auth, baseUrl) {
1238
+ if (auth.kind === "apiKey") return openAiResponsesUrl(baseUrl ?? DEFAULT_OPENAI_BASE_URL);
1239
+ return codexResponsesUrl(baseUrl ?? DEFAULT_CHATGPT_CODEX_BASE_URL);
1240
+ }
1241
+ function openAiResponsesUrl(baseUrl) {
1242
+ const normalized = baseUrl.replace(/\/+$/, "");
1243
+ return normalized.endsWith("/responses") ? normalized : `${normalized}/responses`;
1244
+ }
1245
+ function providerErrorFromUnknown(error) {
1246
+ if (error instanceof CodexAuthError) return {
1247
+ type: "error",
1248
+ message: redactSecretText(error.message),
1249
+ code: error.code
1250
+ };
1251
+ if (error instanceof CodexHttpError) return {
1252
+ type: "error",
1253
+ message: redactSecretText(error.message),
1254
+ code: httpErrorCode(error.status, error.message)
1255
+ };
1256
+ return {
1257
+ type: "error",
1258
+ message: redactSecretText(error instanceof Error ? error.message : String(error)),
1259
+ code: null
1260
+ };
1261
+ }
1262
+ function isRetryableHttpStatus(status) {
1263
+ return status === 408 || status === 409 || status === 425 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
1264
+ }
1265
+ function defaultUserAgent() {
1266
+ return `demi-codex-provider/0.0.0 (${process.platform}; ${process.arch})`;
1267
+ }
1268
+ function sleep(ms, signal) {
1269
+ if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
1270
+ return new Promise((resolve, reject) => {
1271
+ const timeout = setTimeout(resolve, ms);
1272
+ signal.addEventListener("abort", () => {
1273
+ clearTimeout(timeout);
1274
+ reject(new DOMException("Aborted", "AbortError"));
1275
+ }, { once: true });
1276
+ });
1277
+ }
1278
+ //#endregion
1279
+ export { codexAuthStatus, createCodexProvider, listCodexModels };