@bitkyc08/opencodex 2.27.0 → 2.28.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/gui/dist/assets/{index-7jlKgmJd.js → index-D2sP-biU.js} +11 -11
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +59 -0
- package/src/adapters/base.ts +2 -0
- package/src/adapters/google-antigravity-replay.ts +16 -8
- package/src/adapters/google.ts +21 -4
- package/src/adapters/openai-chat.ts +151 -54
- package/src/adapters/openai-responses.ts +37 -0
- package/src/cli/index.ts +19 -6
- package/src/codex/account-usability.ts +3 -0
- package/src/codex/auth-api.ts +22 -5
- package/src/codex/auth-context.ts +55 -2
- package/src/codex/catalog/metadata.ts +17 -3
- package/src/codex/catalog/native-models.ts +22 -14
- package/src/codex/catalog/sync.ts +57 -11
- package/src/codex/convergence.ts +61 -13
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/quota.ts +28 -3
- package/src/codex/routing.ts +14 -8
- package/src/generated/compatibility-version.json +51 -39
- package/src/lib/destination-policy.ts +47 -0
- package/src/lib/shadow-call.ts +15 -0
- package/src/oauth/index.ts +33 -5
- package/src/oauth/store.ts +11 -5
- package/src/providers/fastwire.ts +39 -8
- package/src/providers/quota.ts +9 -2
- package/src/providers/registry.ts +74 -5
- package/src/providers/service-tier.ts +16 -8
- package/src/responses/parser.ts +3 -9
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/router.ts +7 -0
- package/src/routing/capability.ts +26 -9
- package/src/routing/compatibility/behavior.ts +41 -3
- package/src/server/chat-native.ts +11 -2
- package/src/server/index.ts +54 -8
- package/src/server/management/agent-settings-routes.ts +16 -2
- package/src/server/request-log.ts +31 -0
- package/src/server/responses/compact.ts +54 -7
- package/src/server/responses/core.ts +246 -39
- package/src/server/responses/responses-field-backfill.ts +88 -6
- package/src/server/responses/terminal-guard.ts +10 -0
- package/src/server/responses-tool-search-repair.ts +217 -0
- package/src/server/system-env.ts +74 -5
- package/src/usage/log.ts +4 -0
package/src/codex/convergence.ts
CHANGED
|
@@ -66,6 +66,17 @@ import {
|
|
|
66
66
|
supportedCodexReasoningEffortsFromObservedCatalog,
|
|
67
67
|
} from "./catalog/effort";
|
|
68
68
|
import { codexRuntimeStatePath, peekCodexRuntimeProcessCache } from "./runtime";
|
|
69
|
+
import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "./account-namespaces";
|
|
70
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
71
|
+
import {
|
|
72
|
+
availableAccountGatedNativeModels,
|
|
73
|
+
isCodexModelEntitlementSnapshotCurrent,
|
|
74
|
+
resolveCodexModelEntitlements,
|
|
75
|
+
type CodexModelEntitlementSnapshot,
|
|
76
|
+
} from "./model-entitlements";
|
|
77
|
+
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
|
|
78
|
+
import { providerCodexAccountMode } from "../providers/registry";
|
|
79
|
+
import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
69
80
|
import { withCatalogWriteSerialization } from "./catalog-write-serialization";
|
|
70
81
|
import {
|
|
71
82
|
publishHashedCodexCatalogBackup,
|
|
@@ -95,7 +106,7 @@ export interface CatalogWriteReceipt {
|
|
|
95
106
|
|
|
96
107
|
export type CodexCatalogCommitResult =
|
|
97
108
|
| { readonly kind: "committed"; readonly changed: boolean; readonly writes: CatalogWriteReceipt }
|
|
98
|
-
| { readonly kind: "stale"; readonly reason: "generation" | "home-selection" | "source-observation" | "process-local" | "target-identity" | "candidate-consumed" }
|
|
109
|
+
| { readonly kind: "stale"; readonly reason: "generation" | "home-selection" | "source-observation" | "process-local" | "target-identity" | "candidate-consumed" | "account-entitlement" }
|
|
99
110
|
| { readonly kind: "refused"; readonly reason: "source-unreadable" | "source-ambiguous" | "target-unsafe" }
|
|
100
111
|
| { readonly kind: "failed"; readonly surface: "disk"; readonly writes: CatalogWriteReceipt };
|
|
101
112
|
|
|
@@ -122,6 +133,7 @@ interface CandidateState {
|
|
|
122
133
|
readonly legacyBackup?: PreparedCatalogFileWrite;
|
|
123
134
|
readonly changed: boolean;
|
|
124
135
|
readonly notices: readonly CatalogNotice[];
|
|
136
|
+
readonly modelEntitlements: CodexModelEntitlementSnapshot;
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
const candidateStates = new WeakMap<object, CandidateState>();
|
|
@@ -217,6 +229,7 @@ function prepareCatalog(
|
|
|
217
229
|
baseline: ReadonlyMap<string, number>,
|
|
218
230
|
baselineCatalogModels: readonly Readonly<Record<string, unknown>>[],
|
|
219
231
|
degradedProviderNames: ReadonlySet<string>,
|
|
232
|
+
modelEntitlements: CodexModelEntitlementSnapshot,
|
|
220
233
|
nativeRecoverySources: readonly (readonly RawEntry[])[] = [],
|
|
221
234
|
observedAccountNativeEntries: readonly RawEntry[] = [],
|
|
222
235
|
): RawCatalog {
|
|
@@ -229,19 +242,46 @@ function prepareCatalog(
|
|
|
229
242
|
const multiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2"
|
|
230
243
|
? config.multiAgentMode : "default";
|
|
231
244
|
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
232
|
-
const
|
|
245
|
+
const bareEligibleAccountIds = providerCodexAccountMode(
|
|
246
|
+
OPENAI_CODEX_PROVIDER_ID,
|
|
247
|
+
config.providers[OPENAI_CODEX_PROVIDER_ID],
|
|
248
|
+
) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
|
|
249
|
+
const availableBareGatedNativeSlugs = availableAccountGatedNativeModels(
|
|
250
|
+
modelEntitlements,
|
|
251
|
+
bareEligibleAccountIds,
|
|
252
|
+
);
|
|
253
|
+
const availableAccountGatedNativeSlugs = availableAccountGatedNativeModels(modelEntitlements);
|
|
254
|
+
const availableBareNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
255
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
256
|
+
));
|
|
257
|
+
const availableAccountNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
258
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableAccountGatedNativeSlugs.has(slug)
|
|
259
|
+
));
|
|
260
|
+
const suppressedBareNativeSlugs = new Set([
|
|
261
|
+
...desktopAllowlistSuppressedNativeSlugs(config),
|
|
262
|
+
...[...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(slug => !availableBareGatedNativeSlugs.has(slug)),
|
|
263
|
+
]);
|
|
233
264
|
const hasPhysicalComboProvider = Object.hasOwn(config.providers, COMBO_NAMESPACE);
|
|
234
265
|
const enabledProviders = Object.entries(config.providers).filter(([, provider]) => provider.disabled !== true);
|
|
235
266
|
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
236
267
|
const accountSelectors = shouldIncludeAccountBoundNativeOpenAi(config)
|
|
237
268
|
? visibleCodexAccountSelectors(config)
|
|
238
269
|
: [];
|
|
239
|
-
const
|
|
240
|
-
? accountBoundNativeOpenAiSlugs(observedAccountNativeEntries)
|
|
241
|
-
: [];
|
|
270
|
+
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
242
271
|
const accountNativeSlugsBySelector = accountSelectors.length > 0
|
|
243
|
-
? accountBoundNativeOpenAiSlugsBySelector(config, observedAccountNativeEntries)
|
|
272
|
+
? new Map([...accountBoundNativeOpenAiSlugsBySelector(config, observedAccountNativeEntries)].map(([selector, slugs]) => {
|
|
273
|
+
const target = accountTargets.get(selector);
|
|
274
|
+
const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
|
|
275
|
+
const entitled = accountId ? modelEntitlements.modelsByAccount.get(accountId) : undefined;
|
|
276
|
+
const confirmed = accountId ? modelEntitlements.confirmedAccountIds.has(accountId) : false;
|
|
277
|
+
return [selector, slugs.filter(slug => (
|
|
278
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || (confirmed && entitled?.has(slug) === true)
|
|
279
|
+
))] as const;
|
|
280
|
+
}))
|
|
244
281
|
: new Map<string, readonly string[]>();
|
|
282
|
+
const accountNativeSlugs = accountSelectors.length > 0
|
|
283
|
+
? [...new Set([...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]))]
|
|
284
|
+
: [];
|
|
245
285
|
// Unknown account-native ids have no safe bare/global identity. They are only projected through
|
|
246
286
|
// selector-qualified rows when a live selector is configured.
|
|
247
287
|
const observedNativeSlugs: string[] = [];
|
|
@@ -269,7 +309,7 @@ function prepareCatalog(
|
|
|
269
309
|
? []
|
|
270
310
|
: buildCatalogEntriesFromObservedState({
|
|
271
311
|
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
272
|
-
gptSlugs:
|
|
312
|
+
gptSlugs: availableAccountNativeSlugs,
|
|
273
313
|
goModels: [],
|
|
274
314
|
featured,
|
|
275
315
|
wsEnabled: websocketsEnabled(config),
|
|
@@ -314,7 +354,7 @@ function prepareCatalog(
|
|
|
314
354
|
suppressedBareNativeSlugs,
|
|
315
355
|
policy: {
|
|
316
356
|
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
317
|
-
nativeBackfillSlugs: [...
|
|
357
|
+
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
|
|
318
358
|
warningPolicy: "suppress",
|
|
319
359
|
},
|
|
320
360
|
});
|
|
@@ -356,11 +396,14 @@ export async function gatherCodexCatalogCandidate(
|
|
|
356
396
|
const providerModelOutcomes: CatalogGatherProviderModelOutcome[] = [];
|
|
357
397
|
const discoveryPolicies: CatalogProviderDiscoveryPolicySnapshot[] = [];
|
|
358
398
|
providerGatherStarted = true;
|
|
359
|
-
const routedModels = await
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
399
|
+
const [routedModels, modelEntitlements] = await Promise.all([
|
|
400
|
+
gatherRoutedModelsForCatalogGather(snapshot.config, session, {
|
|
401
|
+
providerAuthOutcomes: authOutcomes,
|
|
402
|
+
providerModelOutcomes,
|
|
403
|
+
discoveryPolicySnapshots: discoveryPolicies,
|
|
404
|
+
}),
|
|
405
|
+
resolveCodexModelEntitlements(snapshot.config),
|
|
406
|
+
]);
|
|
364
407
|
const processLocal = processEvidence(source);
|
|
365
408
|
const sourceEvidence = sealCatalogGatherEvidenceSession(session);
|
|
366
409
|
if (!same(sourceEvidence.required, snapshot.sourceEvidence.required)) {
|
|
@@ -399,6 +442,7 @@ export async function gatherCodexCatalogCandidate(
|
|
|
399
442
|
new Set(providerModelOutcomes
|
|
400
443
|
.filter(outcome => outcome.state === "degraded")
|
|
401
444
|
.map(outcome => outcome.provider)),
|
|
445
|
+
modelEntitlements,
|
|
402
446
|
[
|
|
403
447
|
catalogFrom(keyedBackupBytes)?.models ?? [],
|
|
404
448
|
catalogFrom(legacyBackupBytes)?.models ?? [],
|
|
@@ -452,6 +496,7 @@ export async function gatherCodexCatalogCandidate(
|
|
|
452
496
|
changed: Buffer.from(activeBytes ?? []).toString("utf8") !== preparedCatalogBytes
|
|
453
497
|
|| Buffer.from(cacheBytes ?? []).toString("utf8") !== preparedCacheBytes,
|
|
454
498
|
notices: Object.freeze([...notices]),
|
|
499
|
+
modelEntitlements,
|
|
455
500
|
});
|
|
456
501
|
return { kind: "candidate", candidate };
|
|
457
502
|
} catch (error) {
|
|
@@ -469,6 +514,9 @@ export async function gatherCodexCatalogCandidate(
|
|
|
469
514
|
}
|
|
470
515
|
|
|
471
516
|
function revalidateCandidate(state: CandidateState): CodexCatalogCommitResult | null {
|
|
517
|
+
if (!isCodexModelEntitlementSnapshotCurrent(state.modelEntitlements)) {
|
|
518
|
+
return { kind: "stale", reason: "account-entitlement" };
|
|
519
|
+
}
|
|
472
520
|
let session: CatalogFilesystemEvidenceSession;
|
|
473
521
|
let validatingTargets = false;
|
|
474
522
|
try {
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
3
|
+
import type { OcxConfig } from "../types";
|
|
4
|
+
import { isSelectableCodexPoolAccount } from "./account-id";
|
|
5
|
+
import { getValidCodexToken, readCodexAccountRecord } from "./account-store";
|
|
6
|
+
import { getMainAccountToken, MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
7
|
+
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
|
|
8
|
+
|
|
9
|
+
const CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models?client_version=0.0.0";
|
|
10
|
+
const MODEL_ROSTER_TTL_MS = 5 * 60_000;
|
|
11
|
+
const MODEL_ROSTER_FAILURE_TTL_MS = 15_000;
|
|
12
|
+
const MODEL_ROSTER_TIMEOUT_MS = 8_000;
|
|
13
|
+
const MODEL_ROSTER_MAX_BYTES = 2 * 1024 * 1024;
|
|
14
|
+
const MODEL_ROSTER_CACHE_MAX = 64;
|
|
15
|
+
const DIRECT_CALLER_ACCOUNT_PREFIX = "__direct_codex__:";
|
|
16
|
+
|
|
17
|
+
export interface CodexModelEntitlementCredentialSnapshot {
|
|
18
|
+
readonly accountId: string;
|
|
19
|
+
readonly accessToken: string;
|
|
20
|
+
readonly chatgptAccountId: string;
|
|
21
|
+
/** Stable local identity for rejecting a catalog commit after credential replacement. */
|
|
22
|
+
readonly credentialIdentity: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface CachedAccountModels {
|
|
26
|
+
readonly credentialIdentity: string;
|
|
27
|
+
readonly expiresAt: number;
|
|
28
|
+
readonly models: ReadonlySet<string>;
|
|
29
|
+
readonly confirmed: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CodexModelEntitlementSnapshot {
|
|
33
|
+
readonly modelsByAccount: ReadonlyMap<string, ReadonlySet<string>>;
|
|
34
|
+
readonly confirmedAccountIds: ReadonlySet<string>;
|
|
35
|
+
readonly credentialIdentities: ReadonlyMap<string, string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CodexModelEntitlementResolveOptions {
|
|
39
|
+
readonly fetcher?: typeof fetch;
|
|
40
|
+
readonly now?: number;
|
|
41
|
+
/** Test-only credential seam; production callers enumerate local main + Pool credentials. */
|
|
42
|
+
readonly credentials?: readonly CodexModelEntitlementCredentialSnapshot[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const accountModelsCache = new Map<string, CachedAccountModels>();
|
|
46
|
+
const accountModelsFlights = new Map<string, Promise<CachedAccountModels>>();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Direct-caller entries are evicted separately from main/Pool entries.
|
|
50
|
+
*
|
|
51
|
+
* Direct keys are per-credential (`__direct_codex__:<hash>`) and unbounded in practice, while
|
|
52
|
+
* main/Pool keys are the evidence the CATALOG projects from. Sharing one LRU let 64 distinct
|
|
53
|
+
* Direct callers evict `__main__` and the Pool accounts, which makes the gated row vanish from
|
|
54
|
+
* the catalog until rediscovery — fail-closed flapping rather than a leak, but still a visible
|
|
55
|
+
* model disappearing for a reason the operator cannot see. Two budgets keep one class of caller
|
|
56
|
+
* from erasing the other's evidence.
|
|
57
|
+
*/
|
|
58
|
+
function boundedCacheSet(accountId: string, value: CachedAccountModels): void {
|
|
59
|
+
accountModelsCache.delete(accountId);
|
|
60
|
+
accountModelsCache.set(accountId, value);
|
|
61
|
+
const isDirect = (key: string): boolean => key.startsWith(DIRECT_CALLER_ACCOUNT_PREFIX);
|
|
62
|
+
const evictClass = (direct: boolean): void => {
|
|
63
|
+
let count = 0;
|
|
64
|
+
for (const key of accountModelsCache.keys()) if (isDirect(key) === direct) count += 1;
|
|
65
|
+
while (count > MODEL_ROSTER_CACHE_MAX) {
|
|
66
|
+
let oldest: string | undefined;
|
|
67
|
+
for (const key of accountModelsCache.keys()) {
|
|
68
|
+
if (isDirect(key) === direct) { oldest = key; break; }
|
|
69
|
+
}
|
|
70
|
+
if (oldest === undefined) break;
|
|
71
|
+
accountModelsCache.delete(oldest);
|
|
72
|
+
count -= 1;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
evictClass(isDirect(accountId));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function currentCredentialIdentity(accountId: string): string | undefined {
|
|
79
|
+
if (accountId.startsWith(DIRECT_CALLER_ACCOUNT_PREFIX)) {
|
|
80
|
+
return `direct:${accountId.slice(DIRECT_CALLER_ACCOUNT_PREFIX.length)}`;
|
|
81
|
+
}
|
|
82
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
83
|
+
const token = getMainAccountToken();
|
|
84
|
+
return token ? `main:${token.chatgptAccountId}` : undefined;
|
|
85
|
+
}
|
|
86
|
+
const record = readCodexAccountRecord(accountId);
|
|
87
|
+
if (!record?.credential || record.deletedAt != null) return undefined;
|
|
88
|
+
return `pool:${record.generation}:${record.credential.chatgptAccountId}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function accountCredentialSnapshot(accountId: string): Promise<CodexModelEntitlementCredentialSnapshot | null> {
|
|
92
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
93
|
+
const token = getMainAccountToken();
|
|
94
|
+
return token
|
|
95
|
+
? {
|
|
96
|
+
accountId,
|
|
97
|
+
accessToken: token.accessToken,
|
|
98
|
+
chatgptAccountId: token.chatgptAccountId,
|
|
99
|
+
credentialIdentity: `main:${token.chatgptAccountId}`,
|
|
100
|
+
}
|
|
101
|
+
: null;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const token = await getValidCodexToken(accountId);
|
|
105
|
+
return {
|
|
106
|
+
accountId,
|
|
107
|
+
accessToken: token.accessToken,
|
|
108
|
+
chatgptAccountId: token.chatgptAccountId,
|
|
109
|
+
credentialIdentity: `pool:${token.generation}:${token.chatgptAccountId}`,
|
|
110
|
+
};
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseAccountModels(text: string): ReadonlySet<string> | null {
|
|
117
|
+
try {
|
|
118
|
+
const payload = JSON.parse(text) as { models?: unknown };
|
|
119
|
+
if (!Array.isArray(payload.models)) return null;
|
|
120
|
+
const models = payload.models.flatMap(entry => {
|
|
121
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
|
|
122
|
+
const row = entry as { slug?: unknown; supported_in_api?: unknown; visibility?: unknown };
|
|
123
|
+
if (typeof row.slug !== "string" || row.supported_in_api !== true || row.visibility === "hide") return [];
|
|
124
|
+
return [row.slug];
|
|
125
|
+
});
|
|
126
|
+
return new Set(models);
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function fetchAccountModels(
|
|
133
|
+
credential: CodexModelEntitlementCredentialSnapshot,
|
|
134
|
+
fetcher: typeof fetch,
|
|
135
|
+
now: number,
|
|
136
|
+
): Promise<CachedAccountModels> {
|
|
137
|
+
const controller = new AbortController();
|
|
138
|
+
const timer = setTimeout(() => controller.abort(new DOMException("Codex model discovery timed out", "TimeoutError")), MODEL_ROSTER_TIMEOUT_MS);
|
|
139
|
+
try {
|
|
140
|
+
const headers = new Headers({
|
|
141
|
+
Authorization: `Bearer ${credential.accessToken}`,
|
|
142
|
+
Accept: "application/json",
|
|
143
|
+
});
|
|
144
|
+
if (credential.chatgptAccountId) headers.set("ChatGPT-Account-Id", credential.chatgptAccountId);
|
|
145
|
+
const response = await fetcher(CODEX_MODELS_URL, {
|
|
146
|
+
headers,
|
|
147
|
+
redirect: "error",
|
|
148
|
+
signal: controller.signal,
|
|
149
|
+
});
|
|
150
|
+
const body = await readBoundedResponseBody(response, {
|
|
151
|
+
signal: controller.signal,
|
|
152
|
+
maxBytes: MODEL_ROSTER_MAX_BYTES,
|
|
153
|
+
fatalUtf8: true,
|
|
154
|
+
});
|
|
155
|
+
const models = response.ok && body.displaySafe && !body.truncated
|
|
156
|
+
? parseAccountModels(body.text)
|
|
157
|
+
: null;
|
|
158
|
+
return {
|
|
159
|
+
credentialIdentity: credential.credentialIdentity,
|
|
160
|
+
expiresAt: now + (models ? MODEL_ROSTER_TTL_MS : MODEL_ROSTER_FAILURE_TTL_MS),
|
|
161
|
+
models: models ?? new Set(),
|
|
162
|
+
confirmed: models !== null,
|
|
163
|
+
};
|
|
164
|
+
} catch {
|
|
165
|
+
return {
|
|
166
|
+
credentialIdentity: credential.credentialIdentity,
|
|
167
|
+
expiresAt: now + MODEL_ROSTER_FAILURE_TTL_MS,
|
|
168
|
+
models: new Set(),
|
|
169
|
+
confirmed: false,
|
|
170
|
+
};
|
|
171
|
+
} finally {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function directCallerCredential(headers: Headers): CodexModelEntitlementCredentialSnapshot | null {
|
|
177
|
+
const match = /^Bearer\s+(\S+)$/i.exec(headers.get("authorization")?.trim() ?? "");
|
|
178
|
+
if (!match) return null;
|
|
179
|
+
const accessToken = match[1]!;
|
|
180
|
+
const chatgptAccountId = headers.get("chatgpt-account-id")?.trim() ?? "";
|
|
181
|
+
const fingerprint = createHash("sha256")
|
|
182
|
+
.update(accessToken)
|
|
183
|
+
.update("\0")
|
|
184
|
+
.update(chatgptAccountId)
|
|
185
|
+
.digest("hex");
|
|
186
|
+
return {
|
|
187
|
+
accountId: `${DIRECT_CALLER_ACCOUNT_PREFIX}${fingerprint}`,
|
|
188
|
+
accessToken,
|
|
189
|
+
chatgptAccountId,
|
|
190
|
+
credentialIdentity: `direct:${fingerprint}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function modelsForCredential(
|
|
195
|
+
credential: CodexModelEntitlementCredentialSnapshot,
|
|
196
|
+
fetcher: typeof fetch,
|
|
197
|
+
now: number,
|
|
198
|
+
): Promise<CachedAccountModels> {
|
|
199
|
+
const cached = accountModelsCache.get(credential.accountId);
|
|
200
|
+
if (
|
|
201
|
+
cached
|
|
202
|
+
&& cached.credentialIdentity === credential.credentialIdentity
|
|
203
|
+
&& cached.expiresAt > now
|
|
204
|
+
) return cached;
|
|
205
|
+
|
|
206
|
+
const flightKey = `${credential.accountId}\u0000${credential.credentialIdentity}`;
|
|
207
|
+
const existing = accountModelsFlights.get(flightKey);
|
|
208
|
+
if (existing) return existing;
|
|
209
|
+
const flight = fetchAccountModels(credential, fetcher, now)
|
|
210
|
+
.then(result => {
|
|
211
|
+
if (currentCredentialIdentity(credential.accountId) === credential.credentialIdentity) {
|
|
212
|
+
boundedCacheSet(credential.accountId, result);
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
})
|
|
216
|
+
.finally(() => {
|
|
217
|
+
if (accountModelsFlights.get(flightKey) === flight) accountModelsFlights.delete(flightKey);
|
|
218
|
+
});
|
|
219
|
+
accountModelsFlights.set(flightKey, flight);
|
|
220
|
+
return flight;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function candidateAccountIds(config: Pick<OcxConfig, "codexAccounts">): string[] {
|
|
224
|
+
return [
|
|
225
|
+
MAIN_CODEX_ACCOUNT_ID,
|
|
226
|
+
...(config.codexAccounts ?? [])
|
|
227
|
+
.filter(isSelectableCodexPoolAccount)
|
|
228
|
+
.map(account => account.id),
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Fetch the authenticated model roster for every locally usable Codex account.
|
|
234
|
+
*
|
|
235
|
+
* [Decision Log]
|
|
236
|
+
* - 목적과 의도: Account-gated native models must be advertised and selected only for accounts
|
|
237
|
+
* whose own authenticated upstream catalog confirms the model.
|
|
238
|
+
* - 기존 구현 및 제약 조건: The injected Codex catalog is static, while Pool may contain
|
|
239
|
+
* accounts with different entitlements. A global allowlist therefore exposed unusable rows.
|
|
240
|
+
* - 검토한 주요 대안: Infer access from plan labels, learn only after a failed prompt, or rewrite
|
|
241
|
+
* Daybreak to its current physical model.
|
|
242
|
+
* - 선택한 방식: Cache bounded authenticated `/models` rosters per credential generation and
|
|
243
|
+
* fail closed for unconfirmed accounts.
|
|
244
|
+
* - 다른 대안 대신 이 방식을 선택한 이유: Plan names do not prove grants, post-failure
|
|
245
|
+
* learning spends a real turn, and model rewriting changes the requested product identity.
|
|
246
|
+
* - 장점, 단점 및 영향: Catalog and routing share exact account evidence. Cold gated requests
|
|
247
|
+
* pay one bounded discovery call per account; discovery failure temporarily hides the gated row.
|
|
248
|
+
*/
|
|
249
|
+
export async function resolveCodexModelEntitlements(
|
|
250
|
+
config: Pick<OcxConfig, "codexAccounts">,
|
|
251
|
+
options: CodexModelEntitlementResolveOptions = {},
|
|
252
|
+
): Promise<CodexModelEntitlementSnapshot> {
|
|
253
|
+
const now = options.now ?? Date.now();
|
|
254
|
+
const fetcher = options.fetcher ?? fetch;
|
|
255
|
+
const credentials = options.credentials
|
|
256
|
+
? [...options.credentials]
|
|
257
|
+
: (await Promise.all(candidateAccountIds(config).map(accountCredentialSnapshot)))
|
|
258
|
+
.filter((value): value is CodexModelEntitlementCredentialSnapshot => value !== null);
|
|
259
|
+
const results = await Promise.all(credentials.map(async credential => ({
|
|
260
|
+
credential,
|
|
261
|
+
result: await modelsForCredential(credential, fetcher, now),
|
|
262
|
+
})));
|
|
263
|
+
return {
|
|
264
|
+
modelsByAccount: new Map(results.map(({ credential, result }) => [credential.accountId, result.models])),
|
|
265
|
+
confirmedAccountIds: new Set(results.flatMap(({ credential, result }) => result.confirmed ? [credential.accountId] : [])),
|
|
266
|
+
credentialIdentities: new Map(results.map(({ credential }) => [credential.accountId, credential.credentialIdentity])),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Fail-closed entitlement check for a Direct request's own forwarded ChatGPT credential. */
|
|
271
|
+
export async function isDirectCallerEntitledToCodexModel(
|
|
272
|
+
headers: Headers,
|
|
273
|
+
modelId: string,
|
|
274
|
+
options: Pick<CodexModelEntitlementResolveOptions, "fetcher" | "now"> = {},
|
|
275
|
+
): Promise<boolean> {
|
|
276
|
+
if (!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(modelId)) return true;
|
|
277
|
+
const credential = directCallerCredential(headers);
|
|
278
|
+
if (!credential) return false;
|
|
279
|
+
const result = await modelsForCredential(
|
|
280
|
+
credential,
|
|
281
|
+
options.fetcher ?? fetch,
|
|
282
|
+
options.now ?? Date.now(),
|
|
283
|
+
);
|
|
284
|
+
return result.confirmed && result.models.has(modelId);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function entitledCodexAccountIdsForModel(
|
|
288
|
+
snapshot: CodexModelEntitlementSnapshot,
|
|
289
|
+
modelId: string | undefined,
|
|
290
|
+
): ReadonlySet<string> | undefined {
|
|
291
|
+
if (!modelId || !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(modelId)) return undefined;
|
|
292
|
+
return new Set([...snapshot.modelsByAccount].flatMap(([accountId, models]) => (
|
|
293
|
+
snapshot.confirmedAccountIds.has(accountId) && models.has(modelId) ? [accountId] : []
|
|
294
|
+
)));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function availableAccountGatedNativeModels(
|
|
298
|
+
snapshot: CodexModelEntitlementSnapshot,
|
|
299
|
+
eligibleAccountIds?: ReadonlySet<string>,
|
|
300
|
+
): ReadonlySet<string> {
|
|
301
|
+
return new Set([...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(modelId => (
|
|
302
|
+
[...snapshot.modelsByAccount].some(([accountId, models]) => (
|
|
303
|
+
(!eligibleAccountIds || eligibleAccountIds.has(accountId))
|
|
304
|
+
&& snapshot.confirmedAccountIds.has(accountId)
|
|
305
|
+
&& models.has(modelId)
|
|
306
|
+
))
|
|
307
|
+
)));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Synchronous projection for management/catalog readers after a discovery pass. */
|
|
311
|
+
export function cachedAvailableAccountGatedNativeModels(
|
|
312
|
+
now = Date.now(),
|
|
313
|
+
eligibleAccountIds?: ReadonlySet<string>,
|
|
314
|
+
): ReadonlySet<string> {
|
|
315
|
+
return new Set([...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(modelId => (
|
|
316
|
+
[...accountModelsCache].some(([accountId, entry]) => (
|
|
317
|
+
(!eligibleAccountIds || eligibleAccountIds.has(accountId))
|
|
318
|
+
&& !accountId.startsWith(DIRECT_CALLER_ACCOUNT_PREFIX)
|
|
319
|
+
&& entry.confirmed
|
|
320
|
+
&& entry.expiresAt > now
|
|
321
|
+
&& entry.models.has(modelId)
|
|
322
|
+
))
|
|
323
|
+
)));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function isCodexModelEntitlementSnapshotCurrent(snapshot: CodexModelEntitlementSnapshot): boolean {
|
|
327
|
+
for (const [accountId, identity] of snapshot.credentialIdentities) {
|
|
328
|
+
if (currentCredentialIdentity(accountId) !== identity) return false;
|
|
329
|
+
}
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function invalidateCodexModelEntitlementsForAccount(accountId: string | null | undefined): void {
|
|
334
|
+
if (accountId) accountModelsCache.delete(accountId);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function resetCodexModelEntitlementCacheForTests(): void {
|
|
338
|
+
accountModelsCache.clear();
|
|
339
|
+
accountModelsFlights.clear();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function seedCodexModelEntitlementsForTests(
|
|
343
|
+
accountId: string,
|
|
344
|
+
models: readonly string[],
|
|
345
|
+
now = Date.now(),
|
|
346
|
+
): void {
|
|
347
|
+
boundedCacheSet(accountId, {
|
|
348
|
+
credentialIdentity: `test:${accountId}`,
|
|
349
|
+
expiresAt: now + MODEL_ROSTER_TTL_MS,
|
|
350
|
+
models: new Set(models),
|
|
351
|
+
confirmed: true,
|
|
352
|
+
});
|
|
353
|
+
}
|
package/src/codex/quota.ts
CHANGED
|
@@ -186,7 +186,7 @@ function normalizeResetAt(value: unknown): number | undefined {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
function hasKnownQuotaValue(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
189
|
-
return [quota.weeklyPercent, quota.monthlyPercent]
|
|
189
|
+
return [quota.weeklyPercent, quota.monthlyPercent, quota.shortPercent]
|
|
190
190
|
.some(value => typeof value === "number" && Number.isFinite(value));
|
|
191
191
|
}
|
|
192
192
|
|
|
@@ -226,8 +226,14 @@ function snapshotHasMonthly(quota: Omit<StoredAccountQuota, "updatedAt">): boole
|
|
|
226
226
|
return quota.monthlyPercent !== undefined || quota.monthlyResetAt !== undefined;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
function snapshotHasShort(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
230
|
+
return quota.shortPercent !== undefined
|
|
231
|
+
|| quota.shortResetAt !== undefined
|
|
232
|
+
|| quota.shortWindowSeconds !== undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
229
235
|
function snapshotHasUsage(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
230
|
-
return snapshotHasWeekly(quota) || snapshotHasMonthly(quota);
|
|
236
|
+
return snapshotHasWeekly(quota) || snapshotHasMonthly(quota) || snapshotHasShort(quota);
|
|
231
237
|
}
|
|
232
238
|
export function setAccountQuotaFromParsed(
|
|
233
239
|
accountId: string,
|
|
@@ -246,6 +252,9 @@ export function setAccountQuotaFromParsed(
|
|
|
246
252
|
if (existing?.monthlyPercent !== undefined) next.monthlyPercent = existing.monthlyPercent;
|
|
247
253
|
if (existing?.monthlyResetAt !== undefined) next.monthlyResetAt = existing.monthlyResetAt;
|
|
248
254
|
if (existing?.monthlyIsPrimaryWindow === true) next.monthlyIsPrimaryWindow = true;
|
|
255
|
+
if (existing?.shortPercent !== undefined) next.shortPercent = existing.shortPercent;
|
|
256
|
+
if (existing?.shortResetAt !== undefined) next.shortResetAt = existing.shortResetAt;
|
|
257
|
+
if (existing?.shortWindowSeconds !== undefined) next.shortWindowSeconds = existing.shortWindowSeconds;
|
|
249
258
|
next.resetCredits = quota.resetCredits;
|
|
250
259
|
accountQuota.set(accountId, next);
|
|
251
260
|
schedulePersistAccountQuotas();
|
|
@@ -270,12 +279,25 @@ export function setAccountQuotaFromParsed(
|
|
|
270
279
|
// while silently dropping `monthlyIsPrimaryWindow` would look like tertiary-only data to
|
|
271
280
|
// any future reader, and that failure would be invisible.
|
|
272
281
|
if (quota.monthlyIsPrimaryWindow === true) next.monthlyIsPrimaryWindow = true;
|
|
273
|
-
} else if (snapshotHasWeekly(quota)
|
|
282
|
+
} else if ((snapshotHasWeekly(quota) || snapshotHasShort(quota))
|
|
283
|
+
&& existing?.monthlyPercent !== undefined) {
|
|
274
284
|
next.monthlyPercent = existing.monthlyPercent;
|
|
275
285
|
if (existing.monthlyResetAt !== undefined) next.monthlyResetAt = existing.monthlyResetAt;
|
|
276
286
|
if (existing.monthlyIsPrimaryWindow === true) next.monthlyIsPrimaryWindow = true;
|
|
277
287
|
}
|
|
278
288
|
|
|
289
|
+
if (snapshotHasShort(quota)) {
|
|
290
|
+
if (quota.shortPercent !== undefined) next.shortPercent = quota.shortPercent;
|
|
291
|
+
if (quota.shortResetAt !== undefined) next.shortResetAt = quota.shortResetAt;
|
|
292
|
+
if (quota.shortWindowSeconds !== undefined) next.shortWindowSeconds = quota.shortWindowSeconds;
|
|
293
|
+
} else {
|
|
294
|
+
// Header and reset-credit updates are partial snapshots. Preserve the last full WHAM
|
|
295
|
+
// burst tuple when those updates do not carry enough window metadata to replace it.
|
|
296
|
+
if (existing?.shortPercent !== undefined) next.shortPercent = existing.shortPercent;
|
|
297
|
+
if (existing?.shortResetAt !== undefined) next.shortResetAt = existing.shortResetAt;
|
|
298
|
+
if (existing?.shortWindowSeconds !== undefined) next.shortWindowSeconds = existing.shortWindowSeconds;
|
|
299
|
+
}
|
|
300
|
+
|
|
279
301
|
if (quota.resetCredits !== undefined) next.resetCredits = quota.resetCredits;
|
|
280
302
|
else if (existing?.resetCredits !== undefined) next.resetCredits = existing.resetCredits;
|
|
281
303
|
|
|
@@ -369,6 +391,9 @@ export function updateAccountQuota(
|
|
|
369
391
|
: {}),
|
|
370
392
|
...(existing?.weeklyResetAt !== undefined ? { weeklyResetAt: existing.weeklyResetAt } : {}),
|
|
371
393
|
...(existing?.monthlyResetAt !== undefined ? { monthlyResetAt: existing.monthlyResetAt } : {}),
|
|
394
|
+
...(existing?.shortPercent !== undefined ? { shortPercent: existing.shortPercent } : {}),
|
|
395
|
+
...(existing?.shortResetAt !== undefined ? { shortResetAt: existing.shortResetAt } : {}),
|
|
396
|
+
...(existing?.shortWindowSeconds !== undefined ? { shortWindowSeconds: existing.shortWindowSeconds } : {}),
|
|
372
397
|
...(existing?.resetCredits !== undefined ? { resetCredits: existing.resetCredits } : {}),
|
|
373
398
|
updatedAt: Date.now(),
|
|
374
399
|
};
|
package/src/codex/routing.ts
CHANGED
|
@@ -322,16 +322,22 @@ function deleteScopedHealth(accountId: string, scope: CodexQuotaScope): void {
|
|
|
322
322
|
export function computeCodexUsageScore(quota: {
|
|
323
323
|
weeklyPercent?: number;
|
|
324
324
|
monthlyPercent?: number;
|
|
325
|
+
shortPercent?: number;
|
|
325
326
|
} | null, plan?: unknown): number {
|
|
326
327
|
if (!quota) return CODEX_UNKNOWN_USAGE_SCORE;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
328
|
+
const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
|
|
329
|
+
const longWindows = isThirtyDayOnlyCodexPlan(plan)
|
|
330
|
+
? [quota.monthlyPercent]
|
|
331
|
+
: [quota.weeklyPercent, quota.monthlyPercent];
|
|
332
|
+
const knownLong = longWindows.filter(finite);
|
|
333
|
+
// The short burst window only REFINES a known long-window position; it cannot stand in for
|
|
334
|
+
// one. A snapshot carrying just `shortPercent: 0` would otherwise score a flat 0 and make an
|
|
335
|
+
// account whose weekly/monthly usage is entirely unverified look like the emptiest in the
|
|
336
|
+
// pool, so `pickLowestUsageAmong` would send every request to it. Unknown has to stay
|
|
337
|
+
// unknown until a governing window is actually observed.
|
|
338
|
+
if (knownLong.length === 0) return CODEX_UNKNOWN_USAGE_SCORE;
|
|
339
|
+
const values = finite(quota.shortPercent) ? [...knownLong, quota.shortPercent] : knownLong;
|
|
340
|
+
return Math.max(...values);
|
|
335
341
|
}
|
|
336
342
|
|
|
337
343
|
export function classifyCodexUpstreamOutcome(
|