@bitkyc08/opencodex 2.8.2-preview.20260731 → 2.9.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/README.md +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-u5eFOv2y.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +86 -5
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +89 -14
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/effort.ts +16 -6
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +8 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro.ts +10 -2
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +239 -97
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +75 -32
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +37 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +173 -53
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +51 -12
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +23 -3
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +45 -18
- package/src/types.ts +34 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
- package/gui/dist/assets/index-GC0Vlu1Z.js +0 -67
|
@@ -25,6 +25,13 @@ import { removeCredential } from "../../oauth/store";
|
|
|
25
25
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
26
26
|
import { isStreamMode } from "../../lib/bun-stream-caps";
|
|
27
27
|
import { shadowSourceModels } from "../../lib/shadow-call";
|
|
28
|
+
import {
|
|
29
|
+
configureAppOwnedMemoryBudget,
|
|
30
|
+
enforceAppOwnedMemoryBudget,
|
|
31
|
+
MAX_APP_OWNED_MEMORY_BUDGET_MB,
|
|
32
|
+
MIN_APP_OWNED_MEMORY_BUDGET_MB,
|
|
33
|
+
resolveAppOwnedMemoryBudgetBytes,
|
|
34
|
+
} from "../../lib/app-owned-memory";
|
|
28
35
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
29
36
|
import { deriveProviderPresets } from "../../providers/derive";
|
|
30
37
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
@@ -64,6 +71,7 @@ import { displayCodexRuntimePath, effortClampAppliesToRuntime, loadLastEffortCla
|
|
|
64
71
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
65
72
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
66
73
|
import type { ManagementContext } from "./context";
|
|
74
|
+
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
67
75
|
|
|
68
76
|
export async function handleConfigRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
69
77
|
const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
|
|
@@ -120,6 +128,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
120
128
|
port: config.port,
|
|
121
129
|
hostname: config.hostname ?? "127.0.0.1",
|
|
122
130
|
streamMode: config.streamMode ?? "auto",
|
|
131
|
+
appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
|
|
123
132
|
startupHealth: await getCachedStartupHealth(config),
|
|
124
133
|
codexRuntime: {
|
|
125
134
|
path: displayCodexRuntimePath(resolved.runtime.command),
|
|
@@ -147,7 +156,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
147
156
|
|
|
148
157
|
if (url.pathname === "/api/startup-action" && req.method === "POST") {
|
|
149
158
|
let body: { action?: unknown; repair?: unknown };
|
|
150
|
-
try { body = await req
|
|
159
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
151
160
|
if (!body || !["install-service", "install-shim"].includes(String(body.action))) {
|
|
152
161
|
return jsonResponse({ error: "action must be install-service or install-shim" }, 400);
|
|
153
162
|
}
|
|
@@ -176,7 +185,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
176
185
|
|
|
177
186
|
if (url.pathname === "/api/windows-tray" && req.method === "POST") {
|
|
178
187
|
let body: { action?: unknown };
|
|
179
|
-
try { body = await req
|
|
188
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
180
189
|
if (!body || !["install", "start", "stop", "uninstall"].includes(String(body.action))) {
|
|
181
190
|
return jsonResponse({ error: "action must be install, start, stop, or uninstall" }, 400);
|
|
182
191
|
}
|
|
@@ -191,15 +200,15 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
191
200
|
|
|
192
201
|
if (url.pathname === "/api/settings" && req.method === "PUT") {
|
|
193
202
|
// Each field is optional but at least one must be present; fields are
|
|
194
|
-
// validated when present. streamMode-only PUTs must work: Windows
|
|
195
|
-
// troubleshooting
|
|
196
|
-
// not inherit shell env
|
|
203
|
+
// validated when present. streamMode-only PUTs must work: Windows/macOS
|
|
204
|
+
// memory troubleshooting can use this persisted stream-shape escape hatch
|
|
205
|
+
// (a Windows service does not inherit shell env). A stream-shape
|
|
197
206
|
// change applies to NEW turns only — the config object is shared by
|
|
198
207
|
// reference with the request handlers, no restart needed.
|
|
199
|
-
let body: { codexAutoStart?: unknown; streamMode?: unknown };
|
|
200
|
-
try { body = await req
|
|
201
|
-
if (body.codexAutoStart === undefined && body.streamMode === undefined) {
|
|
202
|
-
return jsonResponse({ error: "codexAutoStart
|
|
208
|
+
let body: { codexAutoStart?: unknown; streamMode?: unknown; appOwnedMemoryBudgetMb?: unknown };
|
|
209
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
210
|
+
if (body.codexAutoStart === undefined && body.streamMode === undefined && body.appOwnedMemoryBudgetMb === undefined) {
|
|
211
|
+
return jsonResponse({ error: "provide codexAutoStart, streamMode, or appOwnedMemoryBudgetMb" }, 400);
|
|
203
212
|
}
|
|
204
213
|
if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
|
|
205
214
|
return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
|
|
@@ -207,6 +216,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
207
216
|
if (body.streamMode !== undefined && !isStreamMode(body.streamMode)) {
|
|
208
217
|
return jsonResponse({ error: "streamMode must be auto, legacy-tee, or eager-relay" }, 400);
|
|
209
218
|
}
|
|
219
|
+
if (body.appOwnedMemoryBudgetMb !== undefined && (
|
|
220
|
+
typeof body.appOwnedMemoryBudgetMb !== "number"
|
|
221
|
+
|| !Number.isInteger(body.appOwnedMemoryBudgetMb)
|
|
222
|
+
|| body.appOwnedMemoryBudgetMb < MIN_APP_OWNED_MEMORY_BUDGET_MB
|
|
223
|
+
|| body.appOwnedMemoryBudgetMb > MAX_APP_OWNED_MEMORY_BUDGET_MB
|
|
224
|
+
)) {
|
|
225
|
+
return jsonResponse({ error: `appOwnedMemoryBudgetMb must be an integer from ${MIN_APP_OWNED_MEMORY_BUDGET_MB} to ${MAX_APP_OWNED_MEMORY_BUDGET_MB}` }, 400);
|
|
226
|
+
}
|
|
210
227
|
if (typeof body.codexAutoStart === "boolean") {
|
|
211
228
|
config.codexAutoStart = body.codexAutoStart;
|
|
212
229
|
}
|
|
@@ -217,12 +234,20 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
217
234
|
config.streamMode = body.streamMode as "legacy-tee" | "eager-relay";
|
|
218
235
|
}
|
|
219
236
|
}
|
|
237
|
+
if (typeof body.appOwnedMemoryBudgetMb === "number") {
|
|
238
|
+
config.appOwnedMemoryBudgetMb = body.appOwnedMemoryBudgetMb;
|
|
239
|
+
}
|
|
220
240
|
saveConfigPreservingClaudeCode(config);
|
|
241
|
+
if (typeof body.appOwnedMemoryBudgetMb === "number") {
|
|
242
|
+
configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(body.appOwnedMemoryBudgetMb));
|
|
243
|
+
enforceAppOwnedMemoryBudget();
|
|
244
|
+
}
|
|
221
245
|
invalidateStartupHealthCache();
|
|
222
246
|
return jsonResponse({
|
|
223
247
|
ok: true,
|
|
224
248
|
codexAutoStart: codexAutoStartEnabled(config),
|
|
225
249
|
streamMode: config.streamMode ?? "auto",
|
|
250
|
+
appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
|
|
226
251
|
startupHealth: await getCachedStartupHealth(config),
|
|
227
252
|
});
|
|
228
253
|
}
|
|
@@ -255,7 +280,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
255
280
|
if (url.pathname === "/api/update/run" && req.method === "POST") {
|
|
256
281
|
const { normalizeUpdateChannel, startUpdateJob, UpdateJobError } = await import("../../update/job");
|
|
257
282
|
let body: { tag?: unknown; restart?: unknown };
|
|
258
|
-
try { body = await req
|
|
283
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
259
284
|
if (body.tag !== undefined && body.tag !== "latest" && body.tag !== "preview") {
|
|
260
285
|
return jsonResponse({ error: "tag must be latest or preview" }, 400);
|
|
261
286
|
}
|
|
@@ -294,7 +319,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
294
319
|
|
|
295
320
|
if (url.pathname === "/api/sidecar-settings" && req.method === "PUT") {
|
|
296
321
|
let raw: unknown;
|
|
297
|
-
try { raw = await req
|
|
322
|
+
try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
298
323
|
// Strict shape (review F2): reject non-object bodies and non-object sections instead of throwing
|
|
299
324
|
// on `null` or silently accepting arrays/strings as no-op updates.
|
|
300
325
|
if (!isPlainRecord(raw)) return jsonResponse({ error: "body must be a JSON object" }, 400);
|
|
@@ -369,7 +394,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
369
394
|
|
|
370
395
|
if (url.pathname === "/api/shadow-call-settings" && req.method === "PUT") {
|
|
371
396
|
let raw: unknown;
|
|
372
|
-
try { raw = await req
|
|
397
|
+
try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
373
398
|
if (!isPlainRecord(raw)) return jsonResponse({ error: "body must be a JSON object" }, 400);
|
|
374
399
|
const body = raw as { enabled?: unknown; model?: unknown };
|
|
375
400
|
if (body.enabled !== undefined && typeof body.enabled !== "boolean") {
|
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
} from "../../lib/debug-settings";
|
|
69
69
|
import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
|
|
70
70
|
import { drainAndShutdown } from "../lifecycle";
|
|
71
|
-
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
71
|
+
import { filterRequestLogs, filteredRequestLogCount, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
72
72
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
73
73
|
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
74
74
|
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
@@ -77,14 +77,14 @@ import { applySystemEnvToggle } from "../system-env";
|
|
|
77
77
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
78
78
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
79
79
|
import type { ManagementContext } from "./context";
|
|
80
|
+
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
81
|
+
import {
|
|
82
|
+
discardUsageSummaryCacheEntry,
|
|
83
|
+
getUsageSummaryCacheEntry,
|
|
84
|
+
setUsageSummaryCacheEntry,
|
|
85
|
+
} from "./usage-summary-cache";
|
|
80
86
|
|
|
81
87
|
const USAGE_DAY_MS = 86_400_000;
|
|
82
|
-
const usageSummaryCache = new Map<string, {
|
|
83
|
-
revisionKey: string;
|
|
84
|
-
expiresAt: number;
|
|
85
|
-
summary: UsageSummary;
|
|
86
|
-
}>();
|
|
87
|
-
|
|
88
88
|
function usageEntryMatchesSurface(entry: PersistedUsageEntry, surface: UsageSurface): boolean {
|
|
89
89
|
if (surface === "claude") return entry.surface === "claude" || entry.surface === "claude-desktop";
|
|
90
90
|
if (surface === "grok") return entry.surface === "grok";
|
|
@@ -115,7 +115,7 @@ function usageSummaryExpiresAt(
|
|
|
115
115
|
return expiresAt;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
function refreshedUsageSummary(summary:
|
|
118
|
+
function refreshedUsageSummary<T extends UsageSummary & { historyTruncated: boolean }>(summary: T, range: UsageRange, now: number): T {
|
|
119
119
|
const since = range === "7d" ? now - 7 * USAGE_DAY_MS : range === "30d" ? now - 30 * USAGE_DAY_MS : null;
|
|
120
120
|
return { ...summary, since, generatedAt: now };
|
|
121
121
|
}
|
|
@@ -124,8 +124,14 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
124
124
|
const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
|
|
125
125
|
|
|
126
126
|
if (url.pathname === "/api/logs" && req.method === "GET") {
|
|
127
|
-
const
|
|
128
|
-
|
|
127
|
+
const all = getRequestLogEntries();
|
|
128
|
+
const total = filteredRequestLogCount(all, url.searchParams);
|
|
129
|
+
const logs = filterRequestLogs(all, url.searchParams);
|
|
130
|
+
return jsonResponse({
|
|
131
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
132
|
+
total,
|
|
133
|
+
logs: logs.map(requestLogDto),
|
|
134
|
+
});
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
if (url.pathname === "/api/debug" && req.method === "GET") {
|
|
@@ -155,7 +161,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
155
161
|
|
|
156
162
|
if (url.pathname === "/api/debug" && req.method === "PUT") {
|
|
157
163
|
let body: { debug?: unknown; usage?: unknown; injection?: unknown; claude?: unknown; reset?: unknown };
|
|
158
|
-
try { body = await req
|
|
164
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
159
165
|
if (body.reset === true) return jsonResponse(clearDebugSettings());
|
|
160
166
|
if (body.reset === "debug" || body.reset === "provider") return jsonResponse(clearDebugSetting("debug"));
|
|
161
167
|
if (body.reset === "usage") return jsonResponse(clearDebugSetting("usage"));
|
|
@@ -184,16 +190,26 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
184
190
|
const now = Date.now();
|
|
185
191
|
try {
|
|
186
192
|
const cacheKey = `${range}:${surface}`;
|
|
187
|
-
const
|
|
188
|
-
const
|
|
193
|
+
const effectiveReadLimit = config.managementUsageMaxReadBytes ?? 64 * 1024 * 1024;
|
|
194
|
+
const observedRevisionKey = `${usageLogRevisionKey(currentUsageLogRevision())}\0${effectiveReadLimit}`;
|
|
195
|
+
const cached = getUsageSummaryCacheEntry(cacheKey);
|
|
189
196
|
if (cached && cached.revisionKey === observedRevisionKey && now < cached.expiresAt) {
|
|
190
197
|
return jsonResponse(refreshedUsageSummary(cached.summary, range, now));
|
|
191
198
|
}
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
if (cached) discardUsageSummaryCacheEntry(cacheKey);
|
|
200
|
+
const snapshot = await readUsageSnapshotForManagement(effectiveReadLimit);
|
|
201
|
+
const revisionReadAt = Date.now();
|
|
202
|
+
const summary = {
|
|
203
|
+
...summarizeUsage(snapshot.entries, range, now, surface),
|
|
204
|
+
historyTruncated: snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated,
|
|
205
|
+
truncatedPrefixBytes: snapshot.truncatedPrefixBytes,
|
|
206
|
+
entriesTruncated: snapshot.entriesTruncated,
|
|
207
|
+
entriesDropped: snapshot.entriesDropped,
|
|
208
|
+
};
|
|
209
|
+
setUsageSummaryCacheEntry(cacheKey, {
|
|
210
|
+
revisionKey: `${usageLogRevisionKey(snapshot.revision)}\0${effectiveReadLimit}`,
|
|
196
211
|
expiresAt: usageSummaryExpiresAt(snapshot.entries, range, surface, now),
|
|
212
|
+
revisionReadAt,
|
|
197
213
|
summary,
|
|
198
214
|
});
|
|
199
215
|
return jsonResponse(summary);
|
|
@@ -227,6 +243,10 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
227
243
|
days: [],
|
|
228
244
|
models: [],
|
|
229
245
|
providers: [],
|
|
246
|
+
historyTruncated: false,
|
|
247
|
+
truncatedPrefixBytes: 0,
|
|
248
|
+
entriesTruncated: false,
|
|
249
|
+
entriesDropped: 0,
|
|
230
250
|
error: "read_failed",
|
|
231
251
|
});
|
|
232
252
|
}
|
|
@@ -248,7 +268,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
248
268
|
|
|
249
269
|
if (url.pathname === "/api/storage/cleanup/preview" && req.method === "POST") {
|
|
250
270
|
let body: { percent?: unknown };
|
|
251
|
-
try { body = await req
|
|
271
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
|
|
252
272
|
const percent = typeof body?.percent === "number" ? body.percent : Number.NaN;
|
|
253
273
|
if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
|
|
254
274
|
return jsonResponse({ error: "invalid_percent" }, 400);
|
|
@@ -272,7 +292,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
272
292
|
|
|
273
293
|
if (url.pathname === "/api/storage/cleanup" && req.method === "POST") {
|
|
274
294
|
let body: { percent?: unknown; mode?: unknown; digest?: unknown; _test?: unknown };
|
|
275
|
-
try { body = await req
|
|
295
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
|
|
276
296
|
const percent = typeof body?.percent === "number" ? body.percent : Number.NaN;
|
|
277
297
|
if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
|
|
278
298
|
return jsonResponse({ error: "invalid_percent" }, 400);
|
|
@@ -373,7 +393,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
373
393
|
|
|
374
394
|
if (url.pathname === "/api/storage/trash/restore" && req.method === "POST") {
|
|
375
395
|
let body: { id?: unknown };
|
|
376
|
-
try { body = await req
|
|
396
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
|
|
377
397
|
const id = typeof body?.id === "string" ? body.id : "";
|
|
378
398
|
if (!id.trim()) {
|
|
379
399
|
return jsonResponse({ error: "invalid_trash", message: "Trash entry id is required." }, 400);
|
|
@@ -453,7 +473,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
453
473
|
|
|
454
474
|
if (url.pathname === "/api/storage/cleanup-policy" && req.method === "PUT") {
|
|
455
475
|
let raw: unknown;
|
|
456
|
-
try { raw = await req
|
|
476
|
+
try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
|
|
457
477
|
const previous = normalizeStorageCleanupPolicy(config.storageCleanupPolicy);
|
|
458
478
|
const parsed = parseStorageCleanupPolicyInput(raw, previous);
|
|
459
479
|
if (!parsed.ok) return jsonResponse({ error: parsed.error }, 400);
|
|
@@ -31,6 +31,7 @@ function readInputModalities(raw: unknown): { values?: string[]; error?: string
|
|
|
31
31
|
}
|
|
32
32
|
import type { CatalogModel } from "../../codex/catalog";
|
|
33
33
|
import { catalogModelSlug, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
34
|
+
import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
|
|
34
35
|
import { getProviderLiveModelCount } from "../../codex/model-cache";
|
|
35
36
|
import {
|
|
36
37
|
DEFAULT_SUBAGENT_MODELS,
|
|
@@ -85,10 +86,126 @@ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerS
|
|
|
85
86
|
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
86
87
|
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO, corsHeaders } from "../auth-cors";
|
|
87
88
|
import { applySystemEnvToggle } from "../system-env";
|
|
89
|
+
import {
|
|
90
|
+
EXPORT_CLIENTS,
|
|
91
|
+
EXPORT_CLIENT_IDS,
|
|
92
|
+
OPENCODE_PROVIDER_ID,
|
|
93
|
+
buildClientConfig,
|
|
94
|
+
isExportClientId,
|
|
95
|
+
opencodeProxyBaseUrl,
|
|
96
|
+
} from "../../clients/config-export";
|
|
97
|
+
import type {
|
|
98
|
+
ExportClientId,
|
|
99
|
+
ExportModel,
|
|
100
|
+
OpencodeGeneratedConfig,
|
|
101
|
+
PiGeneratedConfig,
|
|
102
|
+
} from "../../clients/config-export";
|
|
88
103
|
|
|
89
104
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
90
105
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
91
106
|
import type { ManagementContext } from "./context";
|
|
107
|
+
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* One row of the `/api/models` list. Routed rows spread a `CatalogModel`, so the shape is
|
|
111
|
+
* that model plus the identity/visibility fields this boundary computes for every row
|
|
112
|
+
* regardless of source. `disabled` is always present; the rest vary by row origin.
|
|
113
|
+
*/
|
|
114
|
+
type ManagementModelRow = Partial<CatalogModel> & {
|
|
115
|
+
provider: string;
|
|
116
|
+
id: string;
|
|
117
|
+
namespaced: string;
|
|
118
|
+
disabled: boolean;
|
|
119
|
+
native?: boolean;
|
|
120
|
+
custom?: boolean;
|
|
121
|
+
customId?: string;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The exact row list `/api/models` returns. Extracted so `/api/client-config` exports the
|
|
126
|
+
* models the GUI's Models tab shows — including this function's `disabled` computation,
|
|
127
|
+
* which the export core (src/clients/config-export.ts) deliberately does not perform.
|
|
128
|
+
*/
|
|
129
|
+
async function listManagementModelRows(config: OcxConfig): Promise<ManagementModelRow[]> {
|
|
130
|
+
const models = await fetchAllModels(config);
|
|
131
|
+
const disabled = new Set(config.disabledModels ?? []);
|
|
132
|
+
// Native GPT passthrough rows lead (provider "openai", bare-slug namespaced ids): sourced
|
|
133
|
+
// from the static supported set so a disabled model stays listed and re-enableable.
|
|
134
|
+
const native: ManagementModelRow[] = nativeModelRows(config).map(row => ({
|
|
135
|
+
provider: "openai",
|
|
136
|
+
id: row.slug,
|
|
137
|
+
namespaced: row.slug,
|
|
138
|
+
disabled: row.disabled,
|
|
139
|
+
native: true,
|
|
140
|
+
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
141
|
+
}));
|
|
142
|
+
const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
|
|
143
|
+
const namespaced = routedSlug(cm.provider, cm.modelId);
|
|
144
|
+
return {
|
|
145
|
+
provider: cm.provider,
|
|
146
|
+
id: cm.modelId,
|
|
147
|
+
namespaced,
|
|
148
|
+
disabled: [...disabled].some(stored => slugEquals(stored, cm.provider, cm.modelId)),
|
|
149
|
+
custom: true,
|
|
150
|
+
customId: cm.id,
|
|
151
|
+
displayName: cm.displayName,
|
|
152
|
+
...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
|
|
153
|
+
...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
const publicModels = uniqueCatalogModelsForPublicList(models);
|
|
157
|
+
const comboNamespaced = new Set(
|
|
158
|
+
publicModels.filter(model => model.provider === "combo").map(catalogModelSlug),
|
|
159
|
+
);
|
|
160
|
+
const visibleCustomModels = customModels.filter(model => !comboNamespaced.has(model.namespaced));
|
|
161
|
+
// Custom metadata wins when a physical live/static row resolves to the same Codex-facing
|
|
162
|
+
// slug, while a combo keeps the same precedence it has in routing and /v1/models.
|
|
163
|
+
const customNamespaced = new Set(visibleCustomModels.map(c => c.namespaced));
|
|
164
|
+
const dedupedRouted = publicModels.map((m): ManagementModelRow | null => {
|
|
165
|
+
// Codex-facing slug (one "/", slug-codec); disabledModels compares tolerate both forms.
|
|
166
|
+
const namespaced = catalogModelSlug(m);
|
|
167
|
+
if (m.provider !== "combo" && customNamespaced.has(namespaced)) return null;
|
|
168
|
+
const contextCap = providerContextCap(config, m.provider);
|
|
169
|
+
return {
|
|
170
|
+
...m,
|
|
171
|
+
namespaced,
|
|
172
|
+
disabled: [...disabled].some(stored => (
|
|
173
|
+
stored === namespaced || slugEquals(stored, m.provider, m.id)
|
|
174
|
+
)),
|
|
175
|
+
...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
|
|
176
|
+
};
|
|
177
|
+
}).filter((row): row is ManagementModelRow => row !== null);
|
|
178
|
+
return [...native, ...dedupedRouted, ...visibleCustomModels];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** `/api/models` row → the narrower input the client-config serializers accept. */
|
|
182
|
+
function toExportModel(row: ManagementModelRow): ExportModel {
|
|
183
|
+
return {
|
|
184
|
+
namespaced: row.namespaced,
|
|
185
|
+
provider: row.provider,
|
|
186
|
+
id: row.id,
|
|
187
|
+
...(row.native ? { native: true } : {}),
|
|
188
|
+
...(row.displayName ? { displayName: row.displayName } : {}),
|
|
189
|
+
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
190
|
+
...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Counts read back off the SERIALIZED document rather than recomputed from the input rows.
|
|
196
|
+
* `modelsWithoutLimits` drives a GUI line claiming "these models ship without limits", so it
|
|
197
|
+
* has to describe the bytes the user actually receives — a parallel reimplementation of the
|
|
198
|
+
* core's "authoritative context window" rule would be free to drift from it silently.
|
|
199
|
+
*/
|
|
200
|
+
function summarizeExportedModels(client: ExportClientId, document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
201
|
+
if (client === "opencode") {
|
|
202
|
+
const models = (document as OpencodeGeneratedConfig).provider[OPENCODE_PROVIDER_ID].models;
|
|
203
|
+
const entries = Object.values(models);
|
|
204
|
+
return { modelCount: entries.length, modelsWithoutLimits: entries.filter(entry => entry.limit === undefined).length };
|
|
205
|
+
}
|
|
206
|
+
const models = (document as PiGeneratedConfig).providers[OPENCODE_PROVIDER_ID].models;
|
|
207
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(entry => entry.contextWindow === undefined).length };
|
|
208
|
+
}
|
|
92
209
|
|
|
93
210
|
export async function handleModelRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
94
211
|
const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
|
|
@@ -113,62 +230,65 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
113
230
|
}
|
|
114
231
|
|
|
115
232
|
if (url.pathname === "/api/models" && req.method === "GET") {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
233
|
+
return jsonResponse(await listManagementModelRows(config));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Client config document for OpenCode / Pi, built from the SAME function `ocx export`
|
|
238
|
+
* calls, so the bytes a user downloads here and the bytes they pipe from the CLI cannot
|
|
239
|
+
* disagree. Read-only: this route never writes the user's client config.
|
|
240
|
+
*/
|
|
241
|
+
if (url.pathname === "/api/client-config" && req.method === "GET") {
|
|
242
|
+
const requested = url.searchParams.get("client")?.trim() ?? "";
|
|
243
|
+
if (!isExportClientId(requested)) {
|
|
244
|
+
return jsonResponse(
|
|
245
|
+
{ error: `client must be one of: ${EXPORT_CLIENT_IDS.join(", ")}` },
|
|
246
|
+
400,
|
|
247
|
+
req,
|
|
248
|
+
config,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const spec = EXPORT_CLIENTS[requested];
|
|
252
|
+
let rows: ManagementModelRow[];
|
|
253
|
+
try {
|
|
254
|
+
rows = await listManagementModelRows(config);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// A partial or empty `models` block reads as a valid config while offering nothing,
|
|
257
|
+
// so a catalog failure is surfaced as unavailable rather than serialized. The
|
|
258
|
+
// catalog-busy error keeps its own 503 from handleManagementAPI.
|
|
259
|
+
if (error instanceof CatalogGatherBusyError) throw error;
|
|
260
|
+
return jsonResponse(
|
|
261
|
+
{ error: `model catalog unavailable: ${error instanceof Error ? error.message : String(error)}` },
|
|
262
|
+
503,
|
|
263
|
+
req,
|
|
264
|
+
config,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
// The export core does not filter visibility — it serializes what it is given. A model the
|
|
268
|
+
// user disabled in the Models tab is absent from /v1/models, so exporting it would hand the
|
|
269
|
+
// client a selector the proxy refuses to route.
|
|
270
|
+
const models = rows.filter(row => !row.disabled).map(toExportModel);
|
|
271
|
+
const document = buildClientConfig(requested, {
|
|
272
|
+
baseUrl: opencodeProxyBaseUrl(Number(url.port) || config.port, config.hostname),
|
|
273
|
+
models,
|
|
274
|
+
config,
|
|
141
275
|
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Codex-facing slug (one "/", slug-codec); disabledModels compares tolerate both forms.
|
|
152
|
-
const namespaced = catalogModelSlug(m);
|
|
153
|
-
if (m.provider !== "combo" && customNamespaced.has(namespaced)) return null;
|
|
154
|
-
const contextCap = providerContextCap(config, m.provider);
|
|
155
|
-
return {
|
|
156
|
-
...m,
|
|
157
|
-
namespaced,
|
|
158
|
-
disabled: [...disabled].some(stored => (
|
|
159
|
-
stored === namespaced || slugEquals(stored, m.provider, m.id)
|
|
160
|
-
)),
|
|
161
|
-
...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
|
|
162
|
-
};
|
|
163
|
-
}).filter(Boolean);
|
|
164
|
-
return jsonResponse([...native, ...dedupedRouted, ...visibleCustomModels]);
|
|
276
|
+
return jsonResponse({
|
|
277
|
+
client: spec.id,
|
|
278
|
+
filename: spec.filename,
|
|
279
|
+
destination: spec.destination(process.env),
|
|
280
|
+
apiKeyEnv: spec.apiKeyEnv,
|
|
281
|
+
exportHint: spec.exportHint,
|
|
282
|
+
...summarizeExportedModels(requested, document),
|
|
283
|
+
config: document,
|
|
284
|
+
}, 200, req, config);
|
|
165
285
|
}
|
|
166
286
|
|
|
167
287
|
// Enable/disable models: which routed models Codex sees. PUT hides them from the catalog +
|
|
168
288
|
// /v1/models and invalidates Codex's 5-min models cache so it applies on the next turn.
|
|
169
289
|
if (url.pathname === "/api/disabled-models" && req.method === "PUT") {
|
|
170
290
|
let body: { models?: unknown };
|
|
171
|
-
try { body = await req
|
|
291
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
172
292
|
const disabled = Array.isArray(body.models) ? body.models.filter((m): m is string => typeof m === "string") : [];
|
|
173
293
|
config.disabledModels = disabled;
|
|
174
294
|
persistConfig(config);
|
|
@@ -181,7 +301,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
181
301
|
// state. Native rows only use the blocklist; routed/custom rows also join a non-empty allowlist.
|
|
182
302
|
if (url.pathname === "/api/model-visibility" && req.method === "PUT") {
|
|
183
303
|
let parsedBody: unknown;
|
|
184
|
-
try { parsedBody = await req
|
|
304
|
+
try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
185
305
|
if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid model visibility request" }, 400);
|
|
186
306
|
const body = parsedBody;
|
|
187
307
|
const scope = body.scope === "models" || body.scope === "provider" ? body.scope : null;
|
|
@@ -281,7 +401,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
281
401
|
|
|
282
402
|
if (url.pathname === "/api/custom-models" && req.method === "POST") {
|
|
283
403
|
let body: { provider?: unknown; modelId?: unknown; displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown };
|
|
284
|
-
try { body = await req
|
|
404
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
285
405
|
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
286
406
|
const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
|
|
287
407
|
if (!provider || !modelId) return jsonResponse({ error: "provider and modelId are required" }, 400);
|
|
@@ -319,7 +439,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
319
439
|
let id: string;
|
|
320
440
|
try { id = decodeURIComponent(customPutMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
|
|
321
441
|
let body: { displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown; modelId?: unknown };
|
|
322
|
-
try { body = await req
|
|
442
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
323
443
|
const list = config.customModels ?? [];
|
|
324
444
|
const idx = list.findIndex(cm => cm.id === id);
|
|
325
445
|
if (idx === -1) return jsonResponse({ error: "not found" }, 404);
|
|
@@ -386,7 +506,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
386
506
|
}
|
|
387
507
|
if (url.pathname === "/api/selected-models" && req.method === "PUT") {
|
|
388
508
|
let body: { provider?: unknown; models?: unknown };
|
|
389
|
-
try { body = await req
|
|
509
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
390
510
|
const provider = typeof body.provider === "string" ? body.provider : "";
|
|
391
511
|
if (!provider || !hasOwnProvider(config.providers, provider)) {
|
|
392
512
|
return jsonResponse({ error: "unknown provider" }, provider ? 404 : 400);
|