@bitkyc08/opencodex 2.18.2 → 2.20.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 (131) hide show
  1. package/gui/dist/assets/index-DF_UFrGS.css +1 -0
  2. package/gui/dist/assets/index-DSK3S5HY.js +76 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/client-fingerprint.ts +14 -10
  6. package/src/adapters/google-antigravity-wire.ts +4 -3
  7. package/src/adapters/google.ts +1 -1
  8. package/src/adapters/mimo-free.ts +17 -0
  9. package/src/adapters/openai-chat.ts +367 -32
  10. package/src/adapters/registry.ts +144 -0
  11. package/src/chat/inbound.ts +37 -8
  12. package/src/cli/claude.ts +2 -1
  13. package/src/cli/combo.ts +3 -0
  14. package/src/cli/dispatch.ts +8 -0
  15. package/src/cli/export-command.ts +2 -2
  16. package/src/cli/help.ts +2 -0
  17. package/src/cli/index.ts +3 -2
  18. package/src/cli/lab.ts +135 -1
  19. package/src/cli/minimax.ts +491 -0
  20. package/src/cli/models-runtime.ts +22 -1
  21. package/src/cli/models.ts +67 -2
  22. package/src/cli/opencode.ts +2 -1
  23. package/src/cli/registry.ts +22 -2
  24. package/src/clients/config-export.ts +125 -7
  25. package/src/codex/app-server-processes.ts +57 -2
  26. package/src/codex/app-server-restart-service.ts +232 -0
  27. package/src/codex/catalog/aggregation.ts +10 -1
  28. package/src/codex/catalog/effort.ts +15 -3
  29. package/src/codex/catalog/parsing.ts +3 -1
  30. package/src/codex/catalog/provider-fetch.ts +45 -5
  31. package/src/codex/catalog/sync.ts +74 -4
  32. package/src/codex/convergence.ts +2 -0
  33. package/src/codex/shim.ts +100 -5
  34. package/src/combos/index.ts +1 -0
  35. package/src/combos/request.ts +30 -0
  36. package/src/combos/types.ts +6 -0
  37. package/src/config.ts +52 -0
  38. package/src/generated/compatibility-version.json +239 -87
  39. package/src/images/loop.ts +11 -1
  40. package/src/integrations/registry.ts +7 -0
  41. package/src/lab/conformance/jcs.ts +42 -2
  42. package/src/lab/conformance/negative-controls.ts +6 -2
  43. package/src/lab/conformance/runner.ts +16 -5
  44. package/src/lab/fabric/observe.ts +49 -14
  45. package/src/lab/index.ts +16 -0
  46. package/src/lab/ledger/purge.ts +152 -83
  47. package/src/lab/ledger/store.ts +168 -54
  48. package/src/lab/observe/from-conformance.ts +8 -6
  49. package/src/lab/observe/from-live.ts +8 -2
  50. package/src/lab/paths.ts +23 -0
  51. package/src/lab/public/bundle.ts +217 -0
  52. package/src/lab/public/community-authority.ts +175 -0
  53. package/src/lab/public/community-files.ts +29 -0
  54. package/src/lab/public/community.ts +479 -0
  55. package/src/lab/public/file-safety.ts +155 -0
  56. package/src/lab/public/ids.ts +26 -0
  57. package/src/lab/public/index.ts +16 -0
  58. package/src/lab/public/mutation-lock.ts +424 -0
  59. package/src/lab/public/operator.ts +353 -0
  60. package/src/lab/public/origin-purge.ts +79 -0
  61. package/src/lab/public/origin.ts +203 -0
  62. package/src/lab/public/privacy.ts +143 -0
  63. package/src/lab/public/private-file.ts +261 -0
  64. package/src/lab/public/project.ts +124 -0
  65. package/src/lab/public/purge-test-fault.ts +21 -0
  66. package/src/lab/public/purge.ts +223 -0
  67. package/src/lab/public/registry.ts +44 -0
  68. package/src/lab/public/revocation.ts +252 -0
  69. package/src/lab/public/signature.ts +219 -0
  70. package/src/lab/public/storage.ts +105 -0
  71. package/src/lab/public/strict-json.ts +206 -0
  72. package/src/lab/public/time.ts +26 -0
  73. package/src/lab/public/types.ts +172 -0
  74. package/src/lab/public/validate.ts +391 -0
  75. package/src/lib/codex-restart-contract.ts +120 -0
  76. package/src/lib/errors.ts +27 -0
  77. package/src/lib/lab-activation.ts +109 -47
  78. package/src/lib/lab-live-pinned-sender.ts +16 -5
  79. package/src/lib/pinned-http.ts +70 -16
  80. package/src/lib/self-launch-argv.ts +15 -0
  81. package/src/lib/state-store-registrations.ts +2 -0
  82. package/src/lib/upstream-reachability.ts +4 -0
  83. package/src/lib/windows-elevation.ts +10 -1
  84. package/src/providers/derive.ts +24 -4
  85. package/src/providers/registry.ts +9 -3
  86. package/src/providers/request-pacing.ts +310 -0
  87. package/src/providers/service-tier.ts +143 -0
  88. package/src/providers/static-model-discovery.ts +86 -0
  89. package/src/reasoning-effort.ts +27 -1
  90. package/src/responses/spill-store.ts +20 -1
  91. package/src/responses/state.ts +159 -3
  92. package/src/router.ts +23 -6
  93. package/src/routing/capability.ts +4 -2
  94. package/src/routing/compatibility/behavior.ts +5 -1
  95. package/src/server/adapter-resolve.ts +2 -32
  96. package/src/server/auth-cors.ts +8 -0
  97. package/src/server/chat-completions.ts +74 -34
  98. package/src/server/chat-native-sse.ts +331 -0
  99. package/src/server/chat-native.ts +371 -0
  100. package/src/server/effort-policy.ts +18 -0
  101. package/src/server/management/combo-routes.ts +16 -2
  102. package/src/server/management/config-routes.ts +6 -4
  103. package/src/server/management/context.ts +17 -0
  104. package/src/server/management/lab-routes.ts +181 -19
  105. package/src/server/management/model-routes.ts +76 -2
  106. package/src/server/management/model-rows.ts +8 -0
  107. package/src/server/management/provider-capability-config.ts +48 -0
  108. package/src/server/management/provider-routes.ts +76 -4
  109. package/src/server/management/system-restart.ts +4 -2
  110. package/src/server/management/system-routes.ts +38 -0
  111. package/src/server/relay.ts +17 -3
  112. package/src/server/responses/compact.ts +4 -1
  113. package/src/server/responses/core.ts +278 -48
  114. package/src/server/responses/empty-completion-guard.ts +276 -0
  115. package/src/server/responses/fetch-helpers.ts +35 -4
  116. package/src/server/responses/input-admission.ts +169 -0
  117. package/src/server/responses/pacing-overload.ts +13 -0
  118. package/src/server/responses/policy-fallback.ts +16 -2
  119. package/src/server/responses/terminal-guard.ts +1 -1
  120. package/src/server/responses/upstream-error.ts +5 -0
  121. package/src/server/responses.ts +17 -2
  122. package/src/service-manager-probe.ts +2 -3
  123. package/src/types.ts +76 -6
  124. package/src/update/index.ts +6 -5
  125. package/src/update/job.ts +5 -6
  126. package/src/update/notify.ts +5 -3
  127. package/src/usage/log.ts +11 -1
  128. package/src/vision/index.ts +21 -4
  129. package/src/web-search/index.ts +2 -1
  130. package/gui/dist/assets/index-CXI1262_.js +0 -76
  131. package/gui/dist/assets/index-DUCH59lJ.css +0 -1
@@ -98,6 +98,24 @@ export function effortCapAppliesTo(
98
98
  * validates against that backend. A custom responses provider (key mode) serving a
99
99
  * native-looking bare id must NOT inherit the unrelated native ladder.
100
100
  */
101
+ /**
102
+ * Empty ladders mean "no effort control", not "this model cannot emit reasoning".
103
+ * Drop only `effort` so a Chat Completions `include_reasoning` / `reasoning.summary`
104
+ * request still reaches parseRequest and is not hidden by hideThinkingSummary.
105
+ */
106
+ export function stripEmptyLadderEffort(
107
+ reasoning: unknown,
108
+ ladder: readonly string[] | undefined,
109
+ ): unknown {
110
+ if (ladder === undefined || ladder.length > 0) return reasoning;
111
+ if (reasoning === undefined || reasoning === null || typeof reasoning !== "object" || Array.isArray(reasoning)) {
112
+ return reasoning;
113
+ }
114
+ const next = { ...(reasoning as Record<string, unknown>) };
115
+ delete next.effort;
116
+ return Object.keys(next).length > 0 ? next : undefined;
117
+ }
118
+
101
119
  export function supportedLadderFor(route: { provider: OcxProviderConfig; modelId: string }): string[] | undefined {
102
120
  const { provider, modelId } = route;
103
121
  if (modelInList(provider.noReasoningModels, modelId)) return [];
@@ -65,6 +65,16 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
65
65
  import type { ManagementContext } from "./context";
66
66
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
67
67
 
68
+
69
+ /** Management wire shape: omit default imageInput "auto" (persist/response sparse). */
70
+ function sparseComboConfig<T extends { imageInput?: "auto" | "disabled" }>(combo: T): Omit<T, "imageInput"> & { imageInput?: "disabled" } {
71
+ const { imageInput, ...rest } = combo;
72
+ return {
73
+ ...rest,
74
+ ...(imageInput === "disabled" ? { imageInput: "disabled" as const } : {}),
75
+ };
76
+ }
77
+
68
78
  export async function handleComboRoutes(ctx: ManagementContext): Promise<Response | null> {
69
79
  const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
70
80
 
@@ -75,7 +85,7 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
75
85
  return {
76
86
  id,
77
87
  model: comboPublicModelId(id, combo),
78
- ...combo,
88
+ ...sparseComboConfig(combo),
79
89
  };
80
90
  }) });
81
91
  }
@@ -124,10 +134,12 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
124
134
  });
125
135
  if (error) return jsonResponse({ error }, 400);
126
136
  const normalized = normalizeComboConfig(body.combo as import("../../types").OcxComboConfig);
137
+ // Persist only non-default identity/capability fields so config stays sparse.
127
138
  const {
128
139
  alias: normalizedAlias,
129
140
  nativeAlias: normalizedNativeAlias,
130
141
  displayName: normalizedDisplayName,
142
+ imageInput: normalizedImageInput,
131
143
  ...normalizedBase
132
144
  } = normalized;
133
145
  const stored: import("../../types").OcxComboConfig = {
@@ -135,6 +147,7 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
135
147
  ...(normalizedAlias ? { alias: normalizedAlias } : {}),
136
148
  ...(normalizedNativeAlias ? { nativeAlias: true } : {}),
137
149
  ...(normalizedDisplayName ? { displayName: normalizedDisplayName } : {}),
150
+ ...(normalizedImageInput === "disabled" ? { imageInput: "disabled" as const } : {}),
138
151
  };
139
152
  const sourceId = renameFrom ?? id;
140
153
  const previous = config.combos?.[sourceId];
@@ -221,7 +234,8 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
221
234
  }
222
235
  const catalogRefresh = await convergeCodexCatalog();
223
236
  if (shouldSyncClaudeAgentDefs) await syncClaudeAgentDefsBestEffort();
224
- return jsonResponse({ success: true, id, model: newPublicModel, combo: stored, catalogRefresh });
237
+ // Wire shape matches persistence: omit default imageInput "auto".
238
+ return jsonResponse({ success: true, id, model: newPublicModel, combo: sparseComboConfig(stored), catalogRefresh });
225
239
  }
226
240
 
227
241
  if (url.pathname === "/api/combos" && req.method === "DELETE") {
@@ -85,6 +85,7 @@ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from ".
85
85
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
86
86
  import type { PersistedUsageAttempt } from "../../usage/log";
87
87
  import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
88
+ import { withProviderServiceTierDTO } from "./provider-capability-config";
88
89
  import { applySystemEnvToggle } from "../system-env";
89
90
  import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache";
90
91
  import { runWindowsTrayAction } from "../windows-tray-control";
@@ -134,8 +135,9 @@ function publicVisionSidecarSettings(
134
135
 
135
136
  export async function handleConfigRoutes(ctx: ManagementContext): Promise<Response | null> {
136
137
  const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
138
+ const readStartupHealth = deps.getCachedStartupHealth ?? getCachedStartupHealth;
137
139
  if (url.pathname === "/api/config" && req.method === "GET") {
138
- return jsonResponse(safeConfigDTO(config));
140
+ return jsonResponse(withProviderServiceTierDTO(safeConfigDTO(config), config));
139
141
  }
140
142
 
141
143
  if (url.pathname === "/api/config" && req.method === "PUT") {
@@ -189,7 +191,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
189
191
  streamMode: config.streamMode ?? "auto",
190
192
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
191
193
  codexAccountPickerEnabled: codexAccountPickerEnabled(config),
192
- startupHealth: await getCachedStartupHealth(config),
194
+ startupHealth: await readStartupHealth(config),
193
195
  codexRuntime: {
194
196
  path: displayCodexRuntimePath(resolved.runtime.command),
195
197
  version: resolved.runtime.version,
@@ -211,7 +213,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
211
213
  }
212
214
 
213
215
  if (url.pathname === "/api/startup-health" && req.method === "GET") {
214
- return jsonResponse(await getCachedStartupHealth(config));
216
+ return jsonResponse(await readStartupHealth(config));
215
217
  }
216
218
 
217
219
  if (url.pathname === "/api/startup-action" && req.method === "POST") {
@@ -368,7 +370,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
368
370
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
369
371
  codexAccountPickerEnabled: pickerIsEnabled,
370
372
  catalogRefreshPending,
371
- startupHealth: await getCachedStartupHealth(config),
373
+ startupHealth: await readStartupHealth(config),
372
374
  });
373
375
  }
374
376
 
@@ -1,5 +1,6 @@
1
1
  import type { OcxConfig } from "../../types";
2
2
  import type { NativeProfileApiDeps } from "../../codex/native-profile-api";
3
+ import type { StartupHealth } from "../../codex/autostart-health";
3
4
  import type { StartupInstallAction } from "../startup-action-control";
4
5
  import type { ManagementPrincipal } from "../management-auth";
5
6
  import type { CatalogModel } from "../../codex/catalog";
@@ -7,11 +8,17 @@ import type { injectGrokConfig } from "../../grok/inject";
7
8
  import type { removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
8
9
  import type { RuntimePortState } from "../../config";
9
10
  import type { CatalogDisposition, ConvergeCodex } from "../../codex/convergence-types";
11
+ import type {
12
+ performCodexRestart,
13
+ readCodexAppServerState,
14
+ } from "../../codex/app-server-restart-service";
10
15
 
11
16
  export interface ManagementApiDeps {
12
17
  toggleCodexMultiAgentV2?: (enabled: boolean) => void;
13
18
  toggleDefaultModeRequestUserInput?: (enabled: boolean) => void;
14
19
  createManagementConvergeCodex?: (config: Readonly<OcxConfig>) => ConvergeCodex;
20
+ /** Startup-health seam keeps route tests from launching platform probes. */
21
+ getCachedStartupHealth?: (config: Pick<OcxConfig, "codexAutoStart">) => Promise<StartupHealth>;
15
22
  /**
16
23
  * Persistence seam for route-level tests. Production leaves this unset and uses
17
24
  * `saveConfigPreservingClaudeCode`; tests that pass an in-memory fixture config
@@ -52,6 +59,16 @@ export interface ManagementApiDeps {
52
59
  * Native-main profile persistence seam for server-boundary tests. Production
53
60
  * leaves this unset, so the route creates its normal NativeProfileManager.
54
61
  */
62
+ /**
63
+ * Codex app-server restart seam (devlog/_plan/260815_gui_codex_restart).
64
+ * Grouped rather than three separate fields: the route is an adapter over one
65
+ * service, and a route test that could not stub it would really terminate the
66
+ * developer's own Codex app-servers.
67
+ */
68
+ codexRestartService?: {
69
+ readState: typeof readCodexAppServerState;
70
+ performRestart: typeof performCodexRestart;
71
+ };
55
72
  nativeProfileApi?: NativeProfileApiDeps;
56
73
  }
57
74
 
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import {
17
+ ARTIFACT_CLASSES,
17
18
  EVIDENCE_LAYERS,
18
19
  EXECUTION_MODES,
19
20
  EVENT_KINDS,
@@ -30,6 +31,7 @@ import {
30
31
  LabProjectionIncompatibleError,
31
32
  LabProjectionUnavailableError,
32
33
  LAB_QUERY_MAX_PAGE_SIZE,
34
+ PASSIVE_PRODUCTION_MAX_LIMIT,
33
35
  queryLabArtifactByDigest,
34
36
  queryLabArtifacts,
35
37
  queryLabCatalogEntries,
@@ -42,6 +44,15 @@ import {
42
44
  queryLabVerdicts,
43
45
  queryPassiveProductionSignals,
44
46
  } from "../../lab/query";
47
+ import {
48
+ exportLocalPublicEvidence,
49
+ importCommunityEvidenceValue,
50
+ listCommunityEvidenceContext,
51
+ parseStrictPublicJson,
52
+ previewLocalPublicEvidence,
53
+ summarizePublicEvidenceVerification,
54
+ PublicEvidenceValidationError,
55
+ } from "../../lab/public";
45
56
  import { jsonResponse } from "../auth-cors";
46
57
  import type { ManagementContext } from "./context";
47
58
 
@@ -75,20 +86,24 @@ function projectionErrorResponse(err: unknown, ctx: ManagementContext): Response
75
86
  return null;
76
87
  }
77
88
 
78
- function parseLimit(raw: string | null, ctx: ManagementContext): number | undefined | Response {
89
+ function parseLimit(
90
+ raw: string | null,
91
+ ctx: ManagementContext,
92
+ max = LAB_QUERY_MAX_PAGE_SIZE,
93
+ ): number | undefined | Response {
79
94
  const parsed = raw === null ? undefined : parseQueryInt(raw);
80
95
  if (parsed === "invalid") {
81
96
  return errorResponse(
82
97
  "invalid_limit",
83
- `limit must be an integer from 1 to ${LAB_QUERY_MAX_PAGE_SIZE}`,
98
+ `limit must be an integer from 1 to ${max}`,
84
99
  400,
85
100
  ctx,
86
101
  );
87
102
  }
88
- if (parsed !== undefined && (parsed < 1 || parsed > LAB_QUERY_MAX_PAGE_SIZE)) {
103
+ if (parsed !== undefined && (parsed < 1 || parsed > max)) {
89
104
  return errorResponse(
90
105
  "invalid_limit",
91
- `limit must be an integer from 1 to ${LAB_QUERY_MAX_PAGE_SIZE}`,
106
+ `limit must be an integer from 1 to ${max}`,
92
107
  400,
93
108
  ctx,
94
109
  );
@@ -155,7 +170,7 @@ function parseExecutionMode(raw: string | null, ctx: ManagementContext): Executi
155
170
  if (!raw) return undefined;
156
171
  const trimmed = raw.trim();
157
172
  if (!EXECUTION_MODES.includes(trimmed as ExecutionMode)) {
158
- return errorResponse("invalid_execution_mode", "executionMode must be a supported execution mode", 400, ctx);
173
+ return errorResponse("invalid_execution_mode", "executionMode must be a supported lab execution mode", 400, ctx);
159
174
  }
160
175
  return trimmed as ExecutionMode;
161
176
  }
@@ -186,9 +201,154 @@ function paginatedEnvelope<T>(page: { items: T[]; nextCursor?: string; hasMore:
186
201
  };
187
202
  }
188
203
 
204
+ const MAX_PUBLIC_REQUEST_BYTES = 2 * 1024 * 1024;
205
+
206
+ async function readBoundedPublicJson(req: Request): Promise<unknown> {
207
+ const lengthRaw = req.headers.get("content-length");
208
+ if (lengthRaw) {
209
+ const length = Number(lengthRaw);
210
+ if (!Number.isFinite(length) || length < 0 || length > MAX_PUBLIC_REQUEST_BYTES) {
211
+ throw new PublicEvidenceValidationError(
212
+ "public_request_too_large",
213
+ "public evidence request exceeds 2 MiB",
214
+ );
215
+ }
216
+ }
217
+ if (!req.body) {
218
+ throw new PublicEvidenceValidationError("public_request_body", "JSON body is required");
219
+ }
220
+ const reader = req.body.getReader();
221
+ const chunks: Uint8Array[] = [];
222
+ let total = 0;
223
+ while (true) {
224
+ const { done, value } = await reader.read();
225
+ if (done) break;
226
+ total += value.byteLength;
227
+ if (total > MAX_PUBLIC_REQUEST_BYTES) {
228
+ await reader.cancel();
229
+ throw new PublicEvidenceValidationError(
230
+ "public_request_too_large",
231
+ "public evidence request exceeds 2 MiB",
232
+ );
233
+ }
234
+ chunks.push(value);
235
+ }
236
+ const bytes = new Uint8Array(total);
237
+ let offset = 0;
238
+ for (const chunk of chunks) {
239
+ bytes.set(chunk, offset);
240
+ offset += chunk.byteLength;
241
+ }
242
+ return parseStrictPublicJson(bytes, "public evidence request");
243
+ }
244
+
245
+ function publicEventIds(raw: unknown): string[] {
246
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
247
+ throw new PublicEvidenceValidationError("public_request_body", "request body must be an object");
248
+ }
249
+ const keys = Object.keys(raw);
250
+ if (keys.length !== 1 || keys[0] !== "eventIds") {
251
+ throw new PublicEvidenceValidationError("public_request_body", "only eventIds is accepted");
252
+ }
253
+ const eventIds = (raw as { eventIds?: unknown }).eventIds;
254
+ if (!Array.isArray(eventIds) || !eventIds.every((value) => typeof value === "string")) {
255
+ throw new PublicEvidenceValidationError("public_request_body", "eventIds must be a string array");
256
+ }
257
+ return eventIds as string[];
258
+ }
259
+
260
+ function publicBundleValue(raw: unknown): unknown {
261
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
262
+ throw new PublicEvidenceValidationError("public_request_body", "request body must be an object");
263
+ }
264
+ const keys = Object.keys(raw);
265
+ if (keys.length !== 1 || keys[0] !== "bundle") {
266
+ throw new PublicEvidenceValidationError("public_request_body", "only bundle is accepted");
267
+ }
268
+ return (raw as { bundle?: unknown }).bundle;
269
+ }
270
+
271
+ function publicErrorResponse(err: unknown, ctx: ManagementContext): Response {
272
+ if (err instanceof PublicEvidenceValidationError) {
273
+ const status = err.code === "community_cache_busy" ? 503 : 400;
274
+ const response = errorResponse(err.code, err.message, status, ctx);
275
+ if (status === 503) response.headers.set("Retry-After", "1");
276
+ return response;
277
+ }
278
+ const projected = projectionErrorResponse(err, ctx);
279
+ if (projected) return projected;
280
+ return errorResponse("public_evidence_internal", "internal public evidence failure", 500, ctx);
281
+ }
282
+
189
283
  export async function handleLabRoutes(ctx: ManagementContext): Promise<Response | null> {
190
284
  const { url, req, config } = ctx;
191
285
  if (!url.pathname.startsWith("/api/lab")) return null;
286
+
287
+ if (req.method === "GET" && url.pathname === "/api/lab/public/community") {
288
+ try {
289
+ return jsonResponse(listCommunityEvidenceContext(), 200, req, config);
290
+ } catch (err) {
291
+ return publicErrorResponse(err, ctx);
292
+ }
293
+ }
294
+
295
+ if (req.method === "POST") {
296
+ if (url.pathname === "/api/lab/public/preview") {
297
+ try {
298
+ const body = await readBoundedPublicJson(req);
299
+ return jsonResponse(
300
+ previewLocalPublicEvidence({ eventIds: publicEventIds(body) }),
301
+ 200,
302
+ req,
303
+ config,
304
+ );
305
+ } catch (err) {
306
+ return publicErrorResponse(err, ctx);
307
+ }
308
+ }
309
+ if (url.pathname === "/api/lab/public/export") {
310
+ try {
311
+ const body = await readBoundedPublicJson(req);
312
+ return jsonResponse(
313
+ exportLocalPublicEvidence({ eventIds: publicEventIds(body) }),
314
+ 200,
315
+ req,
316
+ config,
317
+ );
318
+ } catch (err) {
319
+ return publicErrorResponse(err, ctx);
320
+ }
321
+ }
322
+ if (url.pathname === "/api/lab/public/verify") {
323
+ try {
324
+ const body = await readBoundedPublicJson(req);
325
+ const result = summarizePublicEvidenceVerification(publicBundleValue(body));
326
+ return jsonResponse(
327
+ result,
328
+ result.status === "cryptographically_valid" ? 200 : 400,
329
+ req,
330
+ config,
331
+ );
332
+ } catch (err) {
333
+ return publicErrorResponse(err, ctx);
334
+ }
335
+ }
336
+ if (url.pathname === "/api/lab/public/community/import") {
337
+ try {
338
+ const body = await readBoundedPublicJson(req);
339
+ return jsonResponse(
340
+ importCommunityEvidenceValue(publicBundleValue(body)),
341
+ 200,
342
+ req,
343
+ config,
344
+ );
345
+ } catch (err) {
346
+ return publicErrorResponse(err, ctx);
347
+ }
348
+ }
349
+ return null;
350
+ }
351
+
192
352
  if (req.method !== "GET") return null;
193
353
 
194
354
  if (url.pathname === "/api/lab/status") {
@@ -198,7 +358,7 @@ export async function handleLabRoutes(ctx: ManagementContext): Promise<Response
198
358
  if (url.pathname === "/api/lab/production-signals") {
199
359
  const subjectId = url.searchParams.get("subjectId")?.trim();
200
360
  if (!subjectId) return errorResponse("invalid_subject", "subjectId is required", 400, ctx);
201
- const limit = parseLimit(url.searchParams.get("limit"), ctx);
361
+ const limit = parseLimit(url.searchParams.get("limit"), ctx, PASSIVE_PRODUCTION_MAX_LIMIT);
202
362
  if (limit instanceof Response) return limit;
203
363
  try {
204
364
  return jsonResponse(queryPassiveProductionSignals(subjectId, limit), 200, req, config);
@@ -213,10 +373,7 @@ export async function handleLabRoutes(ctx: ManagementContext): Promise<Response
213
373
  if (layerParsed instanceof Response) return layerParsed;
214
374
  const suiteId = url.searchParams.get("suiteId")?.trim() || url.searchParams.get("suite")?.trim() || undefined;
215
375
  try {
216
- const scenarios = queryLabCatalogEntries({
217
- layer: layerParsed,
218
- suiteId,
219
- });
376
+ const scenarios = queryLabCatalogEntries({ layer: layerParsed, suiteId });
220
377
  return jsonResponse({ scenarios }, 200, req, config);
221
378
  } catch (err) {
222
379
  const mapped = projectionErrorResponse(err, ctx);
@@ -255,11 +412,7 @@ export async function handleLabRoutes(ctx: ManagementContext): Promise<Response
255
412
  const limit = parseLimit(url.searchParams.get("limit"), ctx);
256
413
  if (limit instanceof Response) return limit;
257
414
  try {
258
- const page = queryLabSubjects(
259
- url.searchParams.get("kind")?.trim() || undefined,
260
- url.searchParams.get("cursor"),
261
- limit,
262
- );
415
+ const page = queryLabSubjects(url.searchParams.get("kind")?.trim() || undefined, url.searchParams.get("cursor"), limit);
263
416
  return jsonResponse(paginatedEnvelope(page, "subjects"), 200, req, config);
264
417
  } catch (err) {
265
418
  const mapped = projectionErrorResponse(err, ctx);
@@ -323,9 +476,10 @@ export async function handleLabRoutes(ctx: ManagementContext): Promise<Response
323
476
  const eventKind = parseEventKind(url.searchParams.get("eventKind"), ctx);
324
477
  if (eventKind instanceof Response) return eventKind;
325
478
  const excludedRaw = url.searchParams.get("excluded");
326
- let excluded: boolean | undefined;
327
- if (excludedRaw === "true") excluded = true;
328
- else if (excludedRaw === "false") excluded = false;
479
+ if (excludedRaw !== null && excludedRaw !== "true" && excludedRaw !== "false") {
480
+ return errorResponse("invalid_excluded", "excluded must be true or false", 400, ctx);
481
+ }
482
+ const excluded = excludedRaw === "true" ? true : excludedRaw === "false" ? false : undefined;
329
483
  try {
330
484
  const page = queryLabEvents({
331
485
  eventKind,
@@ -367,6 +521,14 @@ export async function handleLabRoutes(ctx: ManagementContext): Promise<Response
367
521
  if (statusRaw && !["present", "corrupt", "purged_unavailable"].includes(statusRaw)) {
368
522
  return errorResponse("invalid_status", "status must be present, corrupt, or purged_unavailable", 400, ctx);
369
523
  }
524
+ if (artifactClass && !ARTIFACT_CLASSES.includes(artifactClass as (typeof ARTIFACT_CLASSES)[number])) {
525
+ return errorResponse(
526
+ "invalid_artifact_class",
527
+ "artifactClass must be a supported artifact class",
528
+ 400,
529
+ ctx,
530
+ );
531
+ }
370
532
  try {
371
533
  const page = queryLabArtifacts({
372
534
  status: statusRaw as "present" | "corrupt" | "purged_unavailable" | undefined,
@@ -398,4 +560,4 @@ export async function handleLabRoutes(ctx: ManagementContext): Promise<Response
398
560
  }
399
561
 
400
562
  return null;
401
- }
563
+ }
@@ -29,6 +29,46 @@ function readInputModalities(raw: unknown): { values?: string[]; error?: string
29
29
  }
30
30
  return { values: raw as string[] };
31
31
  }
32
+
33
+ /**
34
+ * Custom-row reasoning ladder. Labels are validated against the Codex ladder (low..ultra)
35
+ * exactly like provider `modelReasoningEfforts` values; unknown labels would otherwise
36
+ * surface in a catalog the upstream never accepts. An empty array is meaningful (explicit
37
+ * "no reasoning" hides the effort control) and must be preserved, not cleared.
38
+ */
39
+ function readReasoningEfforts(raw: unknown): { values?: string[]; error?: string } {
40
+ if (raw === undefined) return {};
41
+ if (!Array.isArray(raw)) return { error: "reasoningEfforts must be an array" };
42
+ const rejected: string[] = [];
43
+ const values: string[] = [];
44
+ for (const value of raw) {
45
+ if (typeof value !== "string") return { error: "reasoningEfforts must contain only strings" };
46
+ if (!isDeclaredReasoningEffort(value)) { rejected.push(value); continue; }
47
+ if (!values.includes(value)) values.push(value);
48
+ }
49
+ if (rejected.length > 0) {
50
+ return { error: `unsupported reasoning effort: ${rejected.join(", ")} (allowed: none, minimal, low, medium, high, xhigh, max, ultra)` };
51
+ }
52
+ // Canonical order: the catalog writes supported_reasoning_levels in input order and the
53
+ // fallback default picks the first entry, so a caller-chosen order must not leak through.
54
+ return { values: canonicalizeReasoningEfforts(values) };
55
+ }
56
+
57
+ /** Default effort must be a ladder member that the declared ladder actually includes. */
58
+ function readDefaultReasoningEffort(raw: unknown, efforts: string[] | undefined): { value?: string; error?: string } {
59
+ if (raw === undefined) return {};
60
+ if (raw === null) return { value: undefined };
61
+ if (typeof raw !== "string" || !isDeclaredReasoningEffort(raw)) {
62
+ return { error: "defaultReasoningEffort must be one of: none, minimal, low, medium, high, xhigh, max, ultra" };
63
+ }
64
+ if (efforts === undefined || efforts.length === 0) {
65
+ return { error: "defaultReasoningEffort requires a non-empty reasoningEfforts ladder" };
66
+ }
67
+ if (!efforts.includes(raw)) {
68
+ return { error: `defaultReasoningEffort "${raw}" is not in the declared reasoningEfforts ladder` };
69
+ }
70
+ return { value: raw };
71
+ }
32
72
  import type { CatalogModel } from "../../codex/catalog";
33
73
  import { accountBoundNativeOpenAiSlugsBySelector, catalogModelSlug, configuredNativeAliasSlugs, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, shouldIncludeAccountBoundNativeOpenAi, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
34
74
  import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
@@ -71,6 +111,7 @@ import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summa
71
111
  import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
72
112
  import { getProviderRegistryEntry } from "../../providers/registry";
73
113
  import { getDebugLogEntries } from "../../lib/debug-log-buffer";
114
+ import { canonicalizeReasoningEfforts, isDeclaredReasoningEffort } from "../../reasoning-effort";
74
115
  import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
75
116
  import {
76
117
  clearDebugSettings,
@@ -330,7 +371,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
330
371
  }
331
372
 
332
373
  if (url.pathname === "/api/custom-models" && req.method === "POST") {
333
- let body: { provider?: unknown; modelId?: unknown; displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown };
374
+ let body: { provider?: unknown; modelId?: unknown; displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown; reasoningEfforts?: unknown; defaultReasoningEffort?: unknown };
334
375
  try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
335
376
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
336
377
  const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
@@ -344,6 +385,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
344
385
  const modalities = readInputModalities(body.inputModalities);
345
386
  if (modalities.error) return jsonResponse({ error: modalities.error }, 400);
346
387
  const inputModalities = modalities.values;
388
+ const reasoning = readReasoningEfforts(body.reasoningEfforts);
389
+ if (reasoning.error) return jsonResponse({ error: reasoning.error }, 400);
390
+ const defaultEffort = readDefaultReasoningEffort(body.defaultReasoningEffort, reasoning.values);
391
+ if (defaultEffort.error) return jsonResponse({ error: defaultEffort.error }, 400);
347
392
  const existing = config.customModels ?? [];
348
393
  const newSlug = routedSlug(provider, modelId);
349
394
  if (existing.some(cm => routedSlug(cm.provider, cm.modelId) === newSlug)) {
@@ -356,6 +401,8 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
356
401
  ...(displayName ? { displayName } : {}),
357
402
  ...(contextWindow ? { contextWindow } : {}),
358
403
  ...(inputModalities && inputModalities.length > 0 ? { inputModalities } : {}),
404
+ ...(reasoning.values !== undefined ? { reasoningEfforts: reasoning.values } : {}),
405
+ ...(defaultEffort.value ? { defaultReasoningEffort: defaultEffort.value } : {}),
359
406
  addedAt: new Date().toISOString(),
360
407
  };
361
408
  config.customModels = [...existing, entry];
@@ -368,7 +415,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
368
415
  if (customPutMatch && req.method === "PUT") {
369
416
  let id: string;
370
417
  try { id = decodeURIComponent(customPutMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
371
- let body: { displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown; modelId?: unknown };
418
+ let body: { displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown; modelId?: unknown; reasoningEfforts?: unknown; defaultReasoningEffort?: unknown };
372
419
  try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
373
420
  const list = config.customModels ?? [];
374
421
  const idx = list.findIndex(cm => cm.id === id);
@@ -391,6 +438,33 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
391
438
  if (edited.error) return jsonResponse({ error: edited.error }, 400);
392
439
  cm.inputModalities = edited.values && edited.values.length > 0 ? edited.values : undefined;
393
440
  }
441
+ // `null` clears the stored ladder back to "inherit from the provider row"; `[]` stays
442
+ // stored as an explicit "no reasoning" override. The default effort rides along and is
443
+ // validated against the ladder the row ends up with.
444
+ if (body.reasoningEfforts !== undefined) {
445
+ if (body.reasoningEfforts === null) {
446
+ cm.reasoningEfforts = undefined;
447
+ } else {
448
+ const edited = readReasoningEfforts(body.reasoningEfforts);
449
+ if (edited.error) return jsonResponse({ error: edited.error }, 400);
450
+ cm.reasoningEfforts = edited.values;
451
+ }
452
+ }
453
+ if (body.defaultReasoningEffort !== undefined) {
454
+ const edited = readDefaultReasoningEffort(body.defaultReasoningEffort, cm.reasoningEfforts);
455
+ if (edited.error) return jsonResponse({ error: edited.error }, 400);
456
+ cm.defaultReasoningEffort = edited.value;
457
+ }
458
+ // Mirror of the POST invariant: a default only survives as a member of the final ladder.
459
+ // Without this, a ladder shrink/clear on a row that was created with a default leaves a
460
+ // stale default that re-applies itself onto the inherited ladder in the generated catalog
461
+ // (the GUI toggle-off path sends only reasoningEfforts, never the default).
462
+ if (cm.defaultReasoningEffort !== undefined) {
463
+ const ladder = cm.reasoningEfforts;
464
+ if (!ladder || ladder.length === 0 || !ladder.includes(cm.defaultReasoningEffort)) {
465
+ cm.defaultReasoningEffort = undefined;
466
+ }
467
+ }
394
468
  const updatedSlug = routedSlug(cm.provider, cm.modelId);
395
469
  if (list.some((other, i) => i !== idx && routedSlug(other.provider, other.modelId) === updatedSlug)) {
396
470
  return jsonResponse({ error: "duplicate model" }, 409);
@@ -91,6 +91,14 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
91
91
  displayName: cm.displayName,
92
92
  ...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
93
93
  ...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
94
+ // Stored override, not the inherited ladder: the edit dialog must show what the user
95
+ // set (including an explicit empty "no reasoning" ladder), not what the provider row
96
+ // happens to advertise today.
97
+ ...(Array.isArray(cm.reasoningEfforts) ? { reasoningEfforts: [...cm.reasoningEfforts] } : {}),
98
+ // The stored default rides along so a client reloading /api/models can restore the
99
+ // full edit state; the GUI has no default-effort control today, but dropping it here
100
+ // would make any future PUT-based edit lose it silently.
101
+ ...(cm.defaultReasoningEffort ? { defaultReasoningEffort: cm.defaultReasoningEffort } : {}),
94
102
  };
95
103
  });
96
104
  const publicModels = uniqueCatalogModelsForPublicList(models);
@@ -0,0 +1,48 @@
1
+ import { booleanRecordConfigError } from "../../config";
2
+ import type { OcxConfig } from "../../types";
3
+
4
+ /**
5
+ * Provider-management validation that belongs to the provider editor, not the
6
+ * request-authentication/CORS boundary. Keeping this here lets the editor
7
+ * evolve its capability schema without widening the auth-cors security surface.
8
+ */
9
+ export function providerServiceTierConfigError(name: unknown, provider: unknown): string | null {
10
+ if (typeof name !== "string" || !provider || typeof provider !== "object" || Array.isArray(provider)) {
11
+ return null;
12
+ }
13
+ const error = booleanRecordConfigError(
14
+ (provider as { modelSupportsServiceTier?: unknown }).modelSupportsServiceTier,
15
+ "modelSupportsServiceTier",
16
+ );
17
+ return error ? `provider ${name} ${error}` : null;
18
+ }
19
+
20
+ function publicServiceTierRecord(value: unknown): Record<string, boolean> | undefined {
21
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
22
+ const entries = Object.entries(value).filter(([model, supported]) =>
23
+ model.trim().length > 0 && typeof supported === "boolean",
24
+ );
25
+ return Object.fromEntries(entries) as Record<string, boolean>;
26
+ }
27
+
28
+ /**
29
+ * Add the provider editor's capability map to the already secret-free config
30
+ * DTO. `safeConfigDTO` remains the owner of auth/cors redaction; this helper
31
+ * only projects a boolean model capability used by the management UI.
32
+ */
33
+ export function withProviderServiceTierDTO(dto: unknown, config: OcxConfig): unknown {
34
+ if (!dto || typeof dto !== "object" || Array.isArray(dto)) return dto;
35
+ const root = dto as { providers?: unknown };
36
+ if (!root.providers || typeof root.providers !== "object" || Array.isArray(root.providers)) return dto;
37
+
38
+ const providers = root.providers as Record<string, unknown>;
39
+ const projectedProviders: Record<string, unknown> = { ...providers };
40
+ for (const [name, provider] of Object.entries(config.providers)) {
41
+ const dtoProvider = providers[name];
42
+ if (!dtoProvider || typeof dtoProvider !== "object" || Array.isArray(dtoProvider)) continue;
43
+ const capabilities = publicServiceTierRecord(provider.modelSupportsServiceTier);
44
+ if (capabilities === undefined) continue;
45
+ projectedProviders[name] = { ...(dtoProvider as Record<string, unknown>), modelSupportsServiceTier: capabilities };
46
+ }
47
+ return { ...root, providers: projectedProviders };
48
+ }