@modelprofile.com/authswitch 9.2.0 → 10.0.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_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/authority-contract.d.ts +52 -6
- package/dist_ts/authority-contract.js +1 -1
- package/dist_ts/classes.authoritybroker.d.ts +20 -4
- package/dist_ts/classes.authoritybroker.js +44 -8
- package/dist_ts/classes.authoritycli.d.ts +4 -1
- package/dist_ts/classes.authoritycli.js +42 -7
- package/dist_ts/classes.authorityclient.d.ts +9 -1
- package/dist_ts/classes.authorityclient.js +9 -3
- package/dist_ts/classes.authoritydaemon.js +8 -5
- package/dist_ts/classes.authoritydatabase.d.ts +6 -2
- package/dist_ts/classes.authoritydatabase.js +13 -6
- package/dist_ts/classes.authorityimport.d.ts +4 -4
- package/dist_ts/classes.authorityimport.js +8 -7
- package/dist_ts/classes.authoritymodels.d.ts +11 -3
- package/dist_ts/classes.authoritymodels.js +43 -13
- package/dist_ts/classes.authoritypreuse.d.ts +8 -2
- package/dist_ts/classes.authoritypreuse.js +27 -17
- package/dist_ts/classes.authorityusage.d.ts +4 -3
- package/dist_ts/classes.authorityusage.js +3 -5
- package/dist_ts/classes.cli.js +3 -5
- package/dist_ts/classes.codexpreuse.d.ts +14 -3
- package/dist_ts/classes.codexpreuse.js +127 -44
- package/dist_ts/classes.codexstatus.js +44 -95
- package/dist_ts/classes.login.d.ts +20 -10
- package/dist_ts/classes.login.js +14 -13
- package/dist_ts/classes.openaiprovider.d.ts +53 -0
- package/dist_ts/classes.openaiprovider.js +340 -0
- package/dist_ts/classes.opencodeharness.js +3 -2
- package/dist_ts/classes.service.js +3 -5
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/interfaces.openai.d.ts +118 -0
- package/dist_ts/interfaces.openai.js +3 -0
- package/dist_ts/openaiauth.d.ts +76 -0
- package/dist_ts/openaiauth.js +371 -0
- package/dist_ts/openaiusage.d.ts +59 -0
- package/dist_ts/openaiusage.js +222 -0
- package/dist_ts/plugins.d.ts +1 -3
- package/dist_ts/plugins.js +2 -4
- package/dist_ts/preuse.d.ts +50 -0
- package/dist_ts/preuse.js +63 -4
- package/dist_ts/ts_migration/legacysources/shared.js +4 -3
- package/openai-codex-license.txt +201 -0
- package/package.json +6 -4
- package/readme.md +86 -10
- package/third-party-notices.md +29 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/authority-contract.ts +56 -8
- package/ts/classes.authoritybroker.ts +52 -13
- package/ts/classes.authoritycli.ts +48 -6
- package/ts/classes.authorityclient.ts +11 -3
- package/ts/classes.authoritydaemon.ts +7 -4
- package/ts/classes.authoritydatabase.ts +14 -6
- package/ts/classes.authorityimport.ts +12 -12
- package/ts/classes.authoritymodels.ts +42 -14
- package/ts/classes.authoritypreuse.ts +30 -17
- package/ts/classes.authorityusage.ts +8 -9
- package/ts/classes.cli.ts +3 -3
- package/ts/classes.codexpreuse.ts +130 -40
- package/ts/classes.codexstatus.ts +40 -73
- package/ts/classes.login.ts +34 -21
- package/ts/classes.openaiprovider.ts +365 -0
- package/ts/classes.opencodeharness.ts +3 -2
- package/ts/classes.service.ts +3 -4
- package/ts/index.ts +2 -0
- package/ts/interfaces.openai.ts +142 -0
- package/ts/openaiauth.ts +422 -0
- package/ts/openaiusage.ts +247 -0
- package/ts/plugins.ts +1 -3
- package/ts/preuse.ts +88 -3
- package/ts/ts_migration/legacysources/shared.ts +4 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IOpenAiAccess } from './interfaces.openai.js';
|
|
2
3
|
import type { IAuthSwitchPreuseOperation } from './authority-contract.js';
|
|
3
4
|
import { AuthSwitchRefusal } from './authority-contract.js';
|
|
4
5
|
import { AuthSwitchAuthorityBroker } from './classes.authoritybroker.js';
|
|
@@ -6,10 +7,10 @@ import { AuthSwitchAuthorityDatabase } from './classes.authoritydatabase.js';
|
|
|
6
7
|
import type { IStoredAuthorityPreuseOperation, TStoredAuthorityPreuseProblem } from './classes.authoritymodels.js';
|
|
7
8
|
import { CodexPreuse, type ICodexPreuseLifecycle } from './classes.codexpreuse.js';
|
|
8
9
|
import type { IHarnessPreuseOptions, IHarnessPreuseResult } from './interfaces.harness.js';
|
|
9
|
-
import { validatePreuseOptions } from './preuse.js';
|
|
10
|
+
import { PreuseFailureError, PreuseLimitError, validatePreuseOptions } from './preuse.js';
|
|
10
11
|
|
|
11
12
|
export interface IAuthSwitchAuthorityPreuseExecutor {
|
|
12
|
-
runAccess(
|
|
13
|
+
runAccess(access: IOpenAiAccess,
|
|
13
14
|
options: IHarnessPreuseOptions, lifecycle: ICodexPreuseLifecycle): Promise<IHarnessPreuseResult>;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -41,7 +42,8 @@ const hash = (value: unknown): value is string => typeof value === 'string' && /
|
|
|
41
42
|
const publicOperation = (operation: IStoredAuthorityPreuseOperation): IAuthSwitchPreuseOperation => ({
|
|
42
43
|
id: operation.id, accountId: operation.accountId, loginId: operation.grantId,
|
|
43
44
|
purpose: operation.purpose, requestedModel: operation.requestedModel, model: operation.model,
|
|
44
|
-
state: operation.state, result: operation.result, problem: operation.problem,
|
|
45
|
+
state: operation.state, result: operation.result, problem: operation.problem, limit: operation.limit ?? null,
|
|
46
|
+
failure: operation.failure ?? null,
|
|
45
47
|
createdAt: operation.createdAt, updatedAt: operation.updatedAt,
|
|
46
48
|
finishedAt: operation.finishedAt, revision: operation.revision,
|
|
47
49
|
});
|
|
@@ -204,11 +206,8 @@ export class AuthSwitchAuthorityPreuse {
|
|
|
204
206
|
const access = await this.broker.resolveUsageAccess(context, undefined, this.minAccessValidityMs);
|
|
205
207
|
signal.throwIfAborted();
|
|
206
208
|
phase = operation.requestedModel === null ? 'catalog' : 'access';
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
accountId: access.accountId, isFedrampAccount: access.isFedrampAccount,
|
|
210
|
-
};
|
|
211
|
-
const result = await this.executor.runAccess(credential,
|
|
209
|
+
const result = await this.executor.runAccess({ accessToken: access.accessToken,
|
|
210
|
+
accountId: access.accountId, isFedrampAccount: access.isFedrampAccount },
|
|
212
211
|
{ prompt, ...(operation.requestedModel === null ? {} : { model: operation.requestedModel }), signal },
|
|
213
212
|
{ beforeRequest: async model => {
|
|
214
213
|
signal.throwIfAborted();
|
|
@@ -224,26 +223,40 @@ export class AuthSwitchAuthorityPreuse {
|
|
|
224
223
|
state: 'complete', result: { inputTokens: count(result.inputTokens), outputTokens: count(result.outputTokens),
|
|
225
224
|
totalTokens: count(result.totalTokens) }, problem: 'none', updatedAt: now, finishedAt: now,
|
|
226
225
|
revision: prior.revision + 1, updateId }));
|
|
227
|
-
} catch {
|
|
228
|
-
await this.fail(operation.id, phase, signal
|
|
226
|
+
} catch (error) {
|
|
227
|
+
await this.fail(operation.id, phase, signal,
|
|
228
|
+
error instanceof PreuseLimitError || error instanceof PreuseFailureError ? error : null);
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Settles a preuse that did not complete. A limit is named: the account's usage reading stopped it before
|
|
234
|
+
* anything was sent, or the provider refused the prompt -- before its answer began, which consumed nothing and
|
|
235
|
+
* settles `refused`, or during it, which stays `outcome_unknown`. Any other stated cause of a sent request is
|
|
236
|
+
* named as its `failure`.
|
|
237
|
+
*/
|
|
238
|
+
private async fail(operationId: string, phase: 'target' | 'access' | 'catalog', signal: AbortSignal,
|
|
239
|
+
stated: PreuseLimitError | PreuseFailureError | null): Promise<void> {
|
|
233
240
|
let current: IStoredAuthorityPreuseOperation;
|
|
234
241
|
try { current = await this.readStored(operationId); }
|
|
235
242
|
catch { return; }
|
|
236
243
|
if (!['preparing', 'request_may_have_started'].includes(current.state)) return;
|
|
237
244
|
const marked = current.state === 'request_may_have_started';
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
245
|
+
const stopped = this.closed ? 'authority_closing' : signal.aborted ? 'cancelled' : null;
|
|
246
|
+
const limit = stopped === null && stated instanceof PreuseLimitError && (stated.source === 'provider') === marked
|
|
247
|
+
? stated : null;
|
|
248
|
+
const failure = stopped === null && marked && stated instanceof PreuseFailureError ? stated.failure : null;
|
|
249
|
+
const problem: TStoredAuthorityPreuseProblem = stopped ?? (limit ? limit.limit.kind
|
|
250
|
+
: marked ? 'request_uncertain'
|
|
251
|
+
: phase === 'target' ? 'target_unavailable' : phase === 'catalog' ? 'catalog_unavailable' : 'access_unavailable');
|
|
252
|
+
const state: IStoredAuthorityPreuseOperation['state'] = !marked ? 'not_sent'
|
|
253
|
+
: limit && !limit.generationStarted ? 'refused' : 'outcome_unknown';
|
|
242
254
|
const updateId = plugins.crypto.randomUUID();
|
|
243
255
|
const now = new Date(this.now()).toISOString();
|
|
244
256
|
try {
|
|
245
|
-
await this.database.changePreuseOperation(updateId, operationId, prior => ({ ...prior,
|
|
246
|
-
|
|
257
|
+
await this.database.changePreuseOperation(updateId, operationId, prior => ({ ...prior, state, problem,
|
|
258
|
+
...(limit ? { limit: { ...limit.limit } } : {}),
|
|
259
|
+
...(failure ? { failure: { ...failure } } : {}),
|
|
247
260
|
updatedAt: now, finishedAt: now, revision: prior.revision + 1, updateId }));
|
|
248
261
|
} catch { /* A durable request marker remains recoverable as outcome_unknown on restart. */ }
|
|
249
262
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import type { AuthSwitchOpenAiProvider } from './classes.openaiprovider.js';
|
|
3
|
+
import type { IOpenAiAccountRateLimits, IOpenAiRateLimitDetails, IOpenAiRateLimitWindow } from './interfaces.openai.js';
|
|
2
4
|
import type { IClaudeLogin, IClaudeStatusRead } from './classes.claudestatus.js';
|
|
3
5
|
import type { IHarnessAccountStatus } from './interfaces.harness.js';
|
|
4
6
|
|
|
5
|
-
type TRateLimits =
|
|
6
|
-
type TAccessCredential = plugins.flexAccounts.IOpenAiChatGptAccessCredential;
|
|
7
|
+
type TRateLimits = IOpenAiAccountRateLimits;
|
|
7
8
|
|
|
8
9
|
export type TAuthSwitchUsageProblem =
|
|
9
10
|
| 'none' | 'unsupported' | 'access_unavailable' | 'provider_unavailable'
|
|
@@ -115,7 +116,7 @@ export interface IAuthSwitchAuthorityUsageOptions {
|
|
|
115
116
|
action: (source: { context: IAuthSwitchUsageContext; login: IClaudeLogin;
|
|
116
117
|
readContext(): Promise<IAuthSwitchUsageContext | null> }) => Promise<IAuthSwitchUsageSnapshot>): Promise<IAuthSwitchUsageSnapshot>;
|
|
117
118
|
store: IAuthSwitchUsageStore;
|
|
118
|
-
provider: Pick<
|
|
119
|
+
provider: Pick<AuthSwitchOpenAiProvider, 'getAccountRateLimits'>;
|
|
119
120
|
now?: () => number;
|
|
120
121
|
currentMaxAgeMs?: number;
|
|
121
122
|
maxStaleAgeMs?: number;
|
|
@@ -199,11 +200,11 @@ const publicClaudeStatus = (source: IHarnessAccountStatus): IAuthSwitchClaudeSta
|
|
|
199
200
|
|
|
200
201
|
/** Allowlist the provider projection before it enters a management DTO or stored usage record. */
|
|
201
202
|
const publicRateLimits = (source: TRateLimits): TRateLimits => {
|
|
202
|
-
const window = (value:
|
|
203
|
+
const window = (value: IOpenAiRateLimitWindow) => ({
|
|
203
204
|
usedPercent: value.usedPercent, limitWindowSeconds: value.limitWindowSeconds,
|
|
204
205
|
resetAfterSeconds: value.resetAfterSeconds, resetsAt: value.resetsAt,
|
|
205
206
|
});
|
|
206
|
-
const detail = (value:
|
|
207
|
+
const detail = (value: IOpenAiRateLimitDetails) => ({
|
|
207
208
|
allowed: value.allowed, limitReached: value.limitReached,
|
|
208
209
|
...(value.primaryWindow ? { primaryWindow: window(value.primaryWindow) } : {}),
|
|
209
210
|
...(value.secondaryWindow ? { secondaryWindow: window(value.secondaryWindow) } : {}),
|
|
@@ -511,10 +512,8 @@ export class AuthSwitchAuthorityUsage {
|
|
|
511
512
|
}
|
|
512
513
|
|
|
513
514
|
private query(access: IAuthSwitchUsageAccess): ReturnType<IAuthSwitchAuthorityUsageOptions['provider']['getAccountRateLimits']> {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
isFedrampAccount: access.isFedrampAccount };
|
|
517
|
-
return this.options.provider.getAccountRateLimits(credential, { timeoutMs: this.policy.providerTimeoutMs });
|
|
515
|
+
return this.options.provider.getAccountRateLimits({ accessToken: access.accessToken, accountId: access.accountId,
|
|
516
|
+
isFedrampAccount: access.isFedrampAccount }, { timeoutMs: this.policy.providerTimeoutMs });
|
|
518
517
|
}
|
|
519
518
|
|
|
520
519
|
/** Wait for admitted reads before the daemon disposes the shared provider or database. */
|
package/ts/classes.cli.ts
CHANGED
|
@@ -372,11 +372,11 @@ ${bold('Environment')}
|
|
|
372
372
|
const cancel = () => { cancelled = true; void handle?.cancel().catch(() => undefined); };
|
|
373
373
|
process.on('SIGINT', cancel); process.on('SIGTERM', cancel);
|
|
374
374
|
try {
|
|
375
|
-
handle = await harness.beginLogin({ providerId: provider.providerId, flow:
|
|
375
|
+
handle = await harness.beginLogin({ providerId: provider.providerId, flow: 'device' });
|
|
376
376
|
if (cancelled) await handle.cancel();
|
|
377
|
-
else
|
|
377
|
+
else {
|
|
378
378
|
process.stdout.write(`Open ${plainText(handle.prompt.verificationUrl)}\nCode: ${plainText(handle.prompt.userCode)}\nWaiting for login. Press Ctrl+C to cancel.\n`);
|
|
379
|
-
}
|
|
379
|
+
}
|
|
380
380
|
return this.reportOutcome((await handle.completion).outcome);
|
|
381
381
|
} finally {
|
|
382
382
|
process.removeListener('SIGINT', cancel); process.removeListener('SIGTERM', cancel);
|
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import type { ICodexIdentity } from './interfaces.js';
|
|
3
3
|
import type { IHarnessPreuseOptions, IHarnessPreuseResult } from './interfaces.harness.js';
|
|
4
|
-
import { PreuseError,
|
|
4
|
+
import { PreuseError, PreuseFailureError, PreuseLimitError, preuseMayHaveConsumed, validatePreuseOptions,
|
|
5
|
+
type IPreuseLimit } from './preuse.js';
|
|
6
|
+
import { parseOpenAiTokenClaims } from './openaiauth.js';
|
|
7
|
+
import { OpenAiBackendError, readOpenAiAccountUsage, type IOpenAiAccountUsage } from './openaiusage.js';
|
|
8
|
+
import { usageWindowPeriod } from './accounts.js';
|
|
9
|
+
import type { IOpenAiAccess } from './interfaces.openai.js';
|
|
10
|
+
|
|
11
|
+
const PREUSE_TIMEOUT_MS = 180_000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The limit the account's own usage reading says is reached, or null. Every window at its limit must reset
|
|
15
|
+
* before a prompt can pass, so the one that resets last is named.
|
|
16
|
+
*/
|
|
17
|
+
const reachedLimit = (usage: IOpenAiAccountUsage): IPreuseLimit | null => {
|
|
18
|
+
const limit = usage.rateLimits.rateLimit;
|
|
19
|
+
if (!limit || (limit.allowed && !limit.limitReached)) return null;
|
|
20
|
+
const exhausted = [limit.primaryWindow, limit.secondaryWindow]
|
|
21
|
+
.filter(window => window !== undefined && window.usedPercent >= 100)
|
|
22
|
+
.sort((left, right) => right!.resetsAt - left!.resetsAt)[0];
|
|
23
|
+
return { kind: 'usage_limit', window: exhausted ? usageWindowPeriod(exhausted.limitWindowSeconds) : null,
|
|
24
|
+
resetsAt: exhausted ? new Date(exhausted.resetsAt * 1000).toISOString() : null };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** A FlexHarness limit window label (`7d`, `5h`, `90m`) as a window name; a slot name says nothing about length. */
|
|
28
|
+
const windowName = (label: string | undefined): string | null => {
|
|
29
|
+
const match = /^([1-9][0-9]{0,5})([mhd])$/.exec(label ?? '');
|
|
30
|
+
return match ? usageWindowPeriod(Number(match[1]) * { m: 60, h: 3600, d: 86400 }[match[2] as 'm' | 'h' | 'd']) : null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** The first error in a cause chain that matches, so a wrapped failure is still told by its own class. */
|
|
34
|
+
const inChain = <T>(error: unknown, match: (value: unknown) => value is T): T | undefined => {
|
|
35
|
+
let current = error;
|
|
36
|
+
for (let depth = 0; depth < 8 && current !== undefined && current !== null; depth++) {
|
|
37
|
+
if (match(current)) return current;
|
|
38
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
};
|
|
5
42
|
|
|
6
43
|
export interface ICodexPreuseLifecycle {
|
|
7
44
|
/** Persist the may-have-started marker before the inference API is invoked. */
|
|
@@ -16,67 +53,65 @@ export class CodexPreuse {
|
|
|
16
53
|
validatePreuseOptions(optionsArg);
|
|
17
54
|
if (identityArg.kind !== 'chatgpt') throw new PreuseError('Codex preuse requires a ChatGPT subscription login; API-key logins have no subscription reset window.');
|
|
18
55
|
if (!identityArg.accountId) throw new PreuseError('This login has no account ID for preuse.');
|
|
19
|
-
let
|
|
20
|
-
let oauth: plugins.flexAccounts.IOpenAiChatGptOAuthCredential;
|
|
56
|
+
let access: IOpenAiAccess;
|
|
21
57
|
try {
|
|
22
58
|
const tokens = JSON.parse(rawArg).tokens;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
59
|
+
const claims = parseOpenAiTokenClaims(tokens.id_token);
|
|
60
|
+
if (typeof tokens.access_token !== 'string' || !tokens.access_token
|
|
61
|
+
|| claims.chatgptAccountId !== identityArg.accountId) throw new Error('Identity mismatch');
|
|
62
|
+
access = { accessToken: tokens.access_token, accountId: claims.chatgptAccountId,
|
|
63
|
+
isFedrampAccount: claims.chatgptAccountIsFedramp };
|
|
26
64
|
} catch {
|
|
27
65
|
throw new PreuseError('No complete, matching Codex login is available for preuse. Log in again with Codex and save the account.');
|
|
28
66
|
}
|
|
29
|
-
return this.
|
|
67
|
+
return this.runWithAccess(access, optionsArg);
|
|
30
68
|
}
|
|
31
69
|
|
|
32
70
|
/** Authority-owned access grants contain no refresh token and are never serialized by this helper. */
|
|
33
|
-
public async runAccess(
|
|
34
|
-
|
|
71
|
+
public async runAccess(accessArg: IOpenAiAccess, optionsArg: IHarnessPreuseOptions,
|
|
72
|
+
lifecycleArg: ICodexPreuseLifecycle): Promise<IHarnessPreuseResult> {
|
|
35
73
|
validatePreuseOptions(optionsArg);
|
|
36
|
-
if (!
|
|
37
|
-
|| typeof
|
|
38
|
-
|| typeof
|
|
39
|
-
|| typeof credentialArg.isFedrampAccount !== 'boolean') {
|
|
74
|
+
if (!accessArg || typeof accessArg.accessToken !== 'string' || !accessArg.accessToken
|
|
75
|
+
|| typeof accessArg.accountId !== 'string' || !accessArg.accountId
|
|
76
|
+
|| typeof accessArg.isFedrampAccount !== 'boolean') {
|
|
40
77
|
throw new PreuseError('No complete, matching Codex login is available for preuse. Log in again with Codex and save the account.');
|
|
41
78
|
}
|
|
42
|
-
return this.
|
|
43
|
-
|
|
44
|
-
isFedrampAccount: credentialArg.isFedrampAccount,
|
|
45
|
-
}, optionsArg, lifecycleArg);
|
|
79
|
+
return this.runWithAccess({ accessToken: accessArg.accessToken, accountId: accessArg.accountId,
|
|
80
|
+
isFedrampAccount: accessArg.isFedrampAccount }, optionsArg, lifecycleArg);
|
|
46
81
|
}
|
|
47
82
|
|
|
48
|
-
private async
|
|
49
|
-
credentialsArg: plugins.flexAuth.IOpenAiChatGptAuthCredentials, optionsArg: IHarnessPreuseOptions,
|
|
83
|
+
private async runWithAccess(accessArg: IOpenAiAccess, optionsArg: IHarnessPreuseOptions,
|
|
50
84
|
lifecycleArg?: ICodexPreuseLifecycle): Promise<IHarnessPreuseResult> {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
85
|
+
const timeout = AbortSignal.timeout(PREUSE_TIMEOUT_MS);
|
|
86
|
+
const signal = optionsArg.signal ? AbortSignal.any([optionsArg.signal, timeout]) : timeout;
|
|
87
|
+
await this.refuseReachedLimit(accessArg, signal);
|
|
54
88
|
let model = optionsArg.model;
|
|
55
89
|
let reasoningEffort: string | undefined;
|
|
56
90
|
if (!model) {
|
|
57
|
-
const adapter = new plugins.flexAccounts.OpenAiProviderAdapter({ fetch: this.fetcher });
|
|
58
91
|
try {
|
|
59
|
-
const catalog = await
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
: 'Could not read the account model catalog. No preuse prompt was sent.');
|
|
63
|
-
const selected = catalog.models.find(item => item.isDefault && item.inputModalities.includes('text'));
|
|
92
|
+
const catalog = await plugins.flexOpenAi.listOpenAiChatGptModels(accessArg,
|
|
93
|
+
{ signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]), fetch: this.fetcher });
|
|
94
|
+
const selected = catalog.find(item => item.isDefault && item.inputModalities.includes('text'));
|
|
64
95
|
if (!selected) throw new PreuseError('The account catalog has no default text model. Specify --model explicitly.');
|
|
65
96
|
model = selected.wireModel;
|
|
66
97
|
reasoningEffort = ['none', 'minimal', 'low'].find(effort => selected.reasoningEfforts.some(item => item.effort === effort)) ?? selected.defaultReasoningEffort;
|
|
67
98
|
} catch (error) {
|
|
68
99
|
if (error instanceof PreuseError) throw error;
|
|
69
|
-
throw new PreuseError(
|
|
70
|
-
|
|
100
|
+
throw new PreuseError(error instanceof plugins.flexOpenAi.OpenAiChatGptAuthError && error.status === 401
|
|
101
|
+
? 'The saved login has expired. Log in again with Codex and save it before preuse.'
|
|
102
|
+
: 'Could not read the account model catalog. No preuse prompt was sent.');
|
|
103
|
+
}
|
|
71
104
|
}
|
|
72
|
-
let
|
|
105
|
+
let phase: 'connecting' | 'marking' | 'sent' = 'connecting';
|
|
106
|
+
// The provider has begun to answer once the stream's first step starts; a refusal before that consumed nothing.
|
|
107
|
+
let generationStarted = false;
|
|
73
108
|
try {
|
|
74
109
|
signal.throwIfAborted();
|
|
75
|
-
const connection = plugins.
|
|
76
|
-
connection.settings.fetch = this.fetcher;
|
|
110
|
+
const connection = plugins.flexOpenAi.createOpenAiChatGptAccessModelConnection(accessArg, { fetch: this.fetcher });
|
|
77
111
|
const languageModel = plugins.flexOpenAi.createOpenAiModelProvider().getModel({ provider: 'openai', model, connection });
|
|
112
|
+
phase = 'marking';
|
|
78
113
|
await lifecycleArg?.beforeRequest(model);
|
|
79
|
-
|
|
114
|
+
phase = 'sent';
|
|
80
115
|
const stream = plugins.flexModels.streamText({
|
|
81
116
|
model: languageModel,
|
|
82
117
|
instructions: 'Answer the user prompt with plain text. Do not use tools.',
|
|
@@ -89,16 +124,71 @@ export class CodexPreuse {
|
|
|
89
124
|
});
|
|
90
125
|
// Drain the stream; generated prose is neither printed nor persisted.
|
|
91
126
|
for await (const part of stream.fullStream) {
|
|
92
|
-
if (part.type === '
|
|
127
|
+
if (part.type === 'start-step') generationStarted = true;
|
|
128
|
+
if (part.type === 'error') throw part.error;
|
|
129
|
+
if (part.type === 'abort') throw signal.reason;
|
|
93
130
|
}
|
|
94
131
|
const [finishReason, usage] = await Promise.all([stream.finishReason, stream.totalUsage]);
|
|
95
|
-
if (finishReason !== 'stop' && finishReason !== 'length')
|
|
132
|
+
if (finishReason !== 'stop' && finishReason !== 'length') {
|
|
133
|
+
throw new PreuseError(`Preuse did not complete: the answer ended with finish reason ${/^[a-z-]{1,32}$/.test(finishReason) ? finishReason : 'unknown'}. ${preuseMayHaveConsumed}`, true);
|
|
134
|
+
}
|
|
96
135
|
const count = (value: number | undefined) => Number.isSafeInteger(value) && value! >= 0 ? value : undefined;
|
|
97
136
|
return { model, inputTokens: count(usage.inputTokens), outputTokens: count(usage.outputTokens), totalTokens: count(usage.totalTokens) };
|
|
98
|
-
} catch {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error instanceof PreuseError) throw error;
|
|
139
|
+
throw this.failure(error, phase, generationStarted, optionsArg.signal, timeout);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Reads the account's usage before anything is sent, and refuses when a limit is reached: a prompt then only
|
|
145
|
+
* meets the provider's refusal. A reading that fails decides nothing -- the prompt is sent, and a limit the
|
|
146
|
+
* provider states instead is reported by name -- except a rejected token, which no prompt would pass either.
|
|
147
|
+
*/
|
|
148
|
+
private async refuseReachedLimit(accessArg: IOpenAiAccess, signalArg: AbortSignal): Promise<void> {
|
|
149
|
+
let usage: IOpenAiAccountUsage;
|
|
150
|
+
try {
|
|
151
|
+
usage = await readOpenAiAccountUsage(accessArg, { fetch: this.fetcher, signal: signalArg });
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (signalArg.aborted) throw new PreuseError('Preuse was cancelled before the prompt was sent.');
|
|
154
|
+
if (error instanceof OpenAiBackendError && error.reason === 'unauthorized') {
|
|
155
|
+
throw new PreuseError('The saved login has expired. Log in again with Codex and save it before preuse.');
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const limit = reachedLimit(usage);
|
|
160
|
+
if (limit) throw new PreuseLimitError(limit, 'usage', false, Date.now());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Names why a started preuse failed, without the provider's text: a limit, a rejected token, a deadline, a cancellation,
|
|
165
|
+
* or the cause FlexHarness describes (an HTTP status, a stream error and its code, a failed connection).
|
|
166
|
+
*/
|
|
167
|
+
private failure(errorArg: unknown, phaseArg: 'connecting' | 'marking' | 'sent', generationStartedArg: boolean,
|
|
168
|
+
callerSignalArg: AbortSignal | undefined, timeoutArg: AbortSignal): PreuseError {
|
|
169
|
+
if (phaseArg !== 'sent') {
|
|
170
|
+
return new PreuseError(callerSignalArg?.aborted ? 'Preuse was cancelled before the prompt was sent.'
|
|
171
|
+
: timeoutArg.aborted ? `Preuse stopped before the prompt was sent: ${PREUSE_TIMEOUT_MS / 1000} seconds passed.`
|
|
172
|
+
: phaseArg === 'marking' ? 'Preuse stopped before the prompt was sent: its request could not be recorded as started.'
|
|
173
|
+
: 'Preuse stopped before the prompt was sent: the model connection could not be prepared.');
|
|
174
|
+
}
|
|
175
|
+
if (callerSignalArg?.aborted) return new PreuseError(`Preuse was cancelled after the prompt was sent. ${preuseMayHaveConsumed}`, true);
|
|
176
|
+
if (timeoutArg.aborted) {
|
|
177
|
+
return new PreuseError(`Preuse did not complete: no answer within ${PREUSE_TIMEOUT_MS / 1000} seconds. ${preuseMayHaveConsumed}`, true);
|
|
178
|
+
}
|
|
179
|
+
const described = plugins.flexModels.describeModelFailure(errorArg);
|
|
180
|
+
if (described.kind === 'limit' && described.limit) {
|
|
181
|
+
return new PreuseLimitError({ kind: described.limit.kind, window: windowName(described.limit.window),
|
|
182
|
+
resetsAt: described.limit.resetsAt === undefined ? null : new Date(described.limit.resetsAt).toISOString() },
|
|
183
|
+
'provider', generationStartedArg, Date.now());
|
|
184
|
+
}
|
|
185
|
+
const auth = inChain(errorArg, (value): value is InstanceType<typeof plugins.flexOpenAi.OpenAiChatGptAuthError> =>
|
|
186
|
+
value instanceof plugins.flexOpenAi.OpenAiChatGptAuthError);
|
|
187
|
+
if (auth?.status === 401 && !generationStartedArg) {
|
|
188
|
+
return new PreuseError('The provider rejected the access token before answering; no tokens were consumed. '
|
|
189
|
+
+ 'Log in again with Codex and save the account before preuse.', true);
|
|
102
190
|
}
|
|
191
|
+
return new PreuseFailureError({ kind: described.kind === 'limit' ? 'unknown' : described.kind,
|
|
192
|
+
status: described.status ?? null, providerCode: described.providerCode ?? null });
|
|
103
193
|
}
|
|
104
194
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ICodexIdentity } from './interfaces.js';
|
|
2
2
|
import type { IHarnessAccountStatus, IHarnessStatusOptions, IHarnessStatusSummary } from './interfaces.harness.js';
|
|
3
|
-
import {
|
|
3
|
+
import { OpenAiBackendError, projectOpenAiAccountUsage, readChatGptBackend } from './openaiusage.js';
|
|
4
|
+
import type { IOpenAiRateLimitDetails } from './interfaces.openai.js';
|
|
4
5
|
import { usageWindowPeriod } from './accounts.js';
|
|
5
6
|
import { latestRetryAt, retryAtFrom } from './ratelimit.js';
|
|
6
7
|
|
|
@@ -23,7 +24,6 @@ const boolean = (valueArg: unknown): boolean => {
|
|
|
23
24
|
if (typeof valueArg !== 'boolean') throw new Error('Unexpected boolean status field.');
|
|
24
25
|
return valueArg;
|
|
25
26
|
};
|
|
26
|
-
const timestamp = (secondsArg: unknown): string => new Date(numeric(secondsArg) * 1000).toISOString();
|
|
27
27
|
const count = (valueArg: unknown): number => {
|
|
28
28
|
const value = numeric(valueArg);
|
|
29
29
|
if (!Number.isSafeInteger(value)) throw new Error('Invalid status count.');
|
|
@@ -106,43 +106,26 @@ export class CodexAccountStatus {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
private async get(pathArg: string, tokenArg: string, accountIdArg: string, cancelArg?: AbortSignal): Promise<Record<string, unknown>> {
|
|
109
|
-
const
|
|
110
|
-
const signal = cancelArg ? AbortSignal.any([cancelArg, timeout]) : timeout;
|
|
109
|
+
const billing = pathArg === 'accounts/check/v4-2023-04-27';
|
|
111
110
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
'User-Agent': `authswitch/${commitinfo.version}`, Accept: 'application/json',
|
|
116
|
-
...(pathArg === 'accounts/check/v4-2023-04-27' ? { originator: 'Codex Desktop', 'OAI-Language': 'en', 'User-Agent': billingUserAgent } : {}) },
|
|
111
|
+
return await readChatGptBackend(pathArg, { accessToken: tokenArg, accountId: accountIdArg, isFedrampAccount: false }, {
|
|
112
|
+
fetch: this.fetcher, signal: cancelArg,
|
|
113
|
+
...(billing ? { originator: 'Codex Desktop', headers: { 'OAI-Language': 'en', 'User-Agent': billingUserAgent } } : {}),
|
|
117
114
|
});
|
|
118
|
-
if (!response.ok) {
|
|
119
|
-
await response.body?.cancel();
|
|
120
|
-
if (response.headers.get('cf-mitigated') === 'challenge') throw new StatusRequestError('Blocked by a service verification challenge (Cloudflare); this lookup is unavailable to the current HTTP client.');
|
|
121
|
-
if (response.status === 429) throw new StatusRateLimitError(retryAtFrom(response.headers.get('retry-after'), this.now()));
|
|
122
|
-
if (response.status === 401) throw new StatusRequestError('Login expired or was rejected. Log in again with the account’s harness, then save the account.');
|
|
123
|
-
if (response.status === 403) throw new StatusRequestError('Access denied by the service (HTTP 403).');
|
|
124
|
-
throw new StatusRequestError(`Service returned HTTP ${response.status}.`);
|
|
125
|
-
}
|
|
126
|
-
const reader = response.body?.getReader();
|
|
127
|
-
if (!reader) throw new StatusRequestError('The service returned an empty response.');
|
|
128
|
-
const chunks: Uint8Array[] = [];
|
|
129
|
-
let size = 0;
|
|
130
|
-
try {
|
|
131
|
-
while (true) {
|
|
132
|
-
const part = await reader.read();
|
|
133
|
-
if (part.done) break;
|
|
134
|
-
size += part.value.byteLength;
|
|
135
|
-
if (size > 1024 * 1024) { await reader.cancel(); throw new StatusRequestError('The status response is too large.'); }
|
|
136
|
-
chunks.push(part.value);
|
|
137
|
-
}
|
|
138
|
-
} finally { reader.releaseLock(); }
|
|
139
|
-
try { return record(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
|
|
140
|
-
catch { throw new StatusRequestError('The service returned invalid status data.'); }
|
|
141
115
|
} catch (errorArg) {
|
|
142
116
|
// Never surface remote bodies, request headers, token strings or arbitrary fetch errors.
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
117
|
+
if (!(errorArg instanceof OpenAiBackendError)) throw new Error('The status service could not be reached.');
|
|
118
|
+
switch (errorArg.reason) {
|
|
119
|
+
case 'challenge': throw new StatusRequestError('Blocked by a service verification challenge (Cloudflare); this lookup is unavailable to the current HTTP client.');
|
|
120
|
+
case 'rate_limited': throw new StatusRateLimitError(retryAtFrom(errorArg.retryAfter, this.now()));
|
|
121
|
+
case 'unauthorized': throw new StatusRequestError('Login expired or was rejected. Log in again with the account’s harness, then save the account.');
|
|
122
|
+
case 'forbidden': throw new StatusRequestError('Access denied by the service (HTTP 403).');
|
|
123
|
+
case 'http': throw new StatusRequestError(`Service returned HTTP ${errorArg.status}.`);
|
|
124
|
+
case 'protocol': throw new StatusRequestError('The service returned invalid or oversized status data.');
|
|
125
|
+
case 'timeout': throw new Error('The status request timed out.');
|
|
126
|
+
case 'aborted': throw new Error('The status request was cancelled.');
|
|
127
|
+
case 'network': throw new Error('The status service could not be reached.');
|
|
128
|
+
}
|
|
146
129
|
}
|
|
147
130
|
}
|
|
148
131
|
|
|
@@ -178,27 +161,22 @@ export class CodexAccountStatus {
|
|
|
178
161
|
}
|
|
179
162
|
|
|
180
163
|
private limits(bodyArg: Record<string, unknown>, accountIdArg: string): Pick<IHarnessAccountStatus, 'facts' | 'summary'> {
|
|
181
|
-
|
|
182
|
-
|
|
164
|
+
const usage = projectOpenAiAccountUsage(bodyArg, new Date(this.now()).toISOString());
|
|
165
|
+
if (usage.accountId !== null && usage.accountId !== accountIdArg) throw new Error('Account mismatch.');
|
|
166
|
+
const plan = usage.rateLimits.plan;
|
|
183
167
|
const facts: IHarnessAccountStatus['facts'] = [{ section: 'Subscription', summaryKey: 'subscription', label: 'Subscription plan', value: `${plan} (live)` }];
|
|
184
168
|
const usageWindows: NonNullable<IHarnessStatusSummary['usageWindows']> = [];
|
|
185
169
|
const summary: IHarnessStatusSummary = { subscription: { plan, source: 'live' }, usageWindows };
|
|
186
|
-
const appendLimit = (nameArg: string,
|
|
187
|
-
if (
|
|
188
|
-
const limit = record(valueArg);
|
|
170
|
+
const appendLimit = (nameArg: string, limitArg: IOpenAiRateLimitDetails | undefined, scopeArg: 'account' | 'feature') => {
|
|
171
|
+
if (!limitArg) return;
|
|
189
172
|
// The service's own reading of the same windows, so a view that shows the windows need not repeat it.
|
|
190
|
-
facts.push({ section: 'Limits & credits', summaryKey: 'usageWindows', label: `${nameArg} usage allowed`, value:
|
|
191
|
-
facts.push({ section: 'Limits & credits', summaryKey: 'usageWindows', label: `${nameArg} limit reached`, value:
|
|
173
|
+
facts.push({ section: 'Limits & credits', summaryKey: 'usageWindows', label: `${nameArg} usage allowed`, value: limitArg.allowed ? 'yes' : 'no' });
|
|
174
|
+
facts.push({ section: 'Limits & credits', summaryKey: 'usageWindows', label: `${nameArg} limit reached`, value: limitArg.limitReached ? 'yes' : 'no' });
|
|
192
175
|
// Both slots are read before either is named: only a second window of the same length makes the
|
|
193
176
|
// vendor's slot worth showing, and that is decided from the pair.
|
|
194
|
-
const windows = (['
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return [{
|
|
198
|
-
slot: slotArg === 'primary_window' ? 'primary' : 'secondary',
|
|
199
|
-
usedPercent: numeric(window.used_percent), durationSeconds: numeric(window.limit_window_seconds), resetAt: timestamp(window.reset_at),
|
|
200
|
-
}];
|
|
201
|
-
});
|
|
177
|
+
const windows = ([['primary', limitArg.primaryWindow], ['secondary', limitArg.secondaryWindow]] as const)
|
|
178
|
+
.flatMap(([slot, window]) => window ? [{ slot, usedPercent: window.usedPercent,
|
|
179
|
+
durationSeconds: window.limitWindowSeconds, resetAt: new Date(window.resetsAt * 1000).toISOString() }] : []);
|
|
202
180
|
for (const window of windows) {
|
|
203
181
|
// Named by its length, as Codex names its own windows: a slot carries no length, and which
|
|
204
182
|
// window the service puts in it varies by plan and by which limit is currently binding.
|
|
@@ -208,31 +186,20 @@ export class CodexAccountStatus {
|
|
|
208
186
|
facts.push({ section: 'Usage', summaryKey: 'usageWindows', label, value: `${window.usedPercent}% used, ${Math.max(0, 100 - window.usedPercent)}% remaining; resets ${window.resetAt}` });
|
|
209
187
|
}
|
|
210
188
|
};
|
|
211
|
-
appendLimit('Codex',
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
appendLimit(text(limit.limit_name), limit.rate_limit, 'feature');
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (bodyArg.credits != null) {
|
|
220
|
-
const credits = record(bodyArg.credits);
|
|
221
|
-
const unlimited = boolean(credits.unlimited);
|
|
222
|
-
const hasCredits = boolean(credits.has_credits);
|
|
223
|
-
facts.push({ section: 'Limits & credits', label: 'Credits', value: unlimited ? 'unlimited' : credits.balance != null ? text(credits.balance) : hasCredits ? 'available (balance not returned)' : 'none available' });
|
|
189
|
+
appendLimit('Codex', usage.rateLimits.rateLimit, 'account');
|
|
190
|
+
for (const limit of usage.rateLimits.additionalRateLimits) appendLimit(limit.limitName, limit.rateLimit, 'feature');
|
|
191
|
+
if (usage.credits) {
|
|
192
|
+
const credits = usage.credits;
|
|
193
|
+
facts.push({ section: 'Limits & credits', label: 'Credits', value: credits.unlimited ? 'unlimited' : credits.balance !== undefined ? credits.balance : credits.hasCredits ? 'available (balance not returned)' : 'none available' });
|
|
224
194
|
}
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (spend.
|
|
229
|
-
const limit = record(spend.individual_limit);
|
|
230
|
-
facts.push({ section: 'Limits & credits', label: 'Individual spend', value: `${text(limit.used)} used of ${text(limit.limit)}; ${text(limit.remaining)} remaining; resets ${timestamp(limit.reset_at)}` });
|
|
231
|
-
}
|
|
195
|
+
if (usage.spendControl) {
|
|
196
|
+
facts.push({ section: 'Limits & credits', label: 'Spend limit reached', value: usage.spendControl.reached ? 'yes' : 'no' });
|
|
197
|
+
const limit = usage.spendControl.individualLimit;
|
|
198
|
+
if (limit) facts.push({ section: 'Limits & credits', label: 'Individual spend', value: `${limit.used} used of ${limit.limit}; ${limit.remaining} remaining; resets ${new Date(limit.resetsAt * 1000).toISOString()}` });
|
|
232
199
|
}
|
|
233
|
-
if (
|
|
234
|
-
if (
|
|
235
|
-
summary.resets = { available:
|
|
200
|
+
if (usage.rateLimits.rateLimitReachedType !== undefined) facts.push({ section: 'Limits & credits', label: 'Limit state', value: usage.rateLimits.rateLimitReachedType });
|
|
201
|
+
if (usage.rateLimits.rateLimitResetCreditsAvailableCount !== undefined) {
|
|
202
|
+
summary.resets = { available: usage.rateLimits.rateLimitResetCreditsAvailableCount };
|
|
236
203
|
facts.push({ section: 'Earned resets', summaryKey: 'resets', label: 'Available earned resets', value: String(summary.resets.available) });
|
|
237
204
|
}
|
|
238
205
|
else facts.push({ section: 'Earned resets', summaryKey: 'resets', label: 'Available earned resets', value: 'Not returned by the limits service' });
|