@bitkyc08/opencodex 2.26.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-RL6b1bTV.js → index-D2sP-biU.js} +14 -14
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +60 -1
- package/src/adapters/base.ts +16 -2
- package/src/adapters/command-code.ts +4 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-antigravity-replay.ts +16 -8
- package/src/adapters/google.ts +22 -5
- package/src/adapters/openai-chat.ts +189 -60
- package/src/adapters/openai-responses.ts +37 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/bridge.ts +11 -5
- package/src/cli/doctor.ts +76 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +19 -6
- package/src/cli/models.ts +13 -6
- package/src/codex/account-usability.ts +3 -0
- package/src/codex/app-server-processes.ts +269 -37
- package/src/codex/auth-api.ts +22 -5
- package/src/codex/auth-context.ts +108 -3
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/metadata.ts +17 -3
- package/src/codex/catalog/native-models.ts +22 -14
- package/src/codex/catalog/parsing.ts +20 -3
- package/src/codex/catalog/provider-fetch.ts +8 -0
- package/src/codex/catalog/sync.ts +63 -15
- package/src/codex/convergence.ts +61 -13
- package/src/codex/log-guard/path-safety.ts +52 -3
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-profile-startup.ts +100 -2
- package/src/codex/quota.ts +28 -3
- package/src/codex/routing.ts +14 -8
- package/src/codex/user-identity.ts +21 -1
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +11 -24
- package/src/generated/compatibility-version.json +110 -70
- package/src/images/loop.ts +11 -4
- package/src/lib/destination-policy.ts +47 -0
- package/src/lib/shadow-call.ts +15 -0
- package/src/lib/state-store-registrations.ts +8 -2
- package/src/oauth/index.ts +33 -5
- package/src/oauth/store.ts +11 -5
- package/src/providers/antigravity-models.ts +70 -5
- package/src/providers/derive.ts +12 -2
- package/src/providers/fastwire.ts +39 -8
- package/src/providers/quota.ts +9 -2
- package/src/providers/registry.ts +120 -6
- package/src/providers/service-tier.ts +50 -15
- package/src/responses/parser.ts +59 -11
- package/src/responses/state.ts +162 -5
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/router.ts +17 -3
- package/src/routing/capability.ts +26 -9
- package/src/routing/compatibility/behavior.ts +44 -6
- package/src/routing/profile.ts +1 -1
- package/src/server/chat-native.ts +11 -2
- package/src/server/index.ts +59 -8
- package/src/server/management/agent-settings-routes.ts +16 -2
- package/src/server/management/shared.ts +3 -1
- package/src/server/request-log.ts +31 -0
- package/src/server/responses/collaboration.ts +34 -9
- package/src/server/responses/compact.ts +54 -7
- package/src/server/responses/core.ts +259 -43
- package/src/server/responses/input-admission.ts +7 -2
- 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/service-manager-probe.ts +99 -0
- package/src/service.ts +86 -6
- package/src/tray/windows.ts +25 -5
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +818 -0
- package/src/types/provider.ts +521 -0
- package/src/types/request.ts +358 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +103 -1883
- package/src/usage/cost.ts +37 -1
- package/src/usage/log.ts +4 -0
- package/src/web-search/loop.ts +11 -4
|
@@ -12,7 +12,8 @@ import { ConfigMutationLockError } from "../config";
|
|
|
12
12
|
import { isCodexAccountUsable } from "./account-usability";
|
|
13
13
|
import { reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
14
14
|
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountToken, isMainAccountTokenLive } from "./main-account";
|
|
15
|
-
import { isNativeMainTrafficBlocked } from "./native-profile-startup";
|
|
15
|
+
import { isNativeMainTrafficBlocked, nativeMainStartupGateSnapshot } from "./native-profile-startup";
|
|
16
|
+
import type { NativeMainStartupBlockReason } from "./native-profile-startup";
|
|
16
17
|
import {
|
|
17
18
|
codexQuotaScopeForModel,
|
|
18
19
|
getCodexQuotaHealthSnapshot,
|
|
@@ -23,6 +24,13 @@ import {
|
|
|
23
24
|
pickAlternateCodexAccount,
|
|
24
25
|
resolveCodexAccountForThreadDetailed,
|
|
25
26
|
} from "./routing";
|
|
27
|
+
import {
|
|
28
|
+
entitledCodexAccountIdsForModel,
|
|
29
|
+
isDirectCallerEntitledToCodexModel,
|
|
30
|
+
resolveCodexModelEntitlements,
|
|
31
|
+
type CodexModelEntitlementSnapshot,
|
|
32
|
+
} from "./model-entitlements";
|
|
33
|
+
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
|
|
26
34
|
import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
|
|
27
35
|
import { maskAccountId } from "../lib/privacy";
|
|
28
36
|
import { formatErrorResponse } from "../bridge";
|
|
@@ -116,12 +124,63 @@ export const CODEX_MAIN_PROFILE_MAINTENANCE_MESSAGE =
|
|
|
116
124
|
"OpenCodex local native-main profile maintenance is active; retry this request";
|
|
117
125
|
|
|
118
126
|
export class CodexMainProfileDrainingError extends Error {
|
|
127
|
+
/**
|
|
128
|
+
* Which startup-gate state fenced this request, when one did. Undefined means the
|
|
129
|
+
* fence came from somewhere other than the startup gate — the turn-drain claim race
|
|
130
|
+
* throws this same error while the gate reads `ready`, and inventing a reason there
|
|
131
|
+
* would point the next report at a gate that never closed.
|
|
132
|
+
*
|
|
133
|
+
* Captured here rather than at the throw sites because this is the last moment it is
|
|
134
|
+
* both in scope and still true: every catch site has already lost it, and re-reading
|
|
135
|
+
* the gate later can observe a recovery that completed in between (#2108).
|
|
136
|
+
*/
|
|
137
|
+
readonly reason?: NativeMainStartupBlockReason;
|
|
138
|
+
|
|
119
139
|
constructor() {
|
|
120
140
|
super(CODEX_MAIN_PROFILE_MAINTENANCE_MESSAGE);
|
|
121
141
|
this.name = "CodexMainProfileDrainingError";
|
|
142
|
+
const gate = nativeMainStartupGateSnapshot();
|
|
143
|
+
if (gate.status !== "blocked") return;
|
|
144
|
+
this.reason = gate.reason;
|
|
145
|
+
reportNativeMainFenceReason(gate.reason);
|
|
122
146
|
}
|
|
123
147
|
}
|
|
124
148
|
|
|
149
|
+
/**
|
|
150
|
+
* #2108: a reboot could leave this fence closed until `ocx restart`, and the report was
|
|
151
|
+
* unactionable because the settled reason was never written anywhere. It cannot ride the
|
|
152
|
+
* message (claude-messages.ts matches that string exactly to keep the fence a 503 rather
|
|
153
|
+
* than an Anthropic 529) and it cannot ride a header (/api/logs reads only error.message
|
|
154
|
+
* from the body, and the Claude surface rebuilds its response headers from scratch), so
|
|
155
|
+
* stdout is the one surface that covers every path this fence fires on.
|
|
156
|
+
*
|
|
157
|
+
* Deduped per distinct reason: the original report shows three 503s in eleven seconds and
|
|
158
|
+
* a real client retries harder than that, so a per-request line would bury the signal.
|
|
159
|
+
* Only the reason is emitted; the snapshot's homeId is derived from a profile directory.
|
|
160
|
+
*/
|
|
161
|
+
const reportedFenceReasons = new Set<NativeMainStartupBlockReason>();
|
|
162
|
+
|
|
163
|
+
function reportNativeMainFenceReason(reason: NativeMainStartupBlockReason): void {
|
|
164
|
+
if (reportedFenceReasons.has(reason)) return;
|
|
165
|
+
reportedFenceReasons.add(reason);
|
|
166
|
+
console.warn(
|
|
167
|
+
`native-main admission is fenced (reason: ${reason}); native model requests return 503 until it clears`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Test-only reset for the dedup set above.
|
|
173
|
+
*
|
|
174
|
+
* The dedup is process-lifetime module state, so it is order-sensitive across test files
|
|
175
|
+
* sharing one Bun process: whichever file constructs this error first consumes the one-shot
|
|
176
|
+
* warn, and a later file asserting on it would see nothing and pass vacuously. Any test that
|
|
177
|
+
* asserts on the warn must call this first — an `afterEach` in the asserting file is not
|
|
178
|
+
* enough on its own, because the consuming file may not be the asserting one.
|
|
179
|
+
*/
|
|
180
|
+
export function __resetNativeMainFenceReasonLog(): void {
|
|
181
|
+
reportedFenceReasons.clear();
|
|
182
|
+
}
|
|
183
|
+
|
|
125
184
|
export function codexMainProfileDrainingResponse(): Response {
|
|
126
185
|
const response = formatErrorResponse(503, "server_busy", CODEX_MAIN_PROFILE_MAINTENANCE_MESSAGE);
|
|
127
186
|
const headers = new Headers(response.headers);
|
|
@@ -237,6 +296,14 @@ export interface ResolveCodexAuthContextOptions {
|
|
|
237
296
|
isMainAccountTokenLive?: () => boolean;
|
|
238
297
|
getMainAccountToken?: typeof getMainAccountToken;
|
|
239
298
|
primeCodexPoolQuotas?: (config: OcxConfig, reason: string) => Promise<void>;
|
|
299
|
+
/** Test seam for account-gated native model discovery. */
|
|
300
|
+
resolveCodexModelEntitlements?: (
|
|
301
|
+
config: Pick<OcxConfig, "codexAccounts">,
|
|
302
|
+
) => Promise<CodexModelEntitlementSnapshot>;
|
|
303
|
+
/** Direct requests admitted with a proxy bearer substitute the stored native-main credential. */
|
|
304
|
+
substituteMainCredentialForDirect?: boolean;
|
|
305
|
+
/** Test seam for a Direct request's own forwarded ChatGPT credential. */
|
|
306
|
+
isDirectCallerEntitledToCodexModel?: (headers: Headers, modelId: string) => Promise<boolean>;
|
|
240
307
|
}
|
|
241
308
|
|
|
242
309
|
export interface CodexAccountSelectionAdmission {
|
|
@@ -260,8 +327,28 @@ export async function resolveCodexAuthContext(
|
|
|
260
327
|
// selected stored credential even while the canonical OpenAI provider is globally Direct.
|
|
261
328
|
if (mode === "direct" && fixedAccountId === undefined) {
|
|
262
329
|
if (!hasCallerCodexBearer(headers)) throw new CodexDirectAuthenticationError();
|
|
330
|
+
if (options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)) {
|
|
331
|
+
const entitled = options.substituteMainCredentialForDirect
|
|
332
|
+
? entitledCodexAccountIdsForModel(
|
|
333
|
+
await (options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements)(config),
|
|
334
|
+
options.modelId,
|
|
335
|
+
)?.has(MAIN_CODEX_ACCOUNT_ID) === true
|
|
336
|
+
: await (options.isDirectCallerEntitledToCodexModel ?? isDirectCallerEntitledToCodexModel)(
|
|
337
|
+
headers,
|
|
338
|
+
options.modelId,
|
|
339
|
+
);
|
|
340
|
+
if (!entitled) {
|
|
341
|
+
throw new CodexPoolAuthenticationError("The selected ChatGPT account does not support this model");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
263
344
|
return { kind: "main", accountId: null };
|
|
264
345
|
}
|
|
346
|
+
const entitlementSnapshot = options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
|
|
347
|
+
? await (options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements)(config)
|
|
348
|
+
: undefined;
|
|
349
|
+
const modelEligibleAccountIds = entitlementSnapshot
|
|
350
|
+
? entitledCodexAccountIdsForModel(entitlementSnapshot, options.modelId)
|
|
351
|
+
: undefined;
|
|
265
352
|
// Retained startup recovery makes the physical main identity ineligible. Routing
|
|
266
353
|
// can still preserve service by selecting a healthy configured pool account.
|
|
267
354
|
const nativeMainTrafficBlocked = isNativeMainTrafficBlocked();
|
|
@@ -273,6 +360,7 @@ export async function resolveCodexAuthContext(
|
|
|
273
360
|
nativeMainSelectionOnly: !nativeMainTrafficBlocked
|
|
274
361
|
&& selectionAdmission?.mainProfileDraining === true,
|
|
275
362
|
isMainAccountTokenLive: options.isMainAccountTokenLive,
|
|
363
|
+
modelEligibleAccountIds,
|
|
276
364
|
};
|
|
277
365
|
let accountId: string;
|
|
278
366
|
const quotaScope = codexQuotaScopeForModel(options.modelId);
|
|
@@ -302,7 +390,11 @@ export async function resolveCodexAuthContext(
|
|
|
302
390
|
const selected = resolution.status === "selected" ? resolution.accountId : null;
|
|
303
391
|
if (!selected) {
|
|
304
392
|
if (fixedAccountId !== undefined) {
|
|
305
|
-
throw new CodexPoolAuthenticationError(
|
|
393
|
+
throw new CodexPoolAuthenticationError(
|
|
394
|
+
modelEligibleAccountIds && !modelEligibleAccountIds.has(fixedAccountId)
|
|
395
|
+
? "Selected Codex account does not support this model"
|
|
396
|
+
: "Selected Codex account is unavailable",
|
|
397
|
+
);
|
|
306
398
|
}
|
|
307
399
|
// Recovery deliberately makes physical main ineligible. If no healthy
|
|
308
400
|
// pool route is configured and main is the intended route, report the
|
|
@@ -312,7 +404,9 @@ export async function resolveCodexAuthContext(
|
|
|
312
404
|
if (nativeMainTrafficBlocked && !options.excludeAccountId) {
|
|
313
405
|
throw new CodexMainProfileDrainingError();
|
|
314
406
|
}
|
|
315
|
-
throw new CodexPoolAuthenticationError(
|
|
407
|
+
throw new CodexPoolAuthenticationError(
|
|
408
|
+
modelEligibleAccountIds ? "No eligible Codex account supports this model" : undefined,
|
|
409
|
+
);
|
|
316
410
|
}
|
|
317
411
|
accountId = selected;
|
|
318
412
|
if (accountId === MAIN_CODEX_ACCOUNT_ID && nativeMainTrafficBlocked) {
|
|
@@ -325,6 +419,17 @@ export async function resolveCodexAuthContext(
|
|
|
325
419
|
) {
|
|
326
420
|
throw new CodexMainProfileDrainingError();
|
|
327
421
|
}
|
|
422
|
+
// Some legacy Pool fallbacks preserve a configured active account even when it is not
|
|
423
|
+
// currently selectable, so token/cooldown code can produce the historical actionable error.
|
|
424
|
+
// Model entitlement is different: sending the request would spend a turn on an account whose
|
|
425
|
+
// authenticated roster already denied the model. Reassert this boundary after every selector.
|
|
426
|
+
if (modelEligibleAccountIds && !modelEligibleAccountIds.has(accountId)) {
|
|
427
|
+
throw new CodexPoolAuthenticationError(
|
|
428
|
+
fixedAccountId !== undefined
|
|
429
|
+
? "Selected Codex account does not support this model"
|
|
430
|
+
: "No eligible Codex account supports this model",
|
|
431
|
+
);
|
|
432
|
+
}
|
|
328
433
|
if (fixedAccountId !== undefined) {
|
|
329
434
|
if (isCodexAccountPaused(config, accountId)) {
|
|
330
435
|
throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
|
|
@@ -183,6 +183,9 @@ export function deriveComboCatalogModel(
|
|
|
183
183
|
? { supportsServiceTier: false }
|
|
184
184
|
: {}),
|
|
185
185
|
...(members.some(member => member.supportsReasoningSummaries === false) ? { supportsReasoningSummaries: false } : {}),
|
|
186
|
+
...(members.every(member => member.codexToolMode === "shell")
|
|
187
|
+
? { codexToolMode: "shell" as const }
|
|
188
|
+
: {}),
|
|
186
189
|
};
|
|
187
190
|
}
|
|
188
191
|
|
|
@@ -12,7 +12,7 @@ import { modelInList } from "../../types";
|
|
|
12
12
|
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
13
13
|
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
|
|
14
14
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
15
|
-
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
15
|
+
import { getProviderRegistryEntry, providerCodexAccountMode } from "../../providers/registry";
|
|
16
16
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
17
17
|
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
18
18
|
import { identifyRoutedModel } from "../../adapters/identity";
|
|
@@ -38,6 +38,7 @@ import { readCurrentCatalogOrCache, readCurrentCodexCatalog, readCurrentCodexMod
|
|
|
38
38
|
import { trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
|
|
39
39
|
import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
40
40
|
import {
|
|
41
|
+
ACCOUNT_GATED_NATIVE_OPENAI_MODELS,
|
|
41
42
|
NATIVE_DAYBREAK_BLUE_MODEL,
|
|
42
43
|
NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS,
|
|
43
44
|
NATIVE_OPENAI_MODELS,
|
|
@@ -45,6 +46,8 @@ import {
|
|
|
45
46
|
isNativeOpenAiCapabilityAliasModel,
|
|
46
47
|
nativeOpenAiCapabilitySourceSlug,
|
|
47
48
|
} from "./native-models";
|
|
49
|
+
import { cachedAvailableAccountGatedNativeModels } from "../model-entitlements";
|
|
50
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
48
51
|
export { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
|
|
49
52
|
export {
|
|
50
53
|
NATIVE_DAYBREAK_BLUE_MODEL,
|
|
@@ -390,7 +393,14 @@ export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "comb
|
|
|
390
393
|
// Both user levers, not just the cap: a per-model window set from the dashboard has to show
|
|
391
394
|
// up on the row the dashboard itself renders.
|
|
392
395
|
const limits = nativeContextLimits(config);
|
|
393
|
-
|
|
396
|
+
const bareEligibleAccountIds = providerCodexAccountMode(
|
|
397
|
+
OPENAI_CODEX_PROVIDER_ID,
|
|
398
|
+
config.providers?.[OPENAI_CODEX_PROVIDER_ID],
|
|
399
|
+
) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
|
|
400
|
+
const availableGated = cachedAvailableAccountGatedNativeModels(Date.now(), bareEligibleAccountIds);
|
|
401
|
+
return NATIVE_OPENAI_MODELS
|
|
402
|
+
.filter(slug => !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableGated.has(slug))
|
|
403
|
+
.filter(slug => !shadowed.has(slug)).map(slug => {
|
|
394
404
|
const contextWindow = nativeOpenAiContextWindow(slug, limits);
|
|
395
405
|
const maxInputTokens = nativeOpenAiMaxInputTokens(slug, limits);
|
|
396
406
|
return {
|
|
@@ -475,7 +485,11 @@ export function shouldUpgradeToUpstreamEntry(entry: RawEntry): boolean {
|
|
|
475
485
|
|
|
476
486
|
export function nativeOpenAiSlugs(): string[] {
|
|
477
487
|
const live = catalogNativeSlugs();
|
|
478
|
-
|
|
488
|
+
const availableGated = cachedAvailableAccountGatedNativeModels();
|
|
489
|
+
const candidates = live.length > 0 ? unique([...live, ...DOCUMENTED_NATIVE_OPENAI_ADDITIONS]) : NATIVE_OPENAI_MODELS;
|
|
490
|
+
return candidates.filter(slug => (
|
|
491
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableGated.has(slug)
|
|
492
|
+
));
|
|
479
493
|
}
|
|
480
494
|
|
|
481
495
|
const ACCOUNT_BOUND_OPENAI_NATIVE_PREFIX = /^(?:gpt-|o1-|o3-|o4-)/;
|
|
@@ -1,12 +1,22 @@
|
|
|
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
|
+
/** Native ChatGPT/Codex ids whose availability is proven per authenticated account. */
|
|
5
|
+
export const ACCOUNT_GATED_NATIVE_OPENAI_MODELS: ReadonlySet<string> = new Set([
|
|
6
|
+
NATIVE_DAYBREAK_BLUE_MODEL,
|
|
7
|
+
]);
|
|
8
|
+
|
|
4
9
|
/**
|
|
5
10
|
* Account-native aliases whose Codex capabilities track another pinned native row.
|
|
6
11
|
*
|
|
7
12
|
* This is catalog metadata inheritance only. Routing always preserves the requested
|
|
8
|
-
* wire id
|
|
9
|
-
*
|
|
13
|
+
* wire id for the separately billed API-key `daybreak-*-latest` surface, so the two never
|
|
14
|
+
* collapse into each other.
|
|
15
|
+
*
|
|
16
|
+
* The ChatGPT/Codex surface is different: an account-gated request IS rewritten to its
|
|
17
|
+
* canonical wire model before it leaves the process (`applyCodexAccountGatedWireNormalization`
|
|
18
|
+
* in src/server/responses/core.ts), because the authenticated backend rejects the gated slug
|
|
19
|
+
* on shards that do not carry it. The catalog keeps the product identity; only the wire moves.
|
|
10
20
|
*/
|
|
11
21
|
const NATIVE_OPENAI_CAPABILITY_SOURCES: Readonly<Record<string, string>> = Object.freeze({
|
|
12
22
|
[NATIVE_DAYBREAK_BLUE_MODEL]: "gpt-5.6-sol",
|
|
@@ -16,14 +26,14 @@ const NATIVE_OPENAI_CAPABILITY_SOURCES: Readonly<Record<string, string>> = Objec
|
|
|
16
26
|
* Native ids whose capability metadata is inherited from another pinned native row.
|
|
17
27
|
*
|
|
18
28
|
* Membership here is about METADATA INHERITANCE only, and is independent of whether the
|
|
19
|
-
* slug is also
|
|
20
|
-
*
|
|
21
|
-
*
|
|
29
|
+
* slug is also present in `NATIVE_OPENAI_MODELS`. `gpt-daybreak-blue-latest` is now in BOTH:
|
|
30
|
+
* it inherits Sol's capability shape AND is a supported account-gated native id (owner decision,
|
|
31
|
+
* devlog 260816_codexrs_multiagent_v2_and_history_perf/011).
|
|
22
32
|
*
|
|
23
33
|
* The maps that consume the union of these two lists (`PINNED_NATIVE_CAPABILITY_ENTRIES`,
|
|
24
34
|
* `UPSTREAM_NATIVE_ENTRIES`) are keyed by slug, so an overlapping id collapses to one
|
|
25
|
-
* entry. Catalog row generation iterates `NATIVE_OPENAI_MODELS
|
|
26
|
-
*
|
|
35
|
+
* entry. Catalog row generation iterates `NATIVE_OPENAI_MODELS`, then entitlement evidence limits
|
|
36
|
+
* it to at most one bare row and one row per entitled account selector.
|
|
27
37
|
*/
|
|
28
38
|
export const NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS = Object.freeze(
|
|
29
39
|
Object.keys(NATIVE_OPENAI_CAPABILITY_SOURCES),
|
|
@@ -42,16 +52,14 @@ export function nativeOpenAiCapabilitySourceSlug(slug: string): string {
|
|
|
42
52
|
*
|
|
43
53
|
* `gpt-daybreak-blue-latest` is entitlement-gated upstream: it is absent from codex-rs's
|
|
44
54
|
* bundled catalog and reaches a client only through an authenticated `/models` response.
|
|
45
|
-
* It is listed here by explicit owner decision so the
|
|
46
|
-
* observation, because opencodex injects `model_catalog_json` and codex-rs therefore builds
|
|
55
|
+
* It is listed here by explicit owner decision so the capability template exists without waiting
|
|
56
|
+
* for an observation, because opencodex injects `model_catalog_json` and codex-rs therefore builds
|
|
47
57
|
* a `StaticModelsManager` whose refresh is a no-op — an entitled account had no way to
|
|
48
58
|
* discover it on a clean install.
|
|
49
59
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* retry one alternate account on that exact body; a selector-qualified route is fixed and
|
|
54
|
-
* relays immediately). `disabledModels` hides the row but is NOT a runtime routing denial.
|
|
60
|
+
* Availability is not static: catalog sync and Pool routing require the account's authenticated
|
|
61
|
+
* `/models` roster to contain the slug. An unconfirmed or unentitled account never receives the
|
|
62
|
+
* request. `disabledModels` remains the independent user visibility control.
|
|
55
63
|
*
|
|
56
64
|
* Devlog: 260816_codexrs_multiagent_v2_and_history_perf/011 §4-bis.
|
|
57
65
|
*/
|
|
@@ -126,6 +126,12 @@ export interface CatalogModel {
|
|
|
126
126
|
/** Whether this exact routed model has a verified OpenAI-compatible service tier. */
|
|
127
127
|
supportsServiceTier?: boolean;
|
|
128
128
|
supportsReasoningSummaries?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Codex tool calling mode for this routed model.
|
|
131
|
+
* "code_mode_only" (default) sets entry.tool_mode = "code_mode_only".
|
|
132
|
+
* "shell" leaves tool_mode unset so Codex declares top-level shell tools (exec_command).
|
|
133
|
+
*/
|
|
134
|
+
codexToolMode?: "code_mode_only" | "shell";
|
|
129
135
|
/** Normalized upstream capability names retained for management/API consumers (#485 follow-up). */
|
|
130
136
|
capabilities?: string[];
|
|
131
137
|
/** OpenCodex-only catalog ownership marker; Codex ignores the serialized extension field. */
|
|
@@ -423,7 +429,14 @@ export function catalogEntryIsNativeChatGpt(entry: RawEntry): boolean {
|
|
|
423
429
|
|
|
424
430
|
export const ROUTED_CODEX_TOOL_MODE = "code_mode_only";
|
|
425
431
|
|
|
426
|
-
export function applyRoutedCodexToolMode(
|
|
432
|
+
export function applyRoutedCodexToolMode(
|
|
433
|
+
entry: RawEntry,
|
|
434
|
+
toolMode?: "code_mode_only" | "shell" | string,
|
|
435
|
+
): RawEntry {
|
|
436
|
+
if (toolMode === "shell") {
|
|
437
|
+
delete entry.tool_mode;
|
|
438
|
+
return entry;
|
|
439
|
+
}
|
|
427
440
|
entry.tool_mode = ROUTED_CODEX_TOOL_MODE;
|
|
428
441
|
return entry;
|
|
429
442
|
}
|
|
@@ -490,10 +503,14 @@ export function applyMultiAgentMode(
|
|
|
490
503
|
return entries;
|
|
491
504
|
}
|
|
492
505
|
|
|
493
|
-
export function normalizeRoutedCatalogEntry(
|
|
506
|
+
export function normalizeRoutedCatalogEntry(
|
|
507
|
+
entry: RawEntry,
|
|
508
|
+
parallelToolCalls = false,
|
|
509
|
+
toolMode?: "code_mode_only" | "shell" | string,
|
|
510
|
+
): RawEntry {
|
|
494
511
|
delete entry.model_messages;
|
|
495
512
|
delete entry.tool_mode;
|
|
496
|
-
applyRoutedCodexToolMode(entry);
|
|
513
|
+
applyRoutedCodexToolMode(entry, toolMode);
|
|
497
514
|
delete entry.multi_agent_version;
|
|
498
515
|
delete entry.use_responses_lite;
|
|
499
516
|
delete entry.supports_websockets;
|
|
@@ -413,6 +413,7 @@ function captureProviderGather(
|
|
|
413
413
|
name,
|
|
414
414
|
provider,
|
|
415
415
|
registryTransportMatch,
|
|
416
|
+
configured,
|
|
416
417
|
);
|
|
417
418
|
const observedAuth = authResolver.kind === "observed"
|
|
418
419
|
&& provider.authMode !== "forward"
|
|
@@ -676,6 +677,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
|
|
|
676
677
|
...(prov.parallelToolCalls === true || (prov.adapter === "openai-chat" && prov.parallelToolCalls !== false)
|
|
677
678
|
? { parallelToolCalls: true }
|
|
678
679
|
: {}),
|
|
680
|
+
...(prov.codexToolMode !== undefined ? { codexToolMode: prov.codexToolMode } : {}),
|
|
679
681
|
};
|
|
680
682
|
const capped = applyProviderContextCap(hinted.contextWindow, providerCap);
|
|
681
683
|
if (providerCap !== undefined && capped !== hinted.contextWindow) {
|
|
@@ -1881,6 +1883,11 @@ async function gatherRoutedModelsUncached(
|
|
|
1881
1883
|
...(Array.isArray(cm.reasoningEfforts) ? { reasoningEfforts: [...cm.reasoningEfforts] } : {}),
|
|
1882
1884
|
...(cm.defaultReasoningEffort ? { defaultReasoningEffort: cm.defaultReasoningEffort } : {}),
|
|
1883
1885
|
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
|
|
1886
|
+
...(cm.codexToolMode !== undefined
|
|
1887
|
+
? { codexToolMode: cm.codexToolMode }
|
|
1888
|
+
: effectiveProvider?.codexToolMode !== undefined
|
|
1889
|
+
? { codexToolMode: effectiveProvider.codexToolMode }
|
|
1890
|
+
: {}),
|
|
1884
1891
|
};
|
|
1885
1892
|
// #962: the dedupe below drops the provider-derived row this custom row replaces. Inherit that
|
|
1886
1893
|
// row's provider capability metadata (reasoning ladder, default effort, parallel tool calls,
|
|
@@ -1905,6 +1912,7 @@ async function gatherRoutedModelsUncached(
|
|
|
1905
1912
|
...(base.parallelToolCalls === undefined && replaced.parallelToolCalls !== undefined ? { parallelToolCalls: replaced.parallelToolCalls } : {}),
|
|
1906
1913
|
...(base.supportsVerbosity === undefined && replaced.supportsVerbosity !== undefined ? { supportsVerbosity: replaced.supportsVerbosity } : {}),
|
|
1907
1914
|
...(base.supportsReasoningSummaries === undefined && replaced.supportsReasoningSummaries !== undefined ? { supportsReasoningSummaries: replaced.supportsReasoningSummaries } : {}),
|
|
1915
|
+
...(base.codexToolMode === undefined && replaced.codexToolMode !== undefined ? { codexToolMode: replaced.codexToolMode } : {}),
|
|
1908
1916
|
...(base.capabilities === undefined && replaced.capabilities !== undefined ? { capabilities: replaced.capabilities } : {}),
|
|
1909
1917
|
} : base;
|
|
1910
1918
|
// Vision-sidecar coverage ONLY: if the custom model is in the enriched provider's
|
|
@@ -30,7 +30,15 @@ import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
|
30
30
|
import { redactSecretString } from "../../lib/redact";
|
|
31
31
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
32
32
|
import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
33
|
+
import { providerCodexAccountMode } from "../../providers/registry";
|
|
33
34
|
import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../account-namespaces";
|
|
35
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
36
|
+
import {
|
|
37
|
+
availableAccountGatedNativeModels,
|
|
38
|
+
isCodexModelEntitlementSnapshotCurrent,
|
|
39
|
+
resolveCodexModelEntitlements,
|
|
40
|
+
type CodexModelEntitlementSnapshot,
|
|
41
|
+
} from "../model-entitlements";
|
|
34
42
|
|
|
35
43
|
|
|
36
44
|
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readNativeBaseline } from "./parsing";
|
|
@@ -64,6 +72,7 @@ import {
|
|
|
64
72
|
} from "../internal/catalog-writer";
|
|
65
73
|
import { codexRuntimeStatePath } from "../runtime";
|
|
66
74
|
import { accountBoundNativeDisplayName, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
|
|
75
|
+
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./native-models";
|
|
67
76
|
|
|
68
77
|
export const MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5;
|
|
69
78
|
|
|
@@ -331,7 +340,9 @@ export function deriveEntry(
|
|
|
331
340
|
// This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
|
|
332
341
|
// native tool/search/responses-lite contract while preserving the routed slug and wire id.
|
|
333
342
|
if (!codexForwardNativeCapabilityAlias) {
|
|
334
|
-
normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true);
|
|
343
|
+
normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true, model?.codexToolMode);
|
|
344
|
+
} else if (model?.codexToolMode !== undefined) {
|
|
345
|
+
applyRoutedCodexToolMode(e, model.codexToolMode);
|
|
335
346
|
}
|
|
336
347
|
if (model) applyCatalogMetadata(e, model.provider, model.id, model.contextCap);
|
|
337
348
|
applyCatalogModelMetadata(e, model);
|
|
@@ -358,8 +369,8 @@ export function deriveEntry(
|
|
|
358
369
|
});
|
|
359
370
|
}
|
|
360
371
|
// Fallback when no template is available (best-effort; strict parser may need more).
|
|
361
|
-
//
|
|
362
|
-
// expands into `exec.description` and can exceed Cursor's 120 KB serialized tool limit (#1830).
|
|
372
|
+
// Routed fallbacks default to code-mode tool exposure (or shell mode when codexToolMode === "shell");
|
|
373
|
+
// otherwise the nested catalog expands into `exec.description` and can exceed Cursor's 120 KB serialized tool limit (#1830).
|
|
363
374
|
// Cursor still omits hosted web-search metadata because runTurn bypasses that separate sidecar.
|
|
364
375
|
const isCursorFallback = isRouted && model?.provider === "cursor";
|
|
365
376
|
const entry: RawEntry = {
|
|
@@ -373,7 +384,7 @@ export function deriveEntry(
|
|
|
373
384
|
: {}),
|
|
374
385
|
};
|
|
375
386
|
if (isRouted) {
|
|
376
|
-
applyRoutedCodexToolMode(entry);
|
|
387
|
+
applyRoutedCodexToolMode(entry, model?.codexToolMode);
|
|
377
388
|
applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
|
|
378
389
|
}
|
|
379
390
|
else {
|
|
@@ -1236,6 +1247,7 @@ interface RetainedCatalogSyncWrite {
|
|
|
1236
1247
|
readonly read: RetainedCatalogSyncRead;
|
|
1237
1248
|
readonly permit: CatalogWritePermit;
|
|
1238
1249
|
readonly owningCodexHome: string;
|
|
1250
|
+
readonly modelEntitlements: CodexModelEntitlementSnapshot;
|
|
1239
1251
|
}
|
|
1240
1252
|
|
|
1241
1253
|
function optionalFileBytes(path: string): string | null {
|
|
@@ -1399,6 +1411,7 @@ function writeRetainedCatalogSync({
|
|
|
1399
1411
|
read,
|
|
1400
1412
|
permit,
|
|
1401
1413
|
owningCodexHome,
|
|
1414
|
+
modelEntitlements,
|
|
1402
1415
|
}: RetainedCatalogSyncWrite): RetainedCatalogSyncResult {
|
|
1403
1416
|
const { catalogPath, catalog, onDiskCatalog } = read;
|
|
1404
1417
|
const catalogModelsForMerge = catalogModelsForMergeWithNativeRecovery(
|
|
@@ -1434,7 +1447,28 @@ function writeRetainedCatalogSync({
|
|
|
1434
1447
|
const modelPickerOrder = config.modelPickerOrder ?? [];
|
|
1435
1448
|
const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
1436
1449
|
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
1437
|
-
const
|
|
1450
|
+
const bareEligibleAccountIds = providerCodexAccountMode(
|
|
1451
|
+
OPENAI_CODEX_PROVIDER_ID,
|
|
1452
|
+
config.providers[OPENAI_CODEX_PROVIDER_ID],
|
|
1453
|
+
) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
|
|
1454
|
+
const availableBareGatedNativeSlugs = availableAccountGatedNativeModels(
|
|
1455
|
+
modelEntitlements,
|
|
1456
|
+
bareEligibleAccountIds,
|
|
1457
|
+
);
|
|
1458
|
+
const availableAccountGatedNativeSlugs = availableAccountGatedNativeModels(modelEntitlements);
|
|
1459
|
+
const availableBareNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
1460
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
1461
|
+
));
|
|
1462
|
+
const availableAccountNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
1463
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableAccountGatedNativeSlugs.has(slug)
|
|
1464
|
+
));
|
|
1465
|
+
const unavailableGatedNativeSlugs = new Set([...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(slug => (
|
|
1466
|
+
!availableBareGatedNativeSlugs.has(slug)
|
|
1467
|
+
)));
|
|
1468
|
+
const suppressedBareNativeSlugs = new Set([
|
|
1469
|
+
...desktopAllowlistSuppressedNativeSlugs(config),
|
|
1470
|
+
...unavailableGatedNativeSlugs,
|
|
1471
|
+
]);
|
|
1438
1472
|
const hasPhysicalComboProvider = Object.hasOwn(config.providers, COMBO_NAMESPACE);
|
|
1439
1473
|
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
1440
1474
|
const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
|
|
@@ -1449,12 +1483,21 @@ function writeRetainedCatalogSync({
|
|
|
1449
1483
|
...(onDiskCatalog?.models ?? []).filter(entry =>
|
|
1450
1484
|
trustedAccountBoundNativeCatalogSlug(entry) !== undefined),
|
|
1451
1485
|
];
|
|
1452
|
-
const
|
|
1453
|
-
? accountBoundNativeOpenAiSlugs(observedAccountNativeEntries)
|
|
1454
|
-
: [];
|
|
1486
|
+
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
1455
1487
|
const accountNativeSlugsBySelector = accountSelectors.length > 0
|
|
1456
|
-
? accountBoundNativeOpenAiSlugsBySelector(config, observedAccountNativeEntries)
|
|
1488
|
+
? new Map([...accountBoundNativeOpenAiSlugsBySelector(config, observedAccountNativeEntries)].map(([selector, slugs]) => {
|
|
1489
|
+
const target = accountTargets.get(selector);
|
|
1490
|
+
const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
|
|
1491
|
+
const entitled = accountId ? modelEntitlements.modelsByAccount.get(accountId) : undefined;
|
|
1492
|
+
const confirmed = accountId ? modelEntitlements.confirmedAccountIds.has(accountId) : false;
|
|
1493
|
+
return [selector, slugs.filter(slug => (
|
|
1494
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || (confirmed && entitled?.has(slug) === true)
|
|
1495
|
+
))] as const;
|
|
1496
|
+
}))
|
|
1457
1497
|
: new Map<string, readonly string[]>();
|
|
1498
|
+
const accountNativeSlugs = accountSelectors.length > 0
|
|
1499
|
+
? [...new Set([...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]))]
|
|
1500
|
+
: [];
|
|
1458
1501
|
// Unknown account-native ids have no safe bare/global identity. They are only projected through
|
|
1459
1502
|
// the selector map above; the no-selector catalog remains the static native/API-key surface.
|
|
1460
1503
|
const observedNativeSlugs: string[] = [];
|
|
@@ -1508,7 +1551,7 @@ function writeRetainedCatalogSync({
|
|
|
1508
1551
|
const accountBoundEntries = includeAccountBoundNativeOpenAi && accountSelectors.length > 0
|
|
1509
1552
|
? buildCatalogEntriesFromObservedState({
|
|
1510
1553
|
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
1511
|
-
gptSlugs:
|
|
1554
|
+
gptSlugs: availableAccountNativeSlugs,
|
|
1512
1555
|
goModels: [],
|
|
1513
1556
|
featured,
|
|
1514
1557
|
wsEnabled,
|
|
@@ -1548,7 +1591,7 @@ function writeRetainedCatalogSync({
|
|
|
1548
1591
|
openaiContextCap,
|
|
1549
1592
|
policy: {
|
|
1550
1593
|
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
1551
|
-
nativeBackfillSlugs: [...
|
|
1594
|
+
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
|
|
1552
1595
|
warningPolicy: "emit",
|
|
1553
1596
|
},
|
|
1554
1597
|
});
|
|
@@ -1662,10 +1705,13 @@ export async function syncCatalogModels(
|
|
|
1662
1705
|
evidence: retainedCatalogSyncEvidence(config, preflightRead.catalogPath, preflightRead.catalog),
|
|
1663
1706
|
processEvidence: retainedCatalogProcessEvidence(),
|
|
1664
1707
|
};
|
|
1665
|
-
const goModels = await
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1708
|
+
const [goModels, modelEntitlements] = await Promise.all([
|
|
1709
|
+
gatherRoutedModels(config, {
|
|
1710
|
+
comboOmissions,
|
|
1711
|
+
providerModelOutcomes,
|
|
1712
|
+
}),
|
|
1713
|
+
resolveCodexModelEntitlements(config),
|
|
1714
|
+
]);
|
|
1669
1715
|
const committed = withCatalogWriteSerialization(owningCodexHome, permit => {
|
|
1670
1716
|
// Desired state can flip OFF during the provider await above. The catalog
|
|
1671
1717
|
// evidence revalidation below cannot see that — intent lives in our config,
|
|
@@ -1685,6 +1731,7 @@ export async function syncCatalogModels(
|
|
|
1685
1731
|
}
|
|
1686
1732
|
const current = revalidateRetainedCatalogSync(config, prepared);
|
|
1687
1733
|
if (current === null) return null;
|
|
1734
|
+
if (!isCodexModelEntitlementSnapshotCurrent(modelEntitlements)) return null;
|
|
1688
1735
|
return writeRetainedCatalogSync({
|
|
1689
1736
|
config,
|
|
1690
1737
|
goModels,
|
|
@@ -1693,6 +1740,7 @@ export async function syncCatalogModels(
|
|
|
1693
1740
|
read: current,
|
|
1694
1741
|
permit,
|
|
1695
1742
|
owningCodexHome,
|
|
1743
|
+
modelEntitlements,
|
|
1696
1744
|
});
|
|
1697
1745
|
});
|
|
1698
1746
|
if (committed.kind === "completed" && committed.value !== null) return committed.value;
|