@bitkyc08/opencodex 2.19.0 → 2.21.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-BOFeam5a.js +76 -0
- package/gui/dist/assets/index-Xq49CY8F.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +2 -28
- package/src/adapters/google.ts +31 -3
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat.ts +392 -40
- package/src/adapters/registry.ts +144 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/bridge.ts +15 -2
- package/src/chat/inbound.ts +13 -7
- package/src/claude/gateway-cache.ts +41 -4
- package/src/cli/claude.ts +3 -2
- 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 +241 -85
- package/src/images/loop.ts +26 -6
- 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 +14 -4
- 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/responses/custom-tool-compat.ts +4 -1
- package/src/responses/parser.ts +7 -1
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/schema.ts +6 -0
- 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 +50 -6
- 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/server/system-env.ts +1 -1
- package/src/types.ts +84 -5
- 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/src/web-search/loop.ts +21 -5
- package/gui/dist/assets/index-CQ7bIKee.css +0 -1
- package/gui/dist/assets/index-D_JUZLEC.js +0 -76
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
+
}
|