@bitkyc08/opencodex 2.22.0 → 2.23.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-ClEcVlFO.js → index-rFWrIE11.js} +19 -19
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic.ts +39 -7
- package/src/adapters/cursor/tool-definitions.ts +48 -0
- package/src/adapters/google.ts +18 -12
- package/src/adapters/openai-chat.ts +106 -13
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +3 -0
- package/src/bridge.ts +16 -5
- package/src/chat/inbound.ts +5 -11
- package/src/claude/context-windows.ts +5 -1
- package/src/claude/desktop-3p.ts +11 -6
- package/src/claude/inbound.ts +39 -1
- package/src/claude/model-info.ts +28 -8
- package/src/cli/account-api.ts +5 -1
- package/src/cli/claude-desktop.ts +3 -0
- package/src/cli/config-command.ts +37 -14
- package/src/codex/app-server-restart-service.ts +1 -1
- package/src/codex/auth-api.ts +5 -0
- package/src/codex/auth-context.ts +43 -2
- package/src/codex/catalog/metadata.ts +55 -8
- package/src/codex/catalog/native-models.ts +32 -2
- package/src/codex/catalog/parsing.ts +21 -7
- package/src/codex/catalog/provider-fetch.ts +35 -7
- package/src/codex/catalog/sync.ts +40 -18
- package/src/codex/catalog-refresh-status.ts +21 -3
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +23 -2
- package/src/codex/desired-state.ts +1 -1
- package/src/codex/inject.ts +38 -7
- package/src/codex/injected-marker.ts +28 -0
- package/src/codex/journal.ts +40 -1
- package/src/codex/management-convergence.ts +55 -2
- package/src/codex/quota-rejection.ts +61 -1
- package/src/codex/quota.ts +60 -6
- package/src/codex/routing.ts +30 -3
- package/src/combos/failover.ts +20 -0
- package/src/config.ts +271 -4
- package/src/generated/compatibility-version.json +86 -74
- package/src/grok/sync.ts +3 -1
- package/src/lab/artifacts/sanitize.ts +1 -1
- package/src/lab/live/manifest.ts +1 -1
- package/src/lib/codex-restart-contract.ts +1 -1
- package/src/lib/config-ownership.ts +1 -0
- package/src/lib/errors.ts +9 -0
- package/src/lib/lab-activation.ts +1 -1
- package/src/lib/optional-shutdown-hooks.ts +1 -1
- package/src/providers/quota.ts +10 -4
- package/src/providers/registry.ts +2 -2
- package/src/responses/parser.ts +42 -7
- package/src/responses/provider-opaque-metadata.ts +1 -1
- package/src/responses/thought-signature-replay.ts +261 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/provider-slot.ts +1 -1
- package/src/routing/evaluator.ts +12 -2
- package/src/routing/health.ts +16 -5
- package/src/routing/history/schema.ts +1 -1
- package/src/routing/trace.ts +1 -1
- package/src/server/auth-cors.ts +56 -21
- package/src/server/chat-completions.ts +6 -2
- package/src/server/index.ts +5 -3
- package/src/server/management/agent-settings-routes.ts +26 -4
- package/src/server/management/context.ts +1 -1
- package/src/server/management/model-rows.ts +5 -0
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/provider-routes.ts +19 -0
- package/src/server/management/shared.ts +3 -3
- package/src/server/management-api.ts +13 -6
- package/src/server/passive-route-linker.ts +1 -1
- package/src/server/relay.ts +16 -0
- package/src/server/responses/compact.ts +10 -3
- package/src/server/responses/core.ts +160 -33
- package/src/server/responses/fetch-helpers.ts +34 -2
- package/src/server/responses/input-admission.ts +17 -9
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/system-env.ts +4 -2
- package/src/service.ts +14 -7
- package/src/types.ts +34 -0
package/src/chat/inbound.ts
CHANGED
|
@@ -99,17 +99,8 @@ function pushSystemText(parts: string[], content: unknown): void {
|
|
|
99
99
|
if (text) parts.push(text);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
function toolCallsToItems(toolCalls: unknown, input: Rec[]): void {
|
|
102
|
+
function toolCallsToItems(toolCalls: unknown, input: Rec[], knownNameByCallId: Map<string, string>): void {
|
|
103
103
|
if (!Array.isArray(toolCalls)) return;
|
|
104
|
-
// Recover names from earlier function_call items in the same transcript when a client
|
|
105
|
-
// re-sends tool_calls with only id/arguments (replace-style merge lost function.name).
|
|
106
|
-
const knownNameByCallId = new Map<string, string>();
|
|
107
|
-
for (const item of input) {
|
|
108
|
-
if (!isRec(item) || item.type !== "function_call") continue;
|
|
109
|
-
if (typeof item.call_id === "string" && typeof item.name === "string" && item.name.length > 0) {
|
|
110
|
-
knownNameByCallId.set(item.call_id, item.name);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
104
|
for (const raw of toolCalls) {
|
|
114
105
|
if (!isRec(raw)) continue;
|
|
115
106
|
const fn = isRec(raw.function) ? raw.function : null;
|
|
@@ -243,6 +234,9 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
|
243
234
|
|
|
244
235
|
const systemParts: string[] = [];
|
|
245
236
|
const input: Rec[] = [];
|
|
237
|
+
// Recover replace-style tool calls incrementally instead of rebuilding the
|
|
238
|
+
// call-id index from the entire translated transcript for every message.
|
|
239
|
+
const knownNameByCallId = new Map<string, string>();
|
|
246
240
|
|
|
247
241
|
for (const msg of raw.messages) {
|
|
248
242
|
if (!isRec(msg)) continue;
|
|
@@ -260,7 +254,7 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
|
260
254
|
case "assistant": {
|
|
261
255
|
const blocks = assistantContentToBlocks(msg.content);
|
|
262
256
|
if (blocks.length > 0) input.push({ type: "message", role: "assistant", content: blocks });
|
|
263
|
-
if (msg.tool_calls !== undefined) toolCallsToItems(msg.tool_calls, input);
|
|
257
|
+
if (msg.tool_calls !== undefined) toolCallsToItems(msg.tool_calls, input, knownNameByCallId);
|
|
264
258
|
break;
|
|
265
259
|
}
|
|
266
260
|
case "tool": {
|
|
@@ -89,6 +89,10 @@ export function shouldMarkOneMillion(window: number | undefined, auto: AutoConte
|
|
|
89
89
|
export function buildClaudeContextWindows(
|
|
90
90
|
nativeSlugs: readonly string[],
|
|
91
91
|
routedModels: readonly CatalogModel[],
|
|
92
|
+
// A configured providerContextCaps.openai has to reach the native rows here too. Without
|
|
93
|
+
// it the Claude surface keeps advertising the uncapped authoritative window while the
|
|
94
|
+
// Codex catalog advertises the capped one, and the two disagree about the same model.
|
|
95
|
+
nativeContextCap?: number,
|
|
92
96
|
): Record<string, number> {
|
|
93
97
|
const out: Record<string, number> = {};
|
|
94
98
|
const put = (key: string | null, value: number) => {
|
|
@@ -96,7 +100,7 @@ export function buildClaudeContextWindows(
|
|
|
96
100
|
if (out[key] === undefined) out[key] = value; // first-wins (registry policy)
|
|
97
101
|
};
|
|
98
102
|
for (const slug of nativeSlugs) {
|
|
99
|
-
const window = nativeOpenAiContextWindow(slug);
|
|
103
|
+
const window = nativeOpenAiContextWindow(slug, nativeContextCap);
|
|
100
104
|
if (typeof window !== "number" || window <= 0) continue;
|
|
101
105
|
put(slug, window);
|
|
102
106
|
put(desktop3pAlias("native", slug), window);
|
package/src/claude/desktop-3p.ts
CHANGED
|
@@ -191,6 +191,7 @@ function collectDesktop3pModels(
|
|
|
191
191
|
nativeSlugs: string[],
|
|
192
192
|
routedModels: Array<Desktop3pRoutedModel>,
|
|
193
193
|
profile?: OcxClaudeDesktopProfile,
|
|
194
|
+
nativeContextCap?: number,
|
|
194
195
|
): { models: Desktop3pModelEntry[]; registry: Map<string, string> } {
|
|
195
196
|
const registry = new Map<string, string>();
|
|
196
197
|
const models: Desktop3pModelEntry[] = [];
|
|
@@ -199,7 +200,7 @@ function collectDesktop3pModels(
|
|
|
199
200
|
// Desktop DTO uses, so a native 1M/372k model resolves identically in the written
|
|
200
201
|
// config and on the dashboard.
|
|
201
202
|
...nativeSlugs.map(id => {
|
|
202
|
-
const contextWindow = nativeOpenAiContextWindow(id);
|
|
203
|
+
const contextWindow = nativeOpenAiContextWindow(id, nativeContextCap);
|
|
203
204
|
return { provider: "native", id, ...(contextWindow !== undefined ? { contextWindow } : {}) };
|
|
204
205
|
}),
|
|
205
206
|
...routedModels,
|
|
@@ -292,8 +293,9 @@ export function buildDesktop3pRegistry(
|
|
|
292
293
|
nativeSlugs: string[],
|
|
293
294
|
routedModels: Array<Desktop3pRoutedModel>,
|
|
294
295
|
profile?: OcxClaudeDesktopProfile,
|
|
296
|
+
nativeContextCap?: number,
|
|
295
297
|
): Map<string, string> {
|
|
296
|
-
const { registry } = collectDesktop3pModels(nativeSlugs, routedModels, profile);
|
|
298
|
+
const { registry } = collectDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap);
|
|
297
299
|
desktop3pRegistry = registry;
|
|
298
300
|
return registry;
|
|
299
301
|
}
|
|
@@ -303,8 +305,9 @@ export function generateDesktop3pModels(
|
|
|
303
305
|
nativeSlugs: string[],
|
|
304
306
|
routedModels: Array<Desktop3pRoutedModel>,
|
|
305
307
|
profile?: OcxClaudeDesktopProfile,
|
|
308
|
+
nativeContextCap?: number,
|
|
306
309
|
): Desktop3pModelEntry[] {
|
|
307
|
-
const { models, registry } = collectDesktop3pModels(nativeSlugs, routedModels, profile);
|
|
310
|
+
const { models, registry } = collectDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap);
|
|
308
311
|
desktop3pRegistry = registry;
|
|
309
312
|
return models;
|
|
310
313
|
}
|
|
@@ -334,6 +337,7 @@ export function generateDesktop3pConfig(
|
|
|
334
337
|
apiKey = "ocx",
|
|
335
338
|
mode: Desktop3pConfigMode = "static",
|
|
336
339
|
profile?: OcxClaudeDesktopProfile,
|
|
340
|
+
nativeContextCap?: number,
|
|
337
341
|
): object {
|
|
338
342
|
const base = {
|
|
339
343
|
inferenceProvider: "gateway",
|
|
@@ -343,14 +347,14 @@ export function generateDesktop3pConfig(
|
|
|
343
347
|
};
|
|
344
348
|
if (mode === "discovery") {
|
|
345
349
|
// Build/refresh the decode registry even though no static list is emitted.
|
|
346
|
-
buildDesktop3pRegistry(nativeSlugs, routedModels, profile);
|
|
350
|
+
buildDesktop3pRegistry(nativeSlugs, routedModels, profile, nativeContextCap);
|
|
347
351
|
return { ...base, modelDiscoveryEnabled: true };
|
|
348
352
|
}
|
|
349
353
|
return {
|
|
350
354
|
...base,
|
|
351
355
|
modelDiscoveryEnabled: mode === "hybrid",
|
|
352
356
|
inferenceModels: (() => {
|
|
353
|
-
const models = generateDesktop3pModels(nativeSlugs, routedModels, profile);
|
|
357
|
+
const models = generateDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap);
|
|
354
358
|
// Fail loud at the write boundary rather than ship a config Desktop rejects:
|
|
355
359
|
// the output counterpart of the request-path guards.
|
|
356
360
|
assertDesktop3pModelsValid(models);
|
|
@@ -554,6 +558,7 @@ export function writeDesktop3pConfig(
|
|
|
554
558
|
apiKey?: string,
|
|
555
559
|
mode: Desktop3pConfigMode = "static",
|
|
556
560
|
profile?: OcxClaudeDesktopProfile,
|
|
561
|
+
nativeContextCap?: number,
|
|
557
562
|
): { written: boolean; path: string; reason?: string; fingerprint?: string } {
|
|
558
563
|
const libraryPath = resolveDesktop3pConfigLibraryPath();
|
|
559
564
|
const metadataPath = join(libraryPath, "_meta.json");
|
|
@@ -571,7 +576,7 @@ export function writeDesktop3pConfig(
|
|
|
571
576
|
? metadata.entries.map(current => current === existing ? entry : current)
|
|
572
577
|
: [...metadata.entries, entry];
|
|
573
578
|
|
|
574
|
-
const configJson = JSON.stringify(generateDesktop3pConfig(port, nativeSlugs, routedModels, apiKey, mode, profile), null, 2) + "\n";
|
|
579
|
+
const configJson = JSON.stringify(generateDesktop3pConfig(port, nativeSlugs, routedModels, apiKey, mode, profile, nativeContextCap), null, 2) + "\n";
|
|
575
580
|
const fingerprint = createHash("sha256").update(configJson).digest("hex").slice(0, 16);
|
|
576
581
|
const { backupPath } = atomicReplaceDesktopConfig(configPath, configJson);
|
|
577
582
|
try {
|
package/src/claude/inbound.ts
CHANGED
|
@@ -25,7 +25,37 @@ function isRec(v: unknown): v is Rec {
|
|
|
25
25
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
function isClaudeClassifierModel(model: string): boolean {
|
|
29
|
+
const stripped = model.replace(/-\d{8}$/, "");
|
|
30
|
+
return /^claude-opus-[45]/.test(stripped);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Explicitly configured classifier route for Claude Code Auto Mode safety checks (#1697).
|
|
35
|
+
*
|
|
36
|
+
* Only OPERATOR-DECLARED targets are used: `classifierModel`, then the ordered
|
|
37
|
+
* `classifierFallbacks`. Both are qualified `provider/model` strings the operator chose, so
|
|
38
|
+
* routing them crosses no boundary the operator did not ask for.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately NOT here: inferring a provider from `claudeCode.model`. That value is the
|
|
41
|
+
* injected/default config slot, not the provider the live session actually selected, so it goes
|
|
42
|
+
* stale the moment the user changes the model picker -- and acting on it would silently move a
|
|
43
|
+
* classifier turn onto a provider with its own privacy and billing consequences. Live session
|
|
44
|
+
* affinity needs the request/session state this function does not have; it is tracked as
|
|
45
|
+
* follow-up work rather than approximated from static config.
|
|
46
|
+
*/
|
|
47
|
+
function configuredClassifierRoute(cc?: OcxClaudeCodeConfig): string | undefined {
|
|
48
|
+
const explicit = typeof cc?.classifierModel === "string" ? cc.classifierModel.trim() : "";
|
|
49
|
+
if (explicit.length > 0) return explicit;
|
|
50
|
+
if (Array.isArray(cc?.classifierFallbacks)) {
|
|
51
|
+
for (const candidate of cc.classifierFallbacks) {
|
|
52
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Alias first, then modelMap: exact id, then date-suffix-stripped (`-\d{8}$`), then classifier affinity/config, else passthrough. */
|
|
29
59
|
export function resolveInboundModel(model: string, cc?: OcxClaudeCodeConfig): string {
|
|
30
60
|
// Defensive: Desktop/CLI strip the [1m] context-variant marker client-side, but a
|
|
31
61
|
// leaking build must not break alias decode (devlog 138 — the 1M signal is the
|
|
@@ -47,6 +77,14 @@ export function resolveInboundModel(model: string, cc?: OcxClaudeCodeConfig): st
|
|
|
47
77
|
const stripped = model.replace(/-\d{8}$/, "");
|
|
48
78
|
const dateless = map[stripped];
|
|
49
79
|
if (typeof dateless === "string" && dateless.length > 0) return dateless;
|
|
80
|
+
|
|
81
|
+
// Claude Code Auto Mode classifier routing (#1697). Bare classifier checks such as
|
|
82
|
+
// `claude-opus-5` carry no provider, so without this they fall through to defaultProvider --
|
|
83
|
+
// which may not speak Anthropic at all. Only an operator-declared target is used.
|
|
84
|
+
if (isClaudeClassifierModel(model)) {
|
|
85
|
+
const configured = configuredClassifierRoute(cc);
|
|
86
|
+
if (configured) return configured;
|
|
87
|
+
}
|
|
50
88
|
return model;
|
|
51
89
|
}
|
|
52
90
|
|
package/src/claude/model-info.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - created_at is a fixed constant; max_input_tokens is authoritative-or-null;
|
|
16
16
|
* max_tokens is always null (no authoritative output limit exists proxy-side).
|
|
17
17
|
*/
|
|
18
|
-
import { catalogModelEfforts, nativeEffortClamp, nativeOpenAiContextWindow, type CatalogModel } from "../codex/catalog";
|
|
18
|
+
import { catalogModelEfforts, nativeEffortClamp, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, type CatalogModel } from "../codex/catalog";
|
|
19
19
|
import { claudeCodeAlias, claudeCodeNativeAlias } from "./alias";
|
|
20
20
|
import { desktop3pAlias } from "./desktop-3p";
|
|
21
21
|
import { AUTO_CONTEXT_OFF, type AutoContextMode } from "./context-windows";
|
|
@@ -108,6 +108,7 @@ export function buildAnthropicModelInfos(
|
|
|
108
108
|
auto: AutoContextMode = AUTO_CONTEXT_OFF,
|
|
109
109
|
idStyle: AnthropicIdStyle = "desktop3p",
|
|
110
110
|
aliasForRoute: (provider: string, modelId: string) => string = desktop3pAlias,
|
|
111
|
+
nativeContextCap?: number,
|
|
111
112
|
): AnthropicModelInfo[] {
|
|
112
113
|
const out: AnthropicModelInfo[] = [];
|
|
113
114
|
const seen = new Set<string>();
|
|
@@ -117,7 +118,7 @@ export function buildAnthropicModelInfos(
|
|
|
117
118
|
// the auto-context widening that let a 372K route carry the marker (and be
|
|
118
119
|
// over-filled) is the #854 defect and does not come back. Guards (audit R1#11):
|
|
119
120
|
// same dedupe set, never double-suffix.
|
|
120
|
-
const push1mVariant = (base: AnthropicModelInfo, contextWindow: number | undefined) => {
|
|
121
|
+
const push1mVariant = (base: AnthropicModelInfo, contextWindow: number | undefined, maxInputTokens?: number) => {
|
|
121
122
|
// The [1m] marker makes Claude Code account 1e6 tokens for the row, so it
|
|
122
123
|
// may only name models whose AUTHORITATIVE effective window is >= 1M —
|
|
123
124
|
// never the auto-context widening, which would mark a 372K route and have
|
|
@@ -127,16 +128,27 @@ export function buildAnthropicModelInfos(
|
|
|
127
128
|
const id = `${base.id}[1m]`;
|
|
128
129
|
if (seen.has(id)) return;
|
|
129
130
|
seen.add(id);
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
// The marker fixes Claude Code's accounting at 1e6, but a model may accept less input
|
|
132
|
+
// than that: GPT-5.6 advertises a 1,050,000 window while refusing anything past 922,000
|
|
133
|
+
// (measured — see devlog/_plan/260817_native_gpt56_1m_context/001_measurement_evidence.md).
|
|
134
|
+
// Advertising the flat 1e6 there would invite mid-session context_length_exceeded, so the
|
|
135
|
+
// variant reports whichever of the two is smaller.
|
|
136
|
+
const advertised = typeof maxInputTokens === "number" && maxInputTokens > 0
|
|
137
|
+
? Math.min(ONE_MILLION, maxInputTokens)
|
|
138
|
+
: ONE_MILLION;
|
|
139
|
+
out.push({ ...base, id, display_name: `${base.display_name} · 1M`, max_input_tokens: advertised });
|
|
132
140
|
};
|
|
133
141
|
for (const slug of nativeSlugs) {
|
|
134
142
|
const id = idStyle === "readable" ? claudeCodeNativeAlias(slug) : aliasForRoute("native", slug);
|
|
135
143
|
if (seen.has(id)) continue;
|
|
136
144
|
seen.add(id);
|
|
137
|
-
const
|
|
145
|
+
const nativeWindow = nativeOpenAiContextWindow(slug, nativeContextCap);
|
|
146
|
+
const nativeMaxInput = nativeOpenAiMaxInputTokens(slug, nativeContextCap);
|
|
147
|
+
// max_input_tokens is an INPUT limit, so it follows the measured input ceiling rather
|
|
148
|
+
// than the total window whenever the model publishes one.
|
|
149
|
+
const info = modelInfo(id, `${slug} (native)`, nativeEffectiveLadder(slug), true, nativeMaxInput ?? nativeWindow);
|
|
138
150
|
out.push(info);
|
|
139
|
-
push1mVariant(info,
|
|
151
|
+
push1mVariant(info, nativeWindow, nativeMaxInput);
|
|
140
152
|
}
|
|
141
153
|
for (const m of routedModels) {
|
|
142
154
|
const id = idStyle === "readable" ? claudeCodeAlias(m.provider, m.id) : aliasForRoute(m.provider, m.id);
|
|
@@ -144,11 +156,19 @@ export function buildAnthropicModelInfos(
|
|
|
144
156
|
seen.add(id);
|
|
145
157
|
const ladder = Array.isArray(m.reasoningEfforts) ? m.reasoningEfforts : [];
|
|
146
158
|
const imageInput = Array.isArray(m.inputModalities) ? m.inputModalities.includes("image") : false;
|
|
147
|
-
|
|
159
|
+
// max_input_tokens is an input limit, so a row that publishes a lower input ceiling than
|
|
160
|
+
// its window (native GPT-5.6 forwarded through a provider: 922k under 1.05M) reports the
|
|
161
|
+
// ceiling. Rows without one keep reporting the window, as before.
|
|
162
|
+
const routedMaxInput = typeof m.maxInputTokens === "number" && m.maxInputTokens > 0
|
|
163
|
+
? (typeof m.contextWindow === "number" && m.contextWindow > 0
|
|
164
|
+
? Math.min(m.maxInputTokens, m.contextWindow)
|
|
165
|
+
: m.maxInputTokens)
|
|
166
|
+
: undefined;
|
|
167
|
+
const info = modelInfo(id, `${m.id} (${m.provider})`, ladder, imageInput, routedMaxInput ?? m.contextWindow);
|
|
148
168
|
out.push(info);
|
|
149
169
|
// Anthropic passthrough guard (audit 021 #3): never auto-widen canonical claude
|
|
150
170
|
// routes — only a genuine >=1M window earns the variant row there.
|
|
151
|
-
push1mVariant(info, m.contextWindow);
|
|
171
|
+
push1mVariant(info, m.contextWindow, routedMaxInput);
|
|
152
172
|
}
|
|
153
173
|
return out;
|
|
154
174
|
}
|
package/src/cli/account-api.ts
CHANGED
|
@@ -145,6 +145,10 @@ export interface CodexQuotaDto {
|
|
|
145
145
|
monthlyPercent?: number;
|
|
146
146
|
weeklyResetAt?: number;
|
|
147
147
|
monthlyResetAt?: number;
|
|
148
|
+
/** Sub-day burst window, when upstream declares one (#1791). */
|
|
149
|
+
shortPercent?: number;
|
|
150
|
+
shortResetAt?: number;
|
|
151
|
+
shortWindowSeconds?: number;
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
export interface ProviderQuotaWindowDto {
|
|
@@ -183,7 +187,7 @@ interface CodexAccountDto {
|
|
|
183
187
|
function projectQuota(quota: CodexQuotaDto | null | undefined): CodexQuotaDto | null {
|
|
184
188
|
if (!quota) return null;
|
|
185
189
|
const projected: CodexQuotaDto = {};
|
|
186
|
-
for (const key of ["weeklyPercent", "monthlyPercent", "weeklyResetAt", "monthlyResetAt"] as const) {
|
|
190
|
+
for (const key of ["weeklyPercent", "monthlyPercent", "weeklyResetAt", "monthlyResetAt", "shortPercent", "shortResetAt", "shortWindowSeconds"] as const) {
|
|
187
191
|
if (typeof quota[key] === "number" && Number.isFinite(quota[key])) projected[key] = quota[key];
|
|
188
192
|
}
|
|
189
193
|
return projected;
|
|
@@ -15,6 +15,8 @@ import { filterCatalogVisibleModels, desktopVisibleNativeSlugs } from "../codex/
|
|
|
15
15
|
import { buildClaudeDesktopState, fetchAllModels } from "../server/management-api";
|
|
16
16
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
17
17
|
import { runtimeRequest } from "./runtime-api";
|
|
18
|
+
import { providerContextCap } from "../providers/context-cap";
|
|
19
|
+
import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
18
20
|
|
|
19
21
|
function isFamily(value: string | undefined): value is DesktopFamily {
|
|
20
22
|
return !!value && (DESKTOP_FAMILIES as readonly string[]).includes(value);
|
|
@@ -98,6 +100,7 @@ export async function applyProfile(
|
|
|
98
100
|
config.apiKeys?.[0]?.key,
|
|
99
101
|
mode,
|
|
100
102
|
state.profile,
|
|
103
|
+
providerContextCap(config, OPENAI_CODEX_PROVIDER_ID),
|
|
101
104
|
);
|
|
102
105
|
return { ok: result.written, path: result.path, reason: result.reason };
|
|
103
106
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { clearCodexAccountPin } from "../codex/account-priority";
|
|
3
|
-
import { getConfigPath, readConfigDiagnostics, sanitizeModelCostsForDisplay, saveConfig, validateConfigCandidate } from "../config";
|
|
3
|
+
import { getConfigPath, mutatePersistedConfig, readConfigDiagnostics, sanitizeModelCostsForDisplay, saveConfig, validateConfigCandidate } from "../config";
|
|
4
4
|
import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../reasoning-effort";
|
|
5
5
|
import type { OcxConfig } from "../types";
|
|
6
6
|
import { normalizeVisionReasoningForModel } from "../vision/reasoning";
|
|
@@ -130,19 +130,42 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
|
|
|
130
130
|
const raw = action === "set" ? args.shift() : undefined;
|
|
131
131
|
if (!path || (action === "set" && raw === undefined)) throw new CliUsageError("config path and value are required", USAGE);
|
|
132
132
|
rejectArgs(args, USAGE);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
133
|
+
// #1835/#1838: the read used to happen OUTSIDE the mutation lock, so a concurrent
|
|
134
|
+
// edit landing between it and the save was reverted by this whole-snapshot write.
|
|
135
|
+
// `mutatePersistedConfig` reruns this callback against the latest validated disk
|
|
136
|
+
// state, so the operation is applied to what is actually there at commit time.
|
|
137
|
+
let savedValue: unknown = null;
|
|
138
|
+
const outcome = mutatePersistedConfig(fresh => {
|
|
139
|
+
// Snapshot BEFORE mutating: comparing after the write compares a value with
|
|
140
|
+
// itself and would report every no-op as a change, bumping the generation.
|
|
141
|
+
const before = JSON.stringify(fresh);
|
|
142
|
+
const candidate = structuredClone(fresh) as unknown as Record<string, unknown>;
|
|
143
|
+
setPath(candidate, path, raw === undefined ? undefined : parseValue(raw), action === "unset");
|
|
144
|
+
const config = validate(candidate);
|
|
145
|
+
savedValue = action === "unset" ? null : getPath(config, path);
|
|
146
|
+
// Setting the order here is the operator restating it, exactly as through
|
|
147
|
+
// `ocx account priority` or the management route, so it releases the manual pin
|
|
148
|
+
// for the same reason those do: a pin made before any order existed would
|
|
149
|
+
// otherwise outrank every order set afterwards, capping the pool at the pinned
|
|
150
|
+
// account's tier with nothing on any surface explaining why. `import` is
|
|
151
|
+
// deliberately not covered — that file supplies its own pin, so there is no
|
|
152
|
+
// stale one to release.
|
|
153
|
+
if (pathSegments(path)[0] === "codexAccountPriorities") clearCodexAccountPin(config);
|
|
154
|
+
// REPLACE rather than merge: `Object.assign` alone cannot remove a key that
|
|
155
|
+
// `unset` deleted, which would make unset silently succeed while changing nothing.
|
|
156
|
+
for (const key of Object.keys(fresh)) {
|
|
157
|
+
if (!(key in (config as unknown as Record<string, unknown>))) {
|
|
158
|
+
delete (fresh as unknown as Record<string, unknown>)[key];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
Object.assign(fresh, config);
|
|
162
|
+
return { changed: JSON.stringify(fresh) !== before, value: undefined };
|
|
163
|
+
});
|
|
164
|
+
if (outcome.status === "unavailable") {
|
|
165
|
+
throw new Error(outcome.reason === "conflict"
|
|
166
|
+
? "config changed while applying this update; retry"
|
|
167
|
+
: `config is ${outcome.reason}`);
|
|
168
|
+
}
|
|
146
169
|
printData({ ok: true, path, value: redact(savedValue, path.split(".").at(-1)) }, wantsJson,
|
|
147
170
|
[`${action === "unset" ? "Unset" : "Set"} ${path}.`]);
|
|
148
171
|
return;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* mocking modules — a route test that could not stub this would really terminate
|
|
17
17
|
* the developer's own Codex.
|
|
18
18
|
*
|
|
19
|
-
* Plan and audit history: `devlog/
|
|
19
|
+
* Plan and audit history: `devlog/_fin/260815_gui_codex_restart/010_phase1_backend_endpoint.md`.
|
|
20
20
|
*/
|
|
21
21
|
import {
|
|
22
22
|
collectCodexAppServerCatalogState,
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -217,6 +217,11 @@ function quotaForPlan<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAc
|
|
|
217
217
|
return {
|
|
218
218
|
...(quota.monthlyPercent !== undefined ? { monthlyPercent: quota.monthlyPercent } : {}),
|
|
219
219
|
...(quota.monthlyResetAt !== undefined ? { monthlyResetAt: quota.monthlyResetAt } : {}),
|
|
220
|
+
// A 30-day plan can still carry a burst window, and it blocks the account on its own.
|
|
221
|
+
// Dropping it here would show a healthy card for an account upstream is refusing (#1791).
|
|
222
|
+
...(quota.shortPercent !== undefined ? { shortPercent: quota.shortPercent } : {}),
|
|
223
|
+
...(quota.shortResetAt !== undefined ? { shortResetAt: quota.shortResetAt } : {}),
|
|
224
|
+
...(quota.shortWindowSeconds !== undefined ? { shortWindowSeconds: quota.shortWindowSeconds } : {}),
|
|
220
225
|
...(quota.resetCredits !== undefined ? { resetCredits: quota.resetCredits } : {}),
|
|
221
226
|
...("updatedAt" in quota ? { updatedAt: quota.updatedAt } : {}),
|
|
222
227
|
} as T;
|
|
@@ -11,7 +11,7 @@ import { isCodexAccountPaused } from "./account-pause";
|
|
|
11
11
|
import { ConfigMutationLockError } from "../config";
|
|
12
12
|
import { isCodexAccountUsable } from "./account-usability";
|
|
13
13
|
import { reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
14
|
-
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountToken } from "./main-account";
|
|
14
|
+
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountToken, isMainAccountTokenLive } from "./main-account";
|
|
15
15
|
import { isNativeMainTrafficBlocked } from "./native-profile-startup";
|
|
16
16
|
import {
|
|
17
17
|
codexQuotaScopeForModel,
|
|
@@ -451,7 +451,32 @@ export function applyCodexAuthContextToProvider(
|
|
|
451
451
|
};
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
-
export
|
|
454
|
+
export class CodexMainSubstitutionUnavailableError extends Error {
|
|
455
|
+
constructor() {
|
|
456
|
+
super("No usable Codex main credential to substitute for an admission bearer");
|
|
457
|
+
this.name = "CodexMainSubstitutionUnavailableError";
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Build the upstream auth headers for one Codex turn.
|
|
463
|
+
*
|
|
464
|
+
* The two credential domains meet here, and only here:
|
|
465
|
+
*
|
|
466
|
+
* - `pool` / `main-pool` always OVERWRITE with the stored account credential. Whatever the
|
|
467
|
+
* caller sent is irrelevant to what we send upstream.
|
|
468
|
+
* - `main` with an admission-bearer caller (#1686) must substitute the stored main credential.
|
|
469
|
+
* The caller proved admission with one of OUR secrets, which must never leave the process, so
|
|
470
|
+
* the only two acceptable outcomes are replaced-with-stored-main or fail-before-any-IO.
|
|
471
|
+
* Silently forwarding would be the leak validateForwardAdmissionCredential exists to prevent.
|
|
472
|
+
* - `main` with a dedicated-header caller keeps the existing intentional passthrough: the bearer
|
|
473
|
+
* there is the user's own ChatGPT credential, not ours.
|
|
474
|
+
*/
|
|
475
|
+
export function materializeCodexUpstreamAuth(
|
|
476
|
+
headers: Headers,
|
|
477
|
+
ctx: CodexAuthContext,
|
|
478
|
+
options: { substituteMainCredential?: boolean } = {},
|
|
479
|
+
): Headers {
|
|
455
480
|
const selected = new Headers();
|
|
456
481
|
for (const name of FORWARD_HEADERS) {
|
|
457
482
|
const value = headers.get(name);
|
|
@@ -460,10 +485,26 @@ export function headersForCodexAuthContext(headers: Headers, ctx: CodexAuthConte
|
|
|
460
485
|
if (ctx.kind === "pool" || ctx.kind === "main-pool") {
|
|
461
486
|
selected.set("authorization", `Bearer ${ctx.accessToken}`);
|
|
462
487
|
selected.set("chatgpt-account-id", ctx.chatgptAccountId);
|
|
488
|
+
return selected;
|
|
489
|
+
}
|
|
490
|
+
if (ctx.kind === "main" && options.substituteMainCredential === true) {
|
|
491
|
+
const stored = getMainAccountToken();
|
|
492
|
+
// Fail BEFORE any upstream I/O. Falling through here would send the admission secret.
|
|
493
|
+
if (!stored?.accessToken || !isMainAccountTokenLive()) {
|
|
494
|
+
throw new CodexMainSubstitutionUnavailableError();
|
|
495
|
+
}
|
|
496
|
+
selected.set("authorization", `Bearer ${stored.accessToken}`);
|
|
497
|
+
if (stored.chatgptAccountId) selected.set("chatgpt-account-id", stored.chatgptAccountId);
|
|
498
|
+
return selected;
|
|
463
499
|
}
|
|
464
500
|
return selected;
|
|
465
501
|
}
|
|
466
502
|
|
|
503
|
+
/** @deprecated Prefer materializeCodexUpstreamAuth; kept for call sites without admission context. */
|
|
504
|
+
export function headersForCodexAuthContext(headers: Headers, ctx: CodexAuthContext): Headers {
|
|
505
|
+
return materializeCodexUpstreamAuth(headers, ctx);
|
|
506
|
+
}
|
|
507
|
+
|
|
467
508
|
export function isCodexAuthContextUsable(ctx: CodexAuthContext, config: OcxConfig): boolean {
|
|
468
509
|
if (ctx.kind === "main") return true;
|
|
469
510
|
if (ctx.kind === "main-pool") return isCodexAccountUsable(config, ctx.accountId);
|
|
@@ -99,16 +99,42 @@ export function isUnsupportedOpenAiNativeSlug(slug: string): boolean {
|
|
|
99
99
|
return /^(?:gpt|codex)-/.test(slug);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Advertised total context for the Codex-login native GPT-5.6 family.
|
|
104
|
+
*
|
|
105
|
+
* Measured on 2026-08-17 against a real Codex-login account, not taken from the live
|
|
106
|
+
* catalog: `GET /backend-api/codex/models` reports `context_window: 272000` /
|
|
107
|
+
* `max_context_window: 872000` for these slugs, yet a `POST /backend-api/codex/responses`
|
|
108
|
+
* probe admitted 921,508 input tokens and refused 922,013 with
|
|
109
|
+
* `error.code: context_length_exceeded` on sol, terra and luna alike. That boundary is
|
|
110
|
+
* exactly the 922,000 max-input the API-key side of this repo already declares
|
|
111
|
+
* (`src/providers/registry.ts`), and 1,050,000 = 922,000 input + 128,000 output.
|
|
112
|
+
* Evidence: devlog/_plan/260817_native_gpt56_1m_context/001_measurement_evidence.md.
|
|
113
|
+
*/
|
|
114
|
+
export const NATIVE_GPT56_CONTEXT_WINDOW = 1_050_000;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Largest input the native GPT-5.6 family actually accepts (measured; see above).
|
|
118
|
+
*
|
|
119
|
+
* This is deliberately NOT `contextWindow * 0.9`: that would be 945,000, which the
|
|
120
|
+
* upstream refuses. Every derived limit — auto-compaction, admission ceilings, the
|
|
121
|
+
* Anthropic `max_input_tokens` surface — has to clamp to this instead.
|
|
122
|
+
*/
|
|
123
|
+
export const NATIVE_GPT56_MAX_INPUT_TOKENS = 922_000;
|
|
103
124
|
|
|
104
|
-
export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: number; maxContextWindow?: number }> = {
|
|
125
|
+
export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: number; maxContextWindow?: number; maxInputTokens?: number }> = {
|
|
105
126
|
"gpt-5.5": { contextWindow: 272_000, maxContextWindow: 272_000 },
|
|
106
127
|
"gpt-5.4": { contextWindow: 1_000_000, maxContextWindow: 1_000_000 },
|
|
107
128
|
"gpt-5.3-codex-spark": { contextWindow: 100_000, maxContextWindow: 100_000 },
|
|
108
|
-
"gpt-5.6-sol": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
|
|
109
|
-
"gpt-5.6-terra": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
|
|
110
|
-
"gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
|
|
111
|
-
|
|
129
|
+
"gpt-5.6-sol": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
|
|
130
|
+
"gpt-5.6-terra": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
|
|
131
|
+
"gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
|
|
132
|
+
// Daybreak Blue borrows Sol's capability metadata and rides the same family contract.
|
|
133
|
+
// Unlike sol/terra/luna its window was NOT measured here: this account cannot reach it
|
|
134
|
+
// (`400 "The 'gpt-daybreak-blue-latest' model is not supported when using Codex with a
|
|
135
|
+
// ChatGPT account."`), so the promotion rests on a report from an account that has
|
|
136
|
+
// access rather than on a probe. Treat it as the weaker evidence of the four.
|
|
137
|
+
[NATIVE_DAYBREAK_BLUE_MODEL]: { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
|
|
112
138
|
};
|
|
113
139
|
|
|
114
140
|
const PINNED_UPSTREAM_MODELS: Map<string, RawEntry> = new Map(
|
|
@@ -140,6 +166,21 @@ export function nativeOpenAiContextWindow(slug: string, contextCap?: number): nu
|
|
|
140
166
|
return applyProviderContextCap(raw, contextCap) ?? raw;
|
|
141
167
|
}
|
|
142
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Largest input a native slug accepts, or undefined when no separate limit is known
|
|
171
|
+
* (the caller then falls back to the context window).
|
|
172
|
+
*
|
|
173
|
+
* A provider context cap lowers this too: a capped 272k window must not keep advertising a
|
|
174
|
+
* 922k input ceiling, or the cap would be cosmetic on every input-side surface.
|
|
175
|
+
*/
|
|
176
|
+
export function nativeOpenAiMaxInputTokens(slug: string, contextCap?: number): number | undefined {
|
|
177
|
+
const raw = NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.maxInputTokens;
|
|
178
|
+
if (raw === undefined) return undefined;
|
|
179
|
+
const window = nativeOpenAiContextWindow(slug, contextCap);
|
|
180
|
+
const capped = applyProviderContextCap(raw, contextCap) ?? raw;
|
|
181
|
+
return window === undefined ? capped : Math.min(capped, window);
|
|
182
|
+
}
|
|
183
|
+
|
|
143
184
|
export function nativeInputModalities(slug: string): string[] {
|
|
144
185
|
const upstream = PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug);
|
|
145
186
|
if (Array.isArray(upstream?.input_modalities) && upstream!.input_modalities!.length > 0) {
|
|
@@ -250,13 +291,19 @@ export function desktopVisibleNativeSlugs(
|
|
|
250
291
|
]);
|
|
251
292
|
}
|
|
252
293
|
|
|
253
|
-
export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps">): Array<{ slug: string; disabled: boolean; contextWindow?: number }> {
|
|
294
|
+
export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps">): Array<{ slug: string; disabled: boolean; contextWindow?: number; maxInputTokens?: number }> {
|
|
254
295
|
const disabled = disabledNativeSlugs(config);
|
|
255
296
|
const shadowed = configuredNativeAliasSlugs(config);
|
|
256
297
|
const openaiContextCap = providerContextCap(config, OPENAI_CODEX_PROVIDER_ID);
|
|
257
298
|
return NATIVE_OPENAI_MODELS.filter(slug => !shadowed.has(slug)).map(slug => {
|
|
258
299
|
const contextWindow = nativeOpenAiContextWindow(slug, openaiContextCap);
|
|
259
|
-
|
|
300
|
+
const maxInputTokens = nativeOpenAiMaxInputTokens(slug, openaiContextCap);
|
|
301
|
+
return {
|
|
302
|
+
slug,
|
|
303
|
+
disabled: disabled.has(slug),
|
|
304
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
305
|
+
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
|
|
306
|
+
};
|
|
260
307
|
});
|
|
261
308
|
}
|
|
262
309
|
|
|
@@ -12,7 +12,19 @@ const NATIVE_OPENAI_CAPABILITY_SOURCES: Readonly<Record<string, string>> = Objec
|
|
|
12
12
|
[NATIVE_DAYBREAK_BLUE_MODEL]: "gpt-5.6-sol",
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Native ids whose capability metadata is inherited from another pinned native row.
|
|
17
|
+
*
|
|
18
|
+
* Membership here is about METADATA INHERITANCE only, and is independent of whether the
|
|
19
|
+
* slug is also globally allowlisted in `NATIVE_OPENAI_MODELS`. `gpt-daybreak-blue-latest`
|
|
20
|
+
* is now in BOTH: it inherits Sol's capability shape AND ships as a globally supported
|
|
21
|
+
* native row (owner decision, devlog 260816_codexrs_multiagent_v2_and_history_perf/011).
|
|
22
|
+
*
|
|
23
|
+
* The maps that consume the union of these two lists (`PINNED_NATIVE_CAPABILITY_ENTRIES`,
|
|
24
|
+
* `UPSTREAM_NATIVE_ENTRIES`) are keyed by slug, so an overlapping id collapses to one
|
|
25
|
+
* entry. Catalog row generation iterates `NATIVE_OPENAI_MODELS` alone, so it still emits
|
|
26
|
+
* exactly one bare row and one row per account selector.
|
|
27
|
+
*/
|
|
16
28
|
export const NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS = Object.freeze(
|
|
17
29
|
Object.keys(NATIVE_OPENAI_CAPABILITY_SOURCES),
|
|
18
30
|
);
|
|
@@ -25,10 +37,28 @@ export function nativeOpenAiCapabilitySourceSlug(slug: string): string {
|
|
|
25
37
|
return NATIVE_OPENAI_CAPABILITY_SOURCES[slug] ?? slug;
|
|
26
38
|
}
|
|
27
39
|
|
|
28
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Native OpenAI model ids that this release can route and restore with authoritative metadata.
|
|
42
|
+
*
|
|
43
|
+
* `gpt-daybreak-blue-latest` is entitlement-gated upstream: it is absent from codex-rs's
|
|
44
|
+
* bundled catalog and reaches a client only through an authenticated `/models` response.
|
|
45
|
+
* It is listed here by explicit owner decision so the row exists without waiting for an
|
|
46
|
+
* observation, because opencodex injects `model_catalog_json` and codex-rs therefore builds
|
|
47
|
+
* a `StaticModelsManager` whose refresh is a no-op — an entitled account had no way to
|
|
48
|
+
* discover it on a clean install.
|
|
49
|
+
*
|
|
50
|
+
* Accepted tradeoff: an UNENTITLED account also sees the row. Catalog sync still succeeds;
|
|
51
|
+
* selecting the model reaches the canonical OpenAI provider and the backend answers 400
|
|
52
|
+
* "model not supported for this account", which is relayed (a bare pooled route may first
|
|
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.
|
|
55
|
+
*
|
|
56
|
+
* Devlog: 260816_codexrs_multiagent_v2_and_history_perf/011 §4-bis.
|
|
57
|
+
*/
|
|
29
58
|
export const NATIVE_OPENAI_MODELS = [
|
|
30
59
|
"gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark",
|
|
31
60
|
"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
|
|
61
|
+
NATIVE_DAYBREAK_BLUE_MODEL,
|
|
32
62
|
];
|
|
33
63
|
|
|
34
64
|
export const SUPPORTED_NATIVE_OPENAI_SLUGS = new Set(NATIVE_OPENAI_MODELS);
|