@yansigit/opencodex 2.31.2 → 2.32.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/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-BG43zwVe.js +102 -0
- package/gui/dist/assets/index-CiSI-jrP.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +6 -8
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code-project-context.ts +377 -0
- package/src/adapters/command-code.ts +7 -4
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/live-transport.ts +21 -0
- package/src/adapters/cursor/native-exec-bridge.ts +141 -0
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/google-http.ts +12 -2
- package/src/adapters/google-wire-compiler.ts +83 -2
- package/src/adapters/google.ts +62 -21
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-responses.ts +3 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +21 -15
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +24 -11
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +11 -4
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +2 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/catalog/aggregation.ts +13 -1
- package/src/codex/catalog/effort.ts +24 -9
- package/src/codex/catalog/model-metadata.ts +566 -0
- package/src/codex/catalog/parsing.ts +33 -0
- package/src/codex/catalog/provider-fetch.ts +123 -32
- package/src/codex/catalog/sync.ts +5 -4
- package/src/codex/desktop-app-restart.ts +342 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config.ts +77 -812
- package/src/generated/compatibility-version.json +162 -94
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/process-control.ts +2 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +3 -0
- package/src/providers/registry.ts +1 -1
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/custom-tool-compat.ts +132 -38
- package/src/responses/parser.ts +3 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/auth-cors.ts +9 -7
- package/src/server/index.ts +102 -21
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +1 -1
- package/src/server/management/config-routes.ts +4 -1
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -4
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +242 -101
- package/src/server/responses/fetch-helpers.ts +2 -97
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +156 -15
- package/src/service.ts +8 -4
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +12 -1
- package/src/types/tools.ts +87 -11
- package/src/types.ts +1 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/gemini-executor.ts +35 -13
- package/src/web-search/index.ts +85 -1
- package/gui/dist/assets/index-BAMgarF9.js +0 -102
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { atomicWriteFile, getConfigDir } from "../../config";
|
|
4
|
+
import { isModelCacheGenerationCurrent } from "../model-cache";
|
|
5
|
+
import type { GenerationContext } from "../../lib/state-store-sweeper";
|
|
6
|
+
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
7
|
+
import { assertNotRealHomeUnderTest } from "../../lib/test-home-guard";
|
|
8
|
+
import { generatedModelMetadata, type CatalogModel } from "./parsing";
|
|
9
|
+
|
|
10
|
+
/** Codex strict-parser placeholder; must never be treated as discovered evidence. */
|
|
11
|
+
export const COMPATIBILITY_CONTEXT_WINDOW = 128_000;
|
|
12
|
+
|
|
13
|
+
export type ModelMetadataSource =
|
|
14
|
+
| "live"
|
|
15
|
+
| "registry"
|
|
16
|
+
| "snapshot"
|
|
17
|
+
| "config_fallback"
|
|
18
|
+
| "unknown"
|
|
19
|
+
| "derived";
|
|
20
|
+
|
|
21
|
+
export interface ModelMetadataFields {
|
|
22
|
+
contextWindow?: number;
|
|
23
|
+
maxInputTokens?: number;
|
|
24
|
+
maxOutputTokens?: number;
|
|
25
|
+
inputModalities?: string[];
|
|
26
|
+
reasoningEfforts?: string[];
|
|
27
|
+
capabilities?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ModelMetadataFieldSources = {
|
|
31
|
+
contextWindow?: ModelMetadataSource;
|
|
32
|
+
maxInputTokens?: ModelMetadataSource;
|
|
33
|
+
maxOutputTokens?: ModelMetadataSource;
|
|
34
|
+
inputModalities?: ModelMetadataSource;
|
|
35
|
+
reasoningEfforts?: ModelMetadataSource;
|
|
36
|
+
capabilities?: ModelMetadataSource;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface ModelMetadataLayer extends ModelMetadataFields {
|
|
40
|
+
observedAt?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface UserContextCaps {
|
|
44
|
+
/** `providers.*.modelContextWindows[id]` */
|
|
45
|
+
modelWindow?: number;
|
|
46
|
+
/** `providers.*.contextWindow` */
|
|
47
|
+
providerWindow?: number;
|
|
48
|
+
/** `providerContextCaps[provider]` */
|
|
49
|
+
providerCap?: number;
|
|
50
|
+
/** `providers.*.modelMaxInputTokens[id]` */
|
|
51
|
+
maxInputCap?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ResolveModelMetadataInput {
|
|
55
|
+
live?: ModelMetadataLayer;
|
|
56
|
+
liveFresh?: boolean;
|
|
57
|
+
snapshot?: ModelMetadataLayer;
|
|
58
|
+
registry?: ModelMetadataLayer;
|
|
59
|
+
caps?: UserContextCaps;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ResolvedModelMetadata extends ModelMetadataFields {
|
|
63
|
+
source: ModelMetadataSource;
|
|
64
|
+
observedAt?: string;
|
|
65
|
+
stale: boolean;
|
|
66
|
+
contextCapped?: boolean;
|
|
67
|
+
contextCap?: number;
|
|
68
|
+
/** Pre-cap discovered window, used by the Models UI when a cap lowered it. */
|
|
69
|
+
detectedContextWindow?: number;
|
|
70
|
+
fieldSources: ModelMetadataFieldSources;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const NUMERIC_FIELDS = ["contextWindow", "maxInputTokens", "maxOutputTokens"] as const;
|
|
74
|
+
const LIST_FIELDS = ["inputModalities", "reasoningEfforts", "capabilities"] as const;
|
|
75
|
+
|
|
76
|
+
function positiveInt(value: unknown): number | undefined {
|
|
77
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function copyList(value: unknown): string[] | undefined {
|
|
81
|
+
if (!Array.isArray(value) || value.length === 0) return undefined;
|
|
82
|
+
const out = value.filter((item): item is string => typeof item === "string" && item.length > 0);
|
|
83
|
+
return out.length > 0 ? [...out] : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Lowest explicit cap among the three user levers; never used to raise a known window. */
|
|
87
|
+
export function effectiveUserContextCap(caps: UserContextCaps | undefined): number | undefined {
|
|
88
|
+
if (!caps) return undefined;
|
|
89
|
+
const model = positiveInt(caps.modelWindow);
|
|
90
|
+
const provider = positiveInt(caps.providerWindow);
|
|
91
|
+
const providerCap = positiveInt(caps.providerCap);
|
|
92
|
+
if (model !== undefined) return providerCap !== undefined ? Math.min(model, providerCap) : model;
|
|
93
|
+
if (provider !== undefined) return providerCap !== undefined ? Math.min(provider, providerCap) : provider;
|
|
94
|
+
return providerCap;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function layerHasFreshLive(input: ResolveModelMetadataInput): boolean {
|
|
98
|
+
return input.liveFresh === true && input.live !== undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function pickNumeric(
|
|
102
|
+
input: ResolveModelMetadataInput,
|
|
103
|
+
field: (typeof NUMERIC_FIELDS)[number],
|
|
104
|
+
): { value: number; source: ModelMetadataSource; observedAt?: string } | undefined {
|
|
105
|
+
const liveValue = positiveInt(input.live?.[field]);
|
|
106
|
+
if (layerHasFreshLive(input) && liveValue !== undefined) {
|
|
107
|
+
return { value: liveValue, source: "live", observedAt: input.live?.observedAt };
|
|
108
|
+
}
|
|
109
|
+
const snapshotValue = positiveInt(input.snapshot?.[field]);
|
|
110
|
+
const registryValue = positiveInt(input.registry?.[field]);
|
|
111
|
+
if (input.liveFresh === true) {
|
|
112
|
+
if (snapshotValue !== undefined) {
|
|
113
|
+
return { value: snapshotValue, source: "snapshot", observedAt: input.snapshot?.observedAt };
|
|
114
|
+
}
|
|
115
|
+
if (registryValue !== undefined) {
|
|
116
|
+
return { value: registryValue, source: "registry", observedAt: input.registry?.observedAt };
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
if (registryValue !== undefined) {
|
|
121
|
+
return { value: registryValue, source: "registry", observedAt: input.registry?.observedAt };
|
|
122
|
+
}
|
|
123
|
+
if (snapshotValue !== undefined) {
|
|
124
|
+
return { value: snapshotValue, source: "snapshot", observedAt: input.snapshot?.observedAt };
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function pickList(
|
|
130
|
+
input: ResolveModelMetadataInput,
|
|
131
|
+
field: (typeof LIST_FIELDS)[number],
|
|
132
|
+
): { value: string[]; source: ModelMetadataSource; observedAt?: string } | undefined {
|
|
133
|
+
const liveValue = copyList(input.live?.[field]);
|
|
134
|
+
if (layerHasFreshLive(input) && liveValue !== undefined) {
|
|
135
|
+
return { value: liveValue, source: "live", observedAt: input.live?.observedAt };
|
|
136
|
+
}
|
|
137
|
+
const snapshotValue = copyList(input.snapshot?.[field]);
|
|
138
|
+
const registryValue = copyList(input.registry?.[field]);
|
|
139
|
+
if (input.liveFresh === true) {
|
|
140
|
+
if (snapshotValue !== undefined) {
|
|
141
|
+
return { value: snapshotValue, source: "snapshot", observedAt: input.snapshot?.observedAt };
|
|
142
|
+
}
|
|
143
|
+
if (registryValue !== undefined) {
|
|
144
|
+
return { value: registryValue, source: "registry", observedAt: input.registry?.observedAt };
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
if (registryValue !== undefined) {
|
|
149
|
+
return { value: registryValue, source: "registry", observedAt: input.registry?.observedAt };
|
|
150
|
+
}
|
|
151
|
+
if (snapshotValue !== undefined) {
|
|
152
|
+
return { value: snapshotValue, source: "snapshot", observedAt: input.snapshot?.observedAt };
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function clampToCap(value: number, cap: number | undefined): number {
|
|
158
|
+
if (cap === undefined) return value;
|
|
159
|
+
return value > cap ? cap : value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function resolveModelMetadata(input: ResolveModelMetadataInput): ResolvedModelMetadata {
|
|
163
|
+
const cap = effectiveUserContextCap(input.caps);
|
|
164
|
+
const maxInputCap = positiveInt(input.caps?.maxInputCap);
|
|
165
|
+
const fieldSources: ModelMetadataFieldSources = {};
|
|
166
|
+
let observedAt: string | undefined;
|
|
167
|
+
let stale = false;
|
|
168
|
+
|
|
169
|
+
const takeObserved = (layerAt?: string, source?: ModelMetadataSource) => {
|
|
170
|
+
if (source === "snapshot") stale = true;
|
|
171
|
+
if (!observedAt && typeof layerAt === "string" && layerAt.length > 0) observedAt = layerAt;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const contextPick = pickNumeric(input, "contextWindow");
|
|
175
|
+
let contextWindow = contextPick?.value;
|
|
176
|
+
let contextSource = contextPick?.source;
|
|
177
|
+
takeObserved(contextPick?.observedAt, contextPick?.source);
|
|
178
|
+
|
|
179
|
+
let contextCapped = false;
|
|
180
|
+
if (contextWindow !== undefined) {
|
|
181
|
+
const lowered = clampToCap(contextWindow, cap);
|
|
182
|
+
contextCapped = cap !== undefined && lowered < contextPick!.value;
|
|
183
|
+
contextWindow = lowered;
|
|
184
|
+
fieldSources.contextWindow = contextSource;
|
|
185
|
+
} else if (cap !== undefined) {
|
|
186
|
+
contextWindow = cap;
|
|
187
|
+
contextSource = "config_fallback";
|
|
188
|
+
fieldSources.contextWindow = "config_fallback";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const maxInputPick = pickNumeric(input, "maxInputTokens");
|
|
192
|
+
let maxInputTokens = maxInputPick?.value;
|
|
193
|
+
takeObserved(maxInputPick?.observedAt, maxInputPick?.source);
|
|
194
|
+
if (maxInputTokens !== undefined) {
|
|
195
|
+
maxInputTokens = clampToCap(maxInputTokens, maxInputCap);
|
|
196
|
+
if (contextWindow !== undefined) maxInputTokens = Math.min(maxInputTokens, contextWindow);
|
|
197
|
+
fieldSources.maxInputTokens = maxInputPick!.source;
|
|
198
|
+
} else if (maxInputCap !== undefined) {
|
|
199
|
+
maxInputTokens = contextWindow !== undefined ? Math.min(maxInputCap, contextWindow) : maxInputCap;
|
|
200
|
+
fieldSources.maxInputTokens = "config_fallback";
|
|
201
|
+
} else if (contextWindow !== undefined && contextCapped) {
|
|
202
|
+
maxInputTokens = contextWindow;
|
|
203
|
+
fieldSources.maxInputTokens = fieldSources.contextWindow;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const maxOutputPick = pickNumeric(input, "maxOutputTokens");
|
|
207
|
+
if (maxOutputPick) {
|
|
208
|
+
fieldSources.maxOutputTokens = maxOutputPick.source;
|
|
209
|
+
takeObserved(maxOutputPick.observedAt, maxOutputPick.source);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const modalitiesPick = pickList(input, "inputModalities");
|
|
213
|
+
if (modalitiesPick) {
|
|
214
|
+
fieldSources.inputModalities = modalitiesPick.source;
|
|
215
|
+
takeObserved(modalitiesPick.observedAt, modalitiesPick.source);
|
|
216
|
+
}
|
|
217
|
+
const effortsPick = pickList(input, "reasoningEfforts");
|
|
218
|
+
if (effortsPick) {
|
|
219
|
+
fieldSources.reasoningEfforts = effortsPick.source;
|
|
220
|
+
takeObserved(effortsPick.observedAt, effortsPick.source);
|
|
221
|
+
}
|
|
222
|
+
const capabilitiesPick = pickList(input, "capabilities");
|
|
223
|
+
if (capabilitiesPick) {
|
|
224
|
+
fieldSources.capabilities = capabilitiesPick.source;
|
|
225
|
+
takeObserved(capabilitiesPick.observedAt, capabilitiesPick.source);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const source: ModelMetadataSource = contextSource
|
|
229
|
+
?? modalitiesPick?.source
|
|
230
|
+
?? maxOutputPick?.source
|
|
231
|
+
?? "unknown";
|
|
232
|
+
const detectedContextWindow = contextPick?.value;
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
source,
|
|
236
|
+
stale,
|
|
237
|
+
fieldSources,
|
|
238
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
239
|
+
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
|
|
240
|
+
...(maxOutputPick ? { maxOutputTokens: maxOutputPick.value } : {}),
|
|
241
|
+
...(modalitiesPick ? { inputModalities: modalitiesPick.value } : {}),
|
|
242
|
+
...(effortsPick ? { reasoningEfforts: effortsPick.value } : {}),
|
|
243
|
+
...(capabilitiesPick ? { capabilities: capabilitiesPick.value } : {}),
|
|
244
|
+
...(observedAt ? { observedAt } : {}),
|
|
245
|
+
...(cap !== undefined ? { contextCap: cap } : {}),
|
|
246
|
+
...(contextWindow !== undefined ? { contextCapped } : {}),
|
|
247
|
+
...(detectedContextWindow !== undefined ? { detectedContextWindow } : {}),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function shouldStampContextProvenance(
|
|
252
|
+
model: Pick<CatalogModel, "contextWindow" | "metadataSource" | "metadataFieldSources">,
|
|
253
|
+
): boolean {
|
|
254
|
+
if (typeof model.contextWindow !== "number" || model.contextWindow <= 0) return false;
|
|
255
|
+
if (model.metadataSource === "unknown" || model.metadataSource === "config_fallback") return false;
|
|
256
|
+
if (model.metadataFieldSources?.contextWindow === "config_fallback") return false;
|
|
257
|
+
if (model.metadataSource === undefined && model.contextWindow === COMPATIBILITY_CONTEXT_WINDOW) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
return model.metadataSource === "live"
|
|
261
|
+
|| model.metadataSource === "registry"
|
|
262
|
+
|| model.metadataSource === "snapshot"
|
|
263
|
+
|| model.metadataSource === "derived"
|
|
264
|
+
|| (model.metadataSource === undefined && model.contextWindow !== COMPATIBILITY_CONTEXT_WINDOW);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function applyResolvedMetadataToCatalogModel(
|
|
268
|
+
model: CatalogModel,
|
|
269
|
+
resolved: ResolvedModelMetadata,
|
|
270
|
+
): CatalogModel {
|
|
271
|
+
return {
|
|
272
|
+
...model,
|
|
273
|
+
...(resolved.contextWindow !== undefined ? { contextWindow: resolved.contextWindow } : {}),
|
|
274
|
+
...(resolved.maxInputTokens !== undefined ? { maxInputTokens: resolved.maxInputTokens } : {}),
|
|
275
|
+
...(resolved.maxOutputTokens !== undefined ? { maxOutputTokens: resolved.maxOutputTokens } : {}),
|
|
276
|
+
...(resolved.inputModalities ? { inputModalities: resolved.inputModalities } : {}),
|
|
277
|
+
...(resolved.reasoningEfforts ? { reasoningEfforts: resolved.reasoningEfforts } : {}),
|
|
278
|
+
...(resolved.capabilities ? { capabilities: resolved.capabilities } : {}),
|
|
279
|
+
metadataSource: resolved.source,
|
|
280
|
+
metadataStale: resolved.stale,
|
|
281
|
+
...(resolved.observedAt ? { metadataObservedAt: resolved.observedAt } : {}),
|
|
282
|
+
...(resolved.fieldSources ? { metadataFieldSources: resolved.fieldSources } : {}),
|
|
283
|
+
...(resolved.contextCap !== undefined ? { contextCap: resolved.contextCap } : {}),
|
|
284
|
+
...(resolved.contextCapped !== undefined ? { contextCapped: resolved.contextCapped } : {}),
|
|
285
|
+
...(resolved.detectedContextWindow !== undefined
|
|
286
|
+
? { detectedContextWindow: resolved.detectedContextWindow }
|
|
287
|
+
: {}),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export const MODEL_METADATA_CACHE_FILENAME = "model-metadata-cache.json";
|
|
292
|
+
const SNAPSHOT_VERSION = 1;
|
|
293
|
+
const SNAPSHOT_MAX_ROWS = 8_192;
|
|
294
|
+
const SNAPSHOT_ID_MAX = 256;
|
|
295
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
296
|
+
|
|
297
|
+
export type LiveSnapshotRow = ModelMetadataLayer & { id: string };
|
|
298
|
+
|
|
299
|
+
type SnapshotFile = {
|
|
300
|
+
version: number;
|
|
301
|
+
models: Record<string, ModelMetadataLayer>;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
let diskHydrated = false;
|
|
305
|
+
const snapshotRows = new Map<string, ModelMetadataLayer>();
|
|
306
|
+
let lastSnapshotReconciledGeneration = 0;
|
|
307
|
+
|
|
308
|
+
function trimSnapshotRowsToCap(): void {
|
|
309
|
+
if (snapshotRows.size <= SNAPSHOT_MAX_ROWS) return;
|
|
310
|
+
const ranked = [...snapshotRows.entries()].sort((a, b) => {
|
|
311
|
+
const aAt = Date.parse(a[1].observedAt ?? "") || 0;
|
|
312
|
+
const bAt = Date.parse(b[1].observedAt ?? "") || 0;
|
|
313
|
+
return aAt - bAt;
|
|
314
|
+
});
|
|
315
|
+
const drop = ranked.length - SNAPSHOT_MAX_ROWS;
|
|
316
|
+
for (let i = 0; i < drop; i += 1) snapshotRows.delete(ranked[i]![0]);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function snapshotKey(provider: string, modelId: string): string {
|
|
320
|
+
return `${provider}/${modelId}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function sanitizeIdentity(value: unknown): string | undefined {
|
|
324
|
+
if (typeof value !== "string" || value.length === 0 || value.length > SNAPSHOT_ID_MAX) return undefined;
|
|
325
|
+
if (CONTROL_CHARS.test(value)) return undefined;
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function sanitizeLayer(raw: unknown): ModelMetadataLayer | undefined {
|
|
330
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
331
|
+
const row = raw as Record<string, unknown>;
|
|
332
|
+
const contextWindow = positiveInt(row.contextWindow);
|
|
333
|
+
const maxInputTokens = positiveInt(row.maxInputTokens);
|
|
334
|
+
const maxOutputTokens = positiveInt(row.maxOutputTokens);
|
|
335
|
+
const inputModalities = copyList(row.inputModalities);
|
|
336
|
+
const reasoningEfforts = copyList(row.reasoningEfforts);
|
|
337
|
+
const capabilities = copyList(row.capabilities);
|
|
338
|
+
const observedAt = typeof row.observedAt === "string" && !CONTROL_CHARS.test(row.observedAt)
|
|
339
|
+
? row.observedAt.slice(0, 40)
|
|
340
|
+
: undefined;
|
|
341
|
+
if (
|
|
342
|
+
contextWindow === undefined
|
|
343
|
+
&& maxInputTokens === undefined
|
|
344
|
+
&& maxOutputTokens === undefined
|
|
345
|
+
&& !inputModalities
|
|
346
|
+
&& !reasoningEfforts
|
|
347
|
+
&& !capabilities
|
|
348
|
+
) {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
353
|
+
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
|
|
354
|
+
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
355
|
+
...(inputModalities ? { inputModalities } : {}),
|
|
356
|
+
...(reasoningEfforts ? { reasoningEfforts } : {}),
|
|
357
|
+
...(capabilities ? { capabilities } : {}),
|
|
358
|
+
...(observedAt ? { observedAt } : {}),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function snapshotPath(): string {
|
|
363
|
+
return join(getConfigDir(), MODEL_METADATA_CACHE_FILENAME);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function hydrateSnapshotFromDisk(): void {
|
|
367
|
+
if (diskHydrated) return;
|
|
368
|
+
diskHydrated = true;
|
|
369
|
+
try {
|
|
370
|
+
const path = snapshotPath();
|
|
371
|
+
if (!existsSync(path)) return;
|
|
372
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as SnapshotFile;
|
|
373
|
+
if (!parsed || parsed.version !== SNAPSHOT_VERSION || !parsed.models || typeof parsed.models !== "object") {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
for (const [key, value] of Object.entries(parsed.models)) {
|
|
377
|
+
if (typeof key !== "string" || CONTROL_CHARS.test(key) || key.length > SNAPSHOT_ID_MAX * 2) continue;
|
|
378
|
+
const layer = sanitizeLayer(value);
|
|
379
|
+
if (layer) snapshotRows.set(key, layer);
|
|
380
|
+
}
|
|
381
|
+
trimSnapshotRowsToCap();
|
|
382
|
+
} catch {
|
|
383
|
+
// Corrupt cache must never block discovery or catalog sync.
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function persistSnapshotToDisk(): void {
|
|
388
|
+
const dir = getConfigDir();
|
|
389
|
+
assertNotRealHomeUnderTest(dir);
|
|
390
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
391
|
+
trimSnapshotRowsToCap();
|
|
392
|
+
const models: Record<string, ModelMetadataLayer> = {};
|
|
393
|
+
for (const [key, row] of snapshotRows) models[key] = row;
|
|
394
|
+
const body: SnapshotFile = { version: SNAPSHOT_VERSION, models };
|
|
395
|
+
atomicWriteFile(snapshotPath(), `${JSON.stringify(body)}\n`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function persistLiveModelMetadata(
|
|
399
|
+
provider: string,
|
|
400
|
+
rows: LiveSnapshotRow[],
|
|
401
|
+
options?: { writerGeneration?: string | number; observedAt?: string },
|
|
402
|
+
): void {
|
|
403
|
+
if (options?.writerGeneration !== undefined) {
|
|
404
|
+
if (typeof options.writerGeneration === "string") {
|
|
405
|
+
if (!isModelCacheGenerationCurrent(provider, options.writerGeneration)) return;
|
|
406
|
+
} else if (options.writerGeneration < captureConfigGeneration()) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const providerId = sanitizeIdentity(provider);
|
|
411
|
+
if (!providerId) return;
|
|
412
|
+
hydrateSnapshotFromDisk();
|
|
413
|
+
const observedAt = options?.observedAt ?? new Date().toISOString();
|
|
414
|
+
let changed = false;
|
|
415
|
+
for (const row of rows) {
|
|
416
|
+
const id = sanitizeIdentity(row.id);
|
|
417
|
+
if (!id) continue;
|
|
418
|
+
const layer = sanitizeLayer({ ...row, observedAt: row.observedAt ?? observedAt });
|
|
419
|
+
if (!layer) continue;
|
|
420
|
+
snapshotRows.set(snapshotKey(providerId, id), layer);
|
|
421
|
+
changed = true;
|
|
422
|
+
}
|
|
423
|
+
if (!changed) return;
|
|
424
|
+
try {
|
|
425
|
+
persistSnapshotToDisk();
|
|
426
|
+
} catch {
|
|
427
|
+
// Best-effort persistence only.
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function readSnapshotLayer(provider: string, modelId: string): ModelMetadataLayer | undefined {
|
|
432
|
+
hydrateSnapshotFromDisk();
|
|
433
|
+
return snapshotRows.get(snapshotKey(provider, modelId));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function resetModelMetadataCacheForTests(options?: { hydrateFromDisk?: boolean }): void {
|
|
437
|
+
snapshotRows.clear();
|
|
438
|
+
diskHydrated = false;
|
|
439
|
+
lastSnapshotReconciledGeneration = 0;
|
|
440
|
+
if (options?.hydrateFromDisk) hydrateSnapshotFromDisk();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function reconcileModelMetadataSnapshot(context: GenerationContext): number {
|
|
444
|
+
if (context.generation <= lastSnapshotReconciledGeneration) return 0;
|
|
445
|
+
hydrateSnapshotFromDisk();
|
|
446
|
+
let removed = 0;
|
|
447
|
+
for (const key of [...snapshotRows.keys()]) {
|
|
448
|
+
const slash = key.indexOf("/");
|
|
449
|
+
if (slash <= 0) {
|
|
450
|
+
snapshotRows.delete(key);
|
|
451
|
+
removed += 1;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const provider = key.slice(0, slash);
|
|
455
|
+
if (!context.providerNames.has(provider)) {
|
|
456
|
+
snapshotRows.delete(key);
|
|
457
|
+
removed += 1;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (removed > 0) {
|
|
461
|
+
try {
|
|
462
|
+
persistSnapshotToDisk();
|
|
463
|
+
} catch {
|
|
464
|
+
// Best-effort persistence only.
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
lastSnapshotReconciledGeneration = context.generation;
|
|
468
|
+
return removed;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function registryLayerForModel(provider: string, modelId: string): ModelMetadataLayer | undefined {
|
|
472
|
+
const meta = generatedModelMetadata(provider, modelId);
|
|
473
|
+
if (!meta) return undefined;
|
|
474
|
+
return {
|
|
475
|
+
...(typeof meta.contextWindow === "number" && meta.contextWindow > 0
|
|
476
|
+
? { contextWindow: meta.contextWindow }
|
|
477
|
+
: {}),
|
|
478
|
+
...(typeof meta.maxTokens === "number" && meta.maxTokens > 0
|
|
479
|
+
? { maxOutputTokens: meta.maxTokens }
|
|
480
|
+
: {}),
|
|
481
|
+
...(Array.isArray(meta.input) && meta.input.length > 0 ? { inputModalities: [...meta.input] } : {}),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function layerFromCatalogModel(model: CatalogModel): ModelMetadataLayer | undefined {
|
|
486
|
+
return sanitizeLayer({
|
|
487
|
+
contextWindow: model.contextWindow,
|
|
488
|
+
maxInputTokens: model.maxInputTokens,
|
|
489
|
+
maxOutputTokens: model.maxOutputTokens,
|
|
490
|
+
inputModalities: model.inputModalities,
|
|
491
|
+
reasoningEfforts: model.reasoningEfforts,
|
|
492
|
+
capabilities: model.capabilities,
|
|
493
|
+
observedAt: model.metadataObservedAt,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function capsFromProvider(
|
|
498
|
+
provider: { contextWindow?: number; modelContextWindows?: Record<string, number>; modelMaxInputTokens?: Record<string, number> } | undefined,
|
|
499
|
+
modelId: string,
|
|
500
|
+
providerCap?: number,
|
|
501
|
+
): UserContextCaps | undefined {
|
|
502
|
+
if (!provider && providerCap === undefined) return undefined;
|
|
503
|
+
const modelWindow = provider?.modelContextWindows?.[modelId];
|
|
504
|
+
const maxInputCap = provider?.modelMaxInputTokens?.[modelId];
|
|
505
|
+
return {
|
|
506
|
+
...(typeof modelWindow === "number" ? { modelWindow } : {}),
|
|
507
|
+
...(typeof provider?.contextWindow === "number" ? { providerWindow: provider.contextWindow } : {}),
|
|
508
|
+
...(providerCap !== undefined ? { providerCap } : {}),
|
|
509
|
+
...(typeof maxInputCap === "number" ? { maxInputCap } : {}),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function mergeLayers(
|
|
514
|
+
preferred?: ModelMetadataLayer,
|
|
515
|
+
fallback?: ModelMetadataLayer,
|
|
516
|
+
): ModelMetadataLayer | undefined {
|
|
517
|
+
if (!preferred && !fallback) return undefined;
|
|
518
|
+
return {
|
|
519
|
+
...fallback,
|
|
520
|
+
...preferred,
|
|
521
|
+
observedAt: preferred?.observedAt ?? fallback?.observedAt,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function enrichCatalogModelMetadata(
|
|
526
|
+
model: CatalogModel,
|
|
527
|
+
options?: {
|
|
528
|
+
caps?: UserContextCaps;
|
|
529
|
+
liveFresh?: boolean;
|
|
530
|
+
snapshot?: ModelMetadataLayer;
|
|
531
|
+
},
|
|
532
|
+
): CatalogModel {
|
|
533
|
+
// Combo rows already min() member evidence; re-resolving would relabel them as snapshot.
|
|
534
|
+
if (model.metadataSource === "derived") return model;
|
|
535
|
+
const disk = options?.snapshot ?? readSnapshotLayer(model.provider, model.id);
|
|
536
|
+
const fromModel = layerFromCatalogModel(model);
|
|
537
|
+
const liveFresh = options?.liveFresh ?? (model.metadataSource === "live" && model.metadataStale !== true);
|
|
538
|
+
// A cached row may already contain the operator cap. Keep the raw last-known-good
|
|
539
|
+
// snapshot as the evidence layer when rebuilding it after a failed refresh; otherwise
|
|
540
|
+
// the capped value overwrites its own detected window on every convergence pass.
|
|
541
|
+
const staleModelPreferred = model.contextCapped !== true;
|
|
542
|
+
const snapshot = liveFresh
|
|
543
|
+
? disk
|
|
544
|
+
: (staleModelPreferred ? mergeLayers(fromModel, disk) : mergeLayers(disk, fromModel));
|
|
545
|
+
const liveLayer = liveFresh && fromModel && model.contextCapped === true
|
|
546
|
+
&& typeof model.detectedContextWindow === "number"
|
|
547
|
+
&& model.detectedContextWindow > (model.contextWindow ?? 0)
|
|
548
|
+
? { ...fromModel, contextWindow: model.detectedContextWindow }
|
|
549
|
+
: fromModel;
|
|
550
|
+
const resolved = resolveModelMetadata({
|
|
551
|
+
...(liveFresh && liveLayer ? { live: liveLayer } : {}),
|
|
552
|
+
liveFresh,
|
|
553
|
+
...(snapshot ? { snapshot } : {}),
|
|
554
|
+
...(registryLayerForModel(model.provider, model.id)
|
|
555
|
+
? { registry: registryLayerForModel(model.provider, model.id) }
|
|
556
|
+
: {}),
|
|
557
|
+
...(options?.caps ? { caps: options.caps } : {}),
|
|
558
|
+
});
|
|
559
|
+
const enriched = applyResolvedMetadataToCatalogModel(model, resolved);
|
|
560
|
+
// A provider-cap marker can be lost when a later convergence pass resolves the
|
|
561
|
+
// already-capped value as its new baseline. Preserve the marker from the prior
|
|
562
|
+
// pass; explicit smaller model windows are never marked by applyProviderConfigHints.
|
|
563
|
+
return model.contextCapped === true && enriched.contextWindow === model.contextWindow
|
|
564
|
+
? { ...enriched, contextCapped: true }
|
|
565
|
+
: enriched;
|
|
566
|
+
}
|
|
@@ -111,8 +111,23 @@ export interface CatalogModel {
|
|
|
111
111
|
defaultReasoningEffort?: string;
|
|
112
112
|
contextWindow?: number;
|
|
113
113
|
maxInputTokens?: number;
|
|
114
|
+
/** Generated `maxTokens` and live output limits; never treated as context. */
|
|
115
|
+
maxOutputTokens?: number;
|
|
114
116
|
contextCap?: number;
|
|
115
117
|
contextCapped?: boolean;
|
|
118
|
+
/** Pre-cap discovered window for Models UI copy when a cap lowered it. */
|
|
119
|
+
detectedContextWindow?: number;
|
|
120
|
+
metadataSource?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
|
|
121
|
+
metadataObservedAt?: string;
|
|
122
|
+
metadataStale?: boolean;
|
|
123
|
+
metadataFieldSources?: {
|
|
124
|
+
contextWindow?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
|
|
125
|
+
maxInputTokens?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
|
|
126
|
+
maxOutputTokens?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
|
|
127
|
+
inputModalities?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
|
|
128
|
+
reasoningEfforts?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
|
|
129
|
+
capabilities?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
|
|
130
|
+
};
|
|
116
131
|
inputModalities?: string[];
|
|
117
132
|
/** Provider opted into parallel tool calls (OcxProviderConfig.parallelToolCalls). */
|
|
118
133
|
parallelToolCalls?: boolean;
|
|
@@ -149,6 +164,9 @@ export const JAWCODE_CATALOG_AUGMENT_PROVIDERS = new Set(["opencode-go", "deepse
|
|
|
149
164
|
export const ROUTED_MODEL_COMPATIBILITY_EXCLUSIONS = new Set([
|
|
150
165
|
// Issue #82: Zen Go /models advertises HY3, but Console Go rejects it as outside the lite list.
|
|
151
166
|
"opencode-go/hy3-preview",
|
|
167
|
+
// Issue #2330: OpenCode Go models absent from current documentation or returning terminal HTTP 400 errors.
|
|
168
|
+
"opencode-go/mimo-v2-omni",
|
|
169
|
+
"opencode-go/mimo-v2-pro",
|
|
152
170
|
]);
|
|
153
171
|
|
|
154
172
|
export function isRoutedModelCompatibilityExcluded(slug: string): boolean {
|
|
@@ -191,6 +209,8 @@ export function shouldExposeRoutedModel(model: CatalogModel): boolean {
|
|
|
191
209
|
}
|
|
192
210
|
|
|
193
211
|
export function readCodexCatalogPath(): string {
|
|
212
|
+
const home = activeCodexHome();
|
|
213
|
+
if (home) return readCodexCatalogPathForHome(home);
|
|
194
214
|
try {
|
|
195
215
|
const configPath = activeCodexConfigPath();
|
|
196
216
|
if (existsSync(configPath)) {
|
|
@@ -202,6 +222,19 @@ export function readCodexCatalogPath(): string {
|
|
|
202
222
|
return activeDefaultCatalogPath();
|
|
203
223
|
}
|
|
204
224
|
|
|
225
|
+
/** Resolve the configured catalog without consulting ambient CODEX_HOME again. */
|
|
226
|
+
export function readCodexCatalogPathForHome(codexHome: string): string {
|
|
227
|
+
try {
|
|
228
|
+
const configPath = join(codexHome, "config.toml");
|
|
229
|
+
if (existsSync(configPath)) {
|
|
230
|
+
const toml = readFileSync(configPath, "utf-8");
|
|
231
|
+
const path = readRootTomlString(toml, "model_catalog_json");
|
|
232
|
+
if (path) return resolve(codexHome, path);
|
|
233
|
+
}
|
|
234
|
+
} catch { /* ignore */ }
|
|
235
|
+
return join(codexHome, "opencodex-catalog.json");
|
|
236
|
+
}
|
|
237
|
+
|
|
205
238
|
export function parseCatalogJson(raw: string): RawCatalog | null {
|
|
206
239
|
try {
|
|
207
240
|
const cat = JSON.parse(raw);
|