@bitkyc08/opencodex 2.44.0 → 2.45.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-CCfD72yq.js +115 -0
- package/gui/dist/assets/index-J96sug5C.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/openai-responses.ts +14 -0
- package/src/chat/outbound.ts +286 -35
- package/src/claude/compatibility.ts +192 -0
- package/src/claude/model-info.ts +14 -2
- package/src/cli/account-extended.ts +24 -1
- package/src/cli/init.ts +70 -13
- package/src/codex/catalog/provider-fetch.ts +38 -9
- package/src/codex/catalog/sync.ts +34 -2
- package/src/codex/catalog.ts +1 -1
- package/src/config/initialize.ts +132 -0
- package/src/config/rebase-provenance.ts +26 -0
- package/src/config.ts +50 -1
- package/src/generated/compatibility-version.json +41 -29
- package/src/lib/windows-secret-acl.ts +8 -4
- package/src/providers/quota.ts +26 -15
- package/src/responses/state.ts +48 -3
- package/src/server/chat-completions.ts +32 -36
- package/src/server/chat-native-sse.ts +23 -3
- package/src/server/chat-native.ts +15 -8
- package/src/server/claude-messages.ts +27 -0
- package/src/server/index.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +101 -14
- package/src/server/management/logs-usage-routes.ts +9 -2
- package/src/server/request-log-cursor.ts +84 -0
- package/src/server/request-log.ts +46 -3
- package/src/server/responses/agent-task-recovery.ts +50 -14
- package/src/server/responses/codex-ws-exchange.ts +79 -0
- package/src/server/responses/compact.ts +39 -33
- package/src/server/responses/core.ts +43 -27
- package/src/storage/cleanup.ts +49 -35
- package/src/types/config.ts +4 -0
- package/src/usage/log.ts +22 -0
- package/gui/dist/assets/index-B7_K1Hsj.js +0 -115
- package/gui/dist/assets/index-ltx3L-WS.css +0 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/** Opt-in admission for the translated Messages path; no adapter or credential state. */
|
|
2
|
+
export type ClaudeCompatibilityMode = "shadow" | "enforce";
|
|
3
|
+
|
|
4
|
+
// False means deliberately tolerated degradation, not lossless representation.
|
|
5
|
+
const FEATURES = {
|
|
6
|
+
cache_control: false,
|
|
7
|
+
input_examples: false,
|
|
8
|
+
thinking_settings: false,
|
|
9
|
+
unknown_beta: false,
|
|
10
|
+
thinking_replay: true,
|
|
11
|
+
documents: true,
|
|
12
|
+
web_search_tool: true,
|
|
13
|
+
tool_search: true,
|
|
14
|
+
tool_reference: true,
|
|
15
|
+
deferred_tools: true,
|
|
16
|
+
strict_tools: true,
|
|
17
|
+
caller_mode: true,
|
|
18
|
+
structured_output: true,
|
|
19
|
+
service_tier: true,
|
|
20
|
+
mcp_tool: true,
|
|
21
|
+
code_execution: true,
|
|
22
|
+
computer_use: true,
|
|
23
|
+
server_tool: true,
|
|
24
|
+
context_management: true,
|
|
25
|
+
container: true,
|
|
26
|
+
inference_geo: true,
|
|
27
|
+
user_profile: true,
|
|
28
|
+
unknown_body_field: true,
|
|
29
|
+
unknown_content_block: true,
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export type ClaudeFeatureCode = keyof typeof FEATURES;
|
|
33
|
+
const FEATURE_CODES = Object.keys(FEATURES) as ClaudeFeatureCode[];
|
|
34
|
+
const MAX_FEATURE_CODES = 32;
|
|
35
|
+
const MAX_REASON_LENGTH = 512;
|
|
36
|
+
type Rec = Record<string, unknown>;
|
|
37
|
+
const isRec = (value: unknown): value is Rec =>
|
|
38
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
39
|
+
|
|
40
|
+
export function isClaudeCompatibilityMode(value: unknown): value is ClaudeCompatibilityMode {
|
|
41
|
+
return value === "shadow" || value === "enforce";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Project only closed codes, including when reading an untrusted persisted row. */
|
|
45
|
+
export function normalizeClaudeFeatureCodes(value: unknown): ClaudeFeatureCode[] {
|
|
46
|
+
if (!Array.isArray(value)) return [];
|
|
47
|
+
const codes = new Set<ClaudeFeatureCode>();
|
|
48
|
+
for (const code of value) {
|
|
49
|
+
if (typeof code === "string" && Object.hasOwn(FEATURES, code)) codes.add(code as ClaudeFeatureCode);
|
|
50
|
+
}
|
|
51
|
+
return FEATURE_CODES.filter(code => codes.has(code)).sort().slice(0, MAX_FEATURE_CODES);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Never accept a caller-supplied reason, header value, model name or tool name. */
|
|
55
|
+
export function claudeCompatibilityReason(codes: readonly ClaudeFeatureCode[], shadow: boolean): string | undefined {
|
|
56
|
+
const unsupported = codes.filter(code => FEATURES[code]);
|
|
57
|
+
if (unsupported.length === 0) return undefined;
|
|
58
|
+
return `${shadow ? "shadow: would reject" : "unsupported translated Claude features"}: ${unsupported.join(", ")}`
|
|
59
|
+
.slice(0, MAX_REASON_LENGTH);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const BODY_FIELDS = new Set([
|
|
63
|
+
"model", "max_tokens", "messages", "system", "tools", "tool_choice", "thinking",
|
|
64
|
+
"output_config", "metadata", "service_tier", "stop_sequences", "stream",
|
|
65
|
+
"temperature", "top_p", "top_k", "cache_control", "context_management",
|
|
66
|
+
"container", "inference_geo", "user_profile_id", "mcp_servers", "defer_tools", "deferred_tools",
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
function activeDeferred(value: unknown): boolean {
|
|
70
|
+
return value === true || (Array.isArray(value) ? value.length > 0 : isRec(value) && Object.keys(value).length > 0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function nonDirectCaller(value: unknown): boolean {
|
|
74
|
+
return value !== undefined && !(Array.isArray(value) && value.length === 1 && value[0] === "direct");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Complete finite detection. Only protocol content positions are visited, never schemas/arguments. */
|
|
78
|
+
function detectFeatures(body: unknown, anthropicBeta?: string): Set<ClaudeFeatureCode> {
|
|
79
|
+
const codes = new Set<ClaudeFeatureCode>();
|
|
80
|
+
// Header-only beta semantics are outside this policy. No header bytes become codes.
|
|
81
|
+
if (anthropicBeta?.trim()) codes.add("unknown_beta");
|
|
82
|
+
if (!isRec(body)) return codes; // The existing Messages parser owns malformed top-level input.
|
|
83
|
+
if (Object.keys(body).some(key => !BODY_FIELDS.has(key))) codes.add("unknown_body_field");
|
|
84
|
+
for (const [field, code] of [
|
|
85
|
+
["cache_control", "cache_control"], ["context_management", "context_management"],
|
|
86
|
+
["container", "container"], ["inference_geo", "inference_geo"],
|
|
87
|
+
["user_profile_id", "user_profile"], ["mcp_servers", "mcp_tool"],
|
|
88
|
+
] as const) {
|
|
89
|
+
if (Object.hasOwn(body, field)) codes.add(code);
|
|
90
|
+
}
|
|
91
|
+
if (body.service_tier !== undefined && body.service_tier !== null) codes.add("service_tier");
|
|
92
|
+
if (isRec(body.thinking)) codes.add("thinking_settings");
|
|
93
|
+
if (isRec(body.output_config)
|
|
94
|
+
&& (body.output_config.format != null || body.output_config.output_format != null)) codes.add("structured_output");
|
|
95
|
+
if (activeDeferred(body.defer_tools) || activeDeferred(body.deferred_tools)) codes.add("deferred_tools");
|
|
96
|
+
|
|
97
|
+
if (Array.isArray(body.tools)) {
|
|
98
|
+
for (const tool of body.tools) {
|
|
99
|
+
if (!isRec(tool)) continue;
|
|
100
|
+
if (Object.hasOwn(tool, "cache_control")) codes.add("cache_control");
|
|
101
|
+
if (Object.hasOwn(tool, "input_examples")) codes.add("input_examples");
|
|
102
|
+
if (tool.strict === true) codes.add("strict_tools");
|
|
103
|
+
if (tool.defer === true || tool.defer_loading === true) codes.add("deferred_tools");
|
|
104
|
+
if (nonDirectCaller(tool.allowed_callers)) codes.add("caller_mode");
|
|
105
|
+
const type = tool.type;
|
|
106
|
+
// Ordinary client function names do not convey hosted execution semantics.
|
|
107
|
+
if (type === undefined || type === "function" || type === "custom") continue;
|
|
108
|
+
if (type === "mcp_toolset") codes.add("mcp_tool");
|
|
109
|
+
else if (typeof type === "string" && /^web_search_\d{8}$/.test(type)) codes.add("web_search_tool");
|
|
110
|
+
else if (typeof type === "string" && /^tool_search(?:_tool_(?:regex|bm25))?(?:_\d{8})?$/.test(type)) codes.add("tool_search");
|
|
111
|
+
else if (typeof type === "string" && /^code_execution_\d{8}$/.test(type)) codes.add("code_execution");
|
|
112
|
+
else if (typeof type === "string" && /^computer(?:_toolset)?_\d{8}$/.test(type)) codes.add("computer_use");
|
|
113
|
+
else codes.add("server_tool");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const scanBlock = (block: unknown, position: "message" | "system" | "result") => {
|
|
118
|
+
if (!isRec(block)) return;
|
|
119
|
+
if (Object.hasOwn(block, "cache_control")) codes.add("cache_control");
|
|
120
|
+
if (position === "system" && block.type !== "text") codes.add("unknown_content_block");
|
|
121
|
+
if (position === "result" && (typeof block.type !== "string" || !["text", "image", "document", "tool_reference"].includes(block.type))) {
|
|
122
|
+
codes.add("unknown_content_block");
|
|
123
|
+
}
|
|
124
|
+
switch (block.type) {
|
|
125
|
+
case "text":
|
|
126
|
+
case "image": break;
|
|
127
|
+
case "document": codes.add("documents"); break;
|
|
128
|
+
case "thinking":
|
|
129
|
+
case "redacted_thinking": codes.add("thinking_replay"); break;
|
|
130
|
+
case "tool_reference": codes.add("tool_reference"); break;
|
|
131
|
+
case "tool_search_tool_result": codes.add("tool_search"); break;
|
|
132
|
+
case "web_search_tool_result": codes.add("web_search_tool"); break;
|
|
133
|
+
case "code_execution_tool_result":
|
|
134
|
+
case "bash_code_execution_tool_result":
|
|
135
|
+
case "text_editor_code_execution_tool_result": codes.add("code_execution"); break;
|
|
136
|
+
case "mcp_tool_use":
|
|
137
|
+
case "mcp_tool_result": codes.add("mcp_tool"); break;
|
|
138
|
+
case "tool_use":
|
|
139
|
+
if (block.caller !== undefined && (!isRec(block.caller) || block.caller.type !== "direct")) codes.add("caller_mode");
|
|
140
|
+
break;
|
|
141
|
+
case "server_tool_use":
|
|
142
|
+
switch (block.name) {
|
|
143
|
+
case "tool_search":
|
|
144
|
+
case "tool_search_tool_regex":
|
|
145
|
+
case "tool_search_tool_bm25": codes.add("tool_search"); break;
|
|
146
|
+
case "web_search": codes.add("web_search_tool"); break;
|
|
147
|
+
case "code_execution": codes.add("code_execution"); break;
|
|
148
|
+
case "computer": codes.add("computer_use"); break;
|
|
149
|
+
default: codes.add("server_tool");
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
case "tool_result": break; // Children are visited below at the one supported nesting level.
|
|
153
|
+
default: codes.add("unknown_content_block");
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
if (Array.isArray(body.system)) for (const block of body.system) scanBlock(block, "system");
|
|
157
|
+
if (Array.isArray(body.messages)) {
|
|
158
|
+
for (const message of body.messages) {
|
|
159
|
+
if (!isRec(message) || !Array.isArray(message.content)) continue;
|
|
160
|
+
for (const block of message.content) {
|
|
161
|
+
scanBlock(block, "message");
|
|
162
|
+
if (isRec(block) && block.type === "tool_result" && Array.isArray(block.content)) {
|
|
163
|
+
for (const child of block.content) scanBlock(child, "result");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return codes;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface ClaudeCompatibilityResult {
|
|
172
|
+
featureCodes: ClaudeFeatureCode[];
|
|
173
|
+
compatible: boolean;
|
|
174
|
+
decision: "allow" | "shadow" | "reject";
|
|
175
|
+
reason?: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** All translated targets share this policy; native passthrough never calls it. */
|
|
179
|
+
export function analyzeClaudeCompatibility(
|
|
180
|
+
body: unknown,
|
|
181
|
+
opts: { mode: ClaudeCompatibilityMode; anthropicBeta?: string },
|
|
182
|
+
): ClaudeCompatibilityResult {
|
|
183
|
+
const detected = detectFeatures(body, opts.anthropicBeta);
|
|
184
|
+
const compatible = !FEATURE_CODES.some(code => detected.has(code) && FEATURES[code]);
|
|
185
|
+
const featureCodes = normalizeClaudeFeatureCodes([...detected]);
|
|
186
|
+
return {
|
|
187
|
+
featureCodes,
|
|
188
|
+
compatible,
|
|
189
|
+
decision: compatible ? "allow" : opts.mode === "shadow" ? "shadow" : "reject",
|
|
190
|
+
...(!compatible ? { reason: claudeCompatibilityReason(featureCodes, opts.mode === "shadow") } : {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
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, nativeOpenAiMaxInputTokens, type CatalogModel, type NativeContextLimitsInput } from "../codex/catalog";
|
|
18
|
+
import { orderForModelPicker, catalogModelEfforts, nativeEffortClamp, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, type CatalogModel, type NativeContextLimitsInput } from "../codex/catalog";
|
|
19
19
|
import { claudeCodeAlias, claudeCodeNativeAlias } from "./alias";
|
|
20
20
|
import { cursorFastIdFor } from "../adapters/cursor/catalog";
|
|
21
21
|
import { desktop3pAlias } from "./desktop-3p";
|
|
@@ -114,6 +114,7 @@ export function buildAnthropicModelInfos(
|
|
|
114
114
|
// Presence is the feature gate: the caller passes undefined when `fastRows` is off, so a
|
|
115
115
|
// default install publishes nothing. The predicate answers ELIGIBILITY, not enablement.
|
|
116
116
|
fastRows?: (model: CatalogModel | { provider: string; id: string }) => boolean,
|
|
117
|
+
ordering?: { modelPickerOrder?: readonly string[]; featured?: readonly string[] },
|
|
117
118
|
): AnthropicModelInfo[] {
|
|
118
119
|
const out: AnthropicModelInfo[] = [];
|
|
119
120
|
const seen = new Set<string>();
|
|
@@ -198,6 +199,8 @@ export function buildAnthropicModelInfos(
|
|
|
198
199
|
// omitting it would leave this surface without the model the feature exists for.
|
|
199
200
|
if (fastRows?.({ provider: "native", id: slug }) === true) pushFastVariant(info);
|
|
200
201
|
}
|
|
202
|
+
const nativeEnd = out.length;
|
|
203
|
+
const routedGroups = new Map<CatalogModel, AnthropicModelInfo[]>();
|
|
201
204
|
for (const m of routedModels) {
|
|
202
205
|
// Global Fast has no toggle on this surface, so the fast identity is what gets listed —
|
|
203
206
|
// a client here can only pick a listed id. Limited to the readable CLI style: Desktop 3P
|
|
@@ -211,6 +214,7 @@ export function buildAnthropicModelInfos(
|
|
|
211
214
|
: aliasForRoute(m.provider, m.id);
|
|
212
215
|
if (seen.has(id)) continue;
|
|
213
216
|
seen.add(id);
|
|
217
|
+
const groupStart = out.length;
|
|
214
218
|
const ladder = Array.isArray(m.reasoningEfforts) ? m.reasoningEfforts : [];
|
|
215
219
|
const imageInput = Array.isArray(m.inputModalities) ? m.inputModalities.includes("image") : false;
|
|
216
220
|
// max_input_tokens is an input limit, so a row that publishes a lower input ceiling than
|
|
@@ -238,6 +242,14 @@ export function buildAnthropicModelInfos(
|
|
|
238
242
|
// namespace with no config.providers entry, so the caller classifies it from the
|
|
239
243
|
// aggregated supportsServiceTier the row already carries.
|
|
240
244
|
if (fastRows?.(m) === true) pushFastVariant(info);
|
|
245
|
+
routedGroups.set(m, out.slice(groupStart));
|
|
241
246
|
}
|
|
242
|
-
return out;
|
|
247
|
+
if (!ordering?.modelPickerOrder?.length) return out;
|
|
248
|
+
// Sort only after deduplication, preserving the registry's original collision winner
|
|
249
|
+
// and keeping each model's base/1M/Fast siblings together.
|
|
250
|
+
return [
|
|
251
|
+
...out.slice(0, nativeEnd),
|
|
252
|
+
...orderForModelPicker([...routedGroups.keys()], ordering.modelPickerOrder, ordering.featured)
|
|
253
|
+
.flatMap(model => routedGroups.get(model)!),
|
|
254
|
+
];
|
|
243
255
|
}
|
|
@@ -367,6 +367,7 @@ export async function cmdAutoSwitch(args: string[], deps: AccountDeps): Promise<
|
|
|
367
367
|
if (threshold !== undefined && (!Number.isInteger(threshold) || threshold < 0 || threshold > 100)) {
|
|
368
368
|
return usage("Error: threshold must be an integer 0-100");
|
|
369
369
|
}
|
|
370
|
+
let settings: Record<string, unknown> = {};
|
|
370
371
|
const baseUrl = await resolveBaseUrl(deps);
|
|
371
372
|
if (!baseUrl) return proxyUnreachable();
|
|
372
373
|
if (action === "status") {
|
|
@@ -378,13 +379,35 @@ export async function cmdAutoSwitch(args: string[], deps: AccountDeps): Promise<
|
|
|
378
379
|
if (response.status !== 200 || (!genericPool && typeof response.json.autoSwitchThreshold !== "number")) {
|
|
379
380
|
return apiError(response.json, "failed to read auto-switch status", response.status);
|
|
380
381
|
}
|
|
381
|
-
|
|
382
|
+
settings = genericPool && (!response.json || typeof response.json !== "object" || Array.isArray(response.json))
|
|
383
|
+
? {} : response.json;
|
|
384
|
+
threshold = typeof settings.autoSwitchThreshold === "number" ? settings.autoSwitchThreshold : 0;
|
|
382
385
|
} else {
|
|
383
386
|
const response = genericPool
|
|
384
387
|
? await apiJson(deps, baseUrl, "PUT", "/api/oauth/accounts/pool", { provider: name, autoSwitchThreshold: threshold })
|
|
385
388
|
: await apiJson(deps, baseUrl, "PUT", "/api/codex-auth/auto-switch", { threshold });
|
|
386
389
|
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
387
390
|
if (response.status !== 200) return apiError(response.json, "failed to update auto-switch", response.status);
|
|
391
|
+
settings = genericPool && (!response.json || typeof response.json !== "object" || Array.isArray(response.json))
|
|
392
|
+
? {} : response.json;
|
|
393
|
+
}
|
|
394
|
+
if (genericPool) {
|
|
395
|
+
// Generic thresholds are stored independently of the enabled override. The
|
|
396
|
+
// latter may inherit global preference and never disables reactive rotation.
|
|
397
|
+
const stored = settings.autoSwitchThreshold;
|
|
398
|
+
const storedThreshold = typeof stored === "number" && Number.isInteger(stored) && stored >= 0 && stored <= 100
|
|
399
|
+
? stored : null;
|
|
400
|
+
const poolEnabled = typeof settings.enabled === "boolean" ? settings.enabled : null;
|
|
401
|
+
const inert = settings.inert === true ? true : null;
|
|
402
|
+
// This CLI understands only the current inert generic threshold contract.
|
|
403
|
+
const enabled = false;
|
|
404
|
+
if (wantsJson) {
|
|
405
|
+
console.log(JSON.stringify({ provider: name, autoSwitchThreshold: storedThreshold, enabled, poolEnabled, inert }, null, 2));
|
|
406
|
+
} else {
|
|
407
|
+
const value = storedThreshold === null ? "unset" : `${storedThreshold}%`;
|
|
408
|
+
console.log(`auto-switch: ${inert === true ? "inactive" : "unavailable"} (stored threshold ${value}; ${inert === true ? "not applied by this pool" : "threshold support is unknown"})`);
|
|
409
|
+
}
|
|
410
|
+
return 0;
|
|
388
411
|
}
|
|
389
412
|
const enabled = threshold! > 0;
|
|
390
413
|
if (wantsJson) console.log(JSON.stringify({ provider: name, autoSwitchThreshold: threshold, enabled }, null, 2));
|
package/src/cli/init.ts
CHANGED
|
@@ -3,24 +3,44 @@ import { modelSelectionGuidance } from "./model-selection-guidance";
|
|
|
3
3
|
import { initializeProviderModelSelection } from "../providers/initial-model-selection";
|
|
4
4
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
5
5
|
import { injectCodexConfig } from "../codex/inject";
|
|
6
|
-
import { classifyOpenAiTierBackup, getConfigPath, getDefaultConfig, isValidProviderName,
|
|
6
|
+
import { classifyOpenAiTierBackup, ConfigMutationLockError, getConfigPath, getDefaultConfig, initializePersistedConfigIfMissing, isValidProviderName, observeInitialConfigState, preserveOpenAiTierRollbackSnapshot } from "../config";
|
|
7
|
+
import { InitialConfigPublicationError } from "../config/initialize";
|
|
8
|
+
import { redactUserPath } from "../lib/redact";
|
|
7
9
|
import { enrichProviderFromCatalog } from "../oauth/key-providers";
|
|
8
10
|
import { deriveInitProviders } from "../providers/derive";
|
|
9
11
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
class InitCancelledError extends Error {
|
|
14
|
+
constructor(readonly exitCode: 1 | 130) {
|
|
15
|
+
super(exitCode === 130 ? "Setup cancelled." : "stdin reached EOF while waiting for input. Re-run `ocx init` in an interactive terminal.");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createPrompt(): { ask(question: string): Promise<string>; throwIfCancelled(): void; close(): void } {
|
|
12
20
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
13
21
|
let closed = false;
|
|
14
|
-
|
|
22
|
+
let cancellation: InitCancelledError | undefined;
|
|
23
|
+
const onInterrupt = () => {
|
|
24
|
+
cancellation = new InitCancelledError(130);
|
|
25
|
+
rl.close();
|
|
26
|
+
};
|
|
27
|
+
rl.on("SIGINT", onInterrupt);
|
|
28
|
+
process.on("SIGINT", onInterrupt);
|
|
29
|
+
rl.on("close", () => {
|
|
30
|
+
closed = true;
|
|
31
|
+
cancellation ??= new InitCancelledError(1);
|
|
32
|
+
process.off("SIGINT", onInterrupt);
|
|
33
|
+
rl.off("SIGINT", onInterrupt);
|
|
34
|
+
});
|
|
15
35
|
return {
|
|
16
36
|
ask(question: string): Promise<string> {
|
|
17
37
|
return new Promise((resolve, reject) => {
|
|
18
38
|
if (closed) {
|
|
19
|
-
reject(new
|
|
39
|
+
reject(cancellation ?? new InitCancelledError(1));
|
|
20
40
|
return;
|
|
21
41
|
}
|
|
22
42
|
const onClose = () => {
|
|
23
|
-
reject(new
|
|
43
|
+
reject(cancellation ?? new InitCancelledError(1));
|
|
24
44
|
};
|
|
25
45
|
rl.once("close", onClose);
|
|
26
46
|
rl.question(question, answer => {
|
|
@@ -29,6 +49,9 @@ function createPrompt(): { ask(question: string): Promise<string>; close(): void
|
|
|
29
49
|
});
|
|
30
50
|
});
|
|
31
51
|
},
|
|
52
|
+
throwIfCancelled() {
|
|
53
|
+
if (cancellation) throw cancellation;
|
|
54
|
+
},
|
|
32
55
|
close() {
|
|
33
56
|
if (!closed) rl.close();
|
|
34
57
|
},
|
|
@@ -88,7 +111,18 @@ export function cleanupOpenAiTierBackupAfterInit(configPath = getConfigPath()):
|
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
export async function runInit(): Promise<void> {
|
|
114
|
+
const initial = observeInitialConfigState();
|
|
115
|
+
if (initial === "exists") {
|
|
116
|
+
console.log(`Keeping existing config at ${redactUserPath(getConfigPath())}. Use \`ocx config\` or the dashboard to update it.`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (initial === "invalid") {
|
|
120
|
+
console.error(`Cannot initialize ${redactUserPath(getConfigPath())}: existing config is invalid, unreadable, or not a regular file. It has been preserved.`);
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
91
124
|
const prompt = createPrompt();
|
|
125
|
+
let configCreated = false;
|
|
92
126
|
try {
|
|
93
127
|
console.log("\n🔧 opencodex (ocx) setup\n");
|
|
94
128
|
|
|
@@ -171,7 +205,14 @@ export async function runInit(): Promise<void> {
|
|
|
171
205
|
modelDiscovery: { newModelPolicy: "off" },
|
|
172
206
|
};
|
|
173
207
|
|
|
174
|
-
|
|
208
|
+
prompt.throwIfCancelled();
|
|
209
|
+
const outcome = initializePersistedConfigIfMissing(config);
|
|
210
|
+
if (outcome !== "created") {
|
|
211
|
+
console.error("Config appeared or changed while setup was running; keeping it and stopping setup.");
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
configCreated = true;
|
|
175
216
|
// Init writes a fresh config, so a stale pre-migration backup from a previous
|
|
176
217
|
// installation would make the next `ocx start` crash on a stale-backup
|
|
177
218
|
// collision (issue #257). But only a STALE backup (unparseable, or already a
|
|
@@ -179,23 +220,34 @@ export async function runInit(): Promise<void> {
|
|
|
179
220
|
// valid pre-migration (v1) config is a user-intentional rollback point and is
|
|
180
221
|
// preserved by renaming it out of the collision path (sol review 260722).
|
|
181
222
|
cleanupOpenAiTierBackupAfterInit();
|
|
182
|
-
console.log(`\n✅ Config saved to
|
|
223
|
+
console.log(`\n✅ Config saved to ${redactUserPath(getConfigPath())}`);
|
|
183
224
|
if (oauthHint) console.log(`🔐 Authenticate this provider with: ocx login ${providerName}`);
|
|
184
225
|
|
|
185
226
|
const injectAnswer = await prompt.ask("Inject into Codex config.toml? [Y/n]: ");
|
|
227
|
+
prompt.throwIfCancelled();
|
|
186
228
|
if (injectAnswer.trim().toLowerCase() !== "n") {
|
|
187
229
|
console.log("Fetching available models from provider...");
|
|
188
|
-
const result = await injectCodexConfig(port, config
|
|
230
|
+
const result = await injectCodexConfig(port, config, {
|
|
231
|
+
beforeClientWrite: () => prompt.throwIfCancelled(),
|
|
232
|
+
}).catch(error => {
|
|
233
|
+
// The injection/lock boundary may wrap the guard's cancellation error.
|
|
234
|
+
prompt.throwIfCancelled();
|
|
235
|
+
throw error;
|
|
236
|
+
});
|
|
237
|
+
prompt.throwIfCancelled();
|
|
189
238
|
console.log(result.success ? `✅ ${result.message}` : `⚠️ ${result.message}`);
|
|
190
239
|
}
|
|
191
240
|
|
|
192
241
|
const shimAnswer = await prompt.ask("Install Codex autostart shim? [Y/n]: ");
|
|
242
|
+
prompt.throwIfCancelled();
|
|
193
243
|
if (shimAnswer.trim().toLowerCase() !== "n") {
|
|
194
244
|
try {
|
|
195
245
|
const { installCodexShim } = await import("../codex/shim");
|
|
246
|
+
prompt.throwIfCancelled();
|
|
196
247
|
const result = installCodexShim();
|
|
197
248
|
console.log(result.installed ? `✅ ${result.message}` : `⚠️ ${result.message}`);
|
|
198
249
|
} catch (err) {
|
|
250
|
+
if (err instanceof InitCancelledError) throw err;
|
|
199
251
|
console.log(`⚠️ Codex autostart shim skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
200
252
|
}
|
|
201
253
|
}
|
|
@@ -203,13 +255,18 @@ export async function runInit(): Promise<void> {
|
|
|
203
255
|
console.log(`\n🚀 Setup complete! Run 'ocx start' to start the proxy.`);
|
|
204
256
|
for (const line of modelSelectionGuidance(providerName)) console.log(line);
|
|
205
257
|
} catch (error) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
258
|
+
if (error instanceof InitCancelledError) {
|
|
259
|
+
console.error(`\n❌ ${error.message}${configCreated ? " The created config has been kept." : ""}`);
|
|
260
|
+
process.exitCode = error.exitCode;
|
|
261
|
+
} else {
|
|
262
|
+
const message = error instanceof InitialConfigPublicationError
|
|
263
|
+
? `${error.message}${error.publication !== "not-published" ? " Config may already exist; inspect it before retrying." : ""}${error.residualTemp ? " A temporary file could not be removed; inspect the config directory." : ""}`
|
|
264
|
+
: error instanceof ConfigMutationLockError
|
|
265
|
+
? "Config initialization could not acquire its write lock. Retry when the other config operation finishes."
|
|
266
|
+
: `Setup did not finish.${configCreated ? " The created config has been kept." : " Check the config directory and setup inputs before retrying."}`;
|
|
267
|
+
console.error(`\n❌ ${message}`);
|
|
209
268
|
process.exitCode = 1;
|
|
210
|
-
return;
|
|
211
269
|
}
|
|
212
|
-
throw error;
|
|
213
270
|
} finally {
|
|
214
271
|
prompt.close();
|
|
215
272
|
}
|
|
@@ -2130,6 +2130,31 @@ async function gatherRoutedModelsWithAuth(
|
|
|
2130
2130
|
return models;
|
|
2131
2131
|
}
|
|
2132
2132
|
|
|
2133
|
+
/** Bound a proven Codex-forward custom row without changing its stored configuration. */
|
|
2134
|
+
function boundCustomNativeReasoning(
|
|
2135
|
+
model: CatalogModel,
|
|
2136
|
+
allowed: readonly string[],
|
|
2137
|
+
nativeDefault: string | undefined,
|
|
2138
|
+
): CatalogModel {
|
|
2139
|
+
if (allowed.length === 0 || model.reasoningEfforts === undefined) return model;
|
|
2140
|
+
const bounded = { ...model };
|
|
2141
|
+
if (model.reasoningEfforts.length === 0) {
|
|
2142
|
+
bounded.reasoningEfforts = [];
|
|
2143
|
+
delete bounded.defaultReasoningEffort;
|
|
2144
|
+
return bounded;
|
|
2145
|
+
}
|
|
2146
|
+
const declared = new Set(model.reasoningEfforts);
|
|
2147
|
+
const surviving = [...new Set(allowed)].filter(effort => declared.has(effort));
|
|
2148
|
+
const fallback = nativeDefault && allowed.includes(nativeDefault) ? nativeDefault : allowed[0]!;
|
|
2149
|
+
// A nonempty but incompatible declaration is not an explicit no-reasoning setting.
|
|
2150
|
+
bounded.reasoningEfforts = surviving.length > 0 ? surviving : [fallback];
|
|
2151
|
+
bounded.defaultReasoningEffort = model.defaultReasoningEffort
|
|
2152
|
+
&& bounded.reasoningEfforts.includes(model.defaultReasoningEffort)
|
|
2153
|
+
? model.defaultReasoningEffort
|
|
2154
|
+
: bounded.reasoningEfforts.includes(fallback) ? fallback : bounded.reasoningEfforts[0]!;
|
|
2155
|
+
return bounded;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2133
2158
|
async function gatherRoutedModelsUncached(
|
|
2134
2159
|
config: OcxConfig,
|
|
2135
2160
|
capture: GatherFlightCapture,
|
|
@@ -2394,7 +2419,7 @@ async function gatherRoutedModelsUncached(
|
|
|
2394
2419
|
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
|
|
2395
2420
|
// Native-alias defaults apply only where the custom row declares nothing: the explicit
|
|
2396
2421
|
// spreads below must win (later in object order), so a stored `[]` stays empty and a
|
|
2397
|
-
// declared ladder is
|
|
2422
|
+
// declared ladder is narrowed to proven native capabilities after the merge below.
|
|
2398
2423
|
...(codexForwardNativeCapabilityAlias
|
|
2399
2424
|
? {
|
|
2400
2425
|
codexForwardNativeCapabilityAlias: true,
|
|
@@ -2409,7 +2434,8 @@ async function gatherRoutedModelsUncached(
|
|
|
2409
2434
|
: {}),
|
|
2410
2435
|
// Explicit custom-row ladder wins over the inherited provider row below: the merge only
|
|
2411
2436
|
// gap-fills, so a stored `[]` (explicit "no reasoning") or a declared ladder is kept
|
|
2412
|
-
//
|
|
2437
|
+
// instead of being replaced by that row's metadata. Only proven native aliases are
|
|
2438
|
+
// bounded against their own capability source after the merge.
|
|
2413
2439
|
...(Array.isArray(cm.reasoningEfforts) ? { reasoningEfforts: [...cm.reasoningEfforts] } : {}),
|
|
2414
2440
|
...(cm.defaultReasoningEffort ? { defaultReasoningEffort: cm.defaultReasoningEffort } : {}),
|
|
2415
2441
|
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
|
|
@@ -2462,22 +2488,25 @@ async function gatherRoutedModelsUncached(
|
|
|
2462
2488
|
...(base.codexToolMode === undefined && replaced.codexToolMode !== undefined ? { codexToolMode: replaced.codexToolMode } : {}),
|
|
2463
2489
|
...(base.capabilities === undefined && replaced.capabilities !== undefined ? { capabilities: replaced.capabilities } : {}),
|
|
2464
2490
|
} : base;
|
|
2491
|
+
const reasoningBounded = codexForwardNativeCapabilityAlias
|
|
2492
|
+
? boundCustomNativeReasoning(merged, nativeReasoningEfforts(cm.modelId), nativeAliasDefaultEffort)
|
|
2493
|
+
: merged;
|
|
2465
2494
|
// Vision-sidecar coverage only: when the enriched provider's shared predicate matches
|
|
2466
2495
|
// noVisionModels or text-without-image modelInputModalities, advertise image input so the
|
|
2467
2496
|
// Codex app lets images reach the sidecar (#349/#344). Deliberately NOT the full
|
|
2468
2497
|
// applyProviderConfigHints pass — custom rows are a
|
|
2469
2498
|
// user override, so their explicit contextWindow / inputModalities / reasoning fields must be
|
|
2470
2499
|
// preserved verbatim (the hint pass would cap context and overwrite modalities from registry).
|
|
2471
|
-
const mergedContext = typeof
|
|
2472
|
-
?
|
|
2500
|
+
const mergedContext = typeof reasoningBounded.contextWindow === "number" && reasoningBounded.contextWindow > 0
|
|
2501
|
+
? reasoningBounded.contextWindow
|
|
2473
2502
|
: undefined;
|
|
2474
|
-
const boundedMergedMaxInput = typeof
|
|
2475
|
-
? (mergedContext !== undefined ? Math.min(
|
|
2503
|
+
const boundedMergedMaxInput = typeof reasoningBounded.maxInputTokens === "number" && reasoningBounded.maxInputTokens > 0
|
|
2504
|
+
? (mergedContext !== undefined ? Math.min(reasoningBounded.maxInputTokens, mergedContext) : reasoningBounded.maxInputTokens)
|
|
2476
2505
|
: undefined;
|
|
2477
2506
|
const mergedWithHardBounds = boundedMergedMaxInput !== undefined
|
|
2478
|
-
&& boundedMergedMaxInput !==
|
|
2479
|
-
? { ...
|
|
2480
|
-
:
|
|
2507
|
+
&& boundedMergedMaxInput !== reasoningBounded.maxInputTokens
|
|
2508
|
+
? { ...reasoningBounded, maxInputTokens: boundedMergedMaxInput }
|
|
2509
|
+
: reasoningBounded;
|
|
2481
2510
|
const mergedSoftCandidates = [mergedWithHardBounds.autoCompactTokenLimit, configuredAutoCompact]
|
|
2482
2511
|
.filter((value): value is number => typeof value === "number" && value > 0);
|
|
2483
2512
|
const mergedWithAutoCompact: CatalogModel = mergedContext !== undefined && mergedSoftCandidates.length > 0
|
|
@@ -471,12 +471,14 @@ export function buildCatalogEntries(
|
|
|
471
471
|
accountNativeSlugs?: readonly string[],
|
|
472
472
|
accountNativeSlugsBySelector?: ReadonlyMap<string, readonly string[]>,
|
|
473
473
|
keepNativeChatGptOnV1 = false,
|
|
474
|
+
modelPickerOrder: readonly string[] = [],
|
|
474
475
|
): RawEntry[] {
|
|
475
|
-
|
|
476
|
+
const entries = buildCatalogEntriesFromObservedState({
|
|
476
477
|
template,
|
|
477
478
|
gptSlugs,
|
|
478
479
|
goModels,
|
|
479
480
|
featured,
|
|
481
|
+
modelPickerOrder,
|
|
480
482
|
wsEnabled,
|
|
481
483
|
multiAgentMode,
|
|
482
484
|
exactComboSlugs,
|
|
@@ -489,6 +491,8 @@ export function buildCatalogEntries(
|
|
|
489
491
|
accountNativeSlugs,
|
|
490
492
|
accountNativeSlugsBySelector,
|
|
491
493
|
});
|
|
494
|
+
applyFullModelPickerOrder(entries, modelPickerOrder);
|
|
495
|
+
return entries;
|
|
492
496
|
}
|
|
493
497
|
|
|
494
498
|
/** Build entries solely from caller-observed inputs, with no feature-state filesystem read. */
|
|
@@ -720,6 +724,30 @@ export function orderForSubagents(goModels: CatalogModel[], featured?: string[])
|
|
|
720
724
|
});
|
|
721
725
|
}
|
|
722
726
|
|
|
727
|
+
/** Routed discovery projection; native groups and alias ownership belong to the caller. */
|
|
728
|
+
export function orderForModelPicker(
|
|
729
|
+
models: readonly CatalogModel[],
|
|
730
|
+
order: readonly string[] = [],
|
|
731
|
+
featured: readonly string[] = [],
|
|
732
|
+
): CatalogModel[] {
|
|
733
|
+
const pickerOrder = normalizeModelPickerOrder(order);
|
|
734
|
+
if (pickerOrder.length === 0) return [...models];
|
|
735
|
+
const pickerRank = modelPickerRank(pickerOrder);
|
|
736
|
+
const featuredRank = modelPickerRank(featured);
|
|
737
|
+
const complete = pickerOrder.some(slug => !slug.includes("/"));
|
|
738
|
+
const rank = (model: CatalogModel): number => {
|
|
739
|
+
const slug = catalogModelSlug(model);
|
|
740
|
+
const featuredIndex = featuredRank(slug) ?? featuredRank(`${model.provider}/${model.id}`);
|
|
741
|
+
const natural = featuredIndex ?? 5;
|
|
742
|
+
const index = pickerRank(slug) ?? pickerRank(`${model.provider}/${model.id}`);
|
|
743
|
+
if (complete) return index ?? pickerOrder.length + natural;
|
|
744
|
+
// Preserve the legacy featured/alias bands, including unlisted rows before listed rows.
|
|
745
|
+
if (featuredIndex !== undefined || model.nativeAlias === true) return natural;
|
|
746
|
+
return index === undefined ? natural : PICKER_ORDER_PRIORITY_BASE + index;
|
|
747
|
+
};
|
|
748
|
+
return [...models].sort((a, b) => rank(a) - rank(b));
|
|
749
|
+
}
|
|
750
|
+
|
|
723
751
|
/**
|
|
724
752
|
* True when an existing catalog row was authored by OpenCodex routing (#855).
|
|
725
753
|
* Every generated routed row — current full-slug form, the June–July 2026
|
|
@@ -875,6 +903,10 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
875
903
|
const detachedBaselineCatalogModels = baselineCatalogModels
|
|
876
904
|
.map(entry => structuredClone(entry) as RawEntry);
|
|
877
905
|
const detachedRoutedEntries = routedEntries.map(entry => structuredClone(entry) as RawEntry);
|
|
906
|
+
// Track this invocation's generated custom rows, not ownership markers read from disk.
|
|
907
|
+
// Their builder already finalized exact native ladders and ordinary routed mock tiers.
|
|
908
|
+
const freshCustomEntries = new Set(detachedRoutedEntries.filter(entry =>
|
|
909
|
+
entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND));
|
|
878
910
|
const detachedAccountBoundEntries = accountBoundEntries
|
|
879
911
|
.map(entry => structuredClone(entry) as RawEntry);
|
|
880
912
|
const disabledModelKeys = new Set([...disabledModels].map(slugEquivalenceKey));
|
|
@@ -1195,7 +1227,7 @@ export function mergeCatalogEntriesFromObservedState({
|
|
|
1195
1227
|
// Mock-max universality (260709): preserved routed entries from disk may predate
|
|
1196
1228
|
// the max rung — ensure it here so subagent max spawns validate on every
|
|
1197
1229
|
// reasoning-capable entry. max only: 5.6 exact ladders (luna: no ultra) stay intact.
|
|
1198
|
-
if (!exactCombo && !reserveProjection && !String(e.slug ?? "").startsWith("opencode-go/")) {
|
|
1230
|
+
if (!freshCustomEntries.has(m) && !exactCombo && !reserveProjection && !String(e.slug ?? "").startsWith("opencode-go/")) {
|
|
1199
1231
|
const levels = Array.isArray(e.supported_reasoning_levels)
|
|
1200
1232
|
? e.supported_reasoning_levels as Array<{ effort?: string }>
|
|
1201
1233
|
: [];
|
package/src/codex/catalog.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, c
|
|
|
8
8
|
export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithMetadata, resolveComboCatalogMember, configuredComboTargetModelsByProvider } from "./catalog/provider-fetch";
|
|
9
9
|
export { deriveComboCatalogModel, exactComboCatalogSlugs, getLastComboCatalogOmissions, resetOpenAiApiCatalogWarningStateForTests, uniqueCatalogModelsForPublicList, uniqueCatalogModelsForRawPublicList, buildComboCatalogOmission, comboCatalogOmissionReason, summarizeComboCatalogOmissions } from "./catalog/aggregation";
|
|
10
10
|
export type { ComboCatalogOmission, ComboCatalogOmissionReason } from "./catalog/aggregation";
|
|
11
|
-
export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, effectiveSubagentRoster, buildCatalogEntries, mergeCatalogEntriesFromObservedState, resetCatalogRuntimeStateForTests, orderForSubagents, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache, finalizeAutoReviewModelOverride } from "./catalog/sync";
|
|
11
|
+
export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, effectiveSubagentRoster, buildCatalogEntries, mergeCatalogEntriesFromObservedState, resetCatalogRuntimeStateForTests, orderForSubagents, orderForModelPicker, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache, finalizeAutoReviewModelOverride } from "./catalog/sync";
|
|
12
12
|
export type { ObservedCatalogMergeInput } from "./catalog/sync";
|
|
13
13
|
export type { SpawnAgentSurface, SubagentRosterExclusionReason, EffectiveSubagentModel, SubagentRosterExclusion, EffectiveSubagentRoster } from "./catalog/sync";
|
|
14
14
|
export { accountBoundNativeDisplayName, accountBoundNativeModelSlugs, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./catalog/account-models";
|