@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
@@ -55,7 +55,7 @@ import {
55
55
  type PersistedUsageEntry,
56
56
  } from "../../usage/log";
57
57
  import { getUsageDebugLogEntries } from "../../usage/debug";
58
- import { parseRange, parseUsageSurface, rangeWindow, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
58
+ import { USAGE_RANGES, USAGE_SURFACES, parseRange, parseUsageSurface, projectUsageSummary, rangeWindow, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
59
59
  import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
60
60
  import { getProviderRegistryEntry } from "../../providers/registry";
61
61
  import { getDebugLogEntries } from "../../lib/debug-log-buffer";
@@ -197,6 +197,17 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
197
197
  if (url.pathname === "/api/usage" && req.method === "GET") {
198
198
  const range = parseRange(url.searchParams.get("range"));
199
199
  const surface = parseUsageSurface(url.searchParams.get("surface"));
200
+ // Applied to the OUTGOING payload only. A filtered summary must never reach
201
+ // the cache or the warm loop below: the key is `range:surface`, so a
202
+ // filtered entry stored under it would be served to the next unfiltered
203
+ // caller, dashboard included.
204
+ const filter = {
205
+ provider: url.searchParams.get("provider"),
206
+ model: url.searchParams.get("model"),
207
+ };
208
+ const project = <T extends UsageSummary>(summary: T, entries?: PersistedUsageEntry[]) =>
209
+ projectUsageSummary(summary, filter, entries);
210
+ const filterRequested = Boolean(filter.provider ?? filter.model);
200
211
  const now = Date.now();
201
212
  try {
202
213
  const cacheKey = `${range}:${surface}`;
@@ -206,6 +217,12 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
206
217
  const observedSize = observed?.size ?? 0;
207
218
  const cached = getUsageSummaryCacheEntry(cacheKey);
208
219
  if (cached
220
+ // A filtered response is re-summarised from entries, which a cached
221
+ // summary does not carry. Serving it from cache would mean projecting
222
+ // over collapsed breakdown rows again — the exact defect the
223
+ // re-summarisation replaced. The unfiltered cache stays warm either
224
+ // way; only the filtered caller pays for the read.
225
+ && !filterRequested
209
226
  && cached.identityKey === identityKey
210
227
  && cached.maxReadBytes === effectiveReadLimit
211
228
  && cached.overlayVersion === userCostOverlayVersion()
@@ -214,7 +231,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
214
231
  && observedSize >= cached.lastSeenSize) {
215
232
  return jsonResponse(refreshedUsageSummary(cached.summary, range, now));
216
233
  }
217
- if (cached) discardUsageSummaryCacheEntry(cacheKey);
234
+ if (cached && !filterRequested) discardUsageSummaryCacheEntry(cacheKey);
218
235
  // Capture the overlay version BEFORE reading/computing: the cache entry
219
236
  // must be stamped with the version the summary was priced under. Reading
220
237
  // it again at stamp time could cache an old-price summary as current,
@@ -238,14 +255,18 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
238
255
  // summary may mix old and new prices. Serve it uncached: the next
239
256
  // request recomputes against the settled overlay instead of caching a
240
257
  // mixed-price entry under either version.
241
- return jsonResponse(summary);
258
+ return jsonResponse(project(summary, snapshot.entries));
242
259
  }
243
260
  const freshUntil = now + 60_000;
244
261
  const snapshotIdentity = `${usageLogIdentityKey(snapshot.revision)}\0${effectiveReadLimit}`;
245
262
  const revisionKey = `${usageLogRevisionKey(snapshot.revision)}\0${effectiveReadLimit}`;
246
263
  const lastSeenSize = snapshot.revision?.size ?? 0;
247
- const ranges: UsageRange[] = ["7d", "30d", "all"];
248
- const surfaces: UsageSurface[] = ["all", "codex", "claude", "grok"];
264
+ // Derived from the canonical constants rather than re-listed: a subset
265
+ // literal type-checks perfectly happily, so a range added to the union
266
+ // and forgotten here would never be warmed and never invalidated
267
+ // alongside its siblings.
268
+ const ranges: readonly UsageRange[] = USAGE_RANGES;
269
+ const surfaces: readonly UsageSurface[] = USAGE_SURFACES;
249
270
  for (const nextRange of ranges) {
250
271
  for (const nextSurface of surfaces) {
251
272
  const nextSummary = nextRange === range && nextSurface === surface ? summary : {
@@ -279,7 +300,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
279
300
  effectiveReadLimit,
280
301
  now,
281
302
  );
282
- return jsonResponse(summary);
303
+ return jsonResponse(project(summary, snapshot.entries));
283
304
  } catch {
284
305
  return jsonResponse({
285
306
  range,
@@ -394,8 +394,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
394
394
  }
395
395
 
396
396
  if (url.pathname === "/api/custom-models" && req.method === "POST") {
397
- let body: { provider?: unknown; modelId?: unknown; displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown; reasoningEfforts?: unknown; defaultReasoningEffort?: unknown };
398
- try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
397
+ let parsedBody: unknown;
398
+ try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
399
+ if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid JSON body" }, 400);
400
+ const body = parsedBody;
399
401
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
400
402
  const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
401
403
  if (!provider || !modelId) return jsonResponse({ error: "provider and modelId are required" }, 400);
@@ -441,8 +443,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
441
443
  if (customPutMatch && req.method === "PUT") {
442
444
  let id: string;
443
445
  try { id = decodeURIComponent(customPutMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
444
- let body: { displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown; modelId?: unknown; reasoningEfforts?: unknown; defaultReasoningEffort?: unknown };
445
- try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
446
+ let parsedBody: unknown;
447
+ try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
448
+ if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid JSON body" }, 400);
449
+ const body = parsedBody;
446
450
  const list = config.customModels ?? [];
447
451
  const idx = list.findIndex(cm => cm.id === id);
448
452
  if (idx === -1) return jsonResponse({ error: "not found" }, 404);
@@ -63,6 +63,7 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
63
63
  disabled: disabled.has(`${selector}/${slug}`) || disabled.has(slug),
64
64
  contextWindow: undefined,
65
65
  maxInputTokens: undefined,
66
+ autoCompactTokenLimit: undefined,
66
67
  })))
67
68
  : [];
68
69
  const native: ManagementModelRow[] = [...nativeRows, ...accountNativeRows].map(row => {
@@ -82,6 +83,9 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
82
83
  // 1.05M). Dropping it here made /api/models describe a native row as if the whole
83
84
  // window were usable as input, which is the claim the measurement disproved.
84
85
  ...(row.maxInputTokens !== undefined ? { maxInputTokens: row.maxInputTokens } : {}),
86
+ ...(row.autoCompactTokenLimit !== undefined
87
+ ? { autoCompactTokenLimit: row.autoCompactTokenLimit }
88
+ : {}),
85
89
  };
86
90
  });
87
91
  const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
@@ -17,7 +17,8 @@
17
17
  * Design of record: devlog/_fin/260803_integrations_toggle_all/030 (routes),
18
18
  * 011 (Claude Code), 012 (Grok).
19
19
  */
20
- import { loadConfig, readRuntimePort, saveConfigPreservingClaudeCode } from "../../config";
20
+ import { loadConfig, saveConfigPreservingClaudeCode } from "../../config";
21
+ import { readRuntimePort } from "../../config/process-state";
21
22
  import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits, nativeOpenAiContextWindow, visibleNativeSlugs } from "../../codex/catalog";
22
23
  import { providerContextCap } from "../../providers/context-cap";
23
24
  import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
@@ -64,6 +64,7 @@ import type { PersistedUsageAttempt } from "../../usage/log";
64
64
  import { AUTH_MATRIX, isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
65
65
  import { applySystemEnvToggle } from "../system-env";
66
66
  import { buildApiAccessEndpoints } from "./api-access";
67
+ import { isAzureIdentityProvider } from "../../config/provider-validation";
67
68
 
68
69
  import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
69
70
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
@@ -136,7 +137,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
136
137
  // the provider's loopback callback server (inside this process) captures the redirect in the
137
138
  // background, then the credential is persisted. The GUI opens the URL and polls /api/oauth/status.
138
139
  if (url.pathname === "/api/oauth/login" && req.method === "POST") {
139
- const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean };
140
+ const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean; openBrowser?: unknown };
140
141
  const provider = (body.provider ?? "").trim().toLowerCase();
141
142
  if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
142
143
  const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
@@ -167,9 +168,15 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
167
168
  reconcileLiveStateStores();
168
169
  },
169
170
  });
170
- if (authUrl && !deviceCode) {
171
- // Open the browser server-side (the proxy runs on the user's machine) the GUI's
172
- // window.open is popup-blocked because it runs after an await, not a direct click.
171
+ // Open the browser server-side (the proxy runs on the user's machine) — the GUI's
172
+ // window.open is popup-blocked because it runs after an await, not a direct click.
173
+ //
174
+ // The operator can decline, which is the only way to finish a login in a
175
+ // browser profile other than the OS default, or on a different machine
176
+ // than the proxy. Declining changes nothing else: the URL is still
177
+ // returned below and every login surface renders it with a copy button.
178
+ const { shouldOpenBrowserForLogin } = await import("../../oauth/open-browser-choice");
179
+ if (authUrl && !deviceCode && shouldOpenBrowserForLogin(body.openBrowser, config)) {
173
180
  const { openUrl } = await import("../../lib/open-url");
174
181
  openUrl(authUrl);
175
182
  }
@@ -300,6 +307,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
300
307
  const { resetAnthropicRoutingForManualSelection } = await import("../../oauth/anthropic-routing");
301
308
  resetAnthropicRoutingForManualSelection(body.accountId);
302
309
  }
310
+ if (provider === "google-antigravity") {
311
+ const { resetAntigravityRoutingForManualSelection } = await import("../../oauth/antigravity-routing");
312
+ resetAntigravityRoutingForManualSelection(body.accountId);
313
+ }
303
314
  const { clearModelCache } = await import("../../codex/model-cache");
304
315
  const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
305
316
  clearModelCache(provider);
@@ -510,6 +521,11 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
510
521
  clearAnthropicAccountCooldown(id);
511
522
  clearAnthropicSessionAffinityForAccount(id);
512
523
  }
524
+ if (provider === "google-antigravity") {
525
+ const { clearAntigravityAccountCooldown, clearAntigravitySessionAffinityForAccount } = await import("../../oauth/antigravity-routing");
526
+ clearAntigravityAccountCooldown(id);
527
+ clearAntigravitySessionAffinityForAccount(id);
528
+ }
513
529
  if (!getAccountSet(provider)) clearLoginState(provider);
514
530
  const { clearModelCache } = await import("../../codex/model-cache");
515
531
  const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
@@ -527,6 +543,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
527
543
  if (url.pathname === "/api/providers/keys" && req.method === "GET") {
528
544
  const name = (url.searchParams.get("name") ?? "").trim();
529
545
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
546
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
530
547
  const { listProviderApiKeys } = await import("../../providers/api-keys");
531
548
  return jsonResponse(listProviderApiKeys(config, name));
532
549
  }
@@ -534,6 +551,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
534
551
  const body = await readManagementJsonBodyOr(req, {}) as { name?: string; key?: string; label?: string };
535
552
  const name = (body.name ?? "").trim();
536
553
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
554
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
537
555
  if (typeof body.key !== "string" || !body.key.trim()) return jsonResponse({ error: "key is required" }, 400);
538
556
  const { addProviderApiKey } = await import("../../providers/api-keys");
539
557
  const result = addProviderApiKey(config, name, body.key, body.label);
@@ -550,6 +568,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
550
568
  const body = await readManagementJsonBodyOr(req, {}) as { name?: string; id?: string };
551
569
  const name = (body.name ?? "").trim();
552
570
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
571
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
553
572
  if (!body.id) return jsonResponse({ error: "missing id" }, 400);
554
573
  const { setActiveProviderApiKey } = await import("../../providers/api-keys");
555
574
  if (!setActiveProviderApiKey(config, name, body.id)) return jsonResponse({ error: "key not found" }, 404);
@@ -567,6 +586,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
567
586
  const id = typeof body.id === "string" ? body.id.trim() : "";
568
587
  const alias = typeof body.alias === "string" ? body.alias.trim() : "";
569
588
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
589
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
570
590
  if (!id) return jsonResponse({ error: "missing id" }, 400);
571
591
  if (typeof body.alias !== "string" || alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) {
572
592
  return jsonResponse({ error: "alias must be at most 80 printable characters" }, 400);
@@ -579,6 +599,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
579
599
  const name = (url.searchParams.get("name") ?? "").trim();
580
600
  const id = url.searchParams.get("id") ?? "";
581
601
  if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
602
+ if (isAzureIdentityProvider(config.providers[name]!)) return jsonResponse({ error: "provider does not use API-key auth" }, 400);
582
603
  if (!id) return jsonResponse({ error: "missing id" }, 400);
583
604
  const { removeProviderApiKey } = await import("../../providers/api-keys");
584
605
  if (!removeProviderApiKey(config, name, id)) return jsonResponse({ error: "key not found" }, 404);
@@ -1,4 +1,4 @@
1
- import { booleanRecordConfigError } from "../../config";
1
+ import { booleanRecordConfigError } from "../../config/provider-validation";
2
2
  import type { OcxConfig } from "../../types";
3
3
 
4
4
  /**
@@ -31,12 +31,17 @@ import {
31
31
  } from "../../oauth";
32
32
  import { replaceProviderAccountSet } from "../../oauth/store";
33
33
  import { providerDestinationResolvedError } from "../../lib/destination-policy";
34
+ import {
35
+ antigravityOAuthDestinationConfigError,
36
+ getProviderTlsProfileStatus,
37
+ isAntigravityOAuthProvider,
38
+ } from "../../lib/provider-tls-profile";
34
39
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
35
40
  import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound";
36
41
  import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
37
42
  import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
38
43
  import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
39
- import { deriveProviderPresets } from "../../providers/derive";
44
+ import { deriveProviderPresets, providerConfigSeed } from "../../providers/derive";
40
45
  import { effectiveGoogleMode, providerCodexAccountMode, providerMatchesRegistryTransport } from "../../providers/registry";
41
46
  import {
42
47
  extractModelEnvelopeRows,
@@ -54,6 +59,7 @@ import { clearThreadAccountMap } from "../../codex/routing";
54
59
  import { primeCodexPoolQuotas } from "../../codex/auth-api";
55
60
  import { clearModelCache, getProviderDiscoveryStatus } from "../../codex/model-cache";
56
61
  import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
62
+ import { modelAutoCompactTokenLimitsConfigError } from "../../providers/auto-compact-budget";
57
63
  import { resolveCodexHomeDir } from "../../codex/home";
58
64
  import { readUsageEntries } from "../../usage/log";
59
65
  import { getUsageDebugLogEntries } from "../../usage/debug";
@@ -157,6 +163,20 @@ function applyProviderPatchFields(
157
163
  return { error: "authMode must be key, forward, oauth, or local" };
158
164
  }
159
165
  }
166
+ if (Object.hasOwn(rawBody, "azureCredential")) {
167
+ const value = rawBody.azureCredential;
168
+ if (value === null) {
169
+ delete next.azureCredential;
170
+ } else {
171
+ if (!isPlainRecord(value)) return { error: "azureCredential must be an object or null" };
172
+ const credential = structuredClone(value) as Record<string, unknown>;
173
+ if (typeof credential.managedIdentityClientId === "string") {
174
+ credential.managedIdentityClientId = credential.managedIdentityClientId.trim();
175
+ }
176
+ next.azureCredential = credential as OcxProviderConfig["azureCredential"];
177
+ }
178
+ touched = true;
179
+ }
160
180
  if (Object.hasOwn(rawBody, "apiKeyTransport")) {
161
181
  const transport = rawBody.apiKeyTransport;
162
182
  if (transport === "x-api-key" || transport === "bearer") {
@@ -214,6 +234,17 @@ function applyProviderPatchFields(
214
234
  }
215
235
  touched = true;
216
236
  }
237
+ if (Object.hasOwn(rawBody, "tlsProfile")) {
238
+ const value = rawBody.tlsProfile;
239
+ if (value === null || value === "") {
240
+ delete next.tlsProfile;
241
+ } else if (value === "antigravity-browser") {
242
+ next.tlsProfile = value;
243
+ } else {
244
+ return { error: "tlsProfile must be antigravity-browser or null" };
245
+ }
246
+ touched = true;
247
+ }
217
248
  if (Object.hasOwn(rawBody, "upstreamHttpVersion")) {
218
249
  const value = rawBody.upstreamHttpVersion;
219
250
  if (value === null || value === "") {
@@ -266,6 +297,29 @@ function applyProviderPatchFields(
266
297
  }
267
298
  touched = true;
268
299
  }
300
+ if (Object.hasOwn(rawBody, "modelAutoCompactTokenLimits")) {
301
+ const value = rawBody.modelAutoCompactTokenLimits;
302
+ const error = modelAutoCompactTokenLimitsConfigError(value, {
303
+ allowTombstones: true,
304
+ requireNativeIds: name === "openai",
305
+ });
306
+ if (error) return { error };
307
+ if (value === null) {
308
+ delete next.modelAutoCompactTokenLimits;
309
+ } else {
310
+ const budgets: Record<string, number> = Object.assign(
311
+ Object.create(null) as Record<string, number>,
312
+ next.modelAutoCompactTokenLimits ?? {},
313
+ );
314
+ for (const [model, budget] of Object.entries(value as Record<string, number | null>)) {
315
+ if (budget === null) delete budgets[model];
316
+ else budgets[model] = budget;
317
+ }
318
+ if (Object.keys(budgets).length > 0) next.modelAutoCompactTokenLimits = budgets;
319
+ else delete next.modelAutoCompactTokenLimits;
320
+ }
321
+ touched = true;
322
+ }
269
323
  if (Object.hasOwn(rawBody, "modelSupportsServiceTier")) {
270
324
  const value = rawBody.modelSupportsServiceTier;
271
325
  if (value === null) {
@@ -369,6 +423,28 @@ function applyProviderPatchFields(
369
423
  return { next, touched, editorTouched, enablingOpenAi, headersTouched };
370
424
  }
371
425
 
426
+ /** Validate the canonical OpenAI soft-budget overlay against a fresh registry seed. */
427
+ function canonicalOpenAiBudgetPatchError(
428
+ provider: OcxProviderConfig,
429
+ rawBody: Record<string, unknown>,
430
+ keys: string[],
431
+ config: OcxConfig,
432
+ ): string | null {
433
+ if (!isCanonicalOpenAiForwardProvider(provider)) {
434
+ return "provider openai must be the canonical built-in provider";
435
+ }
436
+ const entry = getProviderRegistryEntry("openai");
437
+ if (!entry) return "provider openai registry seed is unavailable";
438
+ const seed = providerConfigSeed(entry);
439
+ if (provider.codexAccountMode !== undefined) seed.codexAccountMode = provider.codexAccountMode;
440
+ if (provider.modelAutoCompactTokenLimits !== undefined) {
441
+ seed.modelAutoCompactTokenLimits = { ...provider.modelAutoCompactTokenLimits };
442
+ }
443
+ const applied = applyProviderPatchFields("openai", seed, rawBody, keys, config);
444
+ if ("error" in applied) return applied.error;
445
+ return providerManagementConfigError("openai", applied.next);
446
+ }
447
+
372
448
  export async function handleProviderRoutes(ctx: ManagementContext): Promise<Response | null> {
373
449
  const { req, url, config, deps, principal, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
374
450
 
@@ -405,11 +481,14 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
405
481
  models: p.models ?? [],
406
482
  contextWindow: p.contextWindow,
407
483
  modelContextWindows: p.modelContextWindows,
484
+ modelAutoCompactTokenLimits: p.modelAutoCompactTokenLimits,
408
485
  modelSupportsServiceTier: p.modelSupportsServiceTier,
409
486
  noStructuredOutputModels: p.noStructuredOutputModels,
410
487
  upstreamHttpVersion: p.upstreamHttpVersion,
411
488
  authMode: p.authMode,
412
489
  apiKeyTransport: p.apiKeyTransport,
490
+ tlsProfile: p.tlsProfile,
491
+ tlsProfileStatus: p.tlsProfile === undefined ? "disabled" : getProviderTlsProfileStatus(name, p),
413
492
  disabled: p.disabled === true,
414
493
  codexAccountMode: providerCodexAccountMode(name, p),
415
494
  ...(name === "xai" ? { xaiResponsesOptInState: xaiResponsesOptInState(p) } : {}),
@@ -536,13 +615,14 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
536
615
  // call can never fire.
537
616
  const submittedContextWindow = Object.hasOwn(prov, "contextWindow");
538
617
  const submittedModelContextWindows = Object.hasOwn(prov, "modelContextWindows");
618
+ const submittedModelAutoCompactTokenLimits = Object.hasOwn(prov, "modelAutoCompactTokenLimits");
539
619
  const submittedRequestPacing = Object.hasOwn(prov, "requestPacing");
540
620
  enrichProviderFromCatalog(name, prov);
541
621
  const { saveConfigPreservingClaudeCode: save } = await import("../../config");
542
622
  // Overwriting an existing provider must not drop its multi-key pool: carry it over, then
543
623
  // let the (possibly new) apiKey join the pool as the active entry.
544
624
  const existingPool = config.providers[name]?.apiKeyPool;
545
- if (existingPool && !prov.apiKeyPool) prov.apiKeyPool = existingPool;
625
+ if (existingPool && !prov.apiKeyPool && !prov.azureCredential) prov.apiKeyPool = existingPool;
546
626
  // The same rule applies to user-configured price overlays: the dashboard's
547
627
  // add/edit form does not send modelCosts, so an overwrite must not silently
548
628
  // erase hand-edited per-model prices from Logs/Usage estimates.
@@ -568,6 +648,11 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
568
648
  ? { ...existing.modelContextWindows, ...(prov.modelContextWindows ?? {}) }
569
649
  : { ...existing.modelContextWindows };
570
650
  }
651
+ if (existing?.modelAutoCompactTokenLimits) {
652
+ prov.modelAutoCompactTokenLimits = submittedModelAutoCompactTokenLimits
653
+ ? { ...existing.modelAutoCompactTokenLimits, ...(prov.modelAutoCompactTokenLimits ?? {}) }
654
+ : { ...existing.modelAutoCompactTokenLimits };
655
+ }
571
656
  config.providers[name] = stripRegistryOnlyStaticHeaders(name, prov);
572
657
  if (body.setDefault === true) config.defaultProvider = name;
573
658
  save(config);
@@ -591,6 +676,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
591
676
  const keys = Object.keys(rawBody);
592
677
  const hasMode = Object.hasOwn(rawBody, "codexAccountMode");
593
678
  const hasSetDefault = Object.hasOwn(rawBody, "setDefault");
679
+ const canonicalBudgetOnly = name === "openai"
680
+ && keys.length === 1
681
+ && keys[0] === "modelAutoCompactTokenLimits";
594
682
 
595
683
  // codexAccountMode keeps its dedicated side-effect path (quota cache clear, thread map
596
684
  // clear, pool prime) and is mutually exclusive with every other patch field.
@@ -653,12 +741,16 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
653
741
 
654
742
  const pacingOnly = keys.every(key => key === "requestPacing");
655
743
  if (applied.editorTouched && !pacingOnly) {
656
- const providerError = providerManagementConfigError(name, next);
744
+ const providerError = canonicalBudgetOnly
745
+ ? canonicalOpenAiBudgetPatchError(next, rawBody, keys, config)
746
+ : providerManagementConfigError(name, next);
657
747
  if (providerError) return jsonResponse({ error: providerError }, 400);
658
- const serviceTierError = providerServiceTierConfigError(name, next);
659
- if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
660
- const resolvedError = await providerDestinationResolvedError(name, next);
661
- if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
748
+ if (!canonicalBudgetOnly) {
749
+ const serviceTierError = providerServiceTierConfigError(name, next);
750
+ if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
751
+ const resolvedError = await providerDestinationResolvedError(name, next);
752
+ if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
753
+ }
662
754
  } else if (applied.enablingOpenAi) {
663
755
  // Same DNS gate as POST: Clash fake-IP only. Never honor a persisted
664
756
  // allowPrivateNetwork on this path — it must not bypass the built-in guard.
@@ -682,15 +774,19 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
682
774
  return;
683
775
  }
684
776
  if (replay.editorTouched && !pacingOnly) {
685
- const syncError = providerManagementConfigError(name, replay.next);
777
+ const syncError = canonicalBudgetOnly
778
+ ? canonicalOpenAiBudgetPatchError(replay.next, rawBody, keys, config)
779
+ : providerManagementConfigError(name, replay.next);
686
780
  if (syncError) {
687
781
  replayError = syncError;
688
782
  return;
689
783
  }
690
- const serviceTierError = providerServiceTierConfigError(name, replay.next);
691
- if (serviceTierError) {
692
- replayError = serviceTierError;
693
- return;
784
+ if (!canonicalBudgetOnly) {
785
+ const serviceTierError = providerServiceTierConfigError(name, replay.next);
786
+ if (serviceTierError) {
787
+ replayError = serviceTierError;
788
+ return;
789
+ }
694
790
  }
695
791
  } else if (replay.enablingOpenAi && !isCanonicalOpenAiForwardProvider(replay.next)) {
696
792
  replayError = "provider openai must be the canonical built-in provider";
@@ -733,6 +829,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
733
829
  if (prov.disabled) {
734
830
  return jsonResponse({ ok: false, error: "Provider is disabled", latencyMs: 0 });
735
831
  }
832
+ const antigravityError = antigravityOAuthDestinationConfigError(name, prov);
833
+ if (antigravityError) return jsonResponse({ ok: false, error: antigravityError, latencyMs: 0 }, 400);
736
834
  if (prov.authMode === "forward") {
737
835
  return jsonResponse({
738
836
  ok: true,
@@ -747,12 +845,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
747
845
  return jsonResponse({ applicable: false, reason: "static_catalog", latencyMs: 0 });
748
846
  }
749
847
  const { buildModelsRequest, getValidAccessTokenSnapshot, resolveModelsAuthToken } = await import("../../oauth");
750
- const antigravity = effectiveGoogleMode(name, prov) === "cloud-code-assist";
848
+ const antigravity = isAntigravityOAuthProvider(name, prov);
751
849
  const snapshot = antigravity
752
850
  ? await getValidAccessTokenSnapshot(name).catch(() => undefined)
753
851
  : undefined;
754
852
  const apiKey = snapshot?.accessToken ?? await resolveModelsAuthToken(name, prov);
755
- if (prov.authMode === "oauth" && !apiKey) {
853
+ if ((prov.authMode === "oauth" || antigravity) && !apiKey) {
756
854
  return jsonResponse({ ok: false, latencyMs: 0, error: "static catalog only — upstream not verified (not logged in)" });
757
855
  }
758
856
  if (prov.adapter === "cursor") {
@@ -776,7 +874,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
776
874
  message: `Connected. ${live.models.length} models.`,
777
875
  });
778
876
  }
779
- const project = prov.project ?? snapshot?.projectId;
877
+ const project = antigravity ? snapshot?.projectId : prov.project;
780
878
  if (antigravity && !project) {
781
879
  return jsonResponse({ ok: false, latencyMs: 0, error: "Antigravity project unavailable — re-run `ocx login google-antigravity`" });
782
880
  }
@@ -188,6 +188,9 @@ function migrateProfileModelReferences(
188
188
  if (config.injectionModel && config.injectionModel === oldPublicModel) {
189
189
  config.injectionModel = newPublicModel;
190
190
  }
191
+ if (config.v2NativeParentOverride?.model && config.v2NativeParentOverride.model === oldPublicModel) {
192
+ config.v2NativeParentOverride = { ...config.v2NativeParentOverride, model: newPublicModel };
193
+ }
191
194
  if (config.shadowCallIntercept?.model && config.shadowCallIntercept.model === oldPublicModel) {
192
195
  config.shadowCallIntercept = { ...config.shadowCallIntercept, model: newPublicModel };
193
196
  }
@@ -33,7 +33,7 @@ import {
33
33
  stopServerListener,
34
34
  } from "../lifecycle";
35
35
  import { isServiceViable } from "../../service";
36
- import { readRuntimePort } from "../../config";
36
+ import { readRuntimePort } from "../../config/process-state";
37
37
  import { withProcessRuntimeProvenance } from "../../lib/bun-runtime";
38
38
  import { selfLaunchArgv } from "../../lib/self-launch-argv";
39
39
  import {
@@ -9,7 +9,7 @@
9
9
  * (non-ocx, non-allowlisted) processes are never killed.
10
10
  */
11
11
  import { execFileSync } from "node:child_process";
12
- import { verifyPidIdentity } from "../config";
12
+ import { verifyPidIdentity } from "../config/process-state";
13
13
  import { isProcessAlive, killProxy } from "../lib/process-control";
14
14
  import { isPortAvailable, type WaitForPortOptions } from "./ports";
15
15
  import { dropWindowsTcpRowsForLocalPort } from "./windows-tcp-drop";
@@ -9,7 +9,8 @@
9
9
  *
10
10
  * Lives outside cli.ts (which dispatches argv at module top level) so tests can import it.
11
11
  */
12
- import { loadConfig, readAlivePid, readRuntimePort, verifyPidIdentity } from "../config";
12
+ import { loadConfig } from "../config";
13
+ import { readAlivePid, readRuntimePort, verifyPidIdentity } from "../config/process-state";
13
14
  import { directLocalHttpFetch } from "./direct-local-http";
14
15
 
15
16
  export interface HealthzIdentity {
@@ -61,6 +61,36 @@ export function sessionIdHeaderFromRequest(headers: Headers): string | null {
61
61
  return headers.get("session_id") ?? headers.get("session-id");
62
62
  }
63
63
 
64
+ function firstSanitizedConversationId(
65
+ ...values: Array<string | null | undefined>
66
+ ): string | undefined {
67
+ for (const value of values) {
68
+ const sanitized = sanitizeConversationIdInput(value);
69
+ if (sanitized) return sanitized;
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ /**
75
+ * Conversation namespace for reasoning replay. Unlike the persisted log id, this stays the raw
76
+ * sanitized identity so mixed headers that carry the same conversation still hit one serving
77
+ * record. Do not hash, and do not prefer session_id over a true per-conversation thread/Cursor
78
+ * identity: session_id can be synthesized from a shared prompt_cache_key.
79
+ */
80
+ export function reasoningReplayConversationIdFromResponsesRequest(input: {
81
+ clientThreadId?: string;
82
+ threadIdHeader?: string | null;
83
+ cursorConversationId?: string;
84
+ sessionIdHeader?: string | null;
85
+ }): string | undefined {
86
+ return firstSanitizedConversationId(
87
+ input.clientThreadId,
88
+ input.threadIdHeader,
89
+ input.cursorConversationId,
90
+ input.sessionIdHeader,
91
+ );
92
+ }
93
+
64
94
  export function conversationIdFromResponsesRequest(input: {
65
95
  clientThreadId?: string;
66
96
  sessionIdHeader?: string | null;
@@ -549,6 +549,7 @@ export function requestLogErrorCode(
549
549
  // Keep the high-confidence message fallback for runtimes/providers that stripped the
550
550
  // structured code before emitting response.failed.
551
551
  if (classifiedCode === CYBER_POLICY_ERROR_CODE) return CYBER_POLICY_ERROR_CODE;
552
+ if (classifiedCode === "insufficient_quota") return classifiedCode;
552
553
  if (status === 400 || status === 409) return "invalid_request_error";
553
554
  if (status === 401) return "invalid_api_key";
554
555
  if (status === 403) {
@@ -1003,6 +1004,26 @@ export function filterRequestLogs(logs: RequestLogEntry[], params: URLSearchPara
1003
1004
  filtered = filtered.filter(entry => entry.provider === provider
1004
1005
  || entry.attempts?.some(attempt => attempt.provider === provider));
1005
1006
  }
1007
+ const model = params.get("model")?.trim().toLowerCase();
1008
+ if (model) {
1009
+ filtered = filtered.filter(entry => entry.model.toLowerCase() === model
1010
+ || entry.resolvedModel?.toLowerCase() === model
1011
+ || entry.attempts?.some(attempt => attempt.model.toLowerCase() === model));
1012
+ }
1013
+ const sinceRaw = params.get("since")?.trim();
1014
+ if (sinceRaw) {
1015
+ const since = Number(sinceRaw);
1016
+ if (Number.isFinite(since)) {
1017
+ filtered = filtered.filter(entry => entry.timestamp >= since);
1018
+ }
1019
+ }
1020
+ const untilRaw = params.get("until")?.trim();
1021
+ if (untilRaw) {
1022
+ const until = Number(untilRaw);
1023
+ if (Number.isFinite(until)) {
1024
+ filtered = filtered.filter(entry => entry.timestamp <= until);
1025
+ }
1026
+ }
1006
1027
  const conversationId = params.get("conversationId")?.trim() || params.get("conversation")?.trim();
1007
1028
  if (conversationId) {
1008
1029
  filtered = filtered.filter(entry => matchesLogConversationId(entry.conversationId, conversationId));
@@ -45,7 +45,7 @@ export function agentTaskRecoveryConfig(config: OcxConfig): AgentTaskRecoveryOpt
45
45
  enabled: true,
46
46
  model: typeof raw.model === "string" && raw.model.trim().length > 0
47
47
  ? raw.model.trim()
48
- : "gpt-5.6-sol",
48
+ : "gpt-5.6-luna",
49
49
  timeoutMs: Number.isFinite(raw.timeoutMs) && (raw.timeoutMs ?? 0) >= 1_000
50
50
  ? Math.min(120_000, Math.floor(raw.timeoutMs!))
51
51
  : 45_000,