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