@pure01fx/dsh-openai-codex-auth 0.5.0 → 0.6.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/CHANGELOG.md +50 -0
- package/README.md +33 -5
- package/client.js +186 -16
- package/cordis.patch.yml +2 -6
- package/lib/catalog.d.ts +77 -0
- package/lib/catalog.js +383 -0
- package/lib/endpoint.d.ts +2 -0
- package/lib/endpoint.js +38 -0
- package/lib/index.d.ts +63 -17
- package/lib/index.js +535 -130
- package/lib/native-adapter.d.ts +43 -0
- package/lib/native-adapter.js +246 -0
- package/lib/native-http.d.ts +61 -0
- package/lib/native-http.js +602 -0
- package/lib/native-websocket-session.d.ts +14 -0
- package/lib/native-websocket-session.js +109 -0
- package/lib/native-websocket-socket.d.ts +28 -0
- package/lib/native-websocket-socket.js +215 -0
- package/lib/native-websocket.d.ts +46 -0
- package/lib/native-websocket.js +628 -0
- package/lib/rate-limits.d.ts +35 -0
- package/lib/rate-limits.js +189 -0
- package/lib/replay.d.ts +49 -0
- package/lib/replay.js +240 -0
- package/lib/response-usage.d.ts +14 -0
- package/lib/response-usage.js +35 -0
- package/lib/responses.d.ts +126 -0
- package/lib/responses.js +572 -0
- package/lib/sse.d.ts +13 -0
- package/lib/sse.js +81 -0
- package/lib/upstream.d.ts +6 -0
- package/lib/upstream.js +6 -0
- package/lib/usage.d.ts +29 -0
- package/lib/usage.js +160 -0
- package/package.json +50 -15
package/lib/catalog.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/** Native Codex model discovery, validation, and account-partitioned caching. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { nativeCodexEndpoint } from './endpoint.js';
|
|
4
|
+
import { attributionHeaders, LlmError } from '@deepseek-ai/dsh-llm';
|
|
5
|
+
import { CODEX_CLIENT_VERSION } from './upstream.js';
|
|
6
|
+
export { CODEX_CLIENT_VERSION } from './upstream.js';
|
|
7
|
+
export const CODEX_MODELS_URL = 'https://chatgpt.com/backend-api/codex/models';
|
|
8
|
+
export const CODEX_CATALOG_CACHE_TTL_MS = 5 * 60_000;
|
|
9
|
+
const CODEX_CATALOG_MAX_STALE_MS = 7 * 24 * 60 * 60_000;
|
|
10
|
+
const CODEX_CATALOG_TIMEOUT_MS = 5_000;
|
|
11
|
+
const MAX_CATALOG_BODY_LENGTH = 2 * 1024 * 1024;
|
|
12
|
+
const ORIGINATOR = 'dsh';
|
|
13
|
+
class CatalogFetchError extends LlmError {
|
|
14
|
+
allowsStale;
|
|
15
|
+
constructor(message, code, allowsStale, options) {
|
|
16
|
+
super(message, code, options);
|
|
17
|
+
this.allowsStale = allowsStale;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function throwIfAborted(signal) {
|
|
21
|
+
if (signal?.aborted)
|
|
22
|
+
throw new LlmError('native Codex catalog request was aborted', 'ABORTED');
|
|
23
|
+
}
|
|
24
|
+
function awaitWithSignal(promise, signal) {
|
|
25
|
+
if (signal === undefined)
|
|
26
|
+
return promise;
|
|
27
|
+
throwIfAborted(signal);
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const abort = () => {
|
|
30
|
+
reject(new LlmError('native Codex catalog request was aborted', 'ABORTED'));
|
|
31
|
+
};
|
|
32
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
33
|
+
promise.then(resolve, reject).finally(() => {
|
|
34
|
+
signal.removeEventListener('abort', abort);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function record(value) {
|
|
39
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
40
|
+
? value
|
|
41
|
+
: undefined;
|
|
42
|
+
}
|
|
43
|
+
function nonEmptyString(value) {
|
|
44
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
45
|
+
}
|
|
46
|
+
function positiveNumber(value) {
|
|
47
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
48
|
+
}
|
|
49
|
+
function invalidCatalog(message) {
|
|
50
|
+
return new CatalogFetchError(message, 'CATALOG_INVALID_RESPONSE', true);
|
|
51
|
+
}
|
|
52
|
+
function strictStringList(value, fallback = []) {
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
return fallback;
|
|
55
|
+
if (!Array.isArray(value)
|
|
56
|
+
|| !value.every(item => typeof item === 'string' && item.length > 0))
|
|
57
|
+
return undefined;
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function parseReasoningLevels(value) {
|
|
61
|
+
if (!Array.isArray(value))
|
|
62
|
+
return undefined;
|
|
63
|
+
const levels = [];
|
|
64
|
+
for (const item of value) {
|
|
65
|
+
const row = record(item);
|
|
66
|
+
if (row === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
const effort = nonEmptyString(row?.effort);
|
|
69
|
+
const description = nonEmptyString(row?.description);
|
|
70
|
+
if (effort === undefined || description === undefined)
|
|
71
|
+
return undefined;
|
|
72
|
+
levels.push({ effort, description });
|
|
73
|
+
}
|
|
74
|
+
return levels;
|
|
75
|
+
}
|
|
76
|
+
function parseServiceTiers(value) {
|
|
77
|
+
if (value === undefined)
|
|
78
|
+
return [];
|
|
79
|
+
if (!Array.isArray(value))
|
|
80
|
+
return undefined;
|
|
81
|
+
const tiers = [];
|
|
82
|
+
for (const item of value) {
|
|
83
|
+
const row = record(item);
|
|
84
|
+
if (row === undefined)
|
|
85
|
+
return undefined;
|
|
86
|
+
const id = nonEmptyString(row?.id);
|
|
87
|
+
const name = nonEmptyString(row?.name);
|
|
88
|
+
const description = nonEmptyString(row?.description);
|
|
89
|
+
if (id === undefined || name === undefined || description === undefined)
|
|
90
|
+
return undefined;
|
|
91
|
+
tiers.push({ id, name, description });
|
|
92
|
+
}
|
|
93
|
+
return tiers;
|
|
94
|
+
}
|
|
95
|
+
async function boundedResponseText(response) {
|
|
96
|
+
if (response.body === null)
|
|
97
|
+
return '';
|
|
98
|
+
const reader = response.body.getReader();
|
|
99
|
+
const chunks = [];
|
|
100
|
+
let total = 0;
|
|
101
|
+
try {
|
|
102
|
+
while (true) {
|
|
103
|
+
const { done, value } = await reader.read();
|
|
104
|
+
if (done)
|
|
105
|
+
break;
|
|
106
|
+
total += value.byteLength;
|
|
107
|
+
if (total > MAX_CATALOG_BODY_LENGTH) {
|
|
108
|
+
await reader.cancel();
|
|
109
|
+
throw new CatalogFetchError('native Codex catalog response exceeded the size limit', 'CATALOG_INVALID_RESPONSE', true);
|
|
110
|
+
}
|
|
111
|
+
chunks.push(value);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
reader.releaseLock();
|
|
116
|
+
}
|
|
117
|
+
return Buffer.concat(chunks.map(chunk => Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)), total).toString('utf8');
|
|
118
|
+
}
|
|
119
|
+
function parseModel(value) {
|
|
120
|
+
const row = record(value);
|
|
121
|
+
if (row === undefined)
|
|
122
|
+
throw invalidCatalog('native Codex catalog contained a non-object model entry');
|
|
123
|
+
const slug = nonEmptyString(row?.slug);
|
|
124
|
+
const displayName = nonEmptyString(row?.display_name);
|
|
125
|
+
const reasoning = parseReasoningLevels(row?.supported_reasoning_levels);
|
|
126
|
+
const visibility = row?.visibility;
|
|
127
|
+
const priority = row?.priority;
|
|
128
|
+
const additionalSpeedTiers = strictStringList(row?.additional_speed_tiers);
|
|
129
|
+
const serviceTiers = parseServiceTiers(row?.service_tiers);
|
|
130
|
+
const inputModalities = strictStringList(row?.input_modalities, ['text', 'image']);
|
|
131
|
+
if (slug === undefined
|
|
132
|
+
|| displayName === undefined
|
|
133
|
+
|| reasoning === undefined
|
|
134
|
+
|| !['list', 'hide', 'none'].includes(String(visibility))
|
|
135
|
+
|| !['unified_exec', 'disabled', 'default', 'local', 'shell_command'].includes(String(row?.shell_type))
|
|
136
|
+
|| typeof row.supported_in_api !== 'boolean'
|
|
137
|
+
|| typeof priority !== 'number' || !Number.isSafeInteger(priority)
|
|
138
|
+
|| priority < -2_147_483_648 || priority > 2_147_483_647
|
|
139
|
+
|| additionalSpeedTiers === undefined
|
|
140
|
+
|| serviceTiers === undefined
|
|
141
|
+
|| inputModalities === undefined
|
|
142
|
+
|| !inputModalities.every(value => ['text', 'image', 'audio'].includes(value))) {
|
|
143
|
+
throw invalidCatalog('native Codex catalog contained an invalid model entry');
|
|
144
|
+
}
|
|
145
|
+
if (row.description !== undefined && row.description !== null && typeof row.description !== 'string') {
|
|
146
|
+
throw invalidCatalog('native Codex catalog contained an invalid model description');
|
|
147
|
+
}
|
|
148
|
+
if (row.default_reasoning_level !== undefined
|
|
149
|
+
&& row.default_reasoning_level !== null
|
|
150
|
+
&& nonEmptyString(row.default_reasoning_level) === undefined) {
|
|
151
|
+
throw invalidCatalog('native Codex catalog contained an invalid default reasoning level');
|
|
152
|
+
}
|
|
153
|
+
if (row.default_service_tier !== undefined
|
|
154
|
+
&& row.default_service_tier !== null
|
|
155
|
+
&& nonEmptyString(row.default_service_tier) === undefined) {
|
|
156
|
+
throw invalidCatalog('native Codex catalog contained an invalid default service tier');
|
|
157
|
+
}
|
|
158
|
+
if (row.multi_agent_reasoning_effort !== undefined
|
|
159
|
+
&& row.multi_agent_reasoning_effort !== null
|
|
160
|
+
&& nonEmptyString(row.multi_agent_reasoning_effort) === undefined) {
|
|
161
|
+
throw invalidCatalog('native Codex catalog contained an invalid multi-agent reasoning effort');
|
|
162
|
+
}
|
|
163
|
+
for (const key of ['context_window', 'max_context_window']) {
|
|
164
|
+
if (row[key] !== undefined && row[key] !== null && positiveNumber(row[key]) === undefined) {
|
|
165
|
+
throw invalidCatalog('native Codex catalog contained an invalid context window');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const description = nonEmptyString(row.description);
|
|
169
|
+
const defaultReasoningLevel = nonEmptyString(row.default_reasoning_level);
|
|
170
|
+
const multiAgentReasoningEffort = nonEmptyString(row.multi_agent_reasoning_effort);
|
|
171
|
+
const defaultServiceTier = nonEmptyString(row.default_service_tier);
|
|
172
|
+
const contextWindow = positiveNumber(row.context_window) ?? positiveNumber(row.max_context_window);
|
|
173
|
+
return {
|
|
174
|
+
slug,
|
|
175
|
+
displayName,
|
|
176
|
+
...description === undefined ? {} : { description },
|
|
177
|
+
...defaultReasoningLevel === undefined ? {} : { defaultReasoningLevel },
|
|
178
|
+
supportedReasoningLevels: reasoning,
|
|
179
|
+
...multiAgentReasoningEffort === undefined ? {} : { multiAgentReasoningEffort },
|
|
180
|
+
visibility: visibility,
|
|
181
|
+
supportedInApi: row.supported_in_api,
|
|
182
|
+
priority,
|
|
183
|
+
additionalSpeedTiers,
|
|
184
|
+
serviceTiers,
|
|
185
|
+
...defaultServiceTier === undefined ? {} : { defaultServiceTier },
|
|
186
|
+
...contextWindow === undefined ? {} : { contextWindow },
|
|
187
|
+
inputModalities,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function parseModelsPayload(value) {
|
|
191
|
+
const rows = record(value)?.models;
|
|
192
|
+
if (!Array.isArray(rows))
|
|
193
|
+
throw invalidCatalog('native Codex catalog response has no models array');
|
|
194
|
+
if (rows.length === 0) {
|
|
195
|
+
throw new CatalogFetchError('native Codex catalog response contained no usable models', 'CATALOG_EMPTY', true);
|
|
196
|
+
}
|
|
197
|
+
const models = rows.map(parseModel);
|
|
198
|
+
const slugs = new Set(models.map(model => model.slug));
|
|
199
|
+
if (slugs.size !== models.length)
|
|
200
|
+
throw invalidCatalog('native Codex catalog contained duplicate model ids');
|
|
201
|
+
if (!models.some(model => model.visibility === 'list')) {
|
|
202
|
+
throw new CatalogFetchError('native Codex catalog response contained no picker-visible models', 'CATALOG_EMPTY', true);
|
|
203
|
+
}
|
|
204
|
+
return models;
|
|
205
|
+
}
|
|
206
|
+
export function nativeCodexAuthorityHash(accountId) {
|
|
207
|
+
return createHash('sha256').update(accountId).digest('hex');
|
|
208
|
+
}
|
|
209
|
+
/** Codex-compatible live catalog with bounded stale fallback and no credential retention. */
|
|
210
|
+
export class NativeCodexCatalog {
|
|
211
|
+
options;
|
|
212
|
+
endpoint;
|
|
213
|
+
clientVersion;
|
|
214
|
+
cacheTtlMs;
|
|
215
|
+
maxStaleMs;
|
|
216
|
+
timeoutMs;
|
|
217
|
+
fetchImpl;
|
|
218
|
+
now;
|
|
219
|
+
snapshot;
|
|
220
|
+
refreshes = new Map();
|
|
221
|
+
currentEtag;
|
|
222
|
+
constructor(options) {
|
|
223
|
+
this.options = options;
|
|
224
|
+
this.endpoint = nativeCodexEndpoint(options.endpoint ?? CODEX_MODELS_URL).toString();
|
|
225
|
+
this.clientVersion = options.clientVersion ?? CODEX_CLIENT_VERSION;
|
|
226
|
+
this.cacheTtlMs = options.cacheTtlMs ?? CODEX_CATALOG_CACHE_TTL_MS;
|
|
227
|
+
this.maxStaleMs = options.maxStaleMs ?? CODEX_CATALOG_MAX_STALE_MS;
|
|
228
|
+
this.timeoutMs = options.timeoutMs ?? CODEX_CATALOG_TIMEOUT_MS;
|
|
229
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
230
|
+
this.now = options.now ?? Date.now;
|
|
231
|
+
}
|
|
232
|
+
etag() {
|
|
233
|
+
return this.currentEtag;
|
|
234
|
+
}
|
|
235
|
+
async list(signal) {
|
|
236
|
+
return (await this.listWithAuthority(signal)).models;
|
|
237
|
+
}
|
|
238
|
+
async listWithAuthority(signal) {
|
|
239
|
+
throwIfAborted(signal);
|
|
240
|
+
this.currentEtag = undefined;
|
|
241
|
+
let credential;
|
|
242
|
+
try {
|
|
243
|
+
credential = await this.options.resolveCredential(signal);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
if (error instanceof LlmError && error.code === 'ABORTED')
|
|
247
|
+
throw error;
|
|
248
|
+
if (!(error instanceof LlmError) || error.code !== 'MISSING_CREDENTIAL') {
|
|
249
|
+
this.options.warn?.('native Codex credential is unavailable; model metadata is advisory');
|
|
250
|
+
}
|
|
251
|
+
return { models: [] };
|
|
252
|
+
}
|
|
253
|
+
throwIfAborted(signal);
|
|
254
|
+
const hash = nativeCodexAuthorityHash(credential.accountId);
|
|
255
|
+
const cached = this.snapshot?.clientVersion === this.clientVersion
|
|
256
|
+
&& this.snapshot.accountHash === hash
|
|
257
|
+
? this.snapshot
|
|
258
|
+
: undefined;
|
|
259
|
+
throwIfAborted(signal);
|
|
260
|
+
const age = cached === undefined ? Number.POSITIVE_INFINITY : this.now() - cached.fetchedAt;
|
|
261
|
+
if (cached !== undefined && age >= 0 && age <= this.cacheTtlMs) {
|
|
262
|
+
this.currentEtag = cached.etag;
|
|
263
|
+
return { models: cached.models, authorityHash: hash };
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
let refresh = this.refreshes.get(hash);
|
|
267
|
+
if (refresh === undefined) {
|
|
268
|
+
const started = this.fetchCatalog(credential, hash)
|
|
269
|
+
.then((fresh) => {
|
|
270
|
+
this.snapshot = fresh;
|
|
271
|
+
return fresh;
|
|
272
|
+
})
|
|
273
|
+
.finally(() => {
|
|
274
|
+
if (this.refreshes.get(hash) === started)
|
|
275
|
+
this.refreshes.delete(hash);
|
|
276
|
+
});
|
|
277
|
+
refresh = started;
|
|
278
|
+
this.refreshes.set(hash, refresh);
|
|
279
|
+
}
|
|
280
|
+
const fresh = await awaitWithSignal(refresh, signal);
|
|
281
|
+
this.currentEtag = fresh.etag;
|
|
282
|
+
return { models: fresh.models, authorityHash: hash };
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (error instanceof LlmError && error.code === 'ABORTED')
|
|
286
|
+
throw error;
|
|
287
|
+
if (error instanceof CatalogFetchError
|
|
288
|
+
&& error.allowsStale
|
|
289
|
+
&& cached !== undefined
|
|
290
|
+
&& age >= 0
|
|
291
|
+
&& age <= this.maxStaleMs) {
|
|
292
|
+
this.currentEtag = cached.etag;
|
|
293
|
+
this.options.warn?.(`native Codex catalog refresh failed (${error.code}); using bounded stale cache`);
|
|
294
|
+
return { models: cached.models, authorityHash: hash };
|
|
295
|
+
}
|
|
296
|
+
const code = error instanceof LlmError ? error.code : 'UNKNOWN';
|
|
297
|
+
this.options.warn?.(`native Codex catalog refresh failed (${code}); model metadata is advisory`);
|
|
298
|
+
return { models: [], authorityHash: hash };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async fetchCatalog(credential, hash, signal) {
|
|
302
|
+
const controller = new AbortController();
|
|
303
|
+
const abortFromCaller = () => { controller.abort(signal?.reason); };
|
|
304
|
+
if (signal !== undefined)
|
|
305
|
+
signal.addEventListener('abort', abortFromCaller, { once: true });
|
|
306
|
+
const timeout = setTimeout(() => { controller.abort(new Error('catalog timeout')); }, this.timeoutMs);
|
|
307
|
+
try {
|
|
308
|
+
throwIfAborted(signal);
|
|
309
|
+
const url = new URL(this.endpoint);
|
|
310
|
+
url.searchParams.set('client_version', this.clientVersion);
|
|
311
|
+
let response;
|
|
312
|
+
try {
|
|
313
|
+
response = await this.fetchImpl(url, {
|
|
314
|
+
method: 'GET',
|
|
315
|
+
redirect: 'error',
|
|
316
|
+
headers: {
|
|
317
|
+
authorization: `Bearer ${credential.accessToken}`,
|
|
318
|
+
'chatgpt-account-id': credential.accountId,
|
|
319
|
+
originator: ORIGINATOR,
|
|
320
|
+
...attributionHeaders(),
|
|
321
|
+
},
|
|
322
|
+
signal: controller.signal,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
if (signal?.aborted)
|
|
327
|
+
throw new LlmError('native Codex catalog request was aborted', 'ABORTED');
|
|
328
|
+
if (controller.signal.aborted) {
|
|
329
|
+
throw new CatalogFetchError('native Codex catalog request timed out', 'CATALOG_TIMEOUT', true, { cause: error });
|
|
330
|
+
}
|
|
331
|
+
throw new CatalogFetchError('native Codex catalog request failed', 'CATALOG_UNAVAILABLE', true, { cause: error });
|
|
332
|
+
}
|
|
333
|
+
throwIfAborted(signal);
|
|
334
|
+
if (!response.ok) {
|
|
335
|
+
const allowsStale = response.status === 408 || response.status === 429 || response.status >= 500;
|
|
336
|
+
const code = response.status === 401
|
|
337
|
+
? 'INVALID_CREDENTIAL'
|
|
338
|
+
: response.status === 403
|
|
339
|
+
? 'CATALOG_FORBIDDEN'
|
|
340
|
+
: 'CATALOG_HTTP_ERROR';
|
|
341
|
+
throw new CatalogFetchError(`native Codex catalog request failed (HTTP ${response.status})`, code, allowsStale);
|
|
342
|
+
}
|
|
343
|
+
let body;
|
|
344
|
+
try {
|
|
345
|
+
body = await boundedResponseText(response);
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
if (error instanceof CatalogFetchError)
|
|
349
|
+
throw error;
|
|
350
|
+
if (signal?.aborted)
|
|
351
|
+
throw new LlmError('native Codex catalog request was aborted', 'ABORTED');
|
|
352
|
+
if (controller.signal.aborted) {
|
|
353
|
+
throw new CatalogFetchError('native Codex catalog request timed out', 'CATALOG_TIMEOUT', true, { cause: error });
|
|
354
|
+
}
|
|
355
|
+
throw new CatalogFetchError('native Codex catalog response could not be read', 'CATALOG_UNAVAILABLE', true, { cause: error });
|
|
356
|
+
}
|
|
357
|
+
throwIfAborted(signal);
|
|
358
|
+
let payload;
|
|
359
|
+
try {
|
|
360
|
+
payload = JSON.parse(body);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
throw new CatalogFetchError('native Codex catalog response was not valid JSON', 'CATALOG_INVALID_RESPONSE', true, { cause: error });
|
|
364
|
+
}
|
|
365
|
+
const entry = {
|
|
366
|
+
fetchedAt: this.now(),
|
|
367
|
+
clientVersion: this.clientVersion,
|
|
368
|
+
accountHash: hash,
|
|
369
|
+
...nonEmptyString(response.headers.get('etag')) === undefined
|
|
370
|
+
? {}
|
|
371
|
+
: { etag: response.headers.get('etag') },
|
|
372
|
+
models: parseModelsPayload(payload),
|
|
373
|
+
};
|
|
374
|
+
throwIfAborted(signal);
|
|
375
|
+
return entry;
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
clearTimeout(timeout);
|
|
379
|
+
if (signal !== undefined)
|
|
380
|
+
signal.removeEventListener('abort', abortFromCaller);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
package/lib/endpoint.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Shared credential-bearing endpoint validation for catalog, HTTP, and WebSocket. */
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
4
|
+
function invalid(message, cause) {
|
|
5
|
+
return new LlmError(message, 'INVALID_ARGS', cause === undefined ? undefined : { cause });
|
|
6
|
+
}
|
|
7
|
+
function loopback(hostname) {
|
|
8
|
+
const unbracketed = hostname.startsWith('[') && hostname.endsWith(']')
|
|
9
|
+
? hostname.slice(1, -1) : hostname;
|
|
10
|
+
const family = isIP(unbracketed);
|
|
11
|
+
if (family === 4)
|
|
12
|
+
return unbracketed.split('.')[0] === '127';
|
|
13
|
+
return family === 6 && unbracketed === '::1';
|
|
14
|
+
}
|
|
15
|
+
/** Validate authority, identity components, and TLS before credentials are attached. */
|
|
16
|
+
export function nativeCodexEndpoint(endpoint, allowWebSocketProtocols = false) {
|
|
17
|
+
let url;
|
|
18
|
+
try {
|
|
19
|
+
url = new URL(endpoint);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw invalid('native Codex endpoint is invalid', error);
|
|
23
|
+
}
|
|
24
|
+
if (url.username.length > 0 || url.password.length > 0 || url.hash.length > 0) {
|
|
25
|
+
throw invalid('native Codex endpoint contains forbidden identity');
|
|
26
|
+
}
|
|
27
|
+
const local = loopback(url.hostname);
|
|
28
|
+
if (url.hostname !== 'chatgpt.com' && !local) {
|
|
29
|
+
throw invalid('native Codex endpoint authority is unsupported');
|
|
30
|
+
}
|
|
31
|
+
const secure = url.protocol === 'https:' || (allowWebSocketProtocols && url.protocol === 'wss:');
|
|
32
|
+
const localPlaintext = local && (url.protocol === 'http:'
|
|
33
|
+
|| (allowWebSocketProtocols && url.protocol === 'ws:'));
|
|
34
|
+
if (!secure && !localPlaintext) {
|
|
35
|
+
throw invalid('native Codex plaintext endpoint must be loopback');
|
|
36
|
+
}
|
|
37
|
+
return url;
|
|
38
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ import { Context, Service } from '@deepseek-ai/cordis';
|
|
|
3
3
|
import z from '@deepseek-ai/schemastery';
|
|
4
4
|
import { type IncomingMessage } from 'node:http';
|
|
5
5
|
import type { WebServer } from '@deepseek-ai/dsh-host-webserver';
|
|
6
|
+
import { CODEX_PROVIDER } from './native-adapter.js';
|
|
7
|
+
export { CODEX_PROVIDER, NATIVE_CODEX_PROVIDER } from './native-adapter.js';
|
|
8
|
+
export { normalizeUsage } from './usage.js';
|
|
9
|
+
export { CODEX_CLIENT_VERSION, TRACKED_CODEX_COMMIT, TRACKED_CODEX_RELEASE, TRACKED_CODEX_REPOSITORY, } from './upstream.js';
|
|
6
10
|
/** Persisted OAuth credential. */
|
|
7
11
|
export interface OpenAICodexCredential {
|
|
8
12
|
access: string;
|
|
@@ -14,24 +18,39 @@ export interface OpenAICodexCredential {
|
|
|
14
18
|
export interface Config {
|
|
15
19
|
path?: string;
|
|
16
20
|
dshHome?: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
windowSeconds?: number;
|
|
21
|
-
resetAt?: number;
|
|
22
|
-
}
|
|
23
|
-
interface UsageSummary {
|
|
24
|
-
planType?: string;
|
|
25
|
-
primary?: UsageWindow;
|
|
26
|
-
secondary?: UsageWindow;
|
|
27
|
-
limitReached?: boolean;
|
|
28
|
-
resetCredits?: number;
|
|
29
|
-
fetchedAt: number;
|
|
21
|
+
nativeAdapter?: boolean;
|
|
22
|
+
nativeCompatibilityRoute?: boolean;
|
|
23
|
+
nativeWebSocket?: boolean;
|
|
30
24
|
}
|
|
31
25
|
interface WebRuntimeValues {
|
|
32
26
|
lanAddresses: string[];
|
|
33
27
|
trustedHosts: string[];
|
|
34
28
|
}
|
|
29
|
+
export type CodexRouteOwner = 'native' | 'external' | 'unregistered';
|
|
30
|
+
export type NativeCodexRouteTransport = 'websocket-v2' | 'http-sse';
|
|
31
|
+
/** Host-observed ownership of the production Codex provider route. */
|
|
32
|
+
export interface CodexRouteStatus {
|
|
33
|
+
provider: typeof CODEX_PROVIDER;
|
|
34
|
+
owner: CodexRouteOwner;
|
|
35
|
+
active: boolean;
|
|
36
|
+
registeredName?: string;
|
|
37
|
+
transport?: NativeCodexRouteTransport;
|
|
38
|
+
compatibilityRoute: {
|
|
39
|
+
configured: boolean;
|
|
40
|
+
active: boolean;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
interface NativeRouteConfig {
|
|
44
|
+
nativeAdapter: boolean;
|
|
45
|
+
nativeCompatibilityRoute: boolean;
|
|
46
|
+
nativeWebSocket: boolean;
|
|
47
|
+
}
|
|
48
|
+
interface RouteRuntime {
|
|
49
|
+
listProviders(): Array<{
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
35
54
|
interface DeviceAuthorization {
|
|
36
55
|
deviceAuthId: string;
|
|
37
56
|
userCode: string;
|
|
@@ -47,8 +66,13 @@ interface ParsedDevicePoll {
|
|
|
47
66
|
value?: DeviceTokenCode;
|
|
48
67
|
message?: string;
|
|
49
68
|
}
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
interface TokenResponse {
|
|
70
|
+
access_token?: unknown;
|
|
71
|
+
refresh_token?: unknown;
|
|
72
|
+
expires_in?: unknown;
|
|
73
|
+
id_token?: unknown;
|
|
74
|
+
}
|
|
75
|
+
declare function parseTokenResponse(value: TokenResponse | null, previous?: OpenAICodexCredential): OpenAICodexCredential;
|
|
52
76
|
declare function parseAuthority(authority: string | undefined): URL | undefined;
|
|
53
77
|
declare function isLoopbackHostname(hostname: string): boolean;
|
|
54
78
|
declare function isTrustedHost(authority: string | undefined, trustedHosts: readonly string[]): boolean;
|
|
@@ -58,7 +82,9 @@ declare function callbackHostMatches(authority: string | undefined, redirectUri:
|
|
|
58
82
|
declare function startDeviceAuthorization(signal?: AbortSignal): Promise<DeviceAuthorization>;
|
|
59
83
|
declare function parseDevicePollResponse(response: Response): Promise<ParsedDevicePoll>;
|
|
60
84
|
declare function pollDeviceAuthorization(device: DeviceAuthorization, signal?: AbortSignal): Promise<DeviceTokenCode>;
|
|
85
|
+
declare function resolveCodexRouteStatus(config: NativeRouteConfig, llm: RouteRuntime | undefined): CodexRouteStatus;
|
|
61
86
|
export declare const internals: {
|
|
87
|
+
parseTokenResponse: typeof parseTokenResponse;
|
|
62
88
|
parseAuthority: typeof parseAuthority;
|
|
63
89
|
isLoopbackHostname: typeof isLoopbackHostname;
|
|
64
90
|
isTrustedHost: typeof isTrustedHost;
|
|
@@ -68,6 +94,7 @@ export declare const internals: {
|
|
|
68
94
|
startDeviceAuthorization: typeof startDeviceAuthorization;
|
|
69
95
|
parseDevicePollResponse: typeof parseDevicePollResponse;
|
|
70
96
|
pollDeviceAuthorization: typeof pollDeviceAuthorization;
|
|
97
|
+
resolveCodexRouteStatus: typeof resolveCodexRouteStatus;
|
|
71
98
|
};
|
|
72
99
|
declare module '@deepseek-ai/cordis' {
|
|
73
100
|
interface Context {
|
|
@@ -81,25 +108,44 @@ export declare class OpenAICodexAuth extends Service {
|
|
|
81
108
|
static Config: z<Config>;
|
|
82
109
|
static inject: string[];
|
|
83
110
|
private readonly filename;
|
|
111
|
+
private readonly routeConfig;
|
|
84
112
|
private readonly csrf;
|
|
85
113
|
private usageCache;
|
|
114
|
+
private usageAccountId;
|
|
115
|
+
private responseUsage;
|
|
116
|
+
private credentialAccountId;
|
|
86
117
|
private usageError;
|
|
87
118
|
private usageRefresh;
|
|
88
119
|
private usageGeneration;
|
|
120
|
+
private directUsageSequence;
|
|
121
|
+
private directUsageAccountId;
|
|
122
|
+
private usageHasDirectDefault;
|
|
89
123
|
private readonly codexTurns;
|
|
90
124
|
private loginFlow;
|
|
91
125
|
private startingDevice;
|
|
92
126
|
private startingBrowser;
|
|
93
127
|
private lastLoginError;
|
|
94
128
|
constructor(ctx: Context, config: Config);
|
|
129
|
+
private setCredentialAccount;
|
|
130
|
+
private acceptResponseUsage;
|
|
131
|
+
private acceptRateLimits;
|
|
95
132
|
private markCodexTurn;
|
|
96
133
|
private consumeCodexTurn;
|
|
97
134
|
private performUsageRefresh;
|
|
98
135
|
private refreshUsage;
|
|
99
136
|
private assertCredentialWritable;
|
|
100
|
-
private
|
|
101
|
-
|
|
137
|
+
private publishCredentialToken;
|
|
138
|
+
private restorePublishedCredential;
|
|
139
|
+
private publicationChangedError;
|
|
140
|
+
private failAfterPublicationRollback;
|
|
141
|
+
private commitCredential;
|
|
142
|
+
private resolveManagedCredentialLocked;
|
|
143
|
+
/** Return a valid managed bearer token, refreshing and persisting it when near expiry. */
|
|
102
144
|
bearerToken(signal?: AbortSignal): Promise<string | undefined>;
|
|
145
|
+
private externalNativeCredential;
|
|
146
|
+
private resolveNativeCredential;
|
|
147
|
+
private nativeRecoveryError;
|
|
148
|
+
private recoverNativeCredential;
|
|
103
149
|
private finishCredential;
|
|
104
150
|
private finishAuthorizationCode;
|
|
105
151
|
private settleFlow;
|