@bitkyc08/opencodex 2.40.0 → 2.42.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/README.md +4 -0
- package/gui/dist/assets/index-BU1tE0sr.js +112 -0
- package/gui/dist/assets/index-DL9-iS6J.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/meta.svg +1 -0
- package/package.json +4 -3
- package/src/adapters/cursor/catalog.ts +71 -29
- package/src/adapters/cursor/claude-id.ts +76 -0
- package/src/adapters/cursor/discovery.ts +16 -3
- package/src/adapters/cursor/effort-map.ts +27 -12
- package/src/adapters/cursor/protobuf-request.ts +41 -21
- package/src/adapters/google.ts +39 -2
- package/src/adapters/identity.ts +8 -2
- package/src/adapters/openai-responses.ts +57 -4
- package/src/bridge.ts +25 -3
- package/src/cli/account-auth.ts +28 -3
- package/src/cli/account-extended.ts +7 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/claude.ts +11 -2
- package/src/cli/connect.ts +7 -1
- package/src/cli/observe.ts +3 -1
- package/src/cli/registry.ts +1 -1
- package/src/cli/status.ts +19 -4
- package/src/client/connect.ts +5 -1
- package/src/client/hub-client.ts +29 -5
- package/src/clients/config-export.ts +12 -2
- package/src/codex/auth-api.ts +102 -9
- package/src/codex/catalog/aggregation.ts +8 -0
- package/src/codex/catalog/effort.ts +15 -2
- package/src/codex/catalog/metadata.ts +119 -9
- package/src/codex/catalog/native-models.ts +71 -0
- package/src/codex/catalog/parsing.ts +5 -3
- package/src/codex/catalog/provider-fetch.ts +166 -28
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/data/upstream-models.json +169 -0
- package/src/codex/desired-state.ts +18 -11
- package/src/codex/inject.ts +96 -6
- package/src/codex/injected-marker.ts +30 -4
- package/src/codex/journal.ts +14 -0
- package/src/combos/failover.ts +185 -6
- package/src/combos/index.ts +6 -0
- package/src/combos/resolve.ts +43 -6
- package/src/config.ts +5 -1
- package/src/generated/compatibility-version.json +115 -83
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/sync.ts +10 -2
- package/src/integrations/cursor-effort-table.ts +143 -0
- package/src/integrations/state.ts +1 -1
- package/src/integrations/writer.ts +2 -2
- package/src/lib/app-owned-memory-stores.ts +27 -8
- package/src/lib/bounded-body.ts +16 -1
- package/src/oauth/account-quota-rank.ts +40 -1
- package/src/oauth/chatgpt-device.ts +187 -0
- package/src/oauth/chatgpt.ts +31 -4
- package/src/oauth/generic-account-failover.ts +2 -2
- package/src/oauth/index.ts +24 -3
- package/src/oauth/log.ts +3 -0
- package/src/oauth/meta-muse.ts +235 -0
- package/src/providers/antigravity-models.ts +71 -13
- package/src/providers/command-code-efforts.ts +15 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/muse-subscription-usage.ts +95 -0
- package/src/providers/quota.ts +96 -0
- package/src/providers/registry.ts +116 -8
- package/src/responses/code-mode-helper-compat.ts +4 -1
- package/src/responses/state.ts +5 -4
- package/src/server/auth-cors.ts +241 -56
- package/src/server/chat-completions.ts +11 -2
- package/src/server/chat-native.ts +30 -4
- package/src/server/claude-messages.ts +17 -3
- package/src/server/effort-row.ts +131 -0
- package/src/server/index.ts +82 -45
- package/src/server/live.ts +18 -4
- package/src/server/management/api-key-rotation.ts +2 -1
- package/src/server/management/api-key-usage.ts +97 -43
- package/src/server/management/context.ts +3 -0
- package/src/server/management/cursor-integration-routes.ts +36 -7
- package/src/server/management/logs-usage-routes.ts +64 -87
- package/src/server/management/oauth-account-routes.ts +10 -3
- package/src/server/management/provider-routes.ts +218 -1
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management/usage-aggregate-cache.ts +464 -0
- package/src/server/management/usage-summary-cache.ts +4 -0
- package/src/server/models-capabilities.ts +60 -5
- package/src/server/responses/core.ts +95 -7
- package/src/server/responses/empty-completion-guard.ts +4 -0
- package/src/types/config.ts +10 -1
- package/src/types/request.ts +8 -0
- package/src/types/tools.ts +12 -9
- package/src/usage/expected-prices.ts +43 -7
- package/src/usage/ledger-scanner.ts +448 -0
- package/src/usage/log.ts +1 -1
- package/src/usage/summary.ts +915 -655
- package/src/web-search/index.ts +1 -1
- package/gui/dist/assets/index-BHe2rl_C.js +0 -112
- package/gui/dist/assets/index-CJSb3HPe.css +0 -1
package/src/codex/auth-api.ts
CHANGED
|
@@ -274,6 +274,69 @@ function quotaForPlan<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAc
|
|
|
274
274
|
} as T;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Last reset-credit count this process parsed for the main account, tagged with the
|
|
279
|
+
* physical ChatGPT account it was read from.
|
|
280
|
+
*
|
|
281
|
+
* It is deliberately memory-only. The quota store is keyed by the stable `__main__`
|
|
282
|
+
* ALIAS, and `~/.codex/auth.json` can be swapped for another account while the proxy is
|
|
283
|
+
* not running — `reconcileMainCodexAccountRuntimeState` only purges alias-keyed state
|
|
284
|
+
* when it observes the id CHANGE, and its first observation after a restart has nothing
|
|
285
|
+
* to compare against. A disk-hydrated `__main__` entry can therefore belong to the
|
|
286
|
+
* previous login, so filling the DTO from it would show one account's tickets on
|
|
287
|
+
* another's card. Pool accounts have no such hole because their store key IS the account
|
|
288
|
+
* id. Binding the value to `requestAccountId` keeps the fill honest: after a restart the
|
|
289
|
+
* badge simply waits for the first usage response that carries the summary.
|
|
290
|
+
*/
|
|
291
|
+
let mainResetCreditsProvenance: { accountId: string; credits: number } | null = null;
|
|
292
|
+
|
|
293
|
+
function rememberMainResetCredits(accountId: string | null, credits: number | undefined): void {
|
|
294
|
+
if (accountId === null || credits === undefined) return;
|
|
295
|
+
mainResetCreditsProvenance = { accountId, credits };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Forget the remembered count when the physical main identity is no longer the same. */
|
|
299
|
+
function mainResetCreditsForCurrentIdentity(): number | undefined {
|
|
300
|
+
if (!mainResetCreditsProvenance) return undefined;
|
|
301
|
+
const currentAccountId = getMainChatgptAccountId();
|
|
302
|
+
if (currentAccountId === null) return undefined;
|
|
303
|
+
if (currentAccountId !== mainResetCreditsProvenance.accountId) {
|
|
304
|
+
mainResetCreditsProvenance = null;
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
return mainResetCreditsProvenance.credits;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The main account is the only account whose DTO quota comes from the raw WHAM parse
|
|
312
|
+
* result instead of the merged store: `poolAccountDto` serializes what
|
|
313
|
+
* `commitPoolQuotaResponse` read back out of `getAccountQuota()`, while the main DTO
|
|
314
|
+
* spreads `mainInfo.quota` directly. `/wham/usage` carries `rate_limit_reset_credits`
|
|
315
|
+
* only intermittently, and the store exists to bridge that gap
|
|
316
|
+
* (`setAccountQuotaFromParsed` carries an existing `resetCredits` forward when the new
|
|
317
|
+
* snapshot omits it), so the main card lost its ticket badge on every response that
|
|
318
|
+
* happened to omit the summary while pool cards kept theirs.
|
|
319
|
+
*
|
|
320
|
+
* Only `resetCredits` is carried, deliberately, and only from an identity-tagged
|
|
321
|
+
* in-process observation rather than the alias-keyed store. The window fields have
|
|
322
|
+
* *clearing* semantics — a monthly-only snapshot must drop a stale weekly value (#382) —
|
|
323
|
+
* so reinstating the whole stored object would resurrect a window the parse meant to
|
|
324
|
+
* clear whenever the store write was refused by generation gating. A freshly parsed value
|
|
325
|
+
* always wins, including `0`: zero is defined, so it never takes the fill branch.
|
|
326
|
+
*/
|
|
327
|
+
function mainQuotaWithCarriedResetCredits(
|
|
328
|
+
parsed: Omit<StoredAccountQuota, "updatedAt">,
|
|
329
|
+
): StoredAccountQuota {
|
|
330
|
+
const carried = parsed.resetCredits === undefined
|
|
331
|
+
? mainResetCreditsForCurrentIdentity()
|
|
332
|
+
: undefined;
|
|
333
|
+
return {
|
|
334
|
+
...parsed,
|
|
335
|
+
...(carried !== undefined ? { resetCredits: carried } : {}),
|
|
336
|
+
updatedAt: getAccountQuota(MAIN_CODEX_ACCOUNT_ID)?.updatedAt ?? Date.now(),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
277
340
|
function poolAccountDto(
|
|
278
341
|
account: CodexAccount,
|
|
279
342
|
quotaResult: PoolQuotaResult,
|
|
@@ -836,6 +899,9 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
836
899
|
const plan = nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan());
|
|
837
900
|
const quota = parseUsageQuota({ ...data, ...(plan ? { plan_type: plan } : {}) });
|
|
838
901
|
const freshResetCredits = quota?.resetCredits;
|
|
902
|
+
// Tag the count with the identity it was read from, so a later response that omits the
|
|
903
|
+
// summary can restore the badge without ever crossing an account boundary.
|
|
904
|
+
rememberMainResetCredits(requestAccountId, freshResetCredits);
|
|
839
905
|
const result = {
|
|
840
906
|
email: data.email ?? null,
|
|
841
907
|
plan,
|
|
@@ -1640,10 +1706,7 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1640
1706
|
hasCredential: hasMainCredential,
|
|
1641
1707
|
needsReauth: mainNeedsReauth,
|
|
1642
1708
|
quota: mainInfo.quota ? {
|
|
1643
|
-
...quotaForPlan(
|
|
1644
|
-
...mainInfo.quota,
|
|
1645
|
-
updatedAt: getAccountQuota(MAIN_CODEX_ACCOUNT_ID)?.updatedAt ?? Date.now(),
|
|
1646
|
-
}, mainInfo.plan),
|
|
1709
|
+
...quotaForPlan(mainQuotaWithCarriedResetCredits(mainInfo.quota), mainInfo.plan),
|
|
1647
1710
|
} : null,
|
|
1648
1711
|
...oauthAccountHealthFields("codex", MAIN_CODEX_ACCOUNT_ID, mainHealth),
|
|
1649
1712
|
};
|
|
@@ -2167,7 +2230,15 @@ export async function handleCodexAuthAPI(
|
|
|
2167
2230
|
}
|
|
2168
2231
|
|
|
2169
2232
|
if (url.pathname === "/api/codex-auth/login" && req.method === "POST") {
|
|
2170
|
-
const body = (await req.json().catch(() => ({}))) as {
|
|
2233
|
+
const body = (await req.json().catch(() => ({}))) as {
|
|
2234
|
+
id?: string;
|
|
2235
|
+
reauth?: boolean;
|
|
2236
|
+
openBrowser?: unknown;
|
|
2237
|
+
device?: unknown;
|
|
2238
|
+
};
|
|
2239
|
+
// Device mode: no local browser, no loopback listener. The only way to add
|
|
2240
|
+
// an account to a headless hub (#3366).
|
|
2241
|
+
const useDeviceFlow = body.device === true;
|
|
2171
2242
|
const requestedAccountId = body.id?.trim();
|
|
2172
2243
|
const reauth = body.reauth === true;
|
|
2173
2244
|
if (requestedAccountId && !isValidCodexAccountId(requestedAccountId)) {
|
|
@@ -2197,13 +2268,20 @@ export async function handleCodexAuthAPI(
|
|
|
2197
2268
|
codexAuthLoginState.set(flowId, loginOwner);
|
|
2198
2269
|
try {
|
|
2199
2270
|
const { startLoginFlow, getLoginStatus, publicOAuthAuthenticationErrorMessage } = await import("../oauth");
|
|
2200
|
-
const result = await startLoginFlow("chatgpt", {
|
|
2271
|
+
const result = await startLoginFlow("chatgpt", {
|
|
2272
|
+
forceLogin: true,
|
|
2273
|
+
...(useDeviceFlow ? { flow: "device" as const } : {}),
|
|
2274
|
+
});
|
|
2201
2275
|
|
|
2202
2276
|
// Open the browser server-side (same pattern as /api/oauth/login in management-api.ts).
|
|
2203
2277
|
// The GUI's window.open is popup-blocked because it runs after an await, not a direct click.
|
|
2204
2278
|
// Both login routes share one resolver so this surface cannot drift from the other.
|
|
2205
2279
|
const { shouldOpenBrowserForLogin } = await import("../oauth/open-browser-choice");
|
|
2206
|
-
|
|
2280
|
+
// A device flow's URL is a verification page the user opens on ANOTHER
|
|
2281
|
+
// machine. Opening it on the hub host is useless at best, and on a
|
|
2282
|
+
// headless host it fails. `deviceCode` is the same signal the generic
|
|
2283
|
+
// OAuth login route uses to make this decision.
|
|
2284
|
+
if (result.url && !result.deviceCode && shouldOpenBrowserForLogin(body.openBrowser, runtimeConfig)) {
|
|
2207
2285
|
const { openUrl } = await import("../lib/open-url");
|
|
2208
2286
|
openUrl(result.url);
|
|
2209
2287
|
}
|
|
@@ -2211,7 +2289,14 @@ export async function handleCodexAuthAPI(
|
|
|
2211
2289
|
(async () => {
|
|
2212
2290
|
try {
|
|
2213
2291
|
let completed = false;
|
|
2214
|
-
|
|
2292
|
+
// The device grant lives 15 minutes and the whole point is that the
|
|
2293
|
+
// user walks to another device to enter the code. A 5-minute server
|
|
2294
|
+
// budget would kill the flow at minute five while the grant is still
|
|
2295
|
+
// valid. The extra 30 attempts past 450 are settlement margin: a user
|
|
2296
|
+
// who authorizes in the final seconds still needs the token exchange
|
|
2297
|
+
// and credential write to land before this loop gives up.
|
|
2298
|
+
const pollAttempts = useDeviceFlow ? 480 : 150;
|
|
2299
|
+
for (let i = 0; i < pollAttempts; i++) {
|
|
2215
2300
|
await new Promise(r => setTimeout(r, 2000));
|
|
2216
2301
|
const st = getLoginStatus("chatgpt");
|
|
2217
2302
|
if (st.done && st.loggedIn) {
|
|
@@ -2437,7 +2522,15 @@ export async function handleCodexAuthAPI(
|
|
|
2437
2522
|
})();
|
|
2438
2523
|
|
|
2439
2524
|
setCodexLoginState(flowId, { status: "pending" });
|
|
2440
|
-
return jsonResponse({
|
|
2525
|
+
return jsonResponse({
|
|
2526
|
+
ok: true,
|
|
2527
|
+
flowId,
|
|
2528
|
+
url: result.url,
|
|
2529
|
+
instructions: result.instructions,
|
|
2530
|
+
// Dropped before #3366: every device-code surface renders this field,
|
|
2531
|
+
// so withholding it left the GUI and CLI with no code to show.
|
|
2532
|
+
...(result.deviceCode ? { deviceCode: result.deviceCode } : {}),
|
|
2533
|
+
});
|
|
2441
2534
|
} catch (e) {
|
|
2442
2535
|
if (codexAuthLoginState.get(flowId) === loginOwner) codexAuthLoginState.delete(flowId);
|
|
2443
2536
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -162,6 +162,12 @@ export function deriveComboCatalogModel(
|
|
|
162
162
|
contextWindow,
|
|
163
163
|
...members.map(member => member.maxInputTokens ?? member.contextWindow!),
|
|
164
164
|
);
|
|
165
|
+
const knownMaxOutputTokens = members
|
|
166
|
+
.map(member => member.maxOutputTokens)
|
|
167
|
+
.filter((value): value is number => typeof value === "number" && value > 0);
|
|
168
|
+
const maxOutputTokens = knownMaxOutputTokens.length === members.length
|
|
169
|
+
? Math.min(...knownMaxOutputTokens)
|
|
170
|
+
: undefined;
|
|
165
171
|
const autoCompactTokenLimit = Math.min(
|
|
166
172
|
...members.map(member => clampAutoCompactTokenLimit(
|
|
167
173
|
member.contextWindow!,
|
|
@@ -180,6 +186,7 @@ export function deriveComboCatalogModel(
|
|
|
180
186
|
owned_by: COMBO_NAMESPACE,
|
|
181
187
|
contextWindow,
|
|
182
188
|
maxInputTokens,
|
|
189
|
+
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
183
190
|
autoCompactTokenLimit,
|
|
184
191
|
...(hasLimitingContextCapMetadata ? { contextCapped } : {}),
|
|
185
192
|
inputModalities,
|
|
@@ -320,6 +327,7 @@ export function normalizedOpenAiApiSignature(model: CatalogModel): string {
|
|
|
320
327
|
id: model.id,
|
|
321
328
|
contextWindow: model.contextWindow ?? null,
|
|
322
329
|
maxInputTokens: model.maxInputTokens ?? null,
|
|
330
|
+
maxOutputTokens: model.maxOutputTokens ?? null,
|
|
323
331
|
autoCompactTokenLimit: model.autoCompactTokenLimit ?? null,
|
|
324
332
|
inputModalities: [...new Set(model.inputModalities ?? [])].sort(),
|
|
325
333
|
reasoningEfforts: [...new Set(model.reasoningEfforts ?? [])].sort(),
|
|
@@ -35,7 +35,7 @@ import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
|
35
35
|
import { generatedModelMetadata, readCatalog, readCodexCatalogPath } from "./parsing";
|
|
36
36
|
import type { CatalogModel, RawEntry } from "./parsing";
|
|
37
37
|
import { UPSTREAM_NATIVE_ENTRIES } from "./metadata";
|
|
38
|
-
import { nativeOpenAiCapabilitySourceSlug } from "./native-models";
|
|
38
|
+
import { nativeOpenAiCapabilitySourceSlug, SELF_DESCRIBED_NATIVE_OPENAI_MODELS } from "./native-models";
|
|
39
39
|
import { loadBundledCodexCatalog } from "./bundled";
|
|
40
40
|
import type { BundledCatalogDeps, ReadonlyRawCatalog } from "./bundled";
|
|
41
41
|
import { deriveEntry } from "./sync";
|
|
@@ -258,8 +258,21 @@ export function applyReasoningLevels(
|
|
|
258
258
|
: efforts.find(effort => effort !== "none" && effort !== "minimal") ?? efforts[0];
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Native slugs entitled to the full GPT-5.6-era ladder (low..ultra, with max restored).
|
|
263
|
+
*
|
|
264
|
+
* The name is historical: membership is about the LADDER, not the model generation. `gpt-6-astra`
|
|
265
|
+
* qualifies because upstream ships it with the same six rungs
|
|
266
|
+
* (`supported_reasoning_levels` low/medium/high/xhigh/max/ultra, #42607). It used to qualify only
|
|
267
|
+
* as a side effect of borrowing Sol's capability source; once it became self-described that
|
|
268
|
+
* accident disappeared, and the sync path's else-branch
|
|
269
|
+
* (`applyReasoningLevels(entry, ["low","medium","high","xhigh"])`) would have truncated the
|
|
270
|
+
* shipped ladder, silently dropping `max` and `ultra`.
|
|
271
|
+
*/
|
|
261
272
|
export function isGpt56NativeSlug(slug: string): boolean {
|
|
262
|
-
|
|
273
|
+
if (slug.includes("/")) return false;
|
|
274
|
+
if (SELF_DESCRIBED_NATIVE_OPENAI_MODELS.has(slug)) return true;
|
|
275
|
+
return nativeOpenAiCapabilitySourceSlug(slug).startsWith("gpt-5.6-");
|
|
263
276
|
}
|
|
264
277
|
|
|
265
278
|
export function ensureGpt56ReasoningLevels(entry: RawEntry): void {
|
|
@@ -41,10 +41,14 @@ import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
|
41
41
|
import {
|
|
42
42
|
ACCOUNT_GATED_NATIVE_OPENAI_MODELS,
|
|
43
43
|
NATIVE_DAYBREAK_BLUE_MODEL,
|
|
44
|
+
NATIVE_GPT6_ASTRA_MODEL,
|
|
44
45
|
NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS,
|
|
45
46
|
NATIVE_OPENAI_MODELS,
|
|
47
|
+
SELF_DESCRIBED_NATIVE_OPENAI_MODELS,
|
|
46
48
|
SUPPORTED_NATIVE_OPENAI_SLUGS,
|
|
49
|
+
hasNativeOpenAiCapabilityMetadata,
|
|
47
50
|
isNativeOpenAiCapabilityAliasModel,
|
|
51
|
+
nativeOpenAiAliasPresentation,
|
|
48
52
|
nativeOpenAiCapabilitySourceSlug,
|
|
49
53
|
} from "./native-models";
|
|
50
54
|
import { cachedAvailableAccountGatedNativeModels } from "../model-entitlements";
|
|
@@ -52,16 +56,25 @@ import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
|
52
56
|
export { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
53
57
|
export {
|
|
54
58
|
NATIVE_DAYBREAK_BLUE_MODEL,
|
|
59
|
+
NATIVE_GPT6_ASTRA_MODEL,
|
|
55
60
|
NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS,
|
|
56
61
|
NATIVE_OPENAI_MODELS,
|
|
62
|
+
SELF_DESCRIBED_NATIVE_OPENAI_MODELS,
|
|
57
63
|
SUPPORTED_NATIVE_OPENAI_SLUGS,
|
|
64
|
+
hasNativeOpenAiCapabilityMetadata,
|
|
58
65
|
isNativeOpenAiCapabilityAliasModel,
|
|
66
|
+
nativeOpenAiAliasPresentation,
|
|
59
67
|
nativeOpenAiCapabilitySourceSlug,
|
|
60
68
|
} from "./native-models";
|
|
61
69
|
|
|
62
70
|
export const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = [
|
|
63
71
|
"gpt-5.3-codex-spark",
|
|
64
72
|
"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
|
|
73
|
+
// Preemptive leak-based registration: no shipped codex-rs catalog carries it, so without this
|
|
74
|
+
// entry an install WITH a live catalog would drop the row that native-models.ts deliberately
|
|
75
|
+
// ungated. Listing it here keeps the bare slug reachable so a request actually dispatches and
|
|
76
|
+
// reports the upstream status.
|
|
77
|
+
NATIVE_GPT6_ASTRA_MODEL,
|
|
65
78
|
];
|
|
66
79
|
|
|
67
80
|
export function configuredNativeAliasSlugs(
|
|
@@ -161,6 +174,12 @@ export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: n
|
|
|
161
174
|
// ChatGPT account."`), so the promotion rests on a report from an account that has
|
|
162
175
|
// access rather than on a probe. Treat it as the weaker evidence of the four.
|
|
163
176
|
[NATIVE_DAYBREAK_BLUE_MODEL]: { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_MAX_INPUT_TOKENS, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
|
|
177
|
+
// gpt-6-astra ships its own numbers (upstream models.json, #42607): a 272,000 default window
|
|
178
|
+
// against an 872,000 ceiling. It is NOT in NATIVE_GPT56_FAMILY, so it must not inherit that
|
|
179
|
+
// family's measured 922,000 clamp — advertising 922,000 here over-stated the ceiling by 50k.
|
|
180
|
+
// maxInputTokens is clamped to the resolved window by nativeOpenAiMaxInputTokens, so this reads
|
|
181
|
+
// 272,000 by default and 872,000 only under the long-window opt-in.
|
|
182
|
+
[NATIVE_GPT6_ASTRA_MODEL]: { contextWindow: 272_000, maxContextWindow: 872_000, maxInputTokens: 872_000 },
|
|
164
183
|
};
|
|
165
184
|
|
|
166
185
|
const PINNED_UPSTREAM_MODELS: Map<string, RawEntry> = new Map(
|
|
@@ -246,13 +265,36 @@ export function nativeContextLimits(
|
|
|
246
265
|
}
|
|
247
266
|
|
|
248
267
|
/** Apply the user levers to an authoritative value. */
|
|
268
|
+
/**
|
|
269
|
+
* The ceiling a native slug may be RAISED to by a user lever, or undefined when it has no
|
|
270
|
+
* separate long window.
|
|
271
|
+
*
|
|
272
|
+
* This is what makes the dashboard's 1M opt-in work: without an opt-in ceiling a lever can only
|
|
273
|
+
* ever narrow the advertised window, so the toggle would appear to do nothing. The GPT-5.6 family
|
|
274
|
+
* shares one measured ceiling; a self-described native carries its own in
|
|
275
|
+
* `NATIVE_OPENAI_CONTEXT_OVERRIDES.maxContextWindow` (`gpt-6-astra` ships 872,000 against a
|
|
276
|
+
* 272,000 default), and reading it per-slug is what keeps the toggle honest for a model whose
|
|
277
|
+
* ceiling is not the family's.
|
|
278
|
+
*/
|
|
279
|
+
function longWindowOptInCeiling(slug: string): number | undefined {
|
|
280
|
+
if (NATIVE_GPT56_FAMILY.has(slug)) return NATIVE_GPT56_MAX_INPUT_TOKENS;
|
|
281
|
+
const override = NATIVE_OPENAI_CONTEXT_OVERRIDES[slug];
|
|
282
|
+
const defaultWindow = positiveInt(override?.contextWindow);
|
|
283
|
+
const longWindow = positiveInt(override?.maxContextWindow);
|
|
284
|
+
if (defaultWindow === undefined || longWindow === undefined || longWindow <= defaultWindow) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
return longWindow;
|
|
288
|
+
}
|
|
289
|
+
|
|
249
290
|
function narrowToLimits(raw: number | undefined, slug: string, input: NativeContextLimitsInput): number | undefined {
|
|
250
291
|
if (raw === undefined) return undefined;
|
|
251
292
|
const limits = asLimits(input);
|
|
252
293
|
const overlay = positiveInt(limits.modelWindows?.[slug]) ?? positiveInt(limits.providerWindow);
|
|
253
294
|
const cap = positiveInt(limits.cap);
|
|
254
|
-
|
|
255
|
-
|
|
295
|
+
const optInCeiling = longWindowOptInCeiling(slug);
|
|
296
|
+
if (optInCeiling !== undefined) {
|
|
297
|
+
const ceiling = optInCeiling;
|
|
256
298
|
const chosen = overlay ?? cap ?? raw;
|
|
257
299
|
const window = Math.min(chosen, ceiling);
|
|
258
300
|
return overlay !== undefined && cap !== undefined ? Math.min(window, cap) : window;
|
|
@@ -271,6 +313,11 @@ export function nativeOpenAiContextWindow(slug: string, limits?: NativeContextLi
|
|
|
271
313
|
return narrowToLimits(raw, slug, limits);
|
|
272
314
|
}
|
|
273
315
|
|
|
316
|
+
export function nativeOpenAiMaxOutputTokens(slug: string): number | undefined {
|
|
317
|
+
const sourceSlug = nativeOpenAiCapabilitySourceSlug(slug);
|
|
318
|
+
return positiveInt(getModelMetadata("openai", sourceSlug)?.maxTokens);
|
|
319
|
+
}
|
|
320
|
+
|
|
274
321
|
/**
|
|
275
322
|
* Long-context tier for a native slug as a (default, long) pair, for clients that let the user
|
|
276
323
|
* pick a window per request (Cursor's local-agent "Context" selector). The pair is the family's
|
|
@@ -491,15 +538,23 @@ export function applyNativeVisibility(
|
|
|
491
538
|
|
|
492
539
|
function upstreamNativeEntryForSlug(slug: string): RawEntry | undefined {
|
|
493
540
|
const sourceSlug = nativeOpenAiCapabilitySourceSlug(slug);
|
|
494
|
-
|
|
541
|
+
// A self-described native returns its OWN pinned row; the alias-cloning branch below stays
|
|
542
|
+
// reserved for slugs that genuinely borrow another model's identity. The allowlist is explicit
|
|
543
|
+
// rather than "has a pinned entry", which would also admit gpt-5.5/gpt-5.4/gpt-5.4-mini into
|
|
544
|
+
// the sync-replacement authority this map carries.
|
|
545
|
+
if (!sourceSlug.startsWith("gpt-5.6-") && !SELF_DESCRIBED_NATIVE_OPENAI_MODELS.has(slug)) {
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
495
548
|
const source = PINNED_UPSTREAM_MODELS.get(sourceSlug);
|
|
496
549
|
if (!source) return undefined;
|
|
497
|
-
if (slug === sourceSlug) return source;
|
|
550
|
+
if (slug === sourceSlug) return withDerivedBaseInstructions(source);
|
|
498
551
|
|
|
499
552
|
const alias = structuredClone(source) as RawEntry;
|
|
500
553
|
alias.slug = slug;
|
|
501
|
-
|
|
502
|
-
|
|
554
|
+
const presentation = nativeOpenAiAliasPresentation(slug);
|
|
555
|
+
if (!presentation) return undefined; // an alias with no product identity must not ship a wrong one
|
|
556
|
+
alias.display_name = presentation.displayName;
|
|
557
|
+
alias.description = presentation.description;
|
|
503
558
|
if (typeof alias.base_instructions === "string") {
|
|
504
559
|
alias.base_instructions = identifyRoutedModel(alias.base_instructions, slug);
|
|
505
560
|
}
|
|
@@ -516,6 +571,27 @@ function upstreamNativeEntryForSlug(slug: string): RawEntry | undefined {
|
|
|
516
571
|
return alias;
|
|
517
572
|
}
|
|
518
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Backfill `base_instructions` from `model_messages.instructions_template` when upstream ships
|
|
576
|
+
* only the latter.
|
|
577
|
+
*
|
|
578
|
+
* `gpt-6-astra` is the first pinned row to arrive without a top-level `base_instructions`; every
|
|
579
|
+
* other native carries both. That field is not decorative here — `hasNativeCatalogRowShape`,
|
|
580
|
+
* `findNativeTemplate` and `findSupportedNativeTemplate` all test for it, so a row missing it is
|
|
581
|
+
* not recognized as a native catalog row at all. The two fields hold the same prompt upstream, so
|
|
582
|
+
* deriving one from the other preserves upstream's content while keeping this codebase's row
|
|
583
|
+
* shape intact. The pinned JSON is left byte-identical to upstream; only the projection fills in.
|
|
584
|
+
*/
|
|
585
|
+
function withDerivedBaseInstructions(entry: RawEntry): RawEntry {
|
|
586
|
+
if (typeof entry.base_instructions === "string" && entry.base_instructions.length > 0) return entry;
|
|
587
|
+
const messages = entry.model_messages;
|
|
588
|
+
const template = messages && typeof messages === "object" && !Array.isArray(messages)
|
|
589
|
+
? (messages as Record<string, unknown>).instructions_template
|
|
590
|
+
: undefined;
|
|
591
|
+
if (typeof template !== "string" || template.length === 0) return entry;
|
|
592
|
+
return { ...entry, base_instructions: template };
|
|
593
|
+
}
|
|
594
|
+
|
|
519
595
|
export const UPSTREAM_NATIVE_ENTRIES: Map<string, RawEntry> = new Map(
|
|
520
596
|
[...NATIVE_OPENAI_MODELS, ...NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS].flatMap(slug => {
|
|
521
597
|
const entry = upstreamNativeEntryForSlug(slug);
|
|
@@ -531,10 +607,44 @@ export function upstreamNativeEntry(slug: string): RawEntry | null {
|
|
|
531
607
|
return clone;
|
|
532
608
|
}
|
|
533
609
|
|
|
610
|
+
/**
|
|
611
|
+
* Product label for a native slug whose custom row inherits native metadata.
|
|
612
|
+
*
|
|
613
|
+
* An alias carries a hand-written presentation because upstream never described it. A
|
|
614
|
+
* self-described native gets its label from its own pinned row instead, so the two kinds answer
|
|
615
|
+
* through one accessor and no caller has to know which it holds.
|
|
616
|
+
*/
|
|
617
|
+
export function nativeOpenAiCapabilityDisplayName(slug: string): string | undefined {
|
|
618
|
+
const presentation = nativeOpenAiAliasPresentation(slug);
|
|
619
|
+
if (presentation) return presentation.displayName;
|
|
620
|
+
const pinned = UPSTREAM_NATIVE_ENTRIES.get(slug);
|
|
621
|
+
return typeof pinned?.display_name === "string" ? pinned.display_name : undefined;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Slugs whose persisted row may be replaced by the pinned snapshot even when it carries a real
|
|
626
|
+
* display name — because THIS codebase wrote that name from a guess.
|
|
627
|
+
*
|
|
628
|
+
* `shouldUpgradeToUpstreamEntry`'s normal rule ("upgrade only fallback-quality rows, where
|
|
629
|
+
* `display_name === slug`") assumes any row with a real label came from upstream and is therefore
|
|
630
|
+
* authoritative. That assumption broke for `gpt-6-astra`: opencodex shipped a speculative row with
|
|
631
|
+
* a hand-written "GPT-6 Astra" label and a provisional description while the slug was still a leak.
|
|
632
|
+
* Those rows are already on disk in every install that ran that release, and they look genuine, so
|
|
633
|
+
* without this list they would survive every future sync and permanently shadow the real shipped
|
|
634
|
+
* metadata — the wrong label, the wrong 922k ceiling, the wrong priority.
|
|
635
|
+
*
|
|
636
|
+
* Membership is a statement about opencodex's own history, not about upstream. Add a slug only
|
|
637
|
+
* when a released version of this project wrote a fabricated row for it.
|
|
638
|
+
*/
|
|
639
|
+
const SELF_AUTHORED_NATIVE_ROWS: ReadonlySet<string> = new Set([NATIVE_GPT6_ASTRA_MODEL]);
|
|
640
|
+
|
|
534
641
|
export function shouldUpgradeToUpstreamEntry(entry: RawEntry): boolean {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
642
|
+
if (typeof entry.slug !== "string" || !UPSTREAM_NATIVE_ENTRIES.has(entry.slug)) return false;
|
|
643
|
+
if (entry.display_name === entry.slug) return true;
|
|
644
|
+
// A row this project authored from a guess is not evidence of upstream truth, however genuine
|
|
645
|
+
// its display name looks. Replace it once, from the pin.
|
|
646
|
+
return SELF_AUTHORED_NATIVE_ROWS.has(entry.slug)
|
|
647
|
+
&& entry.display_name !== UPSTREAM_NATIVE_ENTRIES.get(entry.slug)?.display_name;
|
|
538
648
|
}
|
|
539
649
|
|
|
540
650
|
export function nativeOpenAiSlugs(): string[] {
|
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
/** ChatGPT/Codex wire id observed for the account-native Daybreak Blue surface. */
|
|
2
2
|
export const NATIVE_DAYBREAK_BLUE_MODEL = "gpt-daybreak-blue-latest";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Leaked Responses API identifier for the announced next-generation OpenAI model
|
|
6
|
+
* (2026-09-03: OpenAI teased the launch on X; community probes report `gpt-6-astra` returning
|
|
7
|
+
* the same 404 as other internal staging slugs where an arbitrary slug returns 400).
|
|
8
|
+
* Registered preemptively so an entitled account can route it the moment it ships, before any
|
|
9
|
+
* codex-rs catalog carries it. Unlike Daybreak it is NOT wire-normalized to a serving id —
|
|
10
|
+
* the leaked slug IS the wire id.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* SHIPPED as of 2026-09-03: openai/codex `ed391d4dd` (#42607, bundled model catalog) and
|
|
14
|
+
* `1f7b99922` (#42619, Amazon Bedrock catalogs). The registration is no longer speculative —
|
|
15
|
+
* `src/codex/data/upstream-models.json` now pins the real row, so this slug is SELF-DESCRIBED
|
|
16
|
+
* and must not borrow another model's capability metadata.
|
|
17
|
+
*
|
|
18
|
+
* Still NOT wire-normalized: unlike Daybreak the slug IS the wire id.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT account-gated (owner decision, 2026-09-04, reaffirmed during rollout).
|
|
21
|
+
* Upstream `available_in_plans` lists 23 plans including `free`, but the model is rolling out,
|
|
22
|
+
* so a given account's Codex surface may still answer
|
|
23
|
+
* `"The 'gpt-6-astra' model is not supported when using Codex with a ChatGPT account."`
|
|
24
|
+
* — the same refusal Daybreak returns. Gating on an entitlement roster would hide the row until
|
|
25
|
+
* that roster catches up; listing it means the request dispatches and the real upstream status
|
|
26
|
+
* is what the user sees. Evidence: devlog/_plan/260904_astra_release_alignment/021.
|
|
27
|
+
*/
|
|
28
|
+
export const NATIVE_GPT6_ASTRA_MODEL = "gpt-6-astra";
|
|
29
|
+
|
|
4
30
|
/** Native ChatGPT/Codex ids whose availability is proven per authenticated account. */
|
|
5
31
|
export const ACCOUNT_GATED_NATIVE_OPENAI_MODELS: ReadonlySet<string> = new Set([
|
|
6
32
|
"gpt-5.6-sol",
|
|
@@ -25,6 +51,19 @@ const NATIVE_OPENAI_CAPABILITY_SOURCES: Readonly<Record<string, string>> = Objec
|
|
|
25
51
|
[NATIVE_DAYBREAK_BLUE_MODEL]: "gpt-5.6-sol",
|
|
26
52
|
});
|
|
27
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Native slugs that carry their OWN pinned upstream row rather than an alias's borrowed one.
|
|
56
|
+
*
|
|
57
|
+
* Membership authorizes `upstreamNativeEntryForSlug` to return the pinned entry directly. It is
|
|
58
|
+
* an explicit list, not a structural `PINNED_UPSTREAM_MODELS.has(slug)` predicate: the pin also
|
|
59
|
+
* holds `gpt-5.5`, `gpt-5.4` and `gpt-5.4-mini`, and admitting those into
|
|
60
|
+
* `UPSTREAM_NATIVE_ENTRIES` would newly authorize replacing their persisted catalog rows during
|
|
61
|
+
* sync — an invariant that map's own comment reserves for the GPT-5.6 family.
|
|
62
|
+
*/
|
|
63
|
+
export const SELF_DESCRIBED_NATIVE_OPENAI_MODELS: ReadonlySet<string> = new Set([
|
|
64
|
+
NATIVE_GPT6_ASTRA_MODEL,
|
|
65
|
+
]);
|
|
66
|
+
|
|
28
67
|
/**
|
|
29
68
|
* Native ids whose capability metadata is inherited from another pinned native row.
|
|
30
69
|
*
|
|
@@ -46,10 +85,41 @@ export function isNativeOpenAiCapabilityAliasModel(slug: string): boolean {
|
|
|
46
85
|
return Object.hasOwn(NATIVE_OPENAI_CAPABILITY_SOURCES, slug);
|
|
47
86
|
}
|
|
48
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Native slugs whose Codex-forward CUSTOM row inherits authoritative native metadata.
|
|
90
|
+
*
|
|
91
|
+
* Two shapes qualify and the distinction matters only to `upstreamNativeEntryForSlug`:
|
|
92
|
+
* a capability ALIAS borrows another model's pinned row, while a SELF-DESCRIBED native has its
|
|
93
|
+
* own. Every consumer that asks "does this custom row get real native capabilities and a real
|
|
94
|
+
* product label" wants both, which is why they call this rather than the alias check —
|
|
95
|
+
* `gpt-6-astra` stopped being an alias when its own row was pinned, and gating on
|
|
96
|
+
* `isNativeOpenAiCapabilityAliasModel` alone would have silently demoted it to a bare-slug label
|
|
97
|
+
* with no inherited ladder.
|
|
98
|
+
*/
|
|
99
|
+
export function hasNativeOpenAiCapabilityMetadata(slug: string): boolean {
|
|
100
|
+
return isNativeOpenAiCapabilityAliasModel(slug) || SELF_DESCRIBED_NATIVE_OPENAI_MODELS.has(slug);
|
|
101
|
+
}
|
|
102
|
+
|
|
49
103
|
export function nativeOpenAiCapabilitySourceSlug(slug: string): string {
|
|
50
104
|
return NATIVE_OPENAI_CAPABILITY_SOURCES[slug] ?? slug;
|
|
51
105
|
}
|
|
52
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Presentation identity per capability alias. Capability metadata (context, ladder, modalities)
|
|
109
|
+
* is inherited from the source model; the NAME and description are the alias's own product
|
|
110
|
+
* identity — hardcoding one alias's label would present every other alias as the wrong product.
|
|
111
|
+
*/
|
|
112
|
+
export const NATIVE_OPENAI_ALIAS_PRESENTATION: Readonly<Record<string, { displayName: string; description: string }>> = Object.freeze({
|
|
113
|
+
[NATIVE_DAYBREAK_BLUE_MODEL]: {
|
|
114
|
+
displayName: "Daybreak Blue",
|
|
115
|
+
description: "Frontier general-purpose model with safeguards for defensive cybersecurity work.",
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export function nativeOpenAiAliasPresentation(slug: string): { displayName: string; description: string } | undefined {
|
|
120
|
+
return NATIVE_OPENAI_ALIAS_PRESENTATION[slug];
|
|
121
|
+
}
|
|
122
|
+
|
|
53
123
|
/**
|
|
54
124
|
* Native OpenAI model ids that this release can route and restore with authoritative metadata.
|
|
55
125
|
*
|
|
@@ -70,6 +140,7 @@ export const NATIVE_OPENAI_MODELS = [
|
|
|
70
140
|
"gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark",
|
|
71
141
|
"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
|
|
72
142
|
NATIVE_DAYBREAK_BLUE_MODEL,
|
|
143
|
+
NATIVE_GPT6_ASTRA_MODEL,
|
|
73
144
|
];
|
|
74
145
|
|
|
75
146
|
export const SUPPORTED_NATIVE_OPENAI_SLUGS = new Set(NATIVE_OPENAI_MODELS);
|
|
@@ -31,7 +31,7 @@ import { redactSecretString } from "../../lib/redact";
|
|
|
31
31
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES,
|
|
34
|
+
import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, hasNativeOpenAiCapabilityMetadata, nativeMultiAgentVersion, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, type NativeContextLimitsInput } from "./metadata";
|
|
35
35
|
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
36
36
|
import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
37
37
|
import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
@@ -112,6 +112,8 @@ export interface CatalogModel {
|
|
|
112
112
|
defaultReasoningEffort?: string;
|
|
113
113
|
contextWindow?: number;
|
|
114
114
|
maxInputTokens?: number;
|
|
115
|
+
/** Model-scoped output-token ceiling; omitted when no authoritative value is known. */
|
|
116
|
+
maxOutputTokens?: number;
|
|
115
117
|
/** Soft client compaction threshold; hard context/input limits remain authoritative. */
|
|
116
118
|
autoCompactTokenLimit?: number;
|
|
117
119
|
contextCap?: number;
|
|
@@ -524,7 +526,7 @@ export function catalogEntryIsNativeChatGpt(entry: RawEntry): boolean {
|
|
|
524
526
|
if (
|
|
525
527
|
entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND
|
|
526
528
|
&& entry.use_responses_lite === true
|
|
527
|
-
&&
|
|
529
|
+
&& hasNativeOpenAiCapabilityMetadata(routedNativeSlug)
|
|
528
530
|
) return true;
|
|
529
531
|
if (UPSTREAM_NATIVE_ENTRIES.has(slug) || SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) return true;
|
|
530
532
|
return false;
|
|
@@ -582,7 +584,7 @@ export function applyMultiAgentMode(
|
|
|
582
584
|
: "";
|
|
583
585
|
const codexForwardCapabilityAlias = entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND
|
|
584
586
|
&& entry.use_responses_lite === true
|
|
585
|
-
&&
|
|
587
|
+
&& hasNativeOpenAiCapabilityMetadata(routedNativeSlug)
|
|
586
588
|
? routedNativeSlug
|
|
587
589
|
: undefined;
|
|
588
590
|
const upstreamPin = nativeAlias
|