@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.
- package/gui/dist/assets/index-DF_UFrGS.css +1 -0
- package/gui/dist/assets/index-DSK3S5HY.js +76 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat.ts +367 -32
- package/src/adapters/registry.ts +144 -0
- package/src/chat/inbound.ts +13 -7
- package/src/cli/claude.ts +2 -1
- package/src/cli/combo.ts +3 -0
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/export-command.ts +2 -2
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +3 -2
- package/src/cli/lab.ts +135 -1
- package/src/cli/minimax.ts +491 -0
- package/src/cli/models-runtime.ts +22 -1
- package/src/cli/models.ts +67 -2
- package/src/cli/opencode.ts +2 -1
- package/src/cli/registry.ts +22 -2
- package/src/clients/config-export.ts +125 -7
- package/src/codex/app-server-processes.ts +57 -2
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/catalog/aggregation.ts +10 -1
- package/src/codex/catalog/effort.ts +15 -3
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +45 -5
- package/src/codex/catalog/sync.ts +74 -4
- package/src/codex/convergence.ts +2 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/request.ts +30 -0
- package/src/combos/types.ts +6 -0
- package/src/config.ts +52 -0
- package/src/generated/compatibility-version.json +224 -76
- package/src/images/loop.ts +11 -1
- package/src/integrations/registry.ts +7 -0
- package/src/lab/conformance/jcs.ts +42 -2
- package/src/lab/conformance/negative-controls.ts +6 -2
- package/src/lab/conformance/runner.ts +16 -5
- package/src/lab/fabric/observe.ts +49 -14
- package/src/lab/index.ts +16 -0
- package/src/lab/ledger/purge.ts +152 -83
- package/src/lab/ledger/store.ts +168 -54
- package/src/lab/observe/from-conformance.ts +8 -6
- package/src/lab/observe/from-live.ts +8 -2
- package/src/lab/paths.ts +23 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +219 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/lab-activation.ts +109 -47
- package/src/lib/lab-live-pinned-sender.ts +16 -5
- package/src/lib/pinned-http.ts +70 -16
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/upstream-reachability.ts +4 -0
- package/src/lib/windows-elevation.ts +10 -1
- package/src/providers/derive.ts +24 -4
- package/src/providers/registry.ts +7 -3
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +143 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/reasoning-effort.ts +27 -1
- package/src/router.ts +23 -6
- package/src/routing/capability.ts +4 -2
- package/src/routing/compatibility/behavior.ts +5 -1
- package/src/server/adapter-resolve.ts +2 -32
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +74 -36
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +371 -0
- package/src/server/management/combo-routes.ts +16 -2
- package/src/server/management/config-routes.ts +6 -4
- package/src/server/management/context.ts +17 -0
- package/src/server/management/lab-routes.ts +181 -19
- package/src/server/management/model-routes.ts +76 -2
- package/src/server/management/model-rows.ts +8 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +76 -4
- package/src/server/management/system-restart.ts +4 -2
- package/src/server/management/system-routes.ts +38 -0
- package/src/server/relay.ts +17 -3
- package/src/server/responses/compact.ts +4 -1
- package/src/server/responses/core.ts +257 -46
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +16 -2
- package/src/server/responses/terminal-guard.ts +1 -1
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses.ts +17 -2
- package/src/types.ts +66 -4
- package/src/update/index.ts +6 -5
- package/src/update/job.ts +5 -6
- package/src/update/notify.ts +5 -3
- package/src/usage/log.ts +11 -1
- package/gui/dist/assets/index-CQ7bIKee.css +0 -1
- 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(
|
|
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 ${
|
|
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 >
|
|
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 ${
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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,
|