@bitkyc08/opencodex 2.46.0 → 2.47.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 (65) hide show
  1. package/README.md +22 -0
  2. package/SPONSORS.md +105 -0
  3. package/gui/dist/assets/index-B8ZZ0ivI.css +1 -0
  4. package/gui/dist/assets/{index-B72RPblC.js → index-BoXZzNaV.js} +21 -21
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +2 -1
  7. package/src/adapters/cursor/protobuf-request.ts +34 -21
  8. package/src/adapters/cursor/tool-guidance.ts +2 -2
  9. package/src/adapters/cursor/tool-result-normalize.ts +22 -2
  10. package/src/adapters/exec-tool-result-normalize.ts +87 -0
  11. package/src/adapters/kiro.ts +29 -18
  12. package/src/adapters/responses-code-mode.ts +14 -4
  13. package/src/adapters/tool-catalog-nudge.ts +2 -2
  14. package/src/bridge.ts +40 -24
  15. package/src/claude/inbound.ts +21 -6
  16. package/src/claude/outbound.ts +43 -26
  17. package/src/cli/capabilities.ts +27 -0
  18. package/src/cli/integrations.ts +1 -1
  19. package/src/cli/models-runtime-subcommands.ts +2 -0
  20. package/src/cli/models-runtime.ts +108 -1
  21. package/src/cli/observe.ts +18 -3
  22. package/src/cli/usage-report.ts +6 -1
  23. package/src/clients/config-export.ts +5 -3
  24. package/src/codex/auth-api.ts +36 -0
  25. package/src/codex/catalog/provider-fetch.ts +2 -1
  26. package/src/codex/quota-auto-refresh-state.ts +8 -0
  27. package/src/codex/quota-auto-refresh.ts +157 -27
  28. package/src/codex/warmup.ts +5 -0
  29. package/src/config/provider-validation.ts +70 -1
  30. package/src/config.ts +73 -1
  31. package/src/generated/compatibility-version.json +67 -55
  32. package/src/lib/json-byte-size.ts +61 -0
  33. package/src/lib/provider-outbound.ts +11 -10
  34. package/src/oauth/index.ts +42 -8
  35. package/src/oauth/orcarouter.ts +200 -0
  36. package/src/providers/key-store.ts +22 -0
  37. package/src/providers/registry.ts +78 -29
  38. package/src/responses/citation-markers.ts +56 -24
  39. package/src/responses/reasoning-envelope.ts +53 -19
  40. package/src/server/auth-cors.ts +8 -0
  41. package/src/server/chat-completions.ts +7 -0
  42. package/src/server/chat-native.ts +65 -3
  43. package/src/server/claude-messages.ts +30 -10
  44. package/src/server/effort-policy.ts +183 -1
  45. package/src/server/management/agent-settings-routes.ts +50 -9
  46. package/src/server/management/config-routes.ts +2 -0
  47. package/src/server/management/logs-usage-routes.ts +11 -3
  48. package/src/server/management/model-routes.ts +65 -1
  49. package/src/server/management/model-rows.ts +5 -0
  50. package/src/server/management/provider-routes.ts +99 -6
  51. package/src/server/management/route-registry.ts +2 -0
  52. package/src/server/management/usage-aggregate-cache.ts +14 -7
  53. package/src/server/responses/compact.ts +3 -2
  54. package/src/server/responses/core.ts +16 -2
  55. package/src/server/startup-health-cache.ts +31 -9
  56. package/src/types/config.ts +5 -0
  57. package/src/types/provider.ts +4 -0
  58. package/src/usage/cost.ts +27 -31
  59. package/src/usage/summary.ts +52 -9
  60. package/src/usage/time-range.ts +48 -0
  61. package/src/usage/user-cost-overlays.ts +35 -3
  62. package/src/vision/anthropic-describe.ts +46 -2
  63. package/src/web-search/anthropic-executor.ts +49 -3
  64. package/src/web-search/parse.ts +1 -1
  65. package/gui/dist/assets/index-BFgUC17B.css +0 -1
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
3
  import type { CatalogModel } from "../../codex/catalog";
4
4
  import { catalogModelSlug, filterCatalogVisibleModels, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
5
+ import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError } from "../../config/provider-validation";
5
6
  import { captureConfigTopLevelRollback, parsedConfigRebaseDeletionKeys, projectConfigRebaseProvenance } from "../../config/rebase-provenance";
6
7
  import {
7
8
  DEFAULT_SUBAGENT_MODELS,
@@ -16,6 +17,7 @@ import {
16
17
  providerHeadersConfigError,
17
18
  saveConfigPreservingClaudeCode,
18
19
  subagentDefaultSyncEffective,
20
+ validateConfigCandidate,
19
21
  } from "../../config";
20
22
  import {
21
23
  clearLoginState,
@@ -603,24 +605,63 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
603
605
  return jsonResponse({
604
606
  effortCap: config.effortCap ?? null,
605
607
  subagentEffortCap: config.subagentEffortCap ?? null,
608
+ modelPinnedEfforts: config.modelPinnedEfforts ?? {},
606
609
  efforts: CODEX_REASONING_LEVELS.map(l => l.effort),
607
610
  });
608
611
  }
609
612
  if (url.pathname === "/api/effort-caps" && req.method === "PUT") {
610
- let body: { effortCap?: unknown; subagentEffortCap?: unknown };
613
+ let body: unknown;
611
614
  try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
615
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
616
+ return jsonResponse({ error: "effort caps body must be a plain object" }, 400);
617
+ }
618
+ const patch = body as Record<string, unknown>;
612
619
  const { isCodexReasoningEffort } = await import("../../reasoning-effort");
620
+ const draft = { ...projectConfigRebaseProvenance(config) };
621
+ const touched: (keyof OcxConfig)[] = [];
613
622
  for (const key of ["effortCap", "subagentEffortCap"] as const) {
614
- if (!(key in body)) continue;
615
- const value = body[key];
616
- if (value === null || value === "") { deleteConfigTopLevelKey(config, key); continue; }
617
- if (typeof value !== "string" || !isCodexReasoningEffort(value)) {
618
- return jsonResponse({ error: `unknown reasoning effort "${String(value)}"` }, 400);
623
+ if (!Object.hasOwn(patch, key)) continue;
624
+ const value = patch[key];
625
+ if (value === null || value === "") deleteConfigTopLevelKey(draft, key);
626
+ else if (typeof value === "string" && isCodexReasoningEffort(value)) draft[key] = value;
627
+ else return jsonResponse({ error: "caps must be valid reasoning efforts or null" }, 400);
628
+ touched.push(key);
629
+ }
630
+ if (Object.hasOwn(patch, "modelPinnedEfforts")) {
631
+ const error = modelPinnedEffortsConfigError(patch.modelPinnedEfforts, "modelPinnedEfforts", true);
632
+ if (error) return jsonResponse({ error }, 400);
633
+ const pins = mergeModelPinnedEfforts(config.modelPinnedEfforts, patch.modelPinnedEfforts);
634
+ if (pins) draft.modelPinnedEfforts = pins;
635
+ else deleteConfigTopLevelKey(draft, "modelPinnedEfforts");
636
+ touched.push("modelPinnedEfforts");
637
+ }
638
+ const validation = validateConfigCandidate(draft);
639
+ if (!validation.ok) return jsonResponse({ error: validation.error }, 400);
640
+ if (touched.some(key => !Object.hasOwn(draft, key)) && config.configRebaseProvenance !== undefined
641
+ && parsedConfigRebaseDeletionKeys(config) === null) {
642
+ return jsonResponse({ error: "unsupported config deletion provenance" }, 409);
643
+ }
644
+ const projected = projectConfigRebaseProvenance(draft);
645
+ touched.push("configRebaseProvenance");
646
+ const rollback = captureConfigTopLevelRollback(config, touched);
647
+ try {
648
+ for (const key of touched) {
649
+ if (Object.hasOwn(projected, key)) Object.defineProperty(config, key, {
650
+ value: projected[key], writable: true, enumerable: true, configurable: true,
651
+ });
652
+ else deleteConfigTopLevelKey(config, key);
619
653
  }
620
- config[key] = value;
654
+ (deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
655
+ } catch (error) {
656
+ rollback();
657
+ throw error;
621
658
  }
622
- saveConfigPreservingClaudeCode(config);
623
- return jsonResponse({ ok: true, effortCap: config.effortCap ?? null, subagentEffortCap: config.subagentEffortCap ?? null });
659
+ return jsonResponse({
660
+ ok: true,
661
+ effortCap: config.effortCap ?? null,
662
+ subagentEffortCap: config.subagentEffortCap ?? null,
663
+ ...(config.modelPinnedEfforts ? { modelPinnedEfforts: config.modelPinnedEfforts } : {}),
664
+ });
624
665
  }
625
666
 
626
667
  // Featured roster and saved picker order are separate settings. Native Codex advertises
@@ -701,6 +701,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
701
701
  const webSearchCandidates = await webSearchCandidateRows(config);
702
702
  return jsonResponse({
703
703
  webSearch: {
704
+ enabled: ws.enabled !== false,
704
705
  model: ws.model ?? "gpt-5.6-luna",
705
706
  backend: ws.backend,
706
707
  streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
@@ -939,6 +940,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
939
940
  return jsonResponse({
940
941
  ok: true,
941
942
  webSearch: {
943
+ enabled: ws.enabled !== false,
942
944
  model: ws.model ?? "gpt-5.6-luna",
943
945
  backend: ws.backend,
944
946
  streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
@@ -51,6 +51,7 @@ import {
51
51
  usageLogRevisionKey,
52
52
  } from "../../usage/log";
53
53
  import { getUsageDebugLogEntries } from "../../usage/debug";
54
+ import { parseUsageTimeWindow, type UsageTimeWindow } from "../../usage/time-range";
54
55
  import { USAGE_RANGES, USAGE_SURFACES, parseRange, parseUsageSurface, rangeWindow, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
55
56
  import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
56
57
  import { getProviderRegistryEntry } from "../../providers/registry";
@@ -177,6 +178,12 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
177
178
  if (url.pathname === "/api/usage" && req.method === "GET") {
178
179
  const range = parseRange(url.searchParams.get("range"));
179
180
  const surface = parseUsageSurface(url.searchParams.get("surface"));
181
+ let window: UsageTimeWindow | undefined;
182
+ try {
183
+ window = parseUsageTimeWindow(url.searchParams.get("since"), url.searchParams.get("until"));
184
+ } catch (error) {
185
+ return jsonResponse({ error: error instanceof Error ? error.message : "invalid usage time window" }, 400);
186
+ }
180
187
  // A filtered summary must never reach the cache or the warm loop below:
181
188
  // the key is `range:surface`, so a filtered entry stored under it would be
182
189
  // served to the next unfiltered caller, dashboard included.
@@ -185,7 +192,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
185
192
  model: url.searchParams.get("model"),
186
193
  apiKeyId: url.searchParams.get("apiKeyId"),
187
194
  };
188
- const filterRequested = [filter.provider, filter.model, filter.apiKeyId]
195
+ const filterRequested = window !== undefined || [filter.provider, filter.model, filter.apiKeyId]
189
196
  .some(value => typeof value === "string" && value.trim() !== "");
190
197
  const now = Date.now();
191
198
  try {
@@ -211,7 +218,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
211
218
  }
212
219
  if (cached && !filterRequested) discardUsageSummaryCacheEntry(cacheKey);
213
220
  if (filterRequested) {
214
- const filteredAggregate = await getFilteredUsageAggregate(filter);
221
+ const filteredAggregate = await getFilteredUsageAggregate(filter, window);
215
222
  const accumulator = filteredAggregate.accumulator;
216
223
  return jsonResponse({
217
224
  ...accumulator.summarize(range, now, surface),
@@ -289,7 +296,8 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
289
296
  return jsonResponse({
290
297
  range,
291
298
  surface,
292
- since: null,
299
+ since: window?.since ?? null,
300
+ ...(window ? { customWindow: true, until: window.until } : {}),
293
301
  generatedAt: now,
294
302
  summary: {
295
303
  requests: 0,
@@ -85,6 +85,8 @@ import {
85
85
  multiAgentGuidanceEnabled,
86
86
  providerBaseUrlConfigError,
87
87
  providerHeadersConfigError,
88
+ providerModelCostsConfigError,
89
+ sanitizeModelCostsForDisplay,
88
90
  saveConfigPreservingClaudeCode,
89
91
  } from "../../config";
90
92
  import {
@@ -98,6 +100,7 @@ import {
98
100
  } from "../../oauth";
99
101
  import { removeCredential } from "../../oauth/store";
100
102
  import { providerDestinationResolvedError } from "../../lib/destination-policy";
103
+ import { redactSecretString } from "../../lib/redact";
101
104
  import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
102
105
  import { deriveProviderPresets } from "../../providers/derive";
103
106
  import { providerCodexAccountMode } from "../../providers/registry";
@@ -128,7 +131,7 @@ import {
128
131
  setDebugSettings,
129
132
  type DebugFlag,
130
133
  } from "../../lib/debug-settings";
131
- import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
134
+ import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig, ProviderCostOverlay } from "../../types";
132
135
  import { drainAndShutdown } from "../lifecycle";
133
136
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
134
137
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
@@ -369,6 +372,67 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
369
372
  return jsonResponse(await listManagementModelRows(config));
370
373
  }
371
374
 
375
+ const modelCostsMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/model-costs$/);
376
+ if (modelCostsMatch && (req.method === "GET" || req.method === "PUT")) {
377
+ let name: string;
378
+ try { name = decodeURIComponent(modelCostsMatch[1]!); } catch { return jsonResponse({ error: "invalid provider encoding" }, 400); }
379
+ if (!hasOwnProvider(config.providers, name)) {
380
+ return jsonResponse({ error: "provider not found" }, 404, req, config);
381
+ }
382
+ if (req.method === "GET") {
383
+ const provider = config.providers[name]!;
384
+ return jsonResponse({ provider: name, modelCosts: sanitizeModelCostsForDisplay(provider.modelCosts) ?? {} }, 200, req, config);
385
+ }
386
+ let body: unknown;
387
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
388
+ if (!isPlainRecord(body) || !isValidModelDiscoveryModelId(body.modelId)
389
+ || !Object.hasOwn(body, "cost")
390
+ || Object.keys(body).some(key => key !== "modelId" && key !== "cost")) {
391
+ return jsonResponse({ error: "only a valid modelId and cost object or null are allowed" }, 400, req, config);
392
+ }
393
+ const modelId = body.modelId;
394
+ if (redactSecretString(modelId) !== modelId) {
395
+ return jsonResponse({ error: "modelId cannot be displayed safely" }, 400, req, config);
396
+ }
397
+ const submitted = { [modelId]: body.cost };
398
+ const validationError = body.cost === null ? null : providerModelCostsConfigError(submitted);
399
+ if (validationError) return jsonResponse({ error: validationError }, 400, req, config);
400
+ // Copy only validated rate fields; never echo a secret-shaped model key that the
401
+ // shared display boundary suppresses. Model IDs remain exact, including slashes.
402
+ const cost = body.cost === null ? null : sanitizeModelCostsForDisplay(submitted)?.[modelId];
403
+ if (cost === undefined) return jsonResponse({ error: "modelId cannot be displayed safely" }, 400, req, config);
404
+
405
+ // Body parsing yields: a concurrent provider PATCH can replace the row or remove it.
406
+ // Resolve ownership again and keep the merge/save synchronous on the current row.
407
+ if (!hasOwnProvider(config.providers, name)) {
408
+ return jsonResponse({ error: "provider not found" }, 404, req, config);
409
+ }
410
+ const provider = config.providers[name]!;
411
+ const hadModelCosts = Object.hasOwn(provider, "modelCosts");
412
+ const previousModelCosts = provider.modelCosts;
413
+ const nextModelCosts = Object.assign(
414
+ Object.create(null) as Record<string, ProviderCostOverlay>,
415
+ previousModelCosts ?? {},
416
+ );
417
+ if (cost === null) delete nextModelCosts[modelId];
418
+ else nextModelCosts[modelId] = cost;
419
+ const mergedError = providerModelCostsConfigError(nextModelCosts);
420
+ if (mergedError) return jsonResponse({ error: mergedError }, 400, req, config);
421
+ // Keep even an empty map until persistence reconciles individual model keys.
422
+ // Deleting the property would also delete prices another writer added on disk.
423
+ provider.modelCosts = nextModelCosts;
424
+ try {
425
+ // The persistence owner refreshes usage overlays after its atomic write.
426
+ // Price-only edits do not change routing or require catalog convergence.
427
+ persistConfig(config);
428
+ } catch (error) {
429
+ if (hadModelCosts) provider.modelCosts = previousModelCosts;
430
+ else delete provider.modelCosts;
431
+ throw error;
432
+ }
433
+ return jsonResponse({ ok: true, provider: name, modelId, cost }, 200, req, config);
434
+ }
435
+
372
436
  const displayNameMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/model-display-names$/);
373
437
  if (displayNameMatch && req.method === "PUT") {
374
438
  let name: string;
@@ -46,6 +46,7 @@ export type ManagementModelRow = Partial<CatalogModel> & {
46
46
  native?: boolean;
47
47
  custom?: boolean;
48
48
  customId?: string;
49
+ manualPricing?: boolean;
49
50
  fastRowAvailable?: boolean;
50
51
  displayNameOverride?: string;
51
52
  displayNameSource?: "operator" | "provider" | "fallback";
@@ -181,8 +182,12 @@ export async function listManagementModelRows(
181
182
  for (const row of rows) knownIds.add(row.namespaced);
182
183
  return rows.map(row => {
183
184
  const pending = initialModelSelectionPending(config.providers[row.provider]);
185
+ const modelCosts = Object.hasOwn(config.providers, row.provider)
186
+ ? config.providers[row.provider]?.modelCosts : undefined;
184
187
  return {
185
188
  ...row,
189
+ ...(!row.native && modelCosts !== undefined && Object.hasOwn(modelCosts, row.id)
190
+ ? { manualPricing: true } : {}),
186
191
  ...(pending ? { disabled: true, initialSelectionPending: true } : {}),
187
192
  fastRowAvailable: !row.disabled && !pending
188
193
  && !knownIds.has(fastRowId(row.namespaced)) && catalogFastRowEligible(config, row),
@@ -33,6 +33,8 @@ import {
33
33
  submitManualLoginCode,
34
34
  upsertOAuthProvider,
35
35
  } from "../../oauth";
36
+ import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
37
+ import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError, pinnedReasoningEffortConfigError } from "../../config/provider-validation";
36
38
  import { replaceProviderAccountSet } from "../../oauth/store";
37
39
  import { providerDestinationResolvedError } from "../../lib/destination-policy";
38
40
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
@@ -274,6 +276,11 @@ function providerEditorCandidate(
274
276
  if (!validated.ok) {
275
277
  return { ok: false, status: 400, error: validated.error, code: "invalid_provider_editor_config" };
276
278
  }
279
+ for (const [name, provider] of Object.entries(candidate.providers)) {
280
+ if (provider.modelPinnedReasoningEfforts !== undefined) {
281
+ provider.modelPinnedReasoningEfforts = validated.config.providers[name]!.modelPinnedReasoningEfforts;
282
+ }
283
+ }
277
284
  return { ok: true, config: candidate, removedProviders };
278
285
  }
279
286
 
@@ -297,6 +304,27 @@ function adoptProviderEditorCandidate(live: OcxConfig, persisted: OcxConfig): vo
297
304
  else live.modelDiscovery = structuredClone(persisted.modelDiscovery);
298
305
  }
299
306
 
307
+ /** Share pin merge/clear semantics between POST and the PATCH mask. */
308
+ function applyProviderPinFields(
309
+ next: OcxProviderConfig,
310
+ patch: Record<string, unknown>,
311
+ current: OcxProviderConfig | undefined,
312
+ ): string | null {
313
+ const scalarError = pinnedReasoningEffortConfigError(patch.pinnedReasoningEffort, true);
314
+ const mapError = modelPinnedEffortsConfigError(patch.modelPinnedReasoningEfforts, "modelPinnedReasoningEfforts", true);
315
+ if (scalarError || mapError) return scalarError ?? mapError;
316
+ const scalar = Object.hasOwn(patch, "pinnedReasoningEffort")
317
+ ? patch.pinnedReasoningEffort : current?.pinnedReasoningEffort;
318
+ const map = Object.hasOwn(patch, "modelPinnedReasoningEfforts")
319
+ ? mergeModelPinnedEfforts(current?.modelPinnedReasoningEfforts, patch.modelPinnedReasoningEfforts)
320
+ : current?.modelPinnedReasoningEfforts;
321
+ if (scalar === undefined || scalar === null || scalar === "") delete next.pinnedReasoningEffort;
322
+ else next.pinnedReasoningEffort = scalar as string;
323
+ if (map === undefined) delete next.modelPinnedReasoningEfforts;
324
+ else next.modelPinnedReasoningEfforts = { ...map };
325
+ return null;
326
+ }
327
+
300
328
  /**
301
329
  * Apply the recognized PATCH field mask onto a provider copy. The caller runs this once
302
330
  * for validation and again inside the config mutation lock against the newest provider,
@@ -478,6 +506,11 @@ function applyProviderPatchFields(
478
506
  }
479
507
  touched = true;
480
508
  }
509
+ if (Object.hasOwn(rawBody, "pinnedReasoningEffort") || Object.hasOwn(rawBody, "modelPinnedReasoningEfforts")) {
510
+ const error = applyProviderPinFields(next, rawBody, provider);
511
+ if (error) return { error };
512
+ touched = true;
513
+ }
481
514
  if (Object.hasOwn(rawBody, "modelAutoCompactTokenLimits")) {
482
515
  const value = rawBody.modelAutoCompactTokenLimits;
483
516
  const error = modelAutoCompactTokenLimitsConfigError(value, {
@@ -689,6 +722,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
689
722
  models: p.models ?? [],
690
723
  contextWindow: p.contextWindow,
691
724
  modelContextWindows: p.modelContextWindows,
725
+ pinnedReasoningEffort: p.pinnedReasoningEffort,
726
+ modelPinnedReasoningEfforts: p.modelPinnedReasoningEfforts,
692
727
  modelAutoCompactTokenLimits: p.modelAutoCompactTokenLimits,
693
728
  modelSupportsServiceTier: p.modelSupportsServiceTier,
694
729
  noStructuredOutputModels: p.noStructuredOutputModels,
@@ -895,6 +930,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
895
930
  const aliasOwnershipError = providerAliasOverlayOwnershipError(body.provider, existing);
896
931
  if (aliasOwnershipError) return jsonResponse({ error: aliasOwnershipError }, 400);
897
932
  const transportCandidate = providerTransportValidationCandidate(body.provider);
933
+ const pinError = applyProviderPinFields(transportCandidate as unknown as OcxProviderConfig, body.provider, existing);
934
+ if (pinError) return jsonResponse({ error: pinError }, 400);
898
935
  const providerError = providerManagementConfigError(name, transportCandidate)
899
936
  ?? providerEmptyToolOutputConfigError(name, transportCandidate);
900
937
  if (providerError) return jsonResponse({ error: providerError }, 400);
@@ -1021,10 +1058,49 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1021
1058
  prov.xaiResponsesDefaultVersion = latest.xaiResponsesDefaultVersion;
1022
1059
  }
1023
1060
  }
1024
- initializeProviderModelSelection(name, prov, config.providers[name], config);
1025
- config.providers[name] = stripRegistryOnlyStaticHeaders(name, prov);
1026
- if (body.setDefault === true) config.defaultProvider = name;
1027
- save(config);
1061
+ // Reapply pins to the latest live row after DNS/import awaits, then validate the
1062
+ // complete draft before adopting any provider/default state.
1063
+ const latest = config.providers[name];
1064
+ const latestPinError = applyProviderPinFields(prov, body.provider, latest);
1065
+ if (latestPinError) return jsonResponse({ error: latestPinError }, 400);
1066
+ const pinsOwned = Object.hasOwn(body.provider, "pinnedReasoningEffort")
1067
+ || Object.hasOwn(body.provider, "modelPinnedReasoningEfforts")
1068
+ || latest?.pinnedReasoningEffort !== undefined || latest?.modelPinnedReasoningEfforts !== undefined;
1069
+ // New registration also edits discovery/disabled-model state; stage those
1070
+ // side effects with the pin draft instead of mutating live state before validation.
1071
+ const registrationDraft = pinsOwned && !latest ? {
1072
+ ...config,
1073
+ ...(config.modelDiscovery === undefined ? {} : { modelDiscovery: structuredClone(config.modelDiscovery) }),
1074
+ } : undefined;
1075
+ initializeProviderModelSelection(name, prov, latest, registrationDraft ?? config);
1076
+ const candidate = stripRegistryOnlyStaticHeaders(name, prov);
1077
+ if (pinsOwned) {
1078
+ const draft = { ...(registrationDraft ?? config), providers: { ...config.providers, [name]: candidate },
1079
+ ...(body.setDefault === true ? { defaultProvider: name } : {}) };
1080
+ const validation = validateConfigCandidate(draft);
1081
+ if (!validation.ok) return jsonResponse({ error: validation.error }, 400);
1082
+ }
1083
+ const previous = Object.getOwnPropertyDescriptor(config.providers, name);
1084
+ const rollback = pinsOwned ? captureConfigTopLevelRollback(config, ["defaultProvider", "modelDiscovery", "disabledModels"]) : undefined;
1085
+ try {
1086
+ if (registrationDraft) {
1087
+ for (const key of ["modelDiscovery", "disabledModels"] as const) {
1088
+ if (Object.hasOwn(registrationDraft, key)) Object.defineProperty(config, key, {
1089
+ value: registrationDraft[key], writable: true, enumerable: true, configurable: true,
1090
+ });
1091
+ }
1092
+ }
1093
+ config.providers[name] = candidate;
1094
+ if (body.setDefault === true) config.defaultProvider = name;
1095
+ (deps.saveConfigPreservingClaudeCode ?? save)(config);
1096
+ } catch (error) {
1097
+ if (rollback) {
1098
+ if (previous) Object.defineProperty(config.providers, name, previous);
1099
+ else delete config.providers[name];
1100
+ rollback();
1101
+ }
1102
+ throw error;
1103
+ }
1028
1104
  reconcileLiveStateStores();
1029
1105
  if (prov.apiKey && prov.apiKeyPool) {
1030
1106
  const { addProviderApiKey } = await import("../../providers/api-keys");
@@ -1179,8 +1255,25 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1179
1255
  }
1180
1256
  // A PATCH that managed headers owns the resulting block: the clear path restores
1181
1257
  // registry static headers, so exact-match stripping must not erase them again.
1182
- config.providers[name] = replay.headersTouched ? replay.next : stripRegistryOnlyStaticHeaders(name, replay.next);
1183
- saveConfigPreservingClaudeCode(config);
1258
+ const candidate = replay.headersTouched ? replay.next : stripRegistryOnlyStaticHeaders(name, replay.next);
1259
+ const pinsTouched = Object.hasOwn(rawBody, "pinnedReasoningEffort") || Object.hasOwn(rawBody, "modelPinnedReasoningEfforts");
1260
+ if (pinsTouched) {
1261
+ const validation = validateConfigCandidate({ ...config, providers: { ...config.providers, [name]: candidate } });
1262
+ if (!validation.ok) { replayError = validation.error; return; }
1263
+ }
1264
+ const previous = Object.getOwnPropertyDescriptor(config.providers, name);
1265
+ const rollback = pinsTouched ? captureConfigTopLevelRollback(config, []) : undefined;
1266
+ try {
1267
+ config.providers[name] = candidate;
1268
+ (deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
1269
+ } catch (error) {
1270
+ if (rollback) {
1271
+ if (previous) Object.defineProperty(config.providers, name, previous);
1272
+ else delete config.providers[name];
1273
+ rollback();
1274
+ }
1275
+ throw error;
1276
+ }
1184
1277
  });
1185
1278
  if (replayError !== undefined) return jsonResponse({ error: replayError }, 409);
1186
1279
  reconcileLiveStateStores();
@@ -334,6 +334,8 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
334
334
  { method: "GET", path: "/api/request-history/{id}/route-decision", module: "server/management/request-history-routes", mutates: false, mechanism: "ends-with" },
335
335
  { method: "PUT", path: "/api/providers/{provider}/alias", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
336
336
  { method: "PUT", path: "/api/providers/{provider}/model-aliases", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
337
+ { method: "GET", path: "/api/providers/{provider}/model-costs", module: "server/management/model-routes", mutates: false, mechanism: "regex" },
338
+ { method: "PUT", path: "/api/providers/{provider}/model-costs", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
337
339
  { method: "PUT", path: "/api/custom-models/{id}", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
338
340
  { method: "DELETE", path: "/api/custom-models/{id}", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
339
341
  { method: "GET", path: "/api/lab/subjects/{id}", module: "server/management/lab-routes", mutates: false, mechanism: "regex", exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
@@ -14,6 +14,7 @@ import {
14
14
  type UsageSummaryAccumulator,
15
15
  } from "../../usage/summary";
16
16
  import { userCostOverlayVersion } from "../../usage/user-cost-overlays";
17
+ import type { UsageTimeWindow } from "../../usage/time-range";
17
18
 
18
19
  import {
19
20
  cacheApiKeyUsageFromRollup,
@@ -263,7 +264,8 @@ export async function getFilteredUsageAggregate(filter: {
263
264
  provider?: string | null;
264
265
  model?: string | null;
265
266
  apiKeyId?: string | null;
266
- }): Promise<UsageAggregateResult> {
267
+ }, window?: UsageTimeWindow): Promise<UsageAggregateResult> {
268
+ const fixedWindow = window ? Object.freeze({ ...window }) : undefined;
267
269
  const normalizedFilter = {
268
270
  provider: normalizeFilterValue(filter.provider),
269
271
  model: normalizeFilterValue(filter.model),
@@ -273,11 +275,13 @@ export async function getFilteredUsageAggregate(filter: {
273
275
  normalizedFilter.provider,
274
276
  normalizedFilter.model,
275
277
  normalizedFilter.apiKeyId,
278
+ fixedWindow?.since ?? null,
279
+ fixedWindow?.until ?? null,
276
280
  ]);
277
281
  const existing = filteredFlights.get(key);
278
282
  if (existing) return existing;
279
283
 
280
- const flight = refreshFilteredAggregate(key, normalizedFilter);
284
+ const flight = refreshFilteredAggregate(key, normalizedFilter, fixedWindow);
281
285
  filteredFlights.set(key, flight);
282
286
  try {
283
287
  return await flight;
@@ -316,12 +320,13 @@ function publishFilteredAggregate(
316
320
  async function rebuildFilteredAggregate(
317
321
  key: string,
318
322
  filter: NormalizedUsageFilter,
323
+ window?: UsageTimeWindow,
319
324
  ): Promise<UsageAggregateResult> {
320
325
  let lastError: unknown;
321
326
  for (let attempt = 0; attempt < MAX_REBUILD_ATTEMPTS; attempt += 1) {
322
327
  const overlayVersion = userCostOverlayVersion();
323
328
  const timeZone = currentTimeZone();
324
- const accumulator = createUsageSummaryAccumulator({ filter, mode: "row-unique" });
329
+ const accumulator = createUsageSummaryAccumulator({ filter, mode: "row-unique", window });
325
330
  try {
326
331
  const scan = await scanUsageLedgerCooperatively({ onEntry: entry => accumulator.add(entry) });
327
332
  if (scan.oversizedRows > 0) throw new Error("usage ledger contains an oversized row");
@@ -345,6 +350,7 @@ async function appendFilteredAggregate(
345
350
  key: string,
346
351
  state: RetainedUsageAggregate,
347
352
  filter: NormalizedUsageFilter,
353
+ window?: UsageTimeWindow,
348
354
  ): Promise<UsageAggregateResult> {
349
355
  pinnedAggregates.add(state);
350
356
  let rebuildAfterUnpin = false;
@@ -384,28 +390,29 @@ async function appendFilteredAggregate(
384
390
  pinnedAggregates.delete(state);
385
391
  trimRetainedFilteredAggregates();
386
392
  }
387
- if (rebuildAfterUnpin) return rebuildFilteredAggregate(key, filter);
393
+ if (rebuildAfterUnpin) return rebuildFilteredAggregate(key, filter, window);
388
394
  throw new Error("filtered usage append did not settle");
389
395
  }
390
396
 
391
397
  async function refreshFilteredAggregate(
392
398
  key: string,
393
399
  filter: NormalizedUsageFilter,
400
+ window?: UsageTimeWindow,
394
401
  ): Promise<UsageAggregateResult> {
395
402
  const state = retainedFilteredAggregates.get(key);
396
- if (!state) return rebuildFilteredAggregate(key, filter);
403
+ if (!state) return rebuildFilteredAggregate(key, filter, window);
397
404
  const observed = currentUsageLogRevision();
398
405
  const overlayVersion = userCostOverlayVersion();
399
406
  const timeZone = currentTimeZone();
400
407
  if (requiresRebuild(state, observed, overlayVersion, timeZone)) {
401
408
  retainedFilteredAggregates.delete(key);
402
- return rebuildFilteredAggregate(key, filter);
409
+ return rebuildFilteredAggregate(key, filter, window);
403
410
  }
404
411
  if (state.revisionKey === usageLogRevisionKey(observed)) {
405
412
  state.retainedAt = Date.now();
406
413
  return resultFrom(state, "unchanged");
407
414
  }
408
- return appendFilteredAggregate(key, state, filter);
415
+ return appendFilteredAggregate(key, state, filter, window);
409
416
  }
410
417
 
411
418
  export function usageAggregateRetainedStats(): UsageAggregateRetainedStats {
@@ -1094,7 +1094,8 @@ export async function handleResponsesCompact(
1094
1094
  }
1095
1095
  }
1096
1096
  }
1097
- return buffered;
1097
+ // A native compact 404 falls back to a regular Responses compaction turn.
1098
+ if (buffered.status !== 404) return buffered;
1098
1099
  } finally {
1099
1100
  releaseUpstreamHostAdmission(compactHostAdmissionLease);
1100
1101
  releaseCodexAuthContextProbeLease(authCtx);
@@ -1111,7 +1112,7 @@ export async function handleResponsesCompact(
1111
1112
  // the completed event back into the v1 compact JSON contract below. Combo-dispatched
1112
1113
  // turns also go out as SSE: failover can land on a canonical child that rejects a
1113
1114
  // non-streaming turn, and every combo-capable provider already serves streaming traffic.
1114
- stream: accountGatedCompactWireModel || route.combo ? true : false,
1115
+ stream: isCanonicalOpenAiForwardProvider(route.provider) || accountGatedCompactWireModel || route.combo ? true : false,
1115
1116
  input: [...inputItems, { type: "compaction_trigger" }],
1116
1117
  };
1117
1118
  const internalHeaders = new Headers({ "content-type": "application/json" });
@@ -270,7 +270,7 @@ import {
270
270
  upstreamErrorMessageFromPayload,
271
271
  } from "../../lib/errors";
272
272
  import type { AdmissionLease } from "../../lib/admission";
273
- import { supportedLadderFor } from "../effort-policy";
273
+ import { prepareEffortNormalization, supportedLadderFor } from "../effort-policy";
274
274
  import { isThreadSpawnRequest } from "../effort-policy";
275
275
  import {
276
276
  applySubagentModelFallback,
@@ -2280,6 +2280,7 @@ async function applyFinalRouteRequestNormalization(args: {
2280
2280
  inboundTransport?: "websocket";
2281
2281
  }): Promise<void> {
2282
2282
  const { parsed, route, config, req, logCtx, inboundWire, inboundTransport } = args;
2283
+ const effortSelector = prepareEffortNormalization(parsed, route);
2283
2284
 
2284
2285
  // Only Anthropic message routes retain the Codex-facing selector. Other providers must keep
2285
2286
  // their existing response.model contract even when their public and wire model ids differ.
@@ -2304,7 +2305,8 @@ async function applyFinalRouteRequestNormalization(args: {
2304
2305
 
2305
2306
  // Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
2306
2307
  // this request will actually use (#404).
2307
- route.provider = resolveOpenCodeGoTransport(route.provider, sessionLaneIdFromRequest(req.headers));
2308
+ route.provider = resolveOpenCodeGoTransport(route.provider,
2309
+ sessionLaneIdFromRequest(req.headers) ?? normalizeLogConversationId(req.headers.get("x-opencode-session")));
2308
2310
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
2309
2311
  if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
2310
2312
  logCtx.model = route.modelId;
@@ -2402,6 +2404,17 @@ async function applyFinalRouteRequestNormalization(args: {
2402
2404
  }
2403
2405
  }
2404
2406
 
2407
+ {
2408
+ const { applyPinnedEffort } = await import("../effort-policy");
2409
+ const pinned = applyPinnedEffort(parsed, route, config, effortSelector);
2410
+ if (pinned) {
2411
+ logCtx.requestedEffort = pinned.from ? `${pinned.from}->${pinned.to}` : pinned.to;
2412
+ if (isInjectionDebugEnabled()) {
2413
+ injectionDebugLog(`[opencodex] ${route.modelId}: pinned reasoning effort applied (${pinned.from ?? "none"} -> ${pinned.to})`);
2414
+ }
2415
+ }
2416
+ }
2417
+
2405
2418
  {
2406
2419
  const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("../effort-policy");
2407
2420
  const surface = collabSurface(parsed);
@@ -3692,6 +3705,7 @@ async function handleResponsesInner(
3692
3705
  || route.providerName === "github-copilot"
3693
3706
  || route.providerName === "kiro"
3694
3707
  || route.providerName === "google-antigravity"
3708
+ || route.providerName === "orcarouter-oauth"
3695
3709
  ) && route.provider.authMode === "oauth";
3696
3710
  let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
3697
3711
  let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
@@ -50,6 +50,23 @@ export interface StartupHealthCacheDeps {
50
50
  ) => Promise<StartupHealth | null>;
51
51
  }
52
52
 
53
+ /**
54
+ * Return the last completed probe immediately and refresh it in the background.
55
+ *
56
+ * Settings are consumed by several dashboard controls. They must not block on a
57
+ * Windows service-manager probe; the dedicated /api/startup-health route owns
58
+ * the fresh, bounded diagnostic read.
59
+ */
60
+ export function getStartupHealthSnapshot(
61
+ config: Pick<OcxConfig, "codexAutoStart">,
62
+ deps: StartupHealthCacheDeps = {},
63
+ ): StartupHealth {
64
+ const now = deps.now ?? Date.now;
65
+ if (cached && now() - cached.timestamp < CACHE_TTL_MS) return cached.value;
66
+ refreshInBackground(config, deps);
67
+ return cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config);
68
+ }
69
+
53
70
  export function markStartupHealthDiagnosticStale(value: StartupHealth): StartupHealth {
54
71
  if (!value.localRoutingDependency) return { ...value, diagnosticStale: true };
55
72
  return {
@@ -134,15 +151,20 @@ function refreshInBackground(
134
151
  ): void {
135
152
  if (inflight) return;
136
153
  const startedGeneration = generation;
137
- const probe = (deps.probe ?? runProbe)(config).then(value => {
138
- if (startedGeneration === generation) {
139
- cached = { timestamp: (deps.now ?? Date.now)(), value };
140
- }
141
- return value;
142
- });
143
- inflight = probe.finally(() => {
144
- if (inflight === probe || startedGeneration === generation) inflight = null;
145
- });
154
+ const probe: Promise<StartupHealth> = Promise.resolve()
155
+ .then(() => (deps.probe ?? runProbe)(config))
156
+ .then(value => {
157
+ if (startedGeneration === generation) {
158
+ cached = { timestamp: (deps.now ?? Date.now)(), value };
159
+ }
160
+ return value;
161
+ })
162
+ .catch(() => cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config))
163
+ .finally(() => {
164
+ // An invalidated probe must never clear the newer generation's flight.
165
+ if (inflight === probe) inflight = null;
166
+ });
167
+ inflight = probe;
146
168
  }
147
169
 
148
170
  /** Stale-while-revalidate: service-manager probes never hold open a model/UI request. */