@bitkyc08/opencodex 2.19.0 → 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 (119) 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/mimo-free.ts +17 -0
  6. package/src/adapters/openai-chat.ts +367 -32
  7. package/src/adapters/registry.ts +144 -0
  8. package/src/chat/inbound.ts +13 -7
  9. package/src/cli/claude.ts +2 -1
  10. package/src/cli/combo.ts +3 -0
  11. package/src/cli/dispatch.ts +8 -0
  12. package/src/cli/export-command.ts +2 -2
  13. package/src/cli/help.ts +2 -0
  14. package/src/cli/index.ts +3 -2
  15. package/src/cli/lab.ts +135 -1
  16. package/src/cli/minimax.ts +491 -0
  17. package/src/cli/models-runtime.ts +22 -1
  18. package/src/cli/models.ts +67 -2
  19. package/src/cli/opencode.ts +2 -1
  20. package/src/cli/registry.ts +22 -2
  21. package/src/clients/config-export.ts +125 -7
  22. package/src/codex/app-server-processes.ts +57 -2
  23. package/src/codex/app-server-restart-service.ts +232 -0
  24. package/src/codex/catalog/aggregation.ts +10 -1
  25. package/src/codex/catalog/effort.ts +15 -3
  26. package/src/codex/catalog/parsing.ts +3 -1
  27. package/src/codex/catalog/provider-fetch.ts +45 -5
  28. package/src/codex/catalog/sync.ts +74 -4
  29. package/src/codex/convergence.ts +2 -0
  30. package/src/combos/index.ts +1 -0
  31. package/src/combos/request.ts +30 -0
  32. package/src/combos/types.ts +6 -0
  33. package/src/config.ts +52 -0
  34. package/src/generated/compatibility-version.json +224 -76
  35. package/src/images/loop.ts +11 -1
  36. package/src/integrations/registry.ts +7 -0
  37. package/src/lab/conformance/jcs.ts +42 -2
  38. package/src/lab/conformance/negative-controls.ts +6 -2
  39. package/src/lab/conformance/runner.ts +16 -5
  40. package/src/lab/fabric/observe.ts +49 -14
  41. package/src/lab/index.ts +16 -0
  42. package/src/lab/ledger/purge.ts +152 -83
  43. package/src/lab/ledger/store.ts +168 -54
  44. package/src/lab/observe/from-conformance.ts +8 -6
  45. package/src/lab/observe/from-live.ts +8 -2
  46. package/src/lab/paths.ts +23 -0
  47. package/src/lab/public/bundle.ts +217 -0
  48. package/src/lab/public/community-authority.ts +175 -0
  49. package/src/lab/public/community-files.ts +29 -0
  50. package/src/lab/public/community.ts +479 -0
  51. package/src/lab/public/file-safety.ts +155 -0
  52. package/src/lab/public/ids.ts +26 -0
  53. package/src/lab/public/index.ts +16 -0
  54. package/src/lab/public/mutation-lock.ts +424 -0
  55. package/src/lab/public/operator.ts +353 -0
  56. package/src/lab/public/origin-purge.ts +79 -0
  57. package/src/lab/public/origin.ts +203 -0
  58. package/src/lab/public/privacy.ts +143 -0
  59. package/src/lab/public/private-file.ts +261 -0
  60. package/src/lab/public/project.ts +124 -0
  61. package/src/lab/public/purge-test-fault.ts +21 -0
  62. package/src/lab/public/purge.ts +223 -0
  63. package/src/lab/public/registry.ts +44 -0
  64. package/src/lab/public/revocation.ts +252 -0
  65. package/src/lab/public/signature.ts +219 -0
  66. package/src/lab/public/storage.ts +105 -0
  67. package/src/lab/public/strict-json.ts +206 -0
  68. package/src/lab/public/time.ts +26 -0
  69. package/src/lab/public/types.ts +172 -0
  70. package/src/lab/public/validate.ts +391 -0
  71. package/src/lib/codex-restart-contract.ts +120 -0
  72. package/src/lib/lab-activation.ts +109 -47
  73. package/src/lib/lab-live-pinned-sender.ts +16 -5
  74. package/src/lib/pinned-http.ts +70 -16
  75. package/src/lib/self-launch-argv.ts +15 -0
  76. package/src/lib/state-store-registrations.ts +2 -0
  77. package/src/lib/upstream-reachability.ts +4 -0
  78. package/src/lib/windows-elevation.ts +10 -1
  79. package/src/providers/derive.ts +24 -4
  80. package/src/providers/registry.ts +7 -3
  81. package/src/providers/request-pacing.ts +310 -0
  82. package/src/providers/service-tier.ts +143 -0
  83. package/src/providers/static-model-discovery.ts +86 -0
  84. package/src/reasoning-effort.ts +27 -1
  85. package/src/router.ts +23 -6
  86. package/src/routing/capability.ts +4 -2
  87. package/src/routing/compatibility/behavior.ts +5 -1
  88. package/src/server/adapter-resolve.ts +2 -32
  89. package/src/server/auth-cors.ts +8 -0
  90. package/src/server/chat-completions.ts +74 -36
  91. package/src/server/chat-native-sse.ts +331 -0
  92. package/src/server/chat-native.ts +371 -0
  93. package/src/server/management/combo-routes.ts +16 -2
  94. package/src/server/management/config-routes.ts +6 -4
  95. package/src/server/management/context.ts +17 -0
  96. package/src/server/management/lab-routes.ts +181 -19
  97. package/src/server/management/model-routes.ts +76 -2
  98. package/src/server/management/model-rows.ts +8 -0
  99. package/src/server/management/provider-capability-config.ts +48 -0
  100. package/src/server/management/provider-routes.ts +76 -4
  101. package/src/server/management/system-restart.ts +4 -2
  102. package/src/server/management/system-routes.ts +38 -0
  103. package/src/server/relay.ts +17 -3
  104. package/src/server/responses/compact.ts +4 -1
  105. package/src/server/responses/core.ts +257 -46
  106. package/src/server/responses/empty-completion-guard.ts +276 -0
  107. package/src/server/responses/fetch-helpers.ts +35 -4
  108. package/src/server/responses/pacing-overload.ts +13 -0
  109. package/src/server/responses/policy-fallback.ts +16 -2
  110. package/src/server/responses/terminal-guard.ts +1 -1
  111. package/src/server/responses/upstream-error.ts +5 -0
  112. package/src/server/responses.ts +17 -2
  113. package/src/types.ts +66 -4
  114. package/src/update/index.ts +6 -5
  115. package/src/update/job.ts +5 -6
  116. package/src/update/notify.ts +5 -3
  117. package/src/usage/log.ts +11 -1
  118. package/gui/dist/assets/index-CQ7bIKee.css +0 -1
  119. package/gui/dist/assets/index-D_JUZLEC.js +0 -76
@@ -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
+ }
@@ -14,6 +14,7 @@ import {
14
14
  normalizeNonBlankStringArray,
15
15
  providerBaseUrlConfigError,
16
16
  providerHeadersConfigError,
17
+ requestPacingConfigError,
17
18
  readConfigAdmissionSnapshot,
18
19
  saveConfigPreservingClaudeCode,
19
20
  withConfigMutationLockSync,
@@ -44,6 +45,7 @@ import {
44
45
  import { routedSlug, slugEquals } from "../../providers/slug-codec";
45
46
  import { clearAccountQuotaCache, clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
46
47
  import { clearKeyCooldowns } from "../../providers/key-failover";
48
+ import { providerRequestPacingStatus } from "../../providers/request-pacing";
47
49
  import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
48
50
  import { codexAccountNamespaceProviderCollisionError } from "../../codex/account-namespace-match";
49
51
  import { clearThreadAccountMap } from "../../codex/routing";
@@ -71,6 +73,7 @@ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from ".
71
73
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
72
74
  import type { PersistedUsageAttempt } from "../../usage/log";
73
75
  import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
76
+ import { providerServiceTierConfigError } from "./provider-capability-config";
74
77
  import { applySystemEnvToggle } from "../system-env";
75
78
  import {
76
79
  LOCAL_PROVIDER_RELOAD_NAME_HEADER,
@@ -177,6 +180,20 @@ function applyProviderPatchFields(
177
180
  next.liveModels = rawBody.liveModels;
178
181
  touched = true;
179
182
  }
183
+ if (Object.hasOwn(rawBody, "requestPacing")) {
184
+ const value = rawBody.requestPacing;
185
+ if (value === null) {
186
+ delete next.requestPacing;
187
+ } else {
188
+ if (!isPlainRecord(value)) return { error: "requestPacing must be a plain object or null" };
189
+ const pacingError = requestPacingConfigError(value);
190
+ if (pacingError) return { error: pacingError };
191
+ // `requestPacingConfigError` is the runtime narrowing boundary above; keep the
192
+ // assertion explicit because a generic plain record cannot express `enabled`.
193
+ next.requestPacing = structuredClone(value) as unknown as OcxProviderConfig["requestPacing"];
194
+ }
195
+ touched = true;
196
+ }
180
197
  // The Models page edits the catalog hints in place; keep them on the existing
181
198
  // provider mutation path so validation, cache invalidation, and convergence stay unified (#1073).
182
199
  if (Object.hasOwn(rawBody, "contextWindow")) {
@@ -216,6 +233,29 @@ function applyProviderPatchFields(
216
233
  }
217
234
  touched = true;
218
235
  }
236
+ if (Object.hasOwn(rawBody, "modelSupportsServiceTier")) {
237
+ const value = rawBody.modelSupportsServiceTier;
238
+ if (value === null) {
239
+ delete next.modelSupportsServiceTier;
240
+ } else {
241
+ if (!isPlainRecord(value)) return { error: "modelSupportsServiceTier must be a plain object or null" };
242
+ const capabilities: Record<string, boolean> = { ...(next.modelSupportsServiceTier ?? {}) };
243
+ for (const [model, supported] of Object.entries(value)) {
244
+ if (!model.trim()) return { error: "modelSupportsServiceTier keys must be nonblank model ids" };
245
+ if (supported === null) {
246
+ delete capabilities[model];
247
+ continue;
248
+ }
249
+ if (typeof supported !== "boolean") {
250
+ return { error: "modelSupportsServiceTier values must be booleans or null" };
251
+ }
252
+ capabilities[model] = supported;
253
+ }
254
+ if (Object.keys(capabilities).length > 0) next.modelSupportsServiceTier = capabilities;
255
+ else delete next.modelSupportsServiceTier;
256
+ }
257
+ touched = true;
258
+ }
219
259
  if (Object.hasOwn(rawBody, "noStructuredOutputModels")) {
220
260
  const value = rawBody.noStructuredOutputModels;
221
261
  if (value === null) {
@@ -304,6 +344,22 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
304
344
  return jsonResponse(await fetchProviderQuotaReports(config, forceRefresh));
305
345
  }
306
346
 
347
+ if (url.pathname === "/api/provider-request-pacing" && req.method === "GET") {
348
+ const name = url.searchParams.get("name")?.trim();
349
+ if (name) {
350
+ if (!isValidProviderName(name) || !hasOwnProvider(config.providers, name)) {
351
+ return jsonResponse({ error: "unknown provider" }, 404);
352
+ }
353
+ return jsonResponse(providerRequestPacingStatus(name, config.providers[name]!));
354
+ }
355
+ return jsonResponse(Object.fromEntries(
356
+ Object.entries(config.providers).map(([providerName, provider]) => [
357
+ providerName,
358
+ providerRequestPacingStatus(providerName, provider),
359
+ ]),
360
+ ));
361
+ }
362
+
307
363
  if (url.pathname === "/api/providers" && req.method === "GET") {
308
364
  return jsonResponse(Object.entries(config.providers).map(([name, p]) => ({
309
365
  name, adapter: p.adapter, baseUrl: publicProviderBaseUrl(p.baseUrl), defaultModel: p.defaultModel,
@@ -312,9 +368,11 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
312
368
  hasHeaders: !!p.headers && Object.keys(p.headers).length > 0,
313
369
  allowPrivateNetwork: p.allowPrivateNetwork === true,
314
370
  liveModels: p.liveModels !== false,
371
+ requestPacing: p.requestPacing,
315
372
  models: p.models ?? [],
316
373
  contextWindow: p.contextWindow,
317
374
  modelContextWindows: p.modelContextWindows,
375
+ modelSupportsServiceTier: p.modelSupportsServiceTier,
318
376
  noStructuredOutputModels: p.noStructuredOutputModels,
319
377
  authMode: p.authMode,
320
378
  apiKeyTransport: p.apiKeyTransport,
@@ -405,6 +463,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
405
463
  const name = typeof body.name === "string" ? body.name.trim() : "";
406
464
  const providerError = providerManagementConfigError(name, body.provider);
407
465
  if (providerError) return jsonResponse({ error: providerError }, 400);
466
+ const serviceTierError = providerServiceTierConfigError(name, body.provider);
467
+ if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
408
468
  const prov = body.provider ? stripCodexRuntimeProviderFields(body.provider as OcxProviderConfig) : undefined;
409
469
  if (!name || !prov?.adapter || !prov?.baseUrl) {
410
470
  return jsonResponse({ error: "name, provider.adapter and provider.baseUrl are required" }, 400);
@@ -437,6 +497,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
437
497
  // call can never fire.
438
498
  const submittedContextWindow = Object.hasOwn(prov, "contextWindow");
439
499
  const submittedModelContextWindows = Object.hasOwn(prov, "modelContextWindows");
500
+ const submittedRequestPacing = Object.hasOwn(prov, "requestPacing");
440
501
  enrichProviderFromCatalog(name, prov);
441
502
  const { saveConfigPreservingClaudeCode: save } = await import("../../config");
442
503
  // Overwriting an existing provider must not drop its multi-key pool: carry it over, then
@@ -453,6 +514,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
453
514
  // absence in the request means "not carried", never "the user deleted it". Deletion goes
454
515
  // through PATCH with an explicit null (#1409).
455
516
  const existing = config.providers[name];
517
+ if (!submittedRequestPacing && existing?.requestPacing) {
518
+ prov.requestPacing = structuredClone(existing.requestPacing);
519
+ }
456
520
  if (!submittedContextWindow && existing?.contextWindow !== undefined) {
457
521
  prov.contextWindow = existing.contextWindow;
458
522
  }
@@ -548,9 +612,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
548
612
  if ("error" in applied) return jsonResponse({ error: applied.error }, 400);
549
613
  const next = applied.next;
550
614
 
551
- if (applied.editorTouched) {
615
+ const pacingOnly = keys.every(key => key === "requestPacing");
616
+ if (applied.editorTouched && !pacingOnly) {
552
617
  const providerError = providerManagementConfigError(name, next);
553
618
  if (providerError) return jsonResponse({ error: providerError }, 400);
619
+ const serviceTierError = providerServiceTierConfigError(name, next);
620
+ if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
554
621
  const resolvedError = await providerDestinationResolvedError(name, next);
555
622
  if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
556
623
  } else if (applied.enablingOpenAi) {
@@ -575,12 +642,17 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
575
642
  replayError = replay.error;
576
643
  return;
577
644
  }
578
- if (replay.editorTouched) {
645
+ if (replay.editorTouched && !pacingOnly) {
579
646
  const syncError = providerManagementConfigError(name, replay.next);
580
647
  if (syncError) {
581
648
  replayError = syncError;
582
649
  return;
583
650
  }
651
+ const serviceTierError = providerServiceTierConfigError(name, replay.next);
652
+ if (serviceTierError) {
653
+ replayError = serviceTierError;
654
+ return;
655
+ }
584
656
  } else if (replay.enablingOpenAi && !isCanonicalOpenAiForwardProvider(replay.next)) {
585
657
  replayError = "provider openai must be the canonical built-in provider";
586
658
  return;
@@ -592,11 +664,11 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
592
664
  });
593
665
  if (replayError !== undefined) return jsonResponse({ error: replayError }, 409);
594
666
  reconcileLiveStateStores();
595
- if (applied.editorTouched) {
667
+ if (applied.editorTouched && !pacingOnly) {
596
668
  const { clearModelCache } = await import("../../codex/model-cache");
597
669
  clearModelCache(name);
598
670
  }
599
- const catalogRefresh = await convergeCodexCatalog();
671
+ const catalogRefresh = pacingOnly ? null : await convergeCodexCatalog();
600
672
  return jsonResponse({
601
673
  success: true,
602
674
  name,