codex-subscription-oauth-plugin 0.1.1
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/LICENSE +21 -0
- package/README.ja.md +41 -0
- package/README.md +41 -0
- package/THIRD_PARTY_NOTICES.md +55 -0
- package/cordis.patch.yml +14 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.js +1054 -0
- package/lib/models-DaqoXO5b.js +941 -0
- package/lib/openai-codex-responses-DPxdQtbt.js +2827 -0
- package/lib/openai-codex.js +574 -0
- package/lib/provider-env-DRzE1dvc.js +41 -0
- package/package.json +87 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1054 @@
|
|
|
1
|
+
import { c as lazyApi, i as createProvider, r as createModels } from "./models-DaqoXO5b.js";
|
|
2
|
+
import AuthorizationService, { AuthorizationDeclinedError } from "@deepseek-ai/dsh-authorization";
|
|
3
|
+
import { credentialKey, credentialKeyId, credentialKeyScope, isCredentialKeySegment } from "@deepseek-ai/dsh-credentials";
|
|
4
|
+
import { LlmError, resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { PiAiAdapter, recordKeyFor } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
6
|
+
//#region node_modules/@earendil-works/pi-ai/dist/auth/helpers.js
|
|
7
|
+
/**
|
|
8
|
+
* Wraps a dynamically imported `OAuthAuth` so provider definitions can
|
|
9
|
+
* advertise OAuth without importing the implementation. The flow loads on
|
|
10
|
+
* first `login`/`refresh`/`toAuth` call; callers keep Node-only flow code out
|
|
11
|
+
* of bundles by loading through a bundler-opaque dynamic import (variable
|
|
12
|
+
* specifier, see the bedrock lazy wrapper).
|
|
13
|
+
*/
|
|
14
|
+
function lazyOAuth(input) {
|
|
15
|
+
let promise;
|
|
16
|
+
const loaded = () => {
|
|
17
|
+
promise ??= input.load();
|
|
18
|
+
return promise;
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
name: input.name,
|
|
22
|
+
isSubscription: input.isSubscription,
|
|
23
|
+
loginLabel: input.loginLabel,
|
|
24
|
+
login: async (interaction) => (await loaded()).login(interaction),
|
|
25
|
+
refresh: async (credential, signal) => (await loaded()).refresh(credential, signal),
|
|
26
|
+
toAuth: async (credential) => (await loaded()).toAuth(credential)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.lazy.js
|
|
31
|
+
const openAICodexResponsesApi = () => lazyApi(() => import("./openai-codex-responses-DPxdQtbt.js"));
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region node_modules/@earendil-works/pi-ai/dist/auth/oauth/load.js
|
|
34
|
+
var __rewriteRelativeImportExtension = function(path, preserveJsx) {
|
|
35
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
|
|
36
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
|
|
37
|
+
});
|
|
38
|
+
return path;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Loads an OAuth flow module through a variable specifier so bundlers cannot
|
|
42
|
+
* follow the import into Node-only flow code (`node:http` callback servers,
|
|
43
|
+
* `node:crypto` PKCE). The `.ts`/`.js` rewrite keeps the trick working from
|
|
44
|
+
* both source and built output.
|
|
45
|
+
*/
|
|
46
|
+
const importOAuthModule = (specifier) => {
|
|
47
|
+
return import(__rewriteRelativeImportExtension(import.meta.url.endsWith(".js") ? specifier.replace(/\.ts$/, ".js") : specifier));
|
|
48
|
+
};
|
|
49
|
+
const loadOpenAICodexOAuth = async () => {
|
|
50
|
+
return (await importOAuthModule("./openai-codex.ts")).openaiCodexOAuth;
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region node_modules/@earendil-works/pi-ai/dist/providers/data/openai-codex.json
|
|
54
|
+
var openai_codex_default = { "openai-codex-responses": {
|
|
55
|
+
"gpt-5.3-codex-spark": {
|
|
56
|
+
"id": "gpt-5.3-codex-spark",
|
|
57
|
+
"name": "GPT-5.3 Codex Spark",
|
|
58
|
+
"api": "openai-codex-responses",
|
|
59
|
+
"provider": "openai-codex",
|
|
60
|
+
"baseUrl": "https://chatgpt.com/backend-api",
|
|
61
|
+
"reasoning": true,
|
|
62
|
+
"input": ["text"],
|
|
63
|
+
"cost": {
|
|
64
|
+
"input": 1.75,
|
|
65
|
+
"output": 14,
|
|
66
|
+
"cacheRead": .175,
|
|
67
|
+
"cacheWrite": 0
|
|
68
|
+
},
|
|
69
|
+
"contextWindow": 128e3,
|
|
70
|
+
"maxTokens": 128e3,
|
|
71
|
+
"thinkingLevelMap": {
|
|
72
|
+
"xhigh": "xhigh",
|
|
73
|
+
"minimal": "low"
|
|
74
|
+
},
|
|
75
|
+
"compat": { "supportsOpenAIGrammarTools": true }
|
|
76
|
+
},
|
|
77
|
+
"gpt-5.4": {
|
|
78
|
+
"id": "gpt-5.4",
|
|
79
|
+
"name": "GPT-5.4",
|
|
80
|
+
"api": "openai-codex-responses",
|
|
81
|
+
"provider": "openai-codex",
|
|
82
|
+
"baseUrl": "https://chatgpt.com/backend-api",
|
|
83
|
+
"reasoning": true,
|
|
84
|
+
"input": ["text", "image"],
|
|
85
|
+
"cost": {
|
|
86
|
+
"input": 2.5,
|
|
87
|
+
"output": 15,
|
|
88
|
+
"cacheRead": .25,
|
|
89
|
+
"cacheWrite": 0,
|
|
90
|
+
"tiers": [{
|
|
91
|
+
"inputTokensAbove": 272e3,
|
|
92
|
+
"input": 5,
|
|
93
|
+
"output": 22.5,
|
|
94
|
+
"cacheRead": .5,
|
|
95
|
+
"cacheWrite": 0
|
|
96
|
+
}]
|
|
97
|
+
},
|
|
98
|
+
"contextWindow": 272e3,
|
|
99
|
+
"maxTokens": 128e3,
|
|
100
|
+
"thinkingLevelMap": {
|
|
101
|
+
"xhigh": "xhigh",
|
|
102
|
+
"minimal": "low"
|
|
103
|
+
},
|
|
104
|
+
"compat": {
|
|
105
|
+
"supportsOpenAIGrammarTools": true,
|
|
106
|
+
"supportsToolSearch": true
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"gpt-5.4-mini": {
|
|
110
|
+
"id": "gpt-5.4-mini",
|
|
111
|
+
"name": "GPT-5.4 mini",
|
|
112
|
+
"api": "openai-codex-responses",
|
|
113
|
+
"provider": "openai-codex",
|
|
114
|
+
"baseUrl": "https://chatgpt.com/backend-api",
|
|
115
|
+
"reasoning": true,
|
|
116
|
+
"input": ["text", "image"],
|
|
117
|
+
"cost": {
|
|
118
|
+
"input": .75,
|
|
119
|
+
"output": 4.5,
|
|
120
|
+
"cacheRead": .075,
|
|
121
|
+
"cacheWrite": 0
|
|
122
|
+
},
|
|
123
|
+
"contextWindow": 272e3,
|
|
124
|
+
"maxTokens": 128e3,
|
|
125
|
+
"thinkingLevelMap": {
|
|
126
|
+
"xhigh": "xhigh",
|
|
127
|
+
"minimal": "low"
|
|
128
|
+
},
|
|
129
|
+
"compat": {
|
|
130
|
+
"supportsOpenAIGrammarTools": true,
|
|
131
|
+
"supportsToolSearch": true
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"gpt-5.5": {
|
|
135
|
+
"id": "gpt-5.5",
|
|
136
|
+
"name": "GPT-5.5",
|
|
137
|
+
"api": "openai-codex-responses",
|
|
138
|
+
"provider": "openai-codex",
|
|
139
|
+
"baseUrl": "https://chatgpt.com/backend-api",
|
|
140
|
+
"reasoning": true,
|
|
141
|
+
"input": ["text", "image"],
|
|
142
|
+
"cost": {
|
|
143
|
+
"input": 5,
|
|
144
|
+
"output": 30,
|
|
145
|
+
"cacheRead": .5,
|
|
146
|
+
"cacheWrite": 0,
|
|
147
|
+
"tiers": [{
|
|
148
|
+
"inputTokensAbove": 272e3,
|
|
149
|
+
"input": 10,
|
|
150
|
+
"output": 45,
|
|
151
|
+
"cacheRead": 1,
|
|
152
|
+
"cacheWrite": 0
|
|
153
|
+
}]
|
|
154
|
+
},
|
|
155
|
+
"contextWindow": 272e3,
|
|
156
|
+
"maxTokens": 128e3,
|
|
157
|
+
"thinkingLevelMap": {
|
|
158
|
+
"xhigh": "xhigh",
|
|
159
|
+
"minimal": "low"
|
|
160
|
+
},
|
|
161
|
+
"compat": {
|
|
162
|
+
"supportsOpenAIGrammarTools": true,
|
|
163
|
+
"supportsToolSearch": true
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"gpt-5.6-luna": {
|
|
167
|
+
"id": "gpt-5.6-luna",
|
|
168
|
+
"name": "GPT-5.6 Luna",
|
|
169
|
+
"api": "openai-codex-responses",
|
|
170
|
+
"provider": "openai-codex",
|
|
171
|
+
"baseUrl": "https://chatgpt.com/backend-api",
|
|
172
|
+
"reasoning": true,
|
|
173
|
+
"input": ["text", "image"],
|
|
174
|
+
"cost": {
|
|
175
|
+
"input": .2,
|
|
176
|
+
"output": 1.2,
|
|
177
|
+
"cacheRead": .02,
|
|
178
|
+
"cacheWrite": .25,
|
|
179
|
+
"tiers": [{
|
|
180
|
+
"inputTokensAbove": 272e3,
|
|
181
|
+
"input": .4,
|
|
182
|
+
"output": 1.8,
|
|
183
|
+
"cacheRead": .04,
|
|
184
|
+
"cacheWrite": .5
|
|
185
|
+
}]
|
|
186
|
+
},
|
|
187
|
+
"contextWindow": 272e3,
|
|
188
|
+
"maxTokens": 128e3,
|
|
189
|
+
"thinkingLevelMap": {
|
|
190
|
+
"xhigh": "xhigh",
|
|
191
|
+
"max": "max",
|
|
192
|
+
"minimal": "low"
|
|
193
|
+
},
|
|
194
|
+
"compat": {
|
|
195
|
+
"supportsOpenAIGrammarTools": true,
|
|
196
|
+
"supportsAdditionalTools": true,
|
|
197
|
+
"supportsToolSearch": true
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"gpt-5.6-sol": {
|
|
201
|
+
"id": "gpt-5.6-sol",
|
|
202
|
+
"name": "GPT-5.6 Sol",
|
|
203
|
+
"api": "openai-codex-responses",
|
|
204
|
+
"provider": "openai-codex",
|
|
205
|
+
"baseUrl": "https://chatgpt.com/backend-api",
|
|
206
|
+
"reasoning": true,
|
|
207
|
+
"input": ["text", "image"],
|
|
208
|
+
"cost": {
|
|
209
|
+
"input": 5,
|
|
210
|
+
"output": 30,
|
|
211
|
+
"cacheRead": .5,
|
|
212
|
+
"cacheWrite": 6.25,
|
|
213
|
+
"tiers": [{
|
|
214
|
+
"inputTokensAbove": 272e3,
|
|
215
|
+
"input": 10,
|
|
216
|
+
"output": 45,
|
|
217
|
+
"cacheRead": 1,
|
|
218
|
+
"cacheWrite": 12.5
|
|
219
|
+
}]
|
|
220
|
+
},
|
|
221
|
+
"contextWindow": 272e3,
|
|
222
|
+
"maxTokens": 128e3,
|
|
223
|
+
"thinkingLevelMap": {
|
|
224
|
+
"xhigh": "xhigh",
|
|
225
|
+
"max": "max",
|
|
226
|
+
"minimal": "low"
|
|
227
|
+
},
|
|
228
|
+
"compat": {
|
|
229
|
+
"supportsOpenAIGrammarTools": true,
|
|
230
|
+
"supportsAdditionalTools": true,
|
|
231
|
+
"supportsToolSearch": true
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
"gpt-5.6-terra": {
|
|
235
|
+
"id": "gpt-5.6-terra",
|
|
236
|
+
"name": "GPT-5.6 Terra",
|
|
237
|
+
"api": "openai-codex-responses",
|
|
238
|
+
"provider": "openai-codex",
|
|
239
|
+
"baseUrl": "https://chatgpt.com/backend-api",
|
|
240
|
+
"reasoning": true,
|
|
241
|
+
"input": ["text", "image"],
|
|
242
|
+
"cost": {
|
|
243
|
+
"input": 2,
|
|
244
|
+
"output": 12,
|
|
245
|
+
"cacheRead": .2,
|
|
246
|
+
"cacheWrite": 2.5,
|
|
247
|
+
"tiers": [{
|
|
248
|
+
"inputTokensAbove": 272e3,
|
|
249
|
+
"input": 4,
|
|
250
|
+
"output": 18,
|
|
251
|
+
"cacheRead": .4,
|
|
252
|
+
"cacheWrite": 5
|
|
253
|
+
}]
|
|
254
|
+
},
|
|
255
|
+
"contextWindow": 272e3,
|
|
256
|
+
"maxTokens": 128e3,
|
|
257
|
+
"thinkingLevelMap": {
|
|
258
|
+
"xhigh": "xhigh",
|
|
259
|
+
"max": "max",
|
|
260
|
+
"minimal": "low"
|
|
261
|
+
},
|
|
262
|
+
"compat": {
|
|
263
|
+
"supportsOpenAIGrammarTools": true,
|
|
264
|
+
"supportsAdditionalTools": true,
|
|
265
|
+
"supportsToolSearch": true
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
"gpt-6-astra": {
|
|
269
|
+
"id": "gpt-6-astra",
|
|
270
|
+
"name": "GPT-6 Astra",
|
|
271
|
+
"api": "openai-codex-responses",
|
|
272
|
+
"provider": "openai-codex",
|
|
273
|
+
"baseUrl": "https://chatgpt.com/backend-api",
|
|
274
|
+
"reasoning": true,
|
|
275
|
+
"input": ["text", "image"],
|
|
276
|
+
"cost": {
|
|
277
|
+
"input": 10,
|
|
278
|
+
"output": 50,
|
|
279
|
+
"cacheRead": 1,
|
|
280
|
+
"cacheWrite": 12.5,
|
|
281
|
+
"tiers": [{
|
|
282
|
+
"inputTokensAbove": 272e3,
|
|
283
|
+
"input": 20,
|
|
284
|
+
"output": 75,
|
|
285
|
+
"cacheRead": 2,
|
|
286
|
+
"cacheWrite": 25
|
|
287
|
+
}]
|
|
288
|
+
},
|
|
289
|
+
"contextWindow": 272e3,
|
|
290
|
+
"maxTokens": 128e3,
|
|
291
|
+
"thinkingLevelMap": {
|
|
292
|
+
"off": null,
|
|
293
|
+
"minimal": "low",
|
|
294
|
+
"low": "low",
|
|
295
|
+
"medium": "medium",
|
|
296
|
+
"high": "high",
|
|
297
|
+
"xhigh": "xhigh",
|
|
298
|
+
"max": "max"
|
|
299
|
+
},
|
|
300
|
+
"compat": {
|
|
301
|
+
"supportsOpenAIGrammarTools": true,
|
|
302
|
+
"supportsAdditionalTools": true,
|
|
303
|
+
"supportsToolSearch": true
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} };
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region node_modules/@earendil-works/pi-ai/dist/model-catalog.js
|
|
309
|
+
function flattenModelCatalog(_provider, groups) {
|
|
310
|
+
return Object.assign({}, ...Object.values(groups));
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.js
|
|
314
|
+
const OPENAI_CODEX_MODELS = flattenModelCatalog("openai-codex", openai_codex_default);
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.js
|
|
317
|
+
function openaiCodexProvider() {
|
|
318
|
+
return createProvider({
|
|
319
|
+
id: "openai-codex",
|
|
320
|
+
name: "OpenAI Codex",
|
|
321
|
+
baseUrl: "https://chatgpt.com/backend-api",
|
|
322
|
+
auth: { oauth: lazyOAuth({
|
|
323
|
+
name: "OpenAI (ChatGPT Plus/Pro)",
|
|
324
|
+
isSubscription: true,
|
|
325
|
+
load: loadOpenAICodexOAuth
|
|
326
|
+
}) },
|
|
327
|
+
models: Object.values(OPENAI_CODEX_MODELS),
|
|
328
|
+
api: openAICodexResponsesApi()
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/adapter.ts
|
|
333
|
+
/** Catalog-aware wrapper around DSH's public pi-ai adapter. */
|
|
334
|
+
const ROUTE$1 = "openai-codex";
|
|
335
|
+
/** Revalidate at picker time and force one refresh before rejecting an id. */
|
|
336
|
+
var LiveCodexAdapter = class extends PiAiAdapter {
|
|
337
|
+
options;
|
|
338
|
+
constructor(options) {
|
|
339
|
+
super(options);
|
|
340
|
+
this.options = options;
|
|
341
|
+
}
|
|
342
|
+
assertRoute(provider) {
|
|
343
|
+
if (provider !== ROUTE$1) throw new LlmError(`codex-subscription-oauth: adapter does not own provider "${provider}"`, "NO_ADAPTER");
|
|
344
|
+
}
|
|
345
|
+
async ensureModel(provider, model) {
|
|
346
|
+
this.assertRoute(provider);
|
|
347
|
+
await this.options.catalog.ensureInitial(this.options.initialWaitMs);
|
|
348
|
+
this.options.catalog.revalidateIfNeeded();
|
|
349
|
+
if (this.options.catalog.current.models.some((candidate) => candidate.id === model)) return;
|
|
350
|
+
await this.options.catalog.forceRefresh();
|
|
351
|
+
if (!this.options.catalog.current.models.some((candidate) => candidate.id === model)) throw new LlmError(`codex-subscription-oauth: model "${model}" is not in the current executable Codex catalog`, "UNKNOWN_MODEL");
|
|
352
|
+
}
|
|
353
|
+
async listModels(provider) {
|
|
354
|
+
this.assertRoute(provider);
|
|
355
|
+
await this.options.catalog.ensureInitial(this.options.initialWaitMs);
|
|
356
|
+
this.options.catalog.revalidateIfNeeded();
|
|
357
|
+
return super.listModels(provider);
|
|
358
|
+
}
|
|
359
|
+
async resolveModel(provider, model, signal) {
|
|
360
|
+
await this.ensureModel(provider, model);
|
|
361
|
+
return super.resolveModel(provider, model, signal);
|
|
362
|
+
}
|
|
363
|
+
async prepareCall(provider, model, signal) {
|
|
364
|
+
await this.ensureModel(provider, model);
|
|
365
|
+
return super.prepareCall(provider, model, signal);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/catalog.ts
|
|
370
|
+
const MODEL_LIST_URL = "https://chatgpt.com/backend-api/codex/models?client_version=99.99.99";
|
|
371
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
372
|
+
const MAX_MODEL_ID_CHARS = 200;
|
|
373
|
+
const JWT_CLAIM = "https://api.openai.com/auth";
|
|
374
|
+
const JITTER_RATIO = .1;
|
|
375
|
+
/**
|
|
376
|
+
* Own one live account-scoped list. The official endpoint decides visibility
|
|
377
|
+
* and order; the installed pi-ai catalog decides whether DSH can execute it.
|
|
378
|
+
*/
|
|
379
|
+
var CodexCatalog = class {
|
|
380
|
+
baseline;
|
|
381
|
+
baselineById;
|
|
382
|
+
resolveCredential;
|
|
383
|
+
refreshIntervalMs;
|
|
384
|
+
revalidateAfterMs;
|
|
385
|
+
timeoutMs;
|
|
386
|
+
fetchImpl;
|
|
387
|
+
now;
|
|
388
|
+
random;
|
|
389
|
+
onChange;
|
|
390
|
+
warn;
|
|
391
|
+
lifecycle = new AbortController();
|
|
392
|
+
inFlight;
|
|
393
|
+
initial;
|
|
394
|
+
timer;
|
|
395
|
+
etag;
|
|
396
|
+
accountId;
|
|
397
|
+
lastCheckedAt;
|
|
398
|
+
disposed = false;
|
|
399
|
+
revision = 0;
|
|
400
|
+
snapshotValue;
|
|
401
|
+
constructor(options) {
|
|
402
|
+
this.baseline = Object.freeze([...options.baseline]);
|
|
403
|
+
this.baselineById = new Map(this.baseline.map((model) => [model.id, model]));
|
|
404
|
+
this.resolveCredential = options.resolveCredential;
|
|
405
|
+
this.refreshIntervalMs = options.refreshIntervalMs;
|
|
406
|
+
this.revalidateAfterMs = options.revalidateAfterMs;
|
|
407
|
+
this.timeoutMs = options.timeoutMs;
|
|
408
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
409
|
+
this.now = options.now ?? (() => Date.now());
|
|
410
|
+
this.random = options.random ?? Math.random;
|
|
411
|
+
this.onChange = options.onChange ?? (() => {});
|
|
412
|
+
this.warn = options.warn ?? (() => {});
|
|
413
|
+
this.snapshotValue = Object.freeze({
|
|
414
|
+
source: "bundled",
|
|
415
|
+
models: this.baseline,
|
|
416
|
+
revision: 0
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
get current() {
|
|
420
|
+
return this.snapshotValue;
|
|
421
|
+
}
|
|
422
|
+
/** Start one initial refresh followed by jittered periodic revalidation. */
|
|
423
|
+
start() {
|
|
424
|
+
if (this.initial !== void 0) return this.initial;
|
|
425
|
+
this.initial = this.refresh().finally(() => {
|
|
426
|
+
if (!this.disposed) this.schedule();
|
|
427
|
+
});
|
|
428
|
+
return this.initial;
|
|
429
|
+
}
|
|
430
|
+
/** Stop timers and every in-flight request; late completions cannot publish. */
|
|
431
|
+
stop() {
|
|
432
|
+
if (this.disposed) return;
|
|
433
|
+
this.disposed = true;
|
|
434
|
+
this.lifecycle.abort();
|
|
435
|
+
if (this.timer !== void 0) clearTimeout(this.timer);
|
|
436
|
+
this.timer = void 0;
|
|
437
|
+
}
|
|
438
|
+
/** Wait only up to the caller's latency budget for the initial refresh. */
|
|
439
|
+
async ensureInitial(waitMs) {
|
|
440
|
+
const initial = this.initial ?? this.start();
|
|
441
|
+
if (waitMs <= 0) return;
|
|
442
|
+
let handle;
|
|
443
|
+
await Promise.race([initial, new Promise((resolve) => {
|
|
444
|
+
handle = setTimeout(resolve, waitMs);
|
|
445
|
+
})]);
|
|
446
|
+
if (handle !== void 0) clearTimeout(handle);
|
|
447
|
+
}
|
|
448
|
+
/** Revalidate in the background when the display-time TTL has elapsed. */
|
|
449
|
+
revalidateIfNeeded() {
|
|
450
|
+
if (this.disposed) return;
|
|
451
|
+
if (this.lastCheckedAt !== void 0 && this.now() - this.lastCheckedAt < this.revalidateAfterMs) return;
|
|
452
|
+
this.refresh();
|
|
453
|
+
}
|
|
454
|
+
/** Force one bounded refresh, joining an already-running request. */
|
|
455
|
+
forceRefresh() {
|
|
456
|
+
if (this.inFlight !== void 0) return this.inFlight;
|
|
457
|
+
this.lastCheckedAt = void 0;
|
|
458
|
+
return this.refresh();
|
|
459
|
+
}
|
|
460
|
+
/** Coalesce every caller onto one authenticated list request. */
|
|
461
|
+
refresh() {
|
|
462
|
+
if (this.disposed) return Promise.resolve();
|
|
463
|
+
if (this.inFlight !== void 0) return this.inFlight;
|
|
464
|
+
const task = this.runRefresh().catch((error) => {
|
|
465
|
+
if (!this.disposed) this.warn(`codex-subscription-oauth: model catalog refresh failed (${errorCode(error)})`);
|
|
466
|
+
}).finally(() => {
|
|
467
|
+
if (this.inFlight === task) this.inFlight = void 0;
|
|
468
|
+
});
|
|
469
|
+
this.inFlight = task;
|
|
470
|
+
return task;
|
|
471
|
+
}
|
|
472
|
+
async runRefresh() {
|
|
473
|
+
this.lastCheckedAt = this.now();
|
|
474
|
+
const requestController = new AbortController();
|
|
475
|
+
const abort = () => requestController.abort();
|
|
476
|
+
this.lifecycle.signal.addEventListener("abort", abort, { once: true });
|
|
477
|
+
const timeout = setTimeout(abort, this.timeoutMs);
|
|
478
|
+
try {
|
|
479
|
+
let credential;
|
|
480
|
+
try {
|
|
481
|
+
credential = await this.resolveCredential(requestController.signal);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (requestController.signal.aborted) throw error;
|
|
484
|
+
throw Object.assign(/* @__PURE__ */ new Error("catalog authentication failed"), { code: "AUTH" });
|
|
485
|
+
}
|
|
486
|
+
if (this.disposed || requestController.signal.aborted) return;
|
|
487
|
+
if (credential === void 0) {
|
|
488
|
+
this.etag = void 0;
|
|
489
|
+
this.accountId = void 0;
|
|
490
|
+
this.publish("bundled", this.baseline);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (credential.accountId !== this.accountId) {
|
|
494
|
+
this.etag = void 0;
|
|
495
|
+
this.publish("bundled", this.baseline);
|
|
496
|
+
}
|
|
497
|
+
this.accountId = credential.accountId;
|
|
498
|
+
let response;
|
|
499
|
+
try {
|
|
500
|
+
response = await this.fetchImpl(MODEL_LIST_URL, {
|
|
501
|
+
method: "GET",
|
|
502
|
+
headers: {
|
|
503
|
+
accept: "application/json",
|
|
504
|
+
authorization: `Bearer ${credential.accessToken}`,
|
|
505
|
+
...credential.accountId === void 0 ? {} : { "chatgpt-account-id": credential.accountId },
|
|
506
|
+
...this.etag === void 0 ? {} : { "if-none-match": this.etag },
|
|
507
|
+
originator: "pi",
|
|
508
|
+
"user-agent": "dsh-codex/0.1.0"
|
|
509
|
+
},
|
|
510
|
+
redirect: "error",
|
|
511
|
+
signal: requestController.signal
|
|
512
|
+
});
|
|
513
|
+
} catch (error) {
|
|
514
|
+
if (requestController.signal.aborted) throw error;
|
|
515
|
+
throw Object.assign(/* @__PURE__ */ new Error("model list network failure"), { code: "NETWORK" });
|
|
516
|
+
}
|
|
517
|
+
if (response.status === 304) return;
|
|
518
|
+
if (!response.ok) throw Object.assign(/* @__PURE__ */ new Error("HTTP status"), { code: `HTTP_${response.status}` });
|
|
519
|
+
const text = await readBoundedText(response, MAX_RESPONSE_BYTES);
|
|
520
|
+
let payload;
|
|
521
|
+
try {
|
|
522
|
+
payload = JSON.parse(text);
|
|
523
|
+
} catch {
|
|
524
|
+
throw Object.assign(/* @__PURE__ */ new Error("invalid model response JSON"), { code: "INVALID_JSON" });
|
|
525
|
+
}
|
|
526
|
+
const official = parseOfficialModels(payload);
|
|
527
|
+
if (official.length === 0) throw Object.assign(/* @__PURE__ */ new Error("empty model list"), { code: "EMPTY_LIST" });
|
|
528
|
+
const models = official.map((entry) => {
|
|
529
|
+
const baseline = this.baselineById.get(entry.slug);
|
|
530
|
+
if (baseline === void 0) return void 0;
|
|
531
|
+
if (entry.name === void 0 || entry.name === baseline.name) return baseline;
|
|
532
|
+
return Object.freeze({
|
|
533
|
+
...baseline,
|
|
534
|
+
name: entry.name
|
|
535
|
+
});
|
|
536
|
+
}).filter((model) => model !== void 0);
|
|
537
|
+
if (models.length === 0) throw Object.assign(/* @__PURE__ */ new Error("no executable model metadata"), { code: "NO_EXECUTABLE_MODELS" });
|
|
538
|
+
this.etag = boundedHeader(response.headers.get("etag"));
|
|
539
|
+
this.publish("live", Object.freeze(models));
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (requestController.signal.aborted) {
|
|
542
|
+
if (this.disposed) return;
|
|
543
|
+
throw Object.assign(/* @__PURE__ */ new Error("model list timeout"), { code: "TIMEOUT" });
|
|
544
|
+
}
|
|
545
|
+
throw error;
|
|
546
|
+
} finally {
|
|
547
|
+
clearTimeout(timeout);
|
|
548
|
+
this.lifecycle.signal.removeEventListener("abort", abort);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
publish(source, models) {
|
|
552
|
+
if (this.disposed) return;
|
|
553
|
+
const previous = this.snapshotValue;
|
|
554
|
+
if (previous.source === source && sameModels(previous.models, models)) return;
|
|
555
|
+
this.revision += 1;
|
|
556
|
+
const snapshot = Object.freeze({
|
|
557
|
+
source,
|
|
558
|
+
models,
|
|
559
|
+
revision: this.revision
|
|
560
|
+
});
|
|
561
|
+
this.snapshotValue = snapshot;
|
|
562
|
+
this.onChange(snapshot);
|
|
563
|
+
}
|
|
564
|
+
schedule() {
|
|
565
|
+
if (this.disposed) return;
|
|
566
|
+
if (this.timer !== void 0) clearTimeout(this.timer);
|
|
567
|
+
const multiplier = 1 - JITTER_RATIO + this.random() * JITTER_RATIO * 2;
|
|
568
|
+
this.timer = setTimeout(() => {
|
|
569
|
+
this.timer = void 0;
|
|
570
|
+
this.refresh().finally(() => this.schedule());
|
|
571
|
+
}, Math.max(1, Math.round(this.refreshIntervalMs * multiplier)));
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
/** Parse picker-visible entries in official priority order; malformed rows are skipped. */
|
|
575
|
+
function parseOfficialModels(payload) {
|
|
576
|
+
if (!isRecord(payload) || !Array.isArray(payload.models)) throw Object.assign(/* @__PURE__ */ new Error("invalid model response"), { code: "INVALID_RESPONSE" });
|
|
577
|
+
const seen = /* @__PURE__ */ new Set();
|
|
578
|
+
const models = [];
|
|
579
|
+
for (const [index, value] of payload.models.entries()) {
|
|
580
|
+
if (!isRecord(value)) continue;
|
|
581
|
+
const slug = value.slug;
|
|
582
|
+
if (typeof slug !== "string" || slug.length === 0 || slug.length > MAX_MODEL_ID_CHARS || seen.has(slug)) continue;
|
|
583
|
+
if (value.visibility !== "list") continue;
|
|
584
|
+
seen.add(slug);
|
|
585
|
+
const displayName = value.display_name;
|
|
586
|
+
const name = typeof displayName === "string" && displayName.length > 0 && displayName.length <= MAX_MODEL_ID_CHARS ? displayName : void 0;
|
|
587
|
+
const priority = typeof value.priority === "number" && Number.isSafeInteger(value.priority) ? value.priority : index;
|
|
588
|
+
models.push(Object.freeze({
|
|
589
|
+
slug,
|
|
590
|
+
...name === void 0 ? {} : { name },
|
|
591
|
+
priority
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
return Object.freeze(models.sort((left, right) => left.priority - right.priority));
|
|
595
|
+
}
|
|
596
|
+
/** Extract the account id without retaining or exposing any other JWT claim. */
|
|
597
|
+
function accountIdFromAccessToken(accessToken) {
|
|
598
|
+
try {
|
|
599
|
+
const payload = accessToken.split(".")[1];
|
|
600
|
+
if (payload === void 0) return void 0;
|
|
601
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
602
|
+
if (!isRecord(decoded) || !isRecord(decoded[JWT_CLAIM])) return void 0;
|
|
603
|
+
const accountId = decoded[JWT_CLAIM].chatgpt_account_id;
|
|
604
|
+
return typeof accountId === "string" && accountId.length > 0 ? accountId : void 0;
|
|
605
|
+
} catch {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async function readBoundedText(response, maxBytes) {
|
|
610
|
+
const declared = Number(response.headers.get("content-length") ?? NaN);
|
|
611
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
612
|
+
await response.body?.cancel();
|
|
613
|
+
throw Object.assign(/* @__PURE__ */ new Error("response too large"), { code: "TOO_LARGE" });
|
|
614
|
+
}
|
|
615
|
+
if (response.body === null) return "";
|
|
616
|
+
const reader = response.body.getReader();
|
|
617
|
+
const chunks = [];
|
|
618
|
+
let total = 0;
|
|
619
|
+
try {
|
|
620
|
+
for (;;) {
|
|
621
|
+
const { done, value } = await reader.read();
|
|
622
|
+
if (done) break;
|
|
623
|
+
total += value.byteLength;
|
|
624
|
+
if (total > maxBytes) throw Object.assign(/* @__PURE__ */ new Error("response too large"), { code: "TOO_LARGE" });
|
|
625
|
+
chunks.push(value);
|
|
626
|
+
}
|
|
627
|
+
} finally {
|
|
628
|
+
await reader.cancel().catch(() => {});
|
|
629
|
+
}
|
|
630
|
+
const result = new Uint8Array(total);
|
|
631
|
+
let offset = 0;
|
|
632
|
+
for (const chunk of chunks) {
|
|
633
|
+
result.set(chunk, offset);
|
|
634
|
+
offset += chunk.byteLength;
|
|
635
|
+
}
|
|
636
|
+
return new TextDecoder().decode(result);
|
|
637
|
+
}
|
|
638
|
+
function boundedHeader(value) {
|
|
639
|
+
return value !== null && value.length > 0 && value.length <= 1024 ? value : void 0;
|
|
640
|
+
}
|
|
641
|
+
function sameModels(left, right) {
|
|
642
|
+
return left.length === right.length && left.every((model, index) => {
|
|
643
|
+
const candidate = right[index];
|
|
644
|
+
return model.id === candidate?.id && model.name === candidate.name;
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
function isRecord(value) {
|
|
648
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
649
|
+
}
|
|
650
|
+
function errorCode(error) {
|
|
651
|
+
if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code.slice(0, 80);
|
|
652
|
+
return "UNKNOWN";
|
|
653
|
+
}
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/credentials.ts
|
|
656
|
+
const RECORD_SCOPE = "llm-pi-ai";
|
|
657
|
+
/** Remove explicitly undefined plain-object members before durable storage. */
|
|
658
|
+
function jsonImage(value) {
|
|
659
|
+
if (Array.isArray(value)) return value.map((entry) => entry === void 0 ? null : jsonImage(entry));
|
|
660
|
+
if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
|
|
661
|
+
const image = {};
|
|
662
|
+
for (const [key, member] of Object.entries(value)) if (member !== void 0) image[key] = jsonImage(member);
|
|
663
|
+
return image;
|
|
664
|
+
}
|
|
665
|
+
return value;
|
|
666
|
+
}
|
|
667
|
+
/** Translate the record format owned by llm-pi-ai back to a pi-ai credential. */
|
|
668
|
+
function toCredential(record) {
|
|
669
|
+
if (record === void 0) return void 0;
|
|
670
|
+
if (record.kind === "api-key") return {
|
|
671
|
+
type: "api_key",
|
|
672
|
+
...record.key === void 0 ? {} : { key: record.key },
|
|
673
|
+
...record.env === void 0 ? {} : { env: { ...record.env } }
|
|
674
|
+
};
|
|
675
|
+
return record.payload;
|
|
676
|
+
}
|
|
677
|
+
/** Translate a pi-ai credential to the same durable record format. */
|
|
678
|
+
function toRecord(credential) {
|
|
679
|
+
if (credential.type === "api_key") return {
|
|
680
|
+
kind: "api-key",
|
|
681
|
+
...credential.key === void 0 ? {} : { key: credential.key },
|
|
682
|
+
...credential.env === void 0 ? {} : { env: { ...credential.env } }
|
|
683
|
+
};
|
|
684
|
+
return {
|
|
685
|
+
kind: "grant",
|
|
686
|
+
payload: jsonImage(credential)
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Access only records owned by llm-pi-ai. OAuth refresh remains serialized by
|
|
691
|
+
* the DSH credential provider's cross-process modify operation.
|
|
692
|
+
*/
|
|
693
|
+
function credentialStoreFrom(ctx) {
|
|
694
|
+
return {
|
|
695
|
+
async read(providerId) {
|
|
696
|
+
if (!isCredentialKeySegment(providerId)) return void 0;
|
|
697
|
+
return toCredential(await ctx.credentials.readRecord(recordKeyFor(providerId)));
|
|
698
|
+
},
|
|
699
|
+
async list() {
|
|
700
|
+
return (await ctx.credentials.listRecords()).filter((entry) => credentialKeyScope(entry.key) === RECORD_SCOPE).map((entry) => ({
|
|
701
|
+
providerId: credentialKeyId(entry.key),
|
|
702
|
+
type: entry.kind === "api-key" ? "api_key" : "oauth"
|
|
703
|
+
}));
|
|
704
|
+
},
|
|
705
|
+
async modify(providerId, mutate) {
|
|
706
|
+
if (!isCredentialKeySegment(providerId)) throw new Error(`codex-subscription-oauth: invalid credential provider id "${providerId}"`);
|
|
707
|
+
return toCredential(await ctx.credentials.modifyRecord(recordKeyFor(providerId), async (current) => {
|
|
708
|
+
const next = await mutate(toCredential(current));
|
|
709
|
+
return next === void 0 ? void 0 : toRecord(next);
|
|
710
|
+
}));
|
|
711
|
+
},
|
|
712
|
+
async delete(providerId) {
|
|
713
|
+
if (!isCredentialKeySegment(providerId)) return;
|
|
714
|
+
await ctx.credentials.deleteRecord(recordKeyFor(providerId));
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/** Codex OAuth has no environment or filesystem-based authentication path. */
|
|
719
|
+
function codexAuthContext() {
|
|
720
|
+
return {
|
|
721
|
+
env: async () => void 0,
|
|
722
|
+
fileExists: async () => false
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/index.ts
|
|
727
|
+
/** Cordis plugin name. */
|
|
728
|
+
const name = "codex-subscription-oauth";
|
|
729
|
+
/** Services required before the command and model adapter can activate. */
|
|
730
|
+
const inject = [
|
|
731
|
+
"commands",
|
|
732
|
+
"credentials",
|
|
733
|
+
"llm",
|
|
734
|
+
"userQuestions"
|
|
735
|
+
];
|
|
736
|
+
const CODEX_KEY = credentialKey("llm-pi-ai", "openai-codex");
|
|
737
|
+
const USAGE = `Usage: /codex-auth`;
|
|
738
|
+
const SUCCESS = "Codex authentication succeeded.";
|
|
739
|
+
const CANCELLED = "Codex authentication cancelled.";
|
|
740
|
+
const FAILURE = "Codex authentication failed.";
|
|
741
|
+
const SECRET_PROMPT_FAILURE = "Codex authentication cannot handle secret prompts.";
|
|
742
|
+
const SELECT_PROMPT_FAILURE = "Codex authentication cannot handle this selection prompt.";
|
|
743
|
+
const ANSWER_FAILURE = "Codex authentication received an unsupported answer.";
|
|
744
|
+
const PROMPT_QUESTION = "Paste the redirect URL or authorization code.";
|
|
745
|
+
const PROMPT_ID = "codex-auth";
|
|
746
|
+
const MAX_NOTICES = 8;
|
|
747
|
+
const MAX_NOTICE_FIELD_CHARS = 4096;
|
|
748
|
+
const MAX_NOTICE_FIELD_BYTES = 8192;
|
|
749
|
+
const MAX_NOTICE_AGGREGATE_CHARS = 16384;
|
|
750
|
+
const MAX_NOTICE_AGGREGATE_BYTES = 32768;
|
|
751
|
+
const ROUTE = "openai-codex";
|
|
752
|
+
const DEFAULT_REFRESH_INTERVAL_MS = 900 * 1e3;
|
|
753
|
+
const DEFAULT_REVALIDATE_AFTER_MS = 60 * 1e3;
|
|
754
|
+
const DEFAULT_TIMEOUT_MS = 15 * 1e3;
|
|
755
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
756
|
+
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300 * 1e3;
|
|
757
|
+
const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
758
|
+
const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
|
|
759
|
+
const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
|
|
760
|
+
function positiveTimer(value, fallback, field) {
|
|
761
|
+
const resolved = value ?? fallback;
|
|
762
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > MAX_TIMER_DELAY_MS) throw new TypeError(`codex-subscription-oauth: catalog.${field} must be a positive timer value`);
|
|
763
|
+
return resolved;
|
|
764
|
+
}
|
|
765
|
+
function resolveCatalogConfig(config) {
|
|
766
|
+
return {
|
|
767
|
+
refreshIntervalMs: positiveTimer(config.catalog?.refreshIntervalMs, DEFAULT_REFRESH_INTERVAL_MS, "refreshIntervalMs"),
|
|
768
|
+
revalidateAfterMs: positiveTimer(config.catalog?.revalidateAfterMs, DEFAULT_REVALIDATE_AFTER_MS, "revalidateAfterMs"),
|
|
769
|
+
timeoutMs: positiveTimer(config.catalog?.timeoutMs, DEFAULT_TIMEOUT_MS, "timeoutMs")
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
/** Whether one untrusted notice field fits both bounded representations. */
|
|
773
|
+
function noticeFieldFits(value) {
|
|
774
|
+
return typeof value === "string" && value.length <= MAX_NOTICE_FIELD_CHARS && Buffer.byteLength(value, "utf8") <= MAX_NOTICE_FIELD_BYTES;
|
|
775
|
+
}
|
|
776
|
+
/** Whether the complete rendered notice detail fits the aggregate budget. */
|
|
777
|
+
function noticeDetailFits(notices) {
|
|
778
|
+
const detail = renderNoticeDetail(notices);
|
|
779
|
+
return detail !== void 0 && detail.length <= MAX_NOTICE_AGGREGATE_CHARS && Buffer.byteLength(detail, "utf8") <= MAX_NOTICE_AGGREGATE_BYTES;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Keep notices inside one attempt. Dropping the oldest value preserves the
|
|
783
|
+
* latest authentication instruction without allowing an event producer to
|
|
784
|
+
* grow the queue without bound. Invalid or oversized notices are dropped
|
|
785
|
+
* wholesale rather than truncated, so a URL that fits remains byte-for-byte
|
|
786
|
+
* unchanged and an oversized value is never retained.
|
|
787
|
+
* @returns an attempt-local notice queue.
|
|
788
|
+
*/
|
|
789
|
+
function createNoticeQueue() {
|
|
790
|
+
const notices = [];
|
|
791
|
+
let open = true;
|
|
792
|
+
return {
|
|
793
|
+
push(notice) {
|
|
794
|
+
if (!open || !noticeFieldFits(notice.message) || notice.url !== void 0 && !noticeFieldFits(notice.url) || notice.code !== void 0 && !noticeFieldFits(notice.code)) return;
|
|
795
|
+
const copy = Object.freeze({
|
|
796
|
+
message: notice.message,
|
|
797
|
+
...notice.url === void 0 ? {} : { url: notice.url },
|
|
798
|
+
...notice.code === void 0 ? {} : { code: notice.code }
|
|
799
|
+
});
|
|
800
|
+
while (notices.length >= MAX_NOTICES || !noticeDetailFits([...notices, copy])) {
|
|
801
|
+
if (notices.length === 0) return;
|
|
802
|
+
notices.shift();
|
|
803
|
+
}
|
|
804
|
+
notices.push(copy);
|
|
805
|
+
},
|
|
806
|
+
drain() {
|
|
807
|
+
if (!open) return [];
|
|
808
|
+
const drained = notices.splice(0, notices.length);
|
|
809
|
+
return Object.freeze(drained);
|
|
810
|
+
},
|
|
811
|
+
clear() {
|
|
812
|
+
open = false;
|
|
813
|
+
notices.length = 0;
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
/** Render only the non-secret notice fields into the free-text prompt detail. */
|
|
818
|
+
function renderNotice(notice) {
|
|
819
|
+
const lines = [notice.message];
|
|
820
|
+
if (notice.url !== void 0) lines.push(`Open: ${notice.url}`);
|
|
821
|
+
if (notice.code !== void 0) lines.push(`Code: ${notice.code}`);
|
|
822
|
+
return lines.join("\n");
|
|
823
|
+
}
|
|
824
|
+
/** Render queued notices without retaining their source objects. */
|
|
825
|
+
function renderNoticeDetail(notices) {
|
|
826
|
+
if (notices.length === 0) return void 0;
|
|
827
|
+
return notices.map(renderNotice).join("\n\n");
|
|
828
|
+
}
|
|
829
|
+
/** Combine the command lifetime with the lifetime of the one prompt. */
|
|
830
|
+
function promptSignal(invocation, prompt) {
|
|
831
|
+
const signals = [invocation.signal];
|
|
832
|
+
if (prompt.signal !== void 0) signals.push(prompt.signal);
|
|
833
|
+
return AbortSignal.any(signals);
|
|
834
|
+
}
|
|
835
|
+
/** Detect the transported and local forms of a user-question cancellation. */
|
|
836
|
+
function hasCode(error, code) {
|
|
837
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
838
|
+
}
|
|
839
|
+
/** Extract one free-text answer, retaining the exact submitted string only as the return value. */
|
|
840
|
+
function answerText(answer) {
|
|
841
|
+
const item = answer.answers.find((candidate) => candidate.id === PROMPT_ID);
|
|
842
|
+
if (item === void 0 || item.selected.length > 0 || item.custom === void 0) throw new Error(ANSWER_FAILURE);
|
|
843
|
+
if (item.custom.trim().length === 0) throw new AuthorizationDeclinedError();
|
|
844
|
+
return item.custom;
|
|
845
|
+
}
|
|
846
|
+
/** Ask the DSH free-text channel for the manual redirect or code. */
|
|
847
|
+
async function askForManualCode(ctx, invocation, queue, prompt) {
|
|
848
|
+
if (prompt.kind === "secret") throw new Error(SECRET_PROMPT_FAILURE);
|
|
849
|
+
if (prompt.kind === "select") {
|
|
850
|
+
if (prompt.options.some((option) => option.id === "browser")) return "browser";
|
|
851
|
+
throw new Error(SELECT_PROMPT_FAILURE);
|
|
852
|
+
}
|
|
853
|
+
const userQuestions = ctx.get("userQuestions");
|
|
854
|
+
if (userQuestions === void 0) throw new Error(FAILURE);
|
|
855
|
+
const detail = renderNoticeDetail(queue.drain());
|
|
856
|
+
const question = {
|
|
857
|
+
id: PROMPT_ID,
|
|
858
|
+
question: PROMPT_QUESTION,
|
|
859
|
+
...detail === void 0 ? {} : { detail }
|
|
860
|
+
};
|
|
861
|
+
try {
|
|
862
|
+
return answerText(await userQuestions.ask({
|
|
863
|
+
questions: [question],
|
|
864
|
+
agent: invocation.agent,
|
|
865
|
+
signal: promptSignal(invocation, prompt)
|
|
866
|
+
}));
|
|
867
|
+
} catch (error) {
|
|
868
|
+
if (hasCode(error, "ASK_CANCELLED")) throw new AuthorizationDeclinedError();
|
|
869
|
+
throw error;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
/** Translate a pi-ai prompt into the one supported command interaction. */
|
|
873
|
+
function createInteraction(ctx, invocation, queue) {
|
|
874
|
+
return {
|
|
875
|
+
notify: (notice) => {
|
|
876
|
+
queue.push(notice);
|
|
877
|
+
},
|
|
878
|
+
prompt: (prompt) => askForManualCode(ctx, invocation, queue, prompt)
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
/** Execute one sanitized Codex authentication command. */
|
|
882
|
+
async function execute(invocation, ctx) {
|
|
883
|
+
if (invocation.rawInput.trim().length > 0) return {
|
|
884
|
+
kind: "error",
|
|
885
|
+
text: USAGE
|
|
886
|
+
};
|
|
887
|
+
if (invocation.signal.aborted) return {
|
|
888
|
+
kind: "success",
|
|
889
|
+
text: CANCELLED
|
|
890
|
+
};
|
|
891
|
+
const authorization = ctx.get("authorization");
|
|
892
|
+
if (authorization === void 0) return {
|
|
893
|
+
kind: "error",
|
|
894
|
+
text: FAILURE
|
|
895
|
+
};
|
|
896
|
+
const queue = createNoticeQueue();
|
|
897
|
+
try {
|
|
898
|
+
return (await authorization.begin({
|
|
899
|
+
key: CODEX_KEY,
|
|
900
|
+
method: "oauth",
|
|
901
|
+
interaction: createInteraction(ctx, invocation, queue),
|
|
902
|
+
signal: invocation.signal
|
|
903
|
+
})).status === "authorized" ? {
|
|
904
|
+
kind: "success",
|
|
905
|
+
text: SUCCESS
|
|
906
|
+
} : {
|
|
907
|
+
kind: "success",
|
|
908
|
+
text: CANCELLED
|
|
909
|
+
};
|
|
910
|
+
} catch {
|
|
911
|
+
return invocation.signal.aborted ? {
|
|
912
|
+
kind: "success",
|
|
913
|
+
text: CANCELLED
|
|
914
|
+
} : {
|
|
915
|
+
kind: "error",
|
|
916
|
+
text: FAILURE
|
|
917
|
+
};
|
|
918
|
+
} finally {
|
|
919
|
+
queue.clear();
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
/** Bind one immutable catalog generation to the upstream Codex transport. */
|
|
923
|
+
function providerWithModels(provider, models) {
|
|
924
|
+
const generation = Object.freeze([...models]);
|
|
925
|
+
return {
|
|
926
|
+
...provider,
|
|
927
|
+
getModels: () => generation
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
/** Supply executable metadata for GPT-6 models missing from the pinned pi-ai catalog. */
|
|
931
|
+
function bundledCodexModels(provider) {
|
|
932
|
+
const models = provider.getModels();
|
|
933
|
+
const astra = models.find((model) => model.id === "gpt-6-astra");
|
|
934
|
+
if (astra === void 0) throw new Error("codex-subscription-oauth: missing GPT-6 transport metadata");
|
|
935
|
+
const additions = [{
|
|
936
|
+
id: "gpt-6-sol",
|
|
937
|
+
name: "GPT-6 Sol",
|
|
938
|
+
input: 2,
|
|
939
|
+
output: 10,
|
|
940
|
+
cacheRead: .2,
|
|
941
|
+
cacheWrite: 2.5
|
|
942
|
+
}, {
|
|
943
|
+
id: "gpt-6-luna",
|
|
944
|
+
name: "GPT-6 Luna",
|
|
945
|
+
input: .1,
|
|
946
|
+
output: .5,
|
|
947
|
+
cacheRead: .01,
|
|
948
|
+
cacheWrite: .125
|
|
949
|
+
}];
|
|
950
|
+
return Object.freeze([...models, ...additions.filter((entry) => !models.some((model) => model.id === entry.id)).map((entry) => Object.freeze({
|
|
951
|
+
...astra,
|
|
952
|
+
id: entry.id,
|
|
953
|
+
name: entry.name,
|
|
954
|
+
thinkingLevelMap: {
|
|
955
|
+
off: null,
|
|
956
|
+
low: "low",
|
|
957
|
+
medium: "medium",
|
|
958
|
+
high: "high",
|
|
959
|
+
xhigh: "xhigh",
|
|
960
|
+
max: "max"
|
|
961
|
+
},
|
|
962
|
+
cost: {
|
|
963
|
+
input: entry.input,
|
|
964
|
+
output: entry.output,
|
|
965
|
+
cacheRead: entry.cacheRead,
|
|
966
|
+
cacheWrite: entry.cacheWrite,
|
|
967
|
+
tiers: [{
|
|
968
|
+
inputTokensAbove: 272e3,
|
|
969
|
+
input: entry.input * 2,
|
|
970
|
+
output: entry.output * 1.5,
|
|
971
|
+
cacheRead: entry.cacheRead * 2,
|
|
972
|
+
cacheWrite: entry.cacheWrite * 2
|
|
973
|
+
}]
|
|
974
|
+
}
|
|
975
|
+
}))]);
|
|
976
|
+
}
|
|
977
|
+
/** Build the one profile shape consumed by DSH's public pi-ai adapter. */
|
|
978
|
+
function profileFor(provider, models) {
|
|
979
|
+
return /* @__PURE__ */ new Map([[ROUTE, {
|
|
980
|
+
provider: ROUTE,
|
|
981
|
+
displayName: provider.name,
|
|
982
|
+
streamIdleTimeoutMs: DEFAULT_STREAM_IDLE_TIMEOUT_MS,
|
|
983
|
+
maxRequestImageBytes: DEFAULT_MAX_REQUEST_IMAGE_BYTES,
|
|
984
|
+
requestImagePixelBudget: DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
|
|
985
|
+
requestImageMaxBytes: DEFAULT_REQUEST_IMAGE_MAX_BYTES,
|
|
986
|
+
retryPolicy: resolveRetryPolicy(void 0, `codex-subscription-oauth: provider "${ROUTE}" retryPolicy`),
|
|
987
|
+
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
988
|
+
modelErrors: /* @__PURE__ */ new Map(),
|
|
989
|
+
piProvider: providerWithModels(provider, models)
|
|
990
|
+
}]]);
|
|
991
|
+
}
|
|
992
|
+
/** Register the command, dynamic model route, and fallback authorization service. */
|
|
993
|
+
function apply(ctx, config = {}) {
|
|
994
|
+
const catalogConfig = resolveCatalogConfig(config);
|
|
995
|
+
if (ctx.get("authorization") === void 0) ctx.plugin(AuthorizationService);
|
|
996
|
+
ctx.commands.register({
|
|
997
|
+
name: "codex-auth",
|
|
998
|
+
description: "Sign in to ChatGPT for Codex",
|
|
999
|
+
recordInput: false,
|
|
1000
|
+
handler: (invocation) => execute(invocation, ctx)
|
|
1001
|
+
});
|
|
1002
|
+
const upstream = openaiCodexProvider();
|
|
1003
|
+
const bundledModels = bundledCodexModels(upstream);
|
|
1004
|
+
const auth = {
|
|
1005
|
+
credentials: credentialStoreFrom(ctx),
|
|
1006
|
+
authContext: codexAuthContext()
|
|
1007
|
+
};
|
|
1008
|
+
const authModels = createModels(auth);
|
|
1009
|
+
authModels.setProvider(upstream);
|
|
1010
|
+
let profiles;
|
|
1011
|
+
let registration;
|
|
1012
|
+
const catalog = new CodexCatalog({
|
|
1013
|
+
...catalogConfig,
|
|
1014
|
+
baseline: bundledModels,
|
|
1015
|
+
async resolveCredential(signal) {
|
|
1016
|
+
const accessToken = (await authModels.getAuth(ROUTE, { signal }))?.auth.apiKey;
|
|
1017
|
+
if (accessToken === void 0 || accessToken.length === 0) return void 0;
|
|
1018
|
+
const stored = await auth.credentials.read(ROUTE, { signal });
|
|
1019
|
+
const storedAccountId = stored?.type === "oauth" ? stored.accountId : void 0;
|
|
1020
|
+
const accountId = typeof storedAccountId === "string" && storedAccountId.length > 0 ? storedAccountId : accountIdFromAccessToken(accessToken);
|
|
1021
|
+
return {
|
|
1022
|
+
accessToken,
|
|
1023
|
+
...accountId === void 0 ? {} : { accountId }
|
|
1024
|
+
};
|
|
1025
|
+
},
|
|
1026
|
+
onChange(snapshot) {
|
|
1027
|
+
profiles = profileFor(upstream, snapshot.models);
|
|
1028
|
+
registration?.replace([ROUTE]);
|
|
1029
|
+
},
|
|
1030
|
+
warn: (message) => ctx.logger.warn(message)
|
|
1031
|
+
});
|
|
1032
|
+
profiles = profileFor(upstream, catalog.current.models);
|
|
1033
|
+
const adapter = new LiveCodexAdapter({
|
|
1034
|
+
profiles: () => profiles,
|
|
1035
|
+
resolveApiKey: async () => void 0,
|
|
1036
|
+
auth,
|
|
1037
|
+
resolveAttachments: () => ctx.get("attachments"),
|
|
1038
|
+
resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath), ref),
|
|
1039
|
+
catalog,
|
|
1040
|
+
initialWaitMs: catalogConfig.timeoutMs,
|
|
1041
|
+
onReplayDegrade: ({ provider, model, reason }) => {
|
|
1042
|
+
ctx.logger.warn(`codex-subscription-oauth: unusable replay state on "${provider}/${model}"; using provider-neutral history (${reason})`);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
registration = ctx.llm.registerAdapter([ROUTE], adapter);
|
|
1046
|
+
ctx.on("credentials/record-updated", (key) => {
|
|
1047
|
+
if (key !== CODEX_KEY) return;
|
|
1048
|
+
catalog.forceRefresh();
|
|
1049
|
+
});
|
|
1050
|
+
ctx.effect(() => () => catalog.stop(), "codex model catalog");
|
|
1051
|
+
catalog.start();
|
|
1052
|
+
}
|
|
1053
|
+
//#endregion
|
|
1054
|
+
export { apply, inject, name };
|