@yansigit/opencodex 2.32.0 → 2.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/gui/dist/assets/index-DKLr4LTE.js +102 -0
- package/gui/dist/assets/index-DrSQdTRd.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -5
- package/src/adapters/anthropic.ts +20 -6
- package/src/adapters/azure.ts +20 -4
- package/src/adapters/base.ts +3 -1
- package/src/adapters/command-code.ts +40 -7
- package/src/adapters/cursor/live-transport.ts +1 -1
- package/src/adapters/cursor/protobuf-events.ts +158 -7
- package/src/adapters/cursor/protobuf-request.ts +33 -15
- package/src/adapters/cursor/request-builder.ts +4 -3
- package/src/adapters/cursor/tool-definitions.ts +27 -1
- package/src/adapters/cursor/types.ts +4 -3
- package/src/adapters/cursor.ts +9 -0
- package/src/adapters/google-antigravity-replay.ts +2 -2
- package/src/adapters/google-antigravity-wire.ts +7 -0
- package/src/adapters/google-errors.ts +6 -2
- package/src/adapters/google-http.ts +30 -7
- package/src/adapters/google-truncation.ts +5 -0
- package/src/adapters/google-wire-compiler.ts +38 -6
- package/src/adapters/google.ts +148 -26
- package/src/adapters/kiro-tools.ts +20 -9
- package/src/adapters/openai-chat.ts +9 -0
- package/src/adapters/openai-responses.ts +1 -1
- package/src/bridge.ts +112 -9
- package/src/claude/context-windows.ts +16 -9
- package/src/cli/doctor.ts +2 -2
- package/src/cli/index.ts +9 -2
- package/src/cli/provider.ts +6 -0
- package/src/cli/status.ts +23 -0
- package/src/codex/auth-api.ts +4 -2
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +12 -12
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/model-metadata.ts +39 -12
- package/src/codex/catalog/parsing.ts +38 -27
- package/src/codex/catalog/provider-fetch.ts +198 -133
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/convergence.ts +5 -0
- package/src/codex/shim.ts +56 -3
- package/src/config/provider-validation.ts +37 -0
- package/src/config.ts +78 -2
- package/src/generated/compatibility-version.json +120 -96
- package/src/images/loop.ts +37 -6
- package/src/lib/azure-identity.ts +154 -0
- package/src/lib/debug.ts +42 -0
- package/src/lib/errors.ts +14 -0
- package/src/lib/provider-outbound.ts +45 -33
- package/src/lib/provider-tls-profile.ts +309 -0
- package/src/lib/proxy-env.ts +49 -0
- package/src/lib/redact.ts +10 -1
- package/src/oauth/antigravity-routing.ts +282 -236
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/command-code.ts +5 -16
- package/src/oauth/google-antigravity.ts +42 -5
- package/src/oauth/index.ts +15 -3
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +6 -0
- package/src/providers/antigravity-quota.ts +3 -1
- package/src/providers/api-keys.ts +2 -1
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/key-failover.ts +5 -1
- package/src/providers/openai-tiers.ts +5 -0
- package/src/providers/provider-id-rewrite.ts +1 -0
- package/src/providers/quota.ts +59 -13
- package/src/providers/registry.ts +3 -1
- package/src/providers/request-pacing.ts +33 -6
- package/src/providers/xai-transport.ts +21 -0
- package/src/responses/google-provider-options.ts +36 -0
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +11 -0
- package/src/responses/provider-opaque-metadata.ts +3 -3
- package/src/responses/schema.ts +37 -0
- package/src/responses/state.ts +94 -4
- package/src/router.ts +8 -2
- package/src/server/auth-cors.ts +28 -0
- package/src/server/images.ts +19 -35
- package/src/server/management/agent-settings-routes.ts +205 -15
- package/src/server/management/combo-routes.ts +6 -0
- package/src/server/management/config-routes.ts +31 -5
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/oauth-account-routes.ts +25 -4
- package/src/server/management/provider-routes.ts +113 -15
- package/src/server/management/routing-profile-routes.ts +3 -0
- package/src/server/request-log.ts +21 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/compact.ts +30 -1
- package/src/server/responses/core.ts +359 -153
- package/src/server/responses/empty-completion-guard.ts +35 -6
- package/src/server/responses/fetch-helpers.ts +18 -5
- package/src/server/responses/v2-native-parent-override.ts +59 -0
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-undeclared-tool-guard.ts +90 -8
- package/src/service.ts +1 -1
- package/src/types/config.ts +16 -1
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +28 -0
- package/src/types/tools.ts +27 -0
- package/src/types.ts +6 -0
- package/src/web-search/gemini-executor.ts +6 -4
- package/src/web-search/loop.ts +42 -6
- package/gui/dist/assets/index-BG43zwVe.js +0 -102
- package/gui/dist/assets/index-CiSI-jrP.css +0 -1
package/src/bridge.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
awaitThoughtSignatureDurability,
|
|
20
20
|
} from "./responses/thought-signature-replay";
|
|
21
21
|
import { resolveStallTimeoutSec } from "./stall-timeout";
|
|
22
|
+
import { normalizeDeclaredToolName } from "./types";
|
|
22
23
|
import { usageDisplayTotalTokens } from "./usage/totals";
|
|
23
24
|
import { appendSafeWebSearchSource, safeWebSearchSources } from "./web-search/sources";
|
|
24
25
|
import {
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
type TranslatorBudget,
|
|
29
30
|
type TranslatorBufferKind,
|
|
30
31
|
} from "./lib/translator-budget";
|
|
32
|
+
import { debugFingerprint, debugStreamDiagnostic, type DebugStreamDiagnosticContext } from "./lib/debug";
|
|
31
33
|
|
|
32
34
|
function uuid(): string {
|
|
33
35
|
return crypto.randomUUID().replace(/-/g, "");
|
|
@@ -118,7 +120,9 @@ function adapterFailureFromEvent(event: Extract<AdapterEvent, { type: "error" }>
|
|
|
118
120
|
}
|
|
119
121
|
const fallback = adapterFailureFromMessage(event.message);
|
|
120
122
|
let httpStatus = event.status ?? fallback.httpStatus;
|
|
121
|
-
const error =
|
|
123
|
+
const error = httpStatus === 400 && event.errorType === "upstream_error"
|
|
124
|
+
? { message: event.message, type: "upstream_error", code: event.code ?? null }
|
|
125
|
+
: classifyError(httpStatus, event.errorType ?? fallback.error.type, event.message);
|
|
122
126
|
if (event.errorType !== undefined) error.type = event.errorType;
|
|
123
127
|
if (event.code !== undefined) error.code = event.code;
|
|
124
128
|
// Codex maps cyber_policy on HTTP 400 (body) or mid-stream code; never leave it as 502.
|
|
@@ -166,6 +170,88 @@ interface OutputItem {
|
|
|
166
170
|
|
|
167
171
|
export type ResponsesTerminalStatus = "completed" | "failed" | "incomplete";
|
|
168
172
|
|
|
173
|
+
export interface BridgeDiagnosticSequence { value: number }
|
|
174
|
+
|
|
175
|
+
export interface BridgeDiagnosticContext extends DebugStreamDiagnosticContext {
|
|
176
|
+
sequence?: BridgeDiagnosticSequence;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function adapterEventDiagnosticDetails(event: AdapterEvent): Record<string, unknown> {
|
|
180
|
+
switch (event.type) {
|
|
181
|
+
case "text_delta":
|
|
182
|
+
return { byteLength: Buffer.byteLength(event.text), fingerprint: debugFingerprint(event.text) };
|
|
183
|
+
case "thinking_delta":
|
|
184
|
+
return { byteLength: Buffer.byteLength(event.thinking), fingerprint: debugFingerprint(event.thinking) };
|
|
185
|
+
case "reasoning_raw_delta":
|
|
186
|
+
return { byteLength: Buffer.byteLength(event.text), fingerprint: debugFingerprint(event.text) };
|
|
187
|
+
case "thinking_signature":
|
|
188
|
+
case "redacted_thinking":
|
|
189
|
+
case "kiro_redacted_reasoning": {
|
|
190
|
+
const content = event.type === "thinking_signature" ? event.signature : event.data;
|
|
191
|
+
return { byteLength: Buffer.byteLength(content), fingerprint: debugFingerprint(content) };
|
|
192
|
+
}
|
|
193
|
+
case "tool_call_delta":
|
|
194
|
+
return { byteLength: Buffer.byteLength(event.arguments), fingerprint: debugFingerprint(event.arguments) };
|
|
195
|
+
case "tool_call_start":
|
|
196
|
+
return {
|
|
197
|
+
idByteLength: Buffer.byteLength(event.id),
|
|
198
|
+
idFingerprint: debugFingerprint(event.id),
|
|
199
|
+
nameByteLength: Buffer.byteLength(event.name),
|
|
200
|
+
nameFingerprint: debugFingerprint(event.name),
|
|
201
|
+
};
|
|
202
|
+
case "web_search_call_begin":
|
|
203
|
+
return { idByteLength: Buffer.byteLength(event.id), idFingerprint: debugFingerprint(event.id) };
|
|
204
|
+
case "web_search_call_end": {
|
|
205
|
+
const queries = JSON.stringify(event.queries);
|
|
206
|
+
return {
|
|
207
|
+
idByteLength: Buffer.byteLength(event.id),
|
|
208
|
+
idFingerprint: debugFingerprint(event.id),
|
|
209
|
+
byteLength: Buffer.byteLength(queries),
|
|
210
|
+
fingerprint: debugFingerprint(queries),
|
|
211
|
+
status: event.status,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
case "error":
|
|
215
|
+
return {
|
|
216
|
+
byteLength: Buffer.byteLength(event.message),
|
|
217
|
+
fingerprint: debugFingerprint(event.message),
|
|
218
|
+
...(event.status !== undefined ? { status: event.status } : {}),
|
|
219
|
+
...(event.code !== undefined
|
|
220
|
+
? { codeByteLength: Buffer.byteLength(event.code), codeFingerprint: debugFingerprint(event.code) }
|
|
221
|
+
: {}),
|
|
222
|
+
...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
|
|
223
|
+
};
|
|
224
|
+
case "incomplete":
|
|
225
|
+
return {
|
|
226
|
+
...(event.message !== undefined ? { byteLength: Buffer.byteLength(event.message), fingerprint: debugFingerprint(event.message) } : {}),
|
|
227
|
+
reasonByteLength: Buffer.byteLength(event.reason),
|
|
228
|
+
reasonFingerprint: debugFingerprint(event.reason),
|
|
229
|
+
...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
|
|
230
|
+
};
|
|
231
|
+
case "done":
|
|
232
|
+
return {
|
|
233
|
+
...(event.stopReason !== undefined
|
|
234
|
+
? { stopReasonByteLength: Buffer.byteLength(event.stopReason), stopReasonFingerprint: debugFingerprint(event.stopReason) }
|
|
235
|
+
: {}),
|
|
236
|
+
...(event.endTurn !== undefined ? { endTurn: event.endTurn } : {}),
|
|
237
|
+
};
|
|
238
|
+
default:
|
|
239
|
+
return {};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Emit one adapter-stage diagnostic while preserving one sequence across sidecar iterations. */
|
|
244
|
+
export function diagnoseAdapterEvent(context: BridgeDiagnosticContext, event: AdapterEvent): void {
|
|
245
|
+
const sequence = context.sequence ??= { value: 0 };
|
|
246
|
+
debugStreamDiagnostic(
|
|
247
|
+
context,
|
|
248
|
+
"adapter",
|
|
249
|
+
++sequence.value,
|
|
250
|
+
event.type,
|
|
251
|
+
adapterEventDiagnosticDetails(event),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
169
255
|
export function bridgeToResponsesSSE(
|
|
170
256
|
events: AsyncIterable<AdapterEvent>,
|
|
171
257
|
modelId: string,
|
|
@@ -226,6 +312,8 @@ export function bridgeToResponsesSSE(
|
|
|
226
312
|
setInterval: (handler: () => void, ms: number) => unknown;
|
|
227
313
|
clearInterval: (id: unknown) => void;
|
|
228
314
|
};
|
|
315
|
+
/** Internal, opt-in structural stream diagnostics. */
|
|
316
|
+
diagnostic?: BridgeDiagnosticContext;
|
|
229
317
|
},
|
|
230
318
|
): ReadableStream<Uint8Array> {
|
|
231
319
|
const replayCacheScope = options?.replayCacheScope;
|
|
@@ -326,6 +414,7 @@ export function bridgeToResponsesSSE(
|
|
|
326
414
|
};
|
|
327
415
|
const responseId = options?.responseId ?? `resp_${uuid()}`;
|
|
328
416
|
let seq = 0;
|
|
417
|
+
let diagnosticSequence = 0;
|
|
329
418
|
// Set once the client is gone (cancel) or an enqueue throws on a torn-down controller, so we
|
|
330
419
|
// never enqueue again and never throw a second time inside start() — the RC2 double-throw that
|
|
331
420
|
// otherwise surfaced as proxy-side stream noise on every client disconnect.
|
|
@@ -858,6 +947,17 @@ export function bridgeToResponsesSSE(
|
|
|
858
947
|
}
|
|
859
948
|
if (next.done) { upstreamDone = true; break; }
|
|
860
949
|
const event = next.value;
|
|
950
|
+
if (options?.diagnostic) {
|
|
951
|
+
debugStreamDiagnostic(
|
|
952
|
+
options.diagnostic,
|
|
953
|
+
"bridge",
|
|
954
|
+
options.diagnostic.sequence
|
|
955
|
+
? ++options.diagnostic.sequence.value
|
|
956
|
+
: ++diagnosticSequence,
|
|
957
|
+
event.type,
|
|
958
|
+
adapterEventDiagnosticDetails(event),
|
|
959
|
+
);
|
|
960
|
+
}
|
|
861
961
|
let terminalEvent = false;
|
|
862
962
|
// Invisible adapter heartbeats (and buffered web-search progress) count as upstream
|
|
863
963
|
// liveness only — they must not suppress wire keepalives that re-arm Codex idle timers.
|
|
@@ -1041,13 +1141,14 @@ export function bridgeToResponsesSSE(
|
|
|
1041
1141
|
rememberReasoningForCall(event.id, rawReasoningForNextToolCall, replayCacheScope);
|
|
1042
1142
|
}
|
|
1043
1143
|
if (currentToolCall) closeCurrentToolCall();
|
|
1044
|
-
const
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1144
|
+
const effectiveName = normalizeDeclaredToolName(event.name, options?.declaredToolNames);
|
|
1145
|
+
const mapped = toolNsMap?.get(effectiveName);
|
|
1146
|
+
const realName = mapped?.name ?? effectiveName;
|
|
1147
|
+
if (options?.declaredToolNames && !options.declaredToolNames.has(effectiveName)) {
|
|
1047
1148
|
const failure = responseError(
|
|
1048
1149
|
502,
|
|
1049
1150
|
"upstream_error",
|
|
1050
|
-
`routed provider emitted undeclared client tool "${
|
|
1151
|
+
`routed provider emitted undeclared client tool "${effectiveName}"; only request-declared tools may be called`,
|
|
1051
1152
|
);
|
|
1052
1153
|
emit("response.failed", {
|
|
1053
1154
|
response: {
|
|
@@ -1783,7 +1884,7 @@ function buildResponseJSONWithBudget(
|
|
|
1783
1884
|
));
|
|
1784
1885
|
}
|
|
1785
1886
|
break;
|
|
1786
|
-
case "tool_call_start":
|
|
1887
|
+
case "tool_call_start": {
|
|
1787
1888
|
if (currentText) flushText("commentary");
|
|
1788
1889
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1789
1890
|
if (currentRawReasoning) flushRawReasoning();
|
|
@@ -1791,10 +1892,11 @@ function buildResponseJSONWithBudget(
|
|
|
1791
1892
|
rememberReasoningForCall(e.id, rawReasoningForNextToolCall, replayCacheScope);
|
|
1792
1893
|
}
|
|
1793
1894
|
flushToolCall();
|
|
1794
|
-
|
|
1895
|
+
const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames);
|
|
1896
|
+
if (options?.declaredToolNames && !options.declaredToolNames.has(effectiveName)) {
|
|
1795
1897
|
errorEvent = {
|
|
1796
1898
|
type: "error",
|
|
1797
|
-
message: `routed provider emitted undeclared client tool "${
|
|
1899
|
+
message: `routed provider emitted undeclared client tool "${effectiveName}"; only request-declared tools may be called`,
|
|
1798
1900
|
status: 502,
|
|
1799
1901
|
errorType: "upstream_error",
|
|
1800
1902
|
};
|
|
@@ -1802,11 +1904,12 @@ function buildResponseJSONWithBudget(
|
|
|
1802
1904
|
}
|
|
1803
1905
|
currentToolCallId = e.id;
|
|
1804
1906
|
budget?.openCall(e.id);
|
|
1805
|
-
currentToolCallName =
|
|
1907
|
+
currentToolCallName = effectiveName;
|
|
1806
1908
|
currentToolCallArgs = "";
|
|
1807
1909
|
currentToolCallArgsBytes = 0;
|
|
1808
1910
|
currentToolCallProviderMetadata = e.providerMetadata;
|
|
1809
1911
|
break;
|
|
1912
|
+
}
|
|
1810
1913
|
case "tool_call_delta":
|
|
1811
1914
|
{
|
|
1812
1915
|
({ value: currentToolCallArgs, bytes: currentToolCallArgsBytes } = appendBatchString(
|
|
@@ -118,17 +118,24 @@ export function buildClaudeContextWindows(
|
|
|
118
118
|
put(desktop3pAlias("native", slug), window);
|
|
119
119
|
put(aliasForNative(slug), window);
|
|
120
120
|
}
|
|
121
|
+
// Anthropic passthrough guard (audit 021 #3): canonical claude ids ride the
|
|
122
|
+
// subscription passthrough — marking a sub-1M one would strap [1m]/1M-beta onto
|
|
123
|
+
// a model that cannot host it. Register anthropic rows only at >=1M.
|
|
124
|
+
const registrable = routedModels.filter(
|
|
125
|
+
m =>
|
|
126
|
+
typeof m.contextWindow === "number" &&
|
|
127
|
+
m.contextWindow > 0 &&
|
|
128
|
+
!(m.provider === "anthropic" && m.contextWindow < ONE_MILLION),
|
|
129
|
+
);
|
|
121
130
|
// Bare routed ids are registered only when unambiguous across providers (audit
|
|
122
|
-
// 021 #5) — natives are registered first, so a native slug always wins the bare
|
|
131
|
+
// 021 #5) — natives are registered first, so a native slug always wins the bare
|
|
132
|
+
// key. Counted over the rows that can actually claim the key: a row this loop
|
|
133
|
+
// skips contributes no window, so letting it veto the bare key withholds an
|
|
134
|
+
// answer that was never in doubt.
|
|
123
135
|
const bareCounts = new Map<string, number>();
|
|
124
|
-
for (const m of
|
|
125
|
-
for (const m of
|
|
126
|
-
const window = m.contextWindow;
|
|
127
|
-
if (typeof window !== "number" || window <= 0) continue;
|
|
128
|
-
// Anthropic passthrough guard (audit 021 #3): canonical claude ids ride the
|
|
129
|
-
// subscription passthrough — marking a sub-1M one would strap [1m]/1M-beta onto
|
|
130
|
-
// a model that cannot host it. Register anthropic rows only at >=1M.
|
|
131
|
-
if (m.provider === "anthropic" && window < ONE_MILLION) continue;
|
|
136
|
+
for (const m of registrable) bareCounts.set(m.id, (bareCounts.get(m.id) ?? 0) + 1);
|
|
137
|
+
for (const m of registrable) {
|
|
138
|
+
const window = m.contextWindow as number;
|
|
132
139
|
put(`${m.provider}/${m.id}`, window);
|
|
133
140
|
put(desktop3pAlias(m.provider, m.id), window);
|
|
134
141
|
put(aliasForRoute(m.provider, m.id), window);
|
package/src/cli/doctor.ts
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
resolveEffectiveUserIdentity,
|
|
43
43
|
} from "../codex/user-identity";
|
|
44
44
|
import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings";
|
|
45
|
-
import { collectStartupHealth, startupHealthSummary } from "../codex/autostart-health";
|
|
45
|
+
import { collectStartupHealth, formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
|
|
46
46
|
import {
|
|
47
47
|
displayCodexRuntimePath,
|
|
48
48
|
loadLastEffortClamp,
|
|
@@ -983,7 +983,7 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
983
983
|
const startup = collectStartupHealth(doctorConfig);
|
|
984
984
|
console.log("\nCodex restart safety");
|
|
985
985
|
console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
|
|
986
|
-
console.log(`
|
|
986
|
+
console.log(` ${formatStartupRoutingDetail(startup)}`);
|
|
987
987
|
|
|
988
988
|
console.log("\nCodex runtime selection");
|
|
989
989
|
{
|
package/src/cli/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
writePid,
|
|
26
26
|
writeRuntimePort,
|
|
27
27
|
} from "../config/process-state";
|
|
28
|
-
import { collectStatus } from "./status";
|
|
28
|
+
import { collectStatus, unusedProxyWarningLines } from "./status";
|
|
29
29
|
|
|
30
30
|
import {
|
|
31
31
|
discoverStableProxyForRestart,
|
|
@@ -46,7 +46,7 @@ import { runCli } from "./root";
|
|
|
46
46
|
import { ProxyOwnershipRefusedError, stopProxy } from "../lib/process-control";
|
|
47
47
|
import { loadServiceTokenFromFile } from "../lib/service-secrets";
|
|
48
48
|
import { diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
|
|
49
|
-
import { startupHealthSummary } from "../codex/autostart-health";
|
|
49
|
+
import { formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
|
|
50
50
|
import { drainAndShutdown, isRecyclingForExit, startServer } from "../server";
|
|
51
51
|
import { injectSystemEnv, reconcileShellHook, revertSystemEnv, uninstallShellHook } from "../server/system-env";
|
|
52
52
|
import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
@@ -849,6 +849,12 @@ async function handleStatus() {
|
|
|
849
849
|
console.log(`❌ Proxy: ${status.proxyLabel}`);
|
|
850
850
|
}
|
|
851
851
|
console.log(` Health: ${status.healthLabel}`);
|
|
852
|
+
for (const line of unusedProxyWarningLines({
|
|
853
|
+
proxyUp: Boolean(status.json.proxy.pid || status.json.proxy.health.ok),
|
|
854
|
+
routingKind: status.json.startup.routingKind,
|
|
855
|
+
})) {
|
|
856
|
+
console.log(` ${line}`);
|
|
857
|
+
}
|
|
852
858
|
if (!(status.json.proxy.pid || status.json.proxy.health.ok)) {
|
|
853
859
|
console.log(" ↳ Not running — Codex/Claude requests will fail with connection errors.");
|
|
854
860
|
// The service summary a few lines below already tells a registered-but-not-serving
|
|
@@ -868,6 +874,7 @@ async function handleStatus() {
|
|
|
868
874
|
console.log(` Default provider: ${status.json.defaultProvider}`);
|
|
869
875
|
console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`);
|
|
870
876
|
console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}`);
|
|
877
|
+
console.log(` ${formatStartupRoutingDetail(status.json.startup)}`);
|
|
871
878
|
console.log(` Service: ${status.json.service.summary}`);
|
|
872
879
|
console.log(` ${status.json.codexShim.summary}`);
|
|
873
880
|
console.log(` Codex runtime: ${status.json.codexRuntime.path}`);
|
package/src/cli/provider.ts
CHANGED
|
@@ -360,6 +360,12 @@ function handleShow(args: string[]): void {
|
|
|
360
360
|
...(prov.modelCosts !== undefined ? { modelCosts: sanitizeModelCostsForDisplay(prov.modelCosts) } : {}),
|
|
361
361
|
...(prov.apiKey ? { apiKey: maskSecret(prov.apiKey) } : {}),
|
|
362
362
|
...(prov.apiKeyPool ? { apiKeyPool: prov.apiKeyPool.map(e => ({ ...e, key: maskSecret(e.key) })) } : {}),
|
|
363
|
+
...(prov.azureCredential ? {
|
|
364
|
+
azureCredential: {
|
|
365
|
+
type: prov.azureCredential.type,
|
|
366
|
+
hasManagedIdentityClientId: Boolean(prov.azureCredential.managedIdentityClientId?.trim()),
|
|
367
|
+
},
|
|
368
|
+
} : {}),
|
|
363
369
|
};
|
|
364
370
|
|
|
365
371
|
if (wantsJson) {
|
package/src/cli/status.ts
CHANGED
|
@@ -118,6 +118,29 @@ export function proxyHealthFailureReason(error: unknown, signal: AbortSignal): "
|
|
|
118
118
|
: "unreachable";
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* `ocx status` greens on process liveness alone, so a proxy that answers
|
|
123
|
+
* /healthz reads healthy even when Codex is not pointed at it and every routed
|
|
124
|
+
* request goes to OpenAI instead (#2411). The proxy line is not wrong — the
|
|
125
|
+
* listener really is up — so it keeps its check, and this supplies the signal
|
|
126
|
+
* that was missing rather than corrupting the one that was already honest.
|
|
127
|
+
*
|
|
128
|
+
* Only `native` warns. `custom-local` and `unknown` are also "this proxy is
|
|
129
|
+
* unused", but startupHealthSummary already renders both as AT RISK with a
|
|
130
|
+
* remedy command, and `custom-remote` is a deliberate operator choice. Warning
|
|
131
|
+
* on all four would teach operators to skip the line that matters.
|
|
132
|
+
*/
|
|
133
|
+
export function unusedProxyWarningLines(input: {
|
|
134
|
+
proxyUp: boolean;
|
|
135
|
+
routingKind: StartupHealth["routingKind"];
|
|
136
|
+
}): string[] {
|
|
137
|
+
if (!input.proxyUp || input.routingKind !== "native") return [];
|
|
138
|
+
return [
|
|
139
|
+
"⚠️ Codex routing is native — the running proxy is unused.",
|
|
140
|
+
" Codex requests go to OpenAI, not this proxy. Re-point with: ocx start",
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
121
144
|
async function checkProxyHealth(target: ListenTarget): Promise<HealthCheck> {
|
|
122
145
|
const url = target.healthUrl;
|
|
123
146
|
const controller = new AbortController();
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -1806,7 +1806,7 @@ export async function handleCodexAuthAPI(
|
|
|
1806
1806
|
}
|
|
1807
1807
|
|
|
1808
1808
|
if (url.pathname === "/api/codex-auth/login" && req.method === "POST") {
|
|
1809
|
-
const body = (await req.json().catch(() => ({}))) as { id?: string; reauth?: boolean };
|
|
1809
|
+
const body = (await req.json().catch(() => ({}))) as { id?: string; reauth?: boolean; openBrowser?: unknown };
|
|
1810
1810
|
const requestedAccountId = body.id?.trim();
|
|
1811
1811
|
const reauth = body.reauth === true;
|
|
1812
1812
|
if (requestedAccountId && !isValidCodexAccountId(requestedAccountId)) {
|
|
@@ -1840,7 +1840,9 @@ export async function handleCodexAuthAPI(
|
|
|
1840
1840
|
|
|
1841
1841
|
// Open the browser server-side (same pattern as /api/oauth/login in management-api.ts).
|
|
1842
1842
|
// The GUI's window.open is popup-blocked because it runs after an await, not a direct click.
|
|
1843
|
-
|
|
1843
|
+
// Both login routes share one resolver so this surface cannot drift from the other.
|
|
1844
|
+
const { shouldOpenBrowserForLogin } = await import("../oauth/open-browser-choice");
|
|
1845
|
+
if (result.url && shouldOpenBrowserForLogin(body.openBrowser, runtimeConfig)) {
|
|
1844
1846
|
const { openUrl } = await import("../lib/open-url");
|
|
1845
1847
|
openUrl(result.url);
|
|
1846
1848
|
}
|
|
@@ -154,3 +154,19 @@ export function startupHealthSummary(health: StartupHealth): string {
|
|
|
154
154
|
if (health.serviceInstalled && !health.serviceViable) return `AT RISK after restart (installed service is disabled, stopped, or unhealthy; run '${command}')`;
|
|
155
155
|
return `AT RISK after restart (no viable background service; run '${command}')`;
|
|
156
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The routing/service/shim token `ocx doctor` prints under restart safety.
|
|
160
|
+
* Extracted so `ocx status` can show the same string rather than growing a
|
|
161
|
+
* second copy that drifts (#2411). Two management routes computing the same
|
|
162
|
+
* thing separately is exactly how #2457 happened.
|
|
163
|
+
*/
|
|
164
|
+
export function formatStartupRoutingDetail(health: StartupHealth): string {
|
|
165
|
+
const service = health.serviceViable
|
|
166
|
+
? "viable"
|
|
167
|
+
: health.serviceInstalled ? "installed-but-unhealthy" : "absent";
|
|
168
|
+
const shim = health.shimHealthy
|
|
169
|
+
? "healthy"
|
|
170
|
+
: health.shimInstalled ? "stale" : "absent";
|
|
171
|
+
return `routing=${health.routingKind}, service=${service}, shim=${shim}`;
|
|
172
|
+
}
|
|
@@ -13,6 +13,7 @@ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, r
|
|
|
13
13
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
14
14
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
15
15
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
16
|
+
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
16
17
|
import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
|
|
17
18
|
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
18
19
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
@@ -152,19 +153,17 @@ export function deriveComboCatalogModel(
|
|
|
152
153
|
// A combo is cap-limited only when every member defining its effective minimum was
|
|
153
154
|
// itself reduced by a provider cap. An uncapped member at the same minimum means the
|
|
154
155
|
// combo would have the same window even without the cap.
|
|
155
|
-
const contextCapped = limitingMembers.every(member =>
|
|
156
|
-
member.contextCapped === true
|
|
157
|
-
|| (
|
|
158
|
-
typeof member.detectedContextWindow === "number"
|
|
159
|
-
&& typeof member.contextWindow === "number"
|
|
160
|
-
&& member.detectedContextWindow > member.contextWindow
|
|
161
|
-
)
|
|
162
|
-
));
|
|
156
|
+
const contextCapped = limitingMembers.every(member => member.contextCapped === true);
|
|
163
157
|
const maxInputTokens = Math.min(
|
|
158
|
+
contextWindow,
|
|
164
159
|
...members.map(member => member.maxInputTokens ?? member.contextWindow!),
|
|
165
160
|
);
|
|
166
|
-
const
|
|
167
|
-
...
|
|
161
|
+
const autoCompactTokenLimit = Math.min(
|
|
162
|
+
...members.map(member => clampAutoCompactTokenLimit(
|
|
163
|
+
member.contextWindow!,
|
|
164
|
+
member.maxInputTokens,
|
|
165
|
+
member.autoCompactTokenLimit,
|
|
166
|
+
)),
|
|
168
167
|
);
|
|
169
168
|
const defaultReasoningEffort = effectiveComboDefault(
|
|
170
169
|
combo.defaultEffort,
|
|
@@ -177,8 +176,7 @@ export function deriveComboCatalogModel(
|
|
|
177
176
|
owned_by: COMBO_NAMESPACE,
|
|
178
177
|
contextWindow,
|
|
179
178
|
maxInputTokens,
|
|
180
|
-
|
|
181
|
-
detectedContextWindow,
|
|
179
|
+
autoCompactTokenLimit,
|
|
182
180
|
...(hasLimitingContextCapMetadata ? { contextCapped } : {}),
|
|
183
181
|
inputModalities,
|
|
184
182
|
reasoningEfforts,
|
|
@@ -222,6 +220,7 @@ export function comboCatalogWarningSignature(
|
|
|
222
220
|
key,
|
|
223
221
|
contextWindow: member?.contextWindow ?? null,
|
|
224
222
|
maxInputTokens: member?.maxInputTokens ?? null,
|
|
223
|
+
autoCompactTokenLimit: member?.autoCompactTokenLimit ?? null,
|
|
225
224
|
inputModalities: [...new Set(member?.inputModalities ?? [])].sort(),
|
|
226
225
|
reasoningEfforts: [...new Set(member?.reasoningEfforts ?? [])].sort(),
|
|
227
226
|
parallelToolCalls: member?.parallelToolCalls === true,
|
|
@@ -311,6 +310,7 @@ export function normalizedOpenAiApiSignature(model: CatalogModel): string {
|
|
|
311
310
|
id: model.id,
|
|
312
311
|
contextWindow: model.contextWindow ?? null,
|
|
313
312
|
maxInputTokens: model.maxInputTokens ?? null,
|
|
313
|
+
autoCompactTokenLimit: model.autoCompactTokenLimit ?? null,
|
|
314
314
|
inputModalities: [...new Set(model.inputModalities ?? [])].sort(),
|
|
315
315
|
reasoningEfforts: [...new Set(model.reasoningEfforts ?? [])].sort(),
|
|
316
316
|
ownedBy: model.owned_by ?? null,
|
|
@@ -13,6 +13,7 @@ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, r
|
|
|
13
13
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
14
14
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
15
15
|
import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
|
|
16
|
+
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
16
17
|
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
17
18
|
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
18
19
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
@@ -129,9 +130,23 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel)
|
|
|
129
130
|
if (typeof resolvedContext === "number" && resolvedContext > 0) {
|
|
130
131
|
entry.context_window = resolvedContext;
|
|
131
132
|
entry.max_context_window = resolvedContext;
|
|
132
|
-
entry.auto_compact_token_limit =
|
|
133
|
-
|
|
134
|
-
model.maxInputTokens
|
|
133
|
+
entry.auto_compact_token_limit = clampAutoCompactTokenLimit(
|
|
134
|
+
resolvedContext,
|
|
135
|
+
model.maxInputTokens,
|
|
136
|
+
model.autoCompactTokenLimit,
|
|
137
|
+
);
|
|
138
|
+
} else if (
|
|
139
|
+
typeof entry.context_window === "number"
|
|
140
|
+
&& entry.context_window > 0
|
|
141
|
+
&& typeof model.maxInputTokens === "number"
|
|
142
|
+
&& model.maxInputTokens > 0
|
|
143
|
+
) {
|
|
144
|
+
// A conservative routed fallback is not evidence for applying the optional soft policy,
|
|
145
|
+
// but a measured/configured input ceiling is still a hard bound. Compact before that
|
|
146
|
+
// ceiling even when the provider supplied no authoritative context window.
|
|
147
|
+
entry.auto_compact_token_limit = clampAutoCompactTokenLimit(
|
|
148
|
+
entry.context_window,
|
|
149
|
+
model.maxInputTokens,
|
|
135
150
|
);
|
|
136
151
|
}
|
|
137
152
|
if (Array.isArray(model.inputModalities) && model.inputModalities.length > 0) {
|
|
@@ -14,6 +14,7 @@ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, r
|
|
|
14
14
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
15
15
|
import { getProviderRegistryEntry, providerCodexAccountMode } from "../../providers/registry";
|
|
16
16
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
17
|
+
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
17
18
|
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
18
19
|
import { identifyRoutedModel } from "../../adapters/identity";
|
|
19
20
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
@@ -204,6 +205,8 @@ export interface NativeContextLimits {
|
|
|
204
205
|
readonly providerWindow?: number;
|
|
205
206
|
/** `providers.openai.modelContextWindows` — per-model, wins over `providerWindow`. */
|
|
206
207
|
readonly modelWindows?: Readonly<Record<string, number>>;
|
|
208
|
+
/** `providers.openai.modelAutoCompactTokenLimits` — soft, lowering-only budgets. */
|
|
209
|
+
readonly modelAutoCompactTokenLimits?: Readonly<Record<string, number>>;
|
|
207
210
|
}
|
|
208
211
|
|
|
209
212
|
export type NativeContextLimitsInput = NativeContextLimits | number | undefined;
|
|
@@ -227,12 +230,18 @@ export function nativeContextLimits(
|
|
|
227
230
|
const window = positiveInt(value);
|
|
228
231
|
if (window !== undefined) modelWindows[slug] = window;
|
|
229
232
|
}
|
|
233
|
+
const modelAutoCompactTokenLimits: Record<string, number> = {};
|
|
234
|
+
for (const [slug, value] of Object.entries(provider?.modelAutoCompactTokenLimits ?? {})) {
|
|
235
|
+
const budget = positiveInt(value);
|
|
236
|
+
if (budget !== undefined) modelAutoCompactTokenLimits[slug] = budget;
|
|
237
|
+
}
|
|
230
238
|
return {
|
|
231
239
|
...(positiveInt(providerContextCap(config, OPENAI_CODEX_PROVIDER_ID)) !== undefined
|
|
232
240
|
? { cap: providerContextCap(config, OPENAI_CODEX_PROVIDER_ID) }
|
|
233
241
|
: {}),
|
|
234
242
|
...(positiveInt(provider?.contextWindow) !== undefined ? { providerWindow: provider!.contextWindow } : {}),
|
|
235
243
|
...(Object.keys(modelWindows).length > 0 ? { modelWindows } : {}),
|
|
244
|
+
...(Object.keys(modelAutoCompactTokenLimits).length > 0 ? { modelAutoCompactTokenLimits } : {}),
|
|
236
245
|
};
|
|
237
246
|
}
|
|
238
247
|
|
|
@@ -277,6 +286,21 @@ export function nativeOpenAiMaxInputTokens(slug: string, limits?: NativeContextL
|
|
|
277
286
|
return window === undefined ? narrowed : Math.min(narrowed, window);
|
|
278
287
|
}
|
|
279
288
|
|
|
289
|
+
/** Effective native soft budget after every hard window/input limit is resolved. */
|
|
290
|
+
export function nativeOpenAiAutoCompactTokenLimit(
|
|
291
|
+
slug: string,
|
|
292
|
+
limits?: NativeContextLimitsInput,
|
|
293
|
+
): number | undefined {
|
|
294
|
+
const contextWindow = nativeOpenAiContextWindow(slug, limits);
|
|
295
|
+
if (contextWindow === undefined) return undefined;
|
|
296
|
+
const configured = positiveInt(asLimits(limits).modelAutoCompactTokenLimits?.[slug]);
|
|
297
|
+
return clampAutoCompactTokenLimit(
|
|
298
|
+
contextWindow,
|
|
299
|
+
nativeOpenAiMaxInputTokens(slug, limits),
|
|
300
|
+
configured,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
280
304
|
export function nativeInputModalities(slug: string): string[] {
|
|
281
305
|
const upstream = PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug);
|
|
282
306
|
if (Array.isArray(upstream?.input_modalities) && upstream!.input_modalities!.length > 0) {
|
|
@@ -387,7 +411,7 @@ export function desktopVisibleNativeSlugs(
|
|
|
387
411
|
]);
|
|
388
412
|
}
|
|
389
413
|
|
|
390
|
-
export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps" | "providers">): Array<{ slug: string; disabled: boolean; contextWindow?: number; maxInputTokens?: number }> {
|
|
414
|
+
export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps" | "providers">): Array<{ slug: string; disabled: boolean; contextWindow?: number; maxInputTokens?: number; autoCompactTokenLimit?: number }> {
|
|
391
415
|
const disabled = disabledNativeSlugs(config);
|
|
392
416
|
const shadowed = configuredNativeAliasSlugs(config);
|
|
393
417
|
// Both user levers, not just the cap: a per-model window set from the dashboard has to show
|
|
@@ -403,11 +427,13 @@ export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "comb
|
|
|
403
427
|
.filter(slug => !shadowed.has(slug)).map(slug => {
|
|
404
428
|
const contextWindow = nativeOpenAiContextWindow(slug, limits);
|
|
405
429
|
const maxInputTokens = nativeOpenAiMaxInputTokens(slug, limits);
|
|
430
|
+
const autoCompactTokenLimit = nativeOpenAiAutoCompactTokenLimit(slug, limits);
|
|
406
431
|
return {
|
|
407
432
|
slug,
|
|
408
433
|
disabled: disabled.has(slug),
|
|
409
434
|
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
410
435
|
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
|
|
436
|
+
...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
|
|
411
437
|
};
|
|
412
438
|
});
|
|
413
439
|
}
|
|
@@ -5,6 +5,8 @@ import { isModelCacheGenerationCurrent } from "../model-cache";
|
|
|
5
5
|
import type { GenerationContext } from "../../lib/state-store-sweeper";
|
|
6
6
|
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
7
7
|
import { assertNotRealHomeUnderTest } from "../../lib/test-home-guard";
|
|
8
|
+
import { CURSOR_STATIC_MODELS } from "../../adapters/cursor/discovery";
|
|
9
|
+
import { cursorModelEffortLadder } from "../../adapters/cursor/effort-map";
|
|
8
10
|
import { generatedModelMetadata, type CatalogModel } from "./parsing";
|
|
9
11
|
|
|
10
12
|
/** Codex strict-parser placeholder; must never be treated as discovered evidence. */
|
|
@@ -470,16 +472,30 @@ export function reconcileModelMetadataSnapshot(context: GenerationContext): numb
|
|
|
470
472
|
|
|
471
473
|
export function registryLayerForModel(provider: string, modelId: string): ModelMetadataLayer | undefined {
|
|
472
474
|
const meta = generatedModelMetadata(provider, modelId);
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
475
|
+
if (meta) {
|
|
476
|
+
return {
|
|
477
|
+
...(typeof meta.contextWindow === "number" && meta.contextWindow > 0
|
|
478
|
+
? { contextWindow: meta.contextWindow }
|
|
479
|
+
: {}),
|
|
480
|
+
...(typeof meta.maxTokens === "number" && meta.maxTokens > 0
|
|
481
|
+
? { maxOutputTokens: meta.maxTokens }
|
|
482
|
+
: {}),
|
|
483
|
+
...(Array.isArray(meta.input) && meta.input.length > 0 ? { inputModalities: [...meta.input] } : {}),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (provider === "cursor") {
|
|
487
|
+
const cursorModel = CURSOR_STATIC_MODELS.find(m => m.id === modelId || m.id.toLowerCase() === modelId.toLowerCase());
|
|
488
|
+
if (cursorModel) {
|
|
489
|
+
return {
|
|
490
|
+
contextWindow: cursorModel.contextWindow,
|
|
491
|
+
...(cursorModel.inputModalities ? { inputModalities: [...cursorModel.inputModalities] } : {}),
|
|
492
|
+
...(cursorModel.supportsReasoningEffort
|
|
493
|
+
? { reasoningEfforts: cursorModelEffortLadder(cursorModel.id) ?? [] }
|
|
494
|
+
: {}),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return undefined;
|
|
483
499
|
}
|
|
484
500
|
|
|
485
501
|
function layerFromCatalogModel(model: CatalogModel): ModelMetadataLayer | undefined {
|
|
@@ -547,12 +563,23 @@ export function enrichCatalogModelMetadata(
|
|
|
547
563
|
&& model.detectedContextWindow > (model.contextWindow ?? 0)
|
|
548
564
|
? { ...fromModel, contextWindow: model.detectedContextWindow }
|
|
549
565
|
: fromModel;
|
|
566
|
+
const registry = registryLayerForModel(model.provider, model.id);
|
|
567
|
+
// Cursor's GetUsableModels RPC returns ids only. Any capability lists already present on a
|
|
568
|
+
// Cursor row therefore come from explicit provider configuration; keep them ahead of the static
|
|
569
|
+
// registry backfill, including an intentional empty reasoning ladder.
|
|
570
|
+
const effectiveRegistry = registry && model.provider === "cursor"
|
|
571
|
+
? {
|
|
572
|
+
...registry,
|
|
573
|
+
...(Array.isArray(model.inputModalities) ? { inputModalities: undefined } : {}),
|
|
574
|
+
...(Array.isArray(model.reasoningEfforts) ? { reasoningEfforts: undefined } : {}),
|
|
575
|
+
}
|
|
576
|
+
: registry;
|
|
550
577
|
const resolved = resolveModelMetadata({
|
|
551
578
|
...(liveFresh && liveLayer ? { live: liveLayer } : {}),
|
|
552
579
|
liveFresh,
|
|
553
580
|
...(snapshot ? { snapshot } : {}),
|
|
554
|
-
...(
|
|
555
|
-
? { registry:
|
|
581
|
+
...(effectiveRegistry
|
|
582
|
+
? { registry: effectiveRegistry }
|
|
556
583
|
: {}),
|
|
557
584
|
...(options?.caps ? { caps: options.caps } : {}),
|
|
558
585
|
});
|