@bitkyc08/opencodex 2.23.0-preview.20260816 → 2.24.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-CFqJKF2L.js +102 -0
- 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 +20 -4
- 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 +135 -12
- package/src/codex/catalog/native-models.ts +32 -2
- package/src/codex/catalog/parsing.ts +61 -13
- package/src/codex/catalog/provider-fetch.ts +37 -7
- package/src/codex/catalog/sync.ts +49 -25
- 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 +90 -78
- 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/lib/pinned-http.ts +7 -2
- package/src/lib/windows-elevation.ts +3 -3
- 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 +96 -21
- package/src/server/chat-completions.ts +6 -2
- package/src/server/chat-native.ts +32 -6
- package/src/server/index.ts +5 -3
- package/src/server/management/agent-settings-routes.ts +26 -4
- package/src/server/management/config-routes.ts +79 -2
- 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 +22 -7
- package/src/types.ts +35 -1
- package/gui/dist/assets/index-Ch-YtWdA.js +0 -102
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": {
|
|
@@ -14,8 +14,20 @@ import { nativeOpenAiContextWindow, type CatalogModel } from "../codex/catalog";
|
|
|
14
14
|
|
|
15
15
|
const ONE_MILLION = 1_000_000;
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Auto-context defaults (devlog 260712 020, user-approved).
|
|
19
|
+
*
|
|
20
|
+
* The compact window is the token count at which Claude Code starts compacting, and it is
|
|
21
|
+
* also the floor `shouldMarkOneMillion` uses — a model may only carry the marker if it can
|
|
22
|
+
* host this window. 350,000 was chosen when the widest native row advertised 372,000.
|
|
23
|
+
*
|
|
24
|
+
* It now matches the auto-compaction limit the Codex catalog ships for the same models
|
|
25
|
+
* (`nativeAutoCompactLimit`: 829,800 against the 922,000 native window). Leaving the two
|
|
26
|
+
* apart meant one model compacting at 350k under Claude Code and at 829,800 under Codex.
|
|
27
|
+
* The value stays clear of the measured 922,000 ceiling by ~92k, so compaction still has
|
|
28
|
+
* room to run before the upstream refuses.
|
|
29
|
+
*/
|
|
30
|
+
export const AUTO_COMPACT_WINDOW_DEFAULT = 829_800;
|
|
19
31
|
export const AUTO_CONTEXT_FLOOR = 200_000;
|
|
20
32
|
/** Binary-verified accepted range for CLAUDE_CODE_AUTO_COMPACT_WINDOW (2.1.207: pSo=1e5, yDs=1e6). */
|
|
21
33
|
export const AUTO_COMPACT_WINDOW_MIN = 100_000;
|
|
@@ -59,7 +71,7 @@ function inAutoCompactRange(value: number): boolean {
|
|
|
59
71
|
* predicate so marker and threshold never separate (audit 021 #2); an invalid
|
|
60
72
|
* value disables auto marking entirely (the CLI would ignore it, leaving marked
|
|
61
73
|
* sub-1M models without their safety net). Out-of-range CONFIG values fall back
|
|
62
|
-
* to
|
|
74
|
+
* to AUTO_COMPACT_WINDOW_DEFAULT (the management API rejects them; this guards hand-edits).
|
|
63
75
|
*/
|
|
64
76
|
export function resolveAutoContext(claudeCode: AutoContextConfigSlice | undefined, envOverride?: string): AutoContextMode {
|
|
65
77
|
if (claudeCode?.autoContext === false) return AUTO_CONTEXT_OFF;
|
|
@@ -89,6 +101,10 @@ export function shouldMarkOneMillion(window: number | undefined, auto: AutoConte
|
|
|
89
101
|
export function buildClaudeContextWindows(
|
|
90
102
|
nativeSlugs: readonly string[],
|
|
91
103
|
routedModels: readonly CatalogModel[],
|
|
104
|
+
// A configured providerContextCaps.openai has to reach the native rows here too. Without
|
|
105
|
+
// it the Claude surface keeps advertising the uncapped authoritative window while the
|
|
106
|
+
// Codex catalog advertises the capped one, and the two disagree about the same model.
|
|
107
|
+
nativeContextCap?: number,
|
|
92
108
|
): Record<string, number> {
|
|
93
109
|
const out: Record<string, number> = {};
|
|
94
110
|
const put = (key: string | null, value: number) => {
|
|
@@ -96,7 +112,7 @@ export function buildClaudeContextWindows(
|
|
|
96
112
|
if (out[key] === undefined) out[key] = value; // first-wins (registry policy)
|
|
97
113
|
};
|
|
98
114
|
for (const slug of nativeSlugs) {
|
|
99
|
-
const window = nativeOpenAiContextWindow(slug);
|
|
115
|
+
const window = nativeOpenAiContextWindow(slug, nativeContextCap);
|
|
100
116
|
if (typeof window !== "number" || window <= 0) continue;
|
|
101
117
|
put(slug, window);
|
|
102
118
|
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 — a routed GPT-5.6 row runs a 1,050,000 window while refusing 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);
|