@bitkyc08/opencodex 2.7.40 → 2.7.41
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 +63 -12
- package/assets/claude-code-models.gif +0 -0
- package/gui/dist/assets/index-B2J4t3te.css +1 -0
- package/gui/dist/assets/index-BmvM6wRb.js +65 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/google.ts +48 -9
- package/src/adapters/kiro-events.ts +15 -3
- package/src/adapters/kiro.ts +292 -28
- package/src/adapters/openai-chat.ts +63 -15
- package/src/adapters/openai-responses.ts +18 -0
- package/src/bridge.ts +76 -21
- package/src/chat/outbound.ts +66 -34
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p.ts +121 -21
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/inbound-debug.ts +4 -0
- package/src/claude/model-info.ts +9 -3
- package/src/cli/account-extended.ts +34 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/claude-desktop.ts +152 -0
- package/src/cli/claude.ts +37 -3
- package/src/cli/doctor.ts +187 -6
- package/src/cli/help.ts +13 -1
- package/src/cli/index.ts +151 -57
- package/src/cli/status-oauth.ts +68 -0
- package/src/cli/status.ts +4 -0
- package/src/codex/account-lifecycle.ts +31 -0
- package/src/codex/auth-api.ts +133 -26
- package/src/codex/auth-collision.ts +55 -11
- package/src/codex/auth-context.ts +50 -6
- package/src/codex/catalog/provider-fetch.ts +6 -2
- package/src/codex/home.ts +61 -1
- package/src/codex/inject.ts +28 -66
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +39 -3
- package/src/codex/main-account-cache.ts +25 -0
- package/src/codex/model-cache.ts +20 -1
- package/src/codex/paths.ts +5 -0
- package/src/codex/routing.ts +138 -11
- package/src/codex/subagent-model-fallback.ts +455 -0
- package/src/codex/sync.ts +17 -0
- package/src/combos/failover.ts +10 -2
- package/src/combos/index.ts +1 -0
- package/src/combos/types.ts +9 -0
- package/src/config.ts +117 -0
- package/src/grok/inject.ts +339 -0
- package/src/grok/status.ts +88 -0
- package/src/grok/sync.ts +66 -0
- package/src/lib/destination-policy.ts +13 -0
- package/src/lib/errors.ts +59 -0
- package/src/lib/privacy.ts +9 -0
- package/src/lib/process-control.ts +57 -4
- package/src/oauth/health.ts +375 -0
- package/src/oauth/index.ts +69 -22
- package/src/oauth/kiro.ts +5 -3
- package/src/oauth/log.ts +48 -0
- package/src/oauth/store.ts +55 -6
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +143 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/api-keys.ts +5 -5
- package/src/providers/derive.ts +22 -1
- package/src/providers/free-directory.ts +181 -0
- package/src/providers/key-failover.ts +2 -2
- package/src/providers/kiro-models.ts +3 -2
- package/src/providers/openai-tiers.ts +1 -1
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/registry.ts +49 -2
- package/src/responses/parser.ts +49 -20
- package/src/responses/state.ts +156 -2
- package/src/router.ts +73 -5
- package/src/server/chat-completions.ts +78 -22
- package/src/server/claude-messages.ts +8 -0
- package/src/server/images.ts +2 -1
- package/src/server/index.ts +50 -30
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +337 -18
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/combo-routes.ts +3 -3
- package/src/server/management/config-routes.ts +4 -4
- package/src/server/management/logs-usage-routes.ts +64 -4
- package/src/server/management/model-routes.ts +114 -8
- package/src/server/management/oauth-account-routes.ts +31 -5
- package/src/server/management/provider-routes.ts +42 -11
- package/src/server/management/shared.ts +81 -2
- package/src/server/management/system-routes.ts +6 -1
- package/src/server/management-api.ts +24 -7
- package/src/server/port-reclaim.ts +261 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/collaboration.ts +11 -6
- package/src/server/responses/compact.ts +2 -1
- package/src/server/responses/core.ts +358 -155
- package/src/server/responses/passthrough-error.ts +53 -0
- package/src/server/search.ts +2 -1
- package/src/server/system-env.ts +23 -6
- package/src/server/windows-tcp-drop.ts +174 -0
- package/src/service.ts +43 -3
- package/src/types.ts +74 -6
- package/src/update/index.ts +30 -19
- package/src/update/job.ts +30 -15
- package/src/usage/log.ts +158 -2
- package/src/usage/summary.ts +8 -4
- package/src/web-search/loop.ts +4 -1
- package/gui/dist/assets/index-CMip1DzF.css +0 -1
- package/gui/dist/assets/index-cydcmbzC.js +0 -52
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
multiAgentGuidanceEnabled,
|
|
11
11
|
providerBaseUrlConfigError,
|
|
12
12
|
providerHeadersConfigError,
|
|
13
|
-
|
|
13
|
+
saveConfigPreservingClaudeCode,
|
|
14
14
|
} from "../../config";
|
|
15
15
|
import {
|
|
16
16
|
clearLoginState,
|
|
@@ -34,9 +34,14 @@ import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
|
34
34
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
35
35
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
36
36
|
import { scanStorage } from "../../storage/scanner";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
currentUsageLogRevision,
|
|
39
|
+
readUsageSnapshotForManagement,
|
|
40
|
+
usageLogRevisionKey,
|
|
41
|
+
type PersistedUsageEntry,
|
|
42
|
+
} from "../../usage/log";
|
|
38
43
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
39
|
-
import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
44
|
+
import { parseRange, parseUsageSurface, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
|
|
40
45
|
import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
41
46
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
42
47
|
import { getDebugLogEntries } from "../../lib/debug-log-buffer";
|
|
@@ -60,6 +65,48 @@ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostR
|
|
|
60
65
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
61
66
|
import type { ManagementContext } from "./context";
|
|
62
67
|
|
|
68
|
+
const USAGE_DAY_MS = 86_400_000;
|
|
69
|
+
const usageSummaryCache = new Map<string, {
|
|
70
|
+
revisionKey: string;
|
|
71
|
+
expiresAt: number;
|
|
72
|
+
summary: UsageSummary;
|
|
73
|
+
}>();
|
|
74
|
+
|
|
75
|
+
function usageEntryMatchesSurface(entry: PersistedUsageEntry, surface: UsageSurface): boolean {
|
|
76
|
+
if (surface === "claude") return entry.surface === "claude" || entry.surface === "claude-desktop";
|
|
77
|
+
if (surface === "grok") return entry.surface === "grok";
|
|
78
|
+
if (surface === "codex") return entry.surface === undefined;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function nextLocalMidnight(now: number): number {
|
|
83
|
+
const next = new Date(now);
|
|
84
|
+
next.setHours(24, 0, 0, 0);
|
|
85
|
+
return next.getTime();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function usageSummaryExpiresAt(
|
|
89
|
+
entries: PersistedUsageEntry[],
|
|
90
|
+
range: UsageRange,
|
|
91
|
+
surface: UsageSurface,
|
|
92
|
+
now: number,
|
|
93
|
+
): number {
|
|
94
|
+
let expiresAt = nextLocalMidnight(now);
|
|
95
|
+
const windowMs = range === "7d" ? 7 * USAGE_DAY_MS : range === "30d" ? 30 * USAGE_DAY_MS : null;
|
|
96
|
+
if (windowMs === null) return expiresAt;
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
if (!usageEntryMatchesSurface(entry, surface)) continue;
|
|
99
|
+
const expiry = entry.timestamp + windowMs;
|
|
100
|
+
if (expiry > now && expiry < expiresAt) expiresAt = expiry;
|
|
101
|
+
}
|
|
102
|
+
return expiresAt;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function refreshedUsageSummary(summary: UsageSummary, range: UsageRange, now: number): UsageSummary {
|
|
106
|
+
const since = range === "7d" ? now - 7 * USAGE_DAY_MS : range === "30d" ? now - 30 * USAGE_DAY_MS : null;
|
|
107
|
+
return { ...summary, since, generatedAt: now };
|
|
108
|
+
}
|
|
109
|
+
|
|
63
110
|
export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
64
111
|
const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
|
|
65
112
|
|
|
@@ -123,7 +170,20 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
123
170
|
const surface = parseUsageSurface(url.searchParams.get("surface"));
|
|
124
171
|
const now = Date.now();
|
|
125
172
|
try {
|
|
126
|
-
|
|
173
|
+
const cacheKey = `${range}:${surface}`;
|
|
174
|
+
const observedRevisionKey = usageLogRevisionKey(currentUsageLogRevision());
|
|
175
|
+
const cached = usageSummaryCache.get(cacheKey);
|
|
176
|
+
if (cached && cached.revisionKey === observedRevisionKey && now < cached.expiresAt) {
|
|
177
|
+
return jsonResponse(refreshedUsageSummary(cached.summary, range, now));
|
|
178
|
+
}
|
|
179
|
+
const snapshot = await readUsageSnapshotForManagement();
|
|
180
|
+
const summary = summarizeUsage(snapshot.entries, range, now, surface);
|
|
181
|
+
usageSummaryCache.set(cacheKey, {
|
|
182
|
+
revisionKey: usageLogRevisionKey(snapshot.revision),
|
|
183
|
+
expiresAt: usageSummaryExpiresAt(snapshot.entries, range, surface, now),
|
|
184
|
+
summary,
|
|
185
|
+
});
|
|
186
|
+
return jsonResponse(summary);
|
|
127
187
|
} catch {
|
|
128
188
|
return jsonResponse({
|
|
129
189
|
range,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import type { CatalogModel } from "../../codex/catalog";
|
|
4
|
-
import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
4
|
+
import { catalogModelSlug, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
5
|
+
import { getProviderLiveModelCount } from "../../codex/model-cache";
|
|
5
6
|
import {
|
|
6
7
|
DEFAULT_SUBAGENT_MODELS,
|
|
7
8
|
codexAutoStartEnabled,
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
multiAgentGuidanceEnabled,
|
|
11
12
|
providerBaseUrlConfigError,
|
|
12
13
|
providerHeadersConfigError,
|
|
13
|
-
|
|
14
|
+
saveConfigPreservingClaudeCode,
|
|
14
15
|
} from "../../config";
|
|
15
16
|
import {
|
|
16
17
|
clearLoginState,
|
|
@@ -27,6 +28,7 @@ import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/ke
|
|
|
27
28
|
import { deriveProviderPresets } from "../../providers/derive";
|
|
28
29
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
29
30
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
31
|
+
import { COMBO_NAMESPACE, comboModelId, comboPublicModelId, preservesPhysicalComboProvider } from "../../combos";
|
|
30
32
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
31
33
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
32
34
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
@@ -122,12 +124,111 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
122
124
|
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
123
125
|
const disabled = Array.isArray(body.models) ? body.models.filter((m): m is string => typeof m === "string") : [];
|
|
124
126
|
config.disabledModels = disabled;
|
|
125
|
-
const {
|
|
127
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
126
128
|
save(config);
|
|
127
129
|
await refreshCodexCatalogBestEffort();
|
|
128
130
|
return jsonResponse({ ok: true, disabled });
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
// One user-facing visibility switch spans two persisted filters: a provider allowlist and the
|
|
134
|
+
// shared blocklist. Keep the update atomic so an interrupted request cannot expose a half-applied
|
|
135
|
+
// state. Native rows only use the blocklist; routed/custom rows also join a non-empty allowlist.
|
|
136
|
+
if (url.pathname === "/api/model-visibility" && req.method === "PUT") {
|
|
137
|
+
let parsedBody: unknown;
|
|
138
|
+
try { parsedBody = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
139
|
+
if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid model visibility request" }, 400);
|
|
140
|
+
const body = parsedBody;
|
|
141
|
+
const scope = body.scope === "models" || body.scope === "provider" ? body.scope : null;
|
|
142
|
+
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
143
|
+
if (!scope || !provider || !isValidProviderName(provider) || typeof body.enabled !== "boolean" || !Array.isArray(body.targets)) {
|
|
144
|
+
return jsonResponse({ error: "invalid model visibility request" }, 400);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const providerConfig = hasOwnProvider(config.providers, provider) ? config.providers[provider] : undefined;
|
|
148
|
+
const isVirtualComboNamespace = provider === COMBO_NAMESPACE && !preservesPhysicalComboProvider(config);
|
|
149
|
+
if (!providerConfig && provider !== "openai" && !isVirtualComboNamespace) {
|
|
150
|
+
return jsonResponse({ error: "unknown model visibility provider" }, 400);
|
|
151
|
+
}
|
|
152
|
+
const supportedNative = new Set(nativeModelRows(config).map(row => row.slug));
|
|
153
|
+
const targets: Array<{ id: string; native: boolean }> = [];
|
|
154
|
+
const seen = new Set<string>();
|
|
155
|
+
for (const value of body.targets) {
|
|
156
|
+
if (!isPlainRecord(value) || typeof value.id !== "string" || (value.native !== undefined && typeof value.native !== "boolean")) {
|
|
157
|
+
return jsonResponse({ error: "invalid model visibility target" }, 400);
|
|
158
|
+
}
|
|
159
|
+
const id = value.id.trim();
|
|
160
|
+
const native = value.native === true;
|
|
161
|
+
if (!id || (provider === "openai") !== native || (native && !supportedNative.has(id))) {
|
|
162
|
+
return jsonResponse({ error: "invalid model visibility target" }, 400);
|
|
163
|
+
}
|
|
164
|
+
const key = `${native ? "native" : "routed"}:${id}`;
|
|
165
|
+
if (!seen.has(key)) {
|
|
166
|
+
seen.add(key);
|
|
167
|
+
targets.push({ id, native });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (targets.length === 0) return jsonResponse({ error: "model visibility targets required" }, 400);
|
|
171
|
+
|
|
172
|
+
const knownComboSelectors = new Set(
|
|
173
|
+
Object.entries(config.combos ?? {}).flatMap(([id, combo]) => [
|
|
174
|
+
comboModelId(id),
|
|
175
|
+
comboPublicModelId(id, combo),
|
|
176
|
+
]),
|
|
177
|
+
);
|
|
178
|
+
const targetComboSelectors = new Map<string, Set<string>>();
|
|
179
|
+
if (isVirtualComboNamespace) {
|
|
180
|
+
for (const target of targets) {
|
|
181
|
+
const combo = config.combos && Object.hasOwn(config.combos, target.id) ? config.combos[target.id] : undefined;
|
|
182
|
+
if (!combo) return jsonResponse({ error: "invalid model visibility target" }, 400);
|
|
183
|
+
targetComboSelectors.set(target.id, new Set([comboModelId(target.id), comboPublicModelId(target.id, combo)]));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const matchesTarget = (stored: string, target: { id: string; native: boolean }) => target.native
|
|
187
|
+
? stored === target.id
|
|
188
|
+
: isVirtualComboNamespace
|
|
189
|
+
? targetComboSelectors.get(target.id)!.has(stored)
|
|
190
|
+
: slugEquals(stored, provider, target.id);
|
|
191
|
+
|
|
192
|
+
let disabled = [...new Set(config.disabledModels ?? [])];
|
|
193
|
+
if (body.enabled) {
|
|
194
|
+
if (scope === "provider") {
|
|
195
|
+
if (providerConfig && !isVirtualComboNamespace) delete providerConfig.selectedModels;
|
|
196
|
+
if (isVirtualComboNamespace) {
|
|
197
|
+
disabled = disabled.filter(stored => !knownComboSelectors.has(stored));
|
|
198
|
+
} else {
|
|
199
|
+
const nativeIds = provider === "openai"
|
|
200
|
+
? disabledNativeSlugs({ disabledModels: disabled })
|
|
201
|
+
: new Set<string>();
|
|
202
|
+
disabled = disabled.filter(stored => (
|
|
203
|
+
knownComboSelectors.has(stored)
|
|
204
|
+
|| (!stored.startsWith(`${provider}/`) && !nativeIds.has(stored))
|
|
205
|
+
));
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
if (!isVirtualComboNamespace && providerConfig?.selectedModels && providerConfig.selectedModels.length > 0) {
|
|
209
|
+
const additions = targets.filter(target => !target.native).map(target => target.id);
|
|
210
|
+
providerConfig.selectedModels = [...new Set([...providerConfig.selectedModels, ...additions])];
|
|
211
|
+
}
|
|
212
|
+
disabled = disabled.filter(stored => !targets.some(target => matchesTarget(stored, target)));
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
for (const target of targets) {
|
|
216
|
+
const canonical = target.native
|
|
217
|
+
? target.id
|
|
218
|
+
: isVirtualComboNamespace
|
|
219
|
+
? comboModelId(target.id)
|
|
220
|
+
: routedSlug(provider, target.id);
|
|
221
|
+
const alreadyDisabled = disabled.some(stored => matchesTarget(stored, target));
|
|
222
|
+
if (!alreadyDisabled) disabled.push(canonical);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
config.disabledModels = disabled;
|
|
227
|
+
saveConfigPreservingClaudeCode(config);
|
|
228
|
+
await refreshCodexCatalogBestEffort();
|
|
229
|
+
return jsonResponse({ ok: true, scope, provider, enabled: body.enabled, disabled });
|
|
230
|
+
}
|
|
231
|
+
|
|
131
232
|
if (url.pathname === "/api/custom-models" && req.method === "GET") {
|
|
132
233
|
return jsonResponse(config.customModels ?? []);
|
|
133
234
|
}
|
|
@@ -160,7 +261,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
160
261
|
addedAt: new Date().toISOString(),
|
|
161
262
|
};
|
|
162
263
|
config.customModels = [...existing, entry];
|
|
163
|
-
const {
|
|
264
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
164
265
|
save(config);
|
|
165
266
|
await refreshCodexCatalogBestEffort();
|
|
166
267
|
return jsonResponse(entry, 201);
|
|
@@ -197,7 +298,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
197
298
|
}
|
|
198
299
|
list[idx] = cm;
|
|
199
300
|
config.customModels = list;
|
|
200
|
-
const {
|
|
301
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
201
302
|
save(config);
|
|
202
303
|
await refreshCodexCatalogBestEffort();
|
|
203
304
|
return jsonResponse(cm);
|
|
@@ -212,7 +313,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
212
313
|
if (idx === -1) return jsonResponse({ error: "not found" }, 404);
|
|
213
314
|
list.splice(idx, 1);
|
|
214
315
|
config.customModels = list.length > 0 ? list : undefined;
|
|
215
|
-
const {
|
|
316
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
216
317
|
save(config);
|
|
217
318
|
await refreshCodexCatalogBestEffort();
|
|
218
319
|
return jsonResponse({ ok: true });
|
|
@@ -226,10 +327,15 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
226
327
|
const available: Record<string, string[]> = {};
|
|
227
328
|
for (const m of models) (available[m.provider] ??= []).push(m.id);
|
|
228
329
|
const selected: Record<string, string[]> = {};
|
|
330
|
+
// Live-catalog provenance. The GUI cannot infer this by subtracting known custom ids: an id
|
|
331
|
+
// that is both custom and discovered would make a real live catalog look custom-only.
|
|
332
|
+
const liveModelCounts: Record<string, number> = {};
|
|
229
333
|
for (const [name, prov] of Object.entries(config.providers)) {
|
|
230
334
|
if (Array.isArray(prov.selectedModels) && prov.selectedModels.length > 0) selected[name] = [...prov.selectedModels];
|
|
335
|
+
const liveCount = getProviderLiveModelCount(name);
|
|
336
|
+
if (liveCount !== undefined) liveModelCounts[name] = liveCount;
|
|
231
337
|
}
|
|
232
|
-
return jsonResponse({ selected, available });
|
|
338
|
+
return jsonResponse({ selected, available, liveModelCounts });
|
|
233
339
|
}
|
|
234
340
|
if (url.pathname === "/api/selected-models" && req.method === "PUT") {
|
|
235
341
|
let body: { provider?: unknown; models?: unknown };
|
|
@@ -244,7 +350,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
244
350
|
// Empty list clears the allowlist (provider reverts to exposing all models).
|
|
245
351
|
if (models.length > 0) config.providers[provider].selectedModels = models;
|
|
246
352
|
else delete config.providers[provider].selectedModels;
|
|
247
|
-
const {
|
|
353
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
248
354
|
save(config);
|
|
249
355
|
await refreshCodexCatalogBestEffort();
|
|
250
356
|
return jsonResponse({ ok: true, provider, selected: models });
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
multiAgentGuidanceEnabled,
|
|
11
11
|
providerBaseUrlConfigError,
|
|
12
12
|
providerHeadersConfigError,
|
|
13
|
-
|
|
13
|
+
saveConfigPreservingClaudeCode,
|
|
14
14
|
} from "../../config";
|
|
15
15
|
import {
|
|
16
16
|
clearLoginState,
|
|
@@ -55,6 +55,7 @@ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerS
|
|
|
55
55
|
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
56
56
|
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
57
57
|
import { applySystemEnvToggle } from "../system-env";
|
|
58
|
+
import { buildApiAccessEndpoints } from "./api-access";
|
|
58
59
|
|
|
59
60
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
60
61
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
@@ -157,7 +158,24 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
157
158
|
const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
|
|
158
159
|
if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
|
|
159
160
|
const status = getLoginStatus(provider);
|
|
160
|
-
|
|
161
|
+
const { getAccountSet } = await import("../../oauth/store");
|
|
162
|
+
const {
|
|
163
|
+
oauthAccountHealthFields,
|
|
164
|
+
projectOAuthAccountHealth,
|
|
165
|
+
projectStoredOAuthAccountHealth,
|
|
166
|
+
} = await import("../../oauth/health");
|
|
167
|
+
const set = getAccountSet(provider);
|
|
168
|
+
const accounts = (status.accounts ?? []).map(summary => {
|
|
169
|
+
const full = set?.accounts.find(account => account.id === summary.id);
|
|
170
|
+
const health = full
|
|
171
|
+
? projectStoredOAuthAccountHealth(provider, full)
|
|
172
|
+
: projectOAuthAccountHealth({
|
|
173
|
+
needsReauth: summary.needsReauth === true,
|
|
174
|
+
reauthReason: summary.needsReauth === true ? "refresh_failed" : undefined,
|
|
175
|
+
});
|
|
176
|
+
return { ...summary, ...oauthAccountHealthFields(provider, summary.id, health) };
|
|
177
|
+
});
|
|
178
|
+
return jsonResponse({ activeAccountId: status.activeAccountId ?? null, accounts });
|
|
161
179
|
}
|
|
162
180
|
if (url.pathname === "/api/oauth/accounts/active" && req.method === "PUT") {
|
|
163
181
|
const body = await req.json().catch(() => ({})) as { provider?: string; accountId?: string };
|
|
@@ -272,7 +290,15 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
272
290
|
// ---------------------------------------------------------------------------
|
|
273
291
|
if (url.pathname === "/api/keys" && req.method === "GET") {
|
|
274
292
|
const keys = config.apiKeys ?? [];
|
|
275
|
-
|
|
293
|
+
const endpoints = buildApiAccessEndpoints(config, {
|
|
294
|
+
requestUrl: req.url,
|
|
295
|
+
requestHost: req.headers.get("host"),
|
|
296
|
+
requestOrigin: req.headers.get("origin"),
|
|
297
|
+
});
|
|
298
|
+
return jsonResponse({
|
|
299
|
+
keys: keys.map(k => ({ id: k.id, name: k.name, prefix: k.key.slice(0, 8) + "...", createdAt: k.createdAt })),
|
|
300
|
+
...endpoints,
|
|
301
|
+
}, 200, req, config);
|
|
276
302
|
}
|
|
277
303
|
|
|
278
304
|
if (url.pathname === "/api/keys" && req.method === "POST") {
|
|
@@ -286,7 +312,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
286
312
|
const key = "ocx_" + Buffer.from(hashBuf).toString("hex").slice(0, 40);
|
|
287
313
|
const entry = { id: crypto.randomUUID(), name, key, createdAt: new Date().toISOString() };
|
|
288
314
|
config.apiKeys = [...(config.apiKeys ?? []), entry];
|
|
289
|
-
|
|
315
|
+
saveConfigPreservingClaudeCode(config);
|
|
290
316
|
return jsonResponse({ id: entry.id, name: entry.name, key: entry.key, createdAt: entry.createdAt }, 201, req, config);
|
|
291
317
|
}
|
|
292
318
|
|
|
@@ -294,7 +320,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
294
320
|
const body = await req.json() as { id?: string };
|
|
295
321
|
if (!body.id) return jsonResponse({ error: "id required" }, 400, req, config);
|
|
296
322
|
config.apiKeys = (config.apiKeys ?? []).filter(k => k.id !== body.id);
|
|
297
|
-
|
|
323
|
+
saveConfigPreservingClaudeCode(config);
|
|
298
324
|
return jsonResponse({ success: true }, 200, req, config);
|
|
299
325
|
}
|
|
300
326
|
return null;
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
multiAgentGuidanceEnabled,
|
|
11
11
|
providerBaseUrlConfigError,
|
|
12
12
|
providerHeadersConfigError,
|
|
13
|
-
|
|
13
|
+
saveConfigPreservingClaudeCode,
|
|
14
14
|
} from "../../config";
|
|
15
15
|
import {
|
|
16
16
|
clearLoginState,
|
|
@@ -28,7 +28,7 @@ import { deriveProviderPresets } from "../../providers/derive";
|
|
|
28
28
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
29
29
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
30
30
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
31
|
-
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
31
|
+
import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
32
32
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
33
33
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
34
34
|
import { getProviderDiscoveryStatus } from "../../codex/model-cache";
|
|
@@ -101,12 +101,15 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
101
101
|
}
|
|
102
102
|
// Hostname destinations additionally get a DNS-resolved SSRF check at write time —
|
|
103
103
|
// the sync check above only classifies literal IPs (review finding, PR #96).
|
|
104
|
-
|
|
104
|
+
// Canonical openai still runs the resolver: only Clash fake-IP (198.18.0.0/15)
|
|
105
|
+
// answers are ignored; loopback/RFC1918/metadata/mixed sets still fail.
|
|
106
|
+
const allowBenchmarkAddresses = name === "openai" && isCanonicalOpenAiForwardProvider(prov);
|
|
107
|
+
const resolvedError = await providerDestinationResolvedError(name, prov, { allowBenchmarkAddresses });
|
|
105
108
|
if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
|
|
106
109
|
// Catalog providers (e.g. ollama-cloud) carry a models + vision/reasoning classification the GUI
|
|
107
110
|
// doesn't send — merge it in so the sidecars are gated correctly.
|
|
108
111
|
enrichProviderFromCatalog(name, prov);
|
|
109
|
-
const {
|
|
112
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
110
113
|
// Overwriting an existing provider must not drop its multi-key pool: carry it over, then
|
|
111
114
|
// let the (possibly new) apiKey join the pool as the active entry.
|
|
112
115
|
const existingPool = config.providers[name]?.apiKeyPool;
|
|
@@ -148,7 +151,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
148
151
|
if (!provider || !isCanonicalOpenAiForwardProvider(provider)) {
|
|
149
152
|
return jsonResponse({ error: "provider openai must be the canonical built-in provider" }, 400);
|
|
150
153
|
}
|
|
151
|
-
const {
|
|
154
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
152
155
|
config.providers.openai = { ...provider, codexAccountMode: mode };
|
|
153
156
|
save(config);
|
|
154
157
|
(deps.clearProviderQuotaCache ?? clearProviderQuotaCache)();
|
|
@@ -233,18 +236,46 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
233
236
|
|
|
234
237
|
if (!touched) return jsonResponse({ error: "no recognized fields to update" }, 400);
|
|
235
238
|
|
|
236
|
-
// A disabled-only toggle preserves the v2 fast lane: it changes
|
|
237
|
-
// not the provider shape
|
|
238
|
-
//
|
|
239
|
+
// A disabled-only toggle preserves the v2 fast lane for non-openai providers: it changes
|
|
240
|
+
// routing eligibility, not the provider shape. Re-enabling `openai` is different — a
|
|
241
|
+
// malformed disabled row must not come back online unchanged, so canonicalize/reject
|
|
242
|
+
// against the same built-in gate used by mode PATCH and POST.
|
|
239
243
|
const editorTouched = keys.some(key => key !== "disabled");
|
|
244
|
+
const enablingOpenAi = name === "openai"
|
|
245
|
+
&& Object.hasOwn(rawBody, "disabled")
|
|
246
|
+
&& rawBody.disabled === false
|
|
247
|
+
&& config.providers[name]?.disabled === true;
|
|
240
248
|
if (editorTouched) {
|
|
241
249
|
const providerError = providerManagementConfigError(name, next);
|
|
242
250
|
if (providerError) return jsonResponse({ error: providerError }, 400);
|
|
243
251
|
const resolvedError = await providerDestinationResolvedError(name, next);
|
|
244
252
|
if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
|
|
253
|
+
} else if (enablingOpenAi) {
|
|
254
|
+
if (!isCanonicalOpenAiForwardProvider(next)) {
|
|
255
|
+
return jsonResponse({ error: "provider openai must be the canonical built-in provider" }, 400);
|
|
256
|
+
}
|
|
257
|
+
// Persist the byte-identical canonical URL so config.ts startup checks (case-sensitive)
|
|
258
|
+
// accept the row after we fill mode. Equivalent hosts like CHATGPT.com/:443 normalize here.
|
|
259
|
+
next.baseUrl = CODEX_FORWARD_BASE_URL;
|
|
260
|
+
// Fill missing mode so a disabled canonical row becomes a complete live openai entry.
|
|
261
|
+
if (next.codexAccountMode !== "pool" && next.codexAccountMode !== "direct") {
|
|
262
|
+
next.codexAccountMode = "pool";
|
|
263
|
+
}
|
|
264
|
+
// Same DNS gate as POST: Clash fake-IP only. Never honor a persisted
|
|
265
|
+
// allowPrivateNetwork on this path — it must not bypass the built-in guard.
|
|
266
|
+
const resolvedError = await providerDestinationResolvedError(
|
|
267
|
+
"openai",
|
|
268
|
+
{ baseUrl: CODEX_FORWARD_BASE_URL },
|
|
269
|
+
{ allowBenchmarkAddresses: true },
|
|
270
|
+
);
|
|
271
|
+
if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
|
|
272
|
+
if (next.disabled === false) delete next.disabled;
|
|
273
|
+
// Canonical openai never uses private-network opt-in; drop a stale flag that
|
|
274
|
+
// was ignored for the DNS probe so it cannot linger on the live row.
|
|
275
|
+
delete next.allowPrivateNetwork;
|
|
245
276
|
}
|
|
246
277
|
|
|
247
|
-
const {
|
|
278
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
248
279
|
config.providers[name] = stripRegistryOnlyStaticHeaders(name, next);
|
|
249
280
|
save(config);
|
|
250
281
|
if (editorTouched) {
|
|
@@ -335,7 +366,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
335
366
|
combos: dependentCombos,
|
|
336
367
|
}, 409);
|
|
337
368
|
}
|
|
338
|
-
const {
|
|
369
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
339
370
|
delete config.providers[name];
|
|
340
371
|
setProviderContextCap(config, name, false);
|
|
341
372
|
save(config);
|
|
@@ -352,7 +383,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
352
383
|
if (url.pathname === "/api/provider-context-caps" && req.method === "PUT") {
|
|
353
384
|
let body: { provider?: unknown; enabled?: unknown; value?: unknown; setAll?: unknown };
|
|
354
385
|
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
355
|
-
const {
|
|
386
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
356
387
|
const { clearModelCache } = await import("../../codex/model-cache");
|
|
357
388
|
const respond = () => jsonResponse({ ok: true, cap: DEFAULT_PROVIDER_CONTEXT_CAP, value: globalContextCapValue(config), caps: providerContextCaps(config) });
|
|
358
389
|
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
multiAgentGuidanceEnabled,
|
|
11
11
|
providerBaseUrlConfigError,
|
|
12
12
|
providerHeadersConfigError,
|
|
13
|
-
|
|
13
|
+
saveConfigPreservingClaudeCode,
|
|
14
14
|
} from "../../config";
|
|
15
15
|
import {
|
|
16
16
|
clearLoginState,
|
|
@@ -48,7 +48,8 @@ import {
|
|
|
48
48
|
setDebugSettings,
|
|
49
49
|
type DebugFlag,
|
|
50
50
|
} from "../../lib/debug-settings";
|
|
51
|
-
import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
|
|
51
|
+
import type { OcxClaudeCodeConfig, OcxClaudeDesktopProfile, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
|
|
52
|
+
import type { DesktopProfileModel } from "../../claude/desktop-profile";
|
|
52
53
|
import { drainAndShutdown } from "../lifecycle";
|
|
53
54
|
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
54
55
|
import { estimateComboCost, estimateRequestCost, effectiveServiceTier, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
@@ -172,6 +173,34 @@ export async function fetchAllModels(config: OcxConfig): Promise<CatalogModel[]>
|
|
|
172
173
|
return gatherRoutedModels(config);
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
export interface GrokCandidateModel {
|
|
177
|
+
id: string;
|
|
178
|
+
contextWindow?: number;
|
|
179
|
+
native: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The model list `syncGrokConfig` would inject, BEFORE the user's exclusions. The Grok
|
|
184
|
+
* page needs this to show a switch for a model the user has already excluded — such a
|
|
185
|
+
* model is absent from the fence, so readGrokStatus alone could never list it. Built
|
|
186
|
+
* from the same two sources as the sync so the two can never disagree.
|
|
187
|
+
*/
|
|
188
|
+
export async function fetchGrokCandidateModels(config: OcxConfig): Promise<GrokCandidateModel[]> {
|
|
189
|
+
const { filterCatalogVisibleModels, nativeOpenAiContextWindow, visibleNativeSlugs } = await import("../../codex/catalog");
|
|
190
|
+
const routed = filterCatalogVisibleModels(await fetchAllModels(config), config);
|
|
191
|
+
return [
|
|
192
|
+
...visibleNativeSlugs(config).map(id => {
|
|
193
|
+
const contextWindow = nativeOpenAiContextWindow(id);
|
|
194
|
+
return { id, native: true, ...(contextWindow !== undefined ? { contextWindow } : {}) };
|
|
195
|
+
}),
|
|
196
|
+
...routed.map(m => ({
|
|
197
|
+
id: m.alias ?? `${m.provider}/${m.id}`,
|
|
198
|
+
native: false,
|
|
199
|
+
...(m.contextWindow !== undefined ? { contextWindow: m.contextWindow } : {}),
|
|
200
|
+
})),
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
|
|
175
204
|
export function stripRegistryOnlyStaticHeaders(name: string, provider: OcxProviderConfig): OcxProviderConfig {
|
|
176
205
|
const entry = getProviderRegistryEntry(name);
|
|
177
206
|
if (!entry?.staticHeaders || !provider.headers) return provider;
|
|
@@ -184,3 +213,53 @@ export function stripRegistryOnlyStaticHeaders(name: string, provider: OcxProvid
|
|
|
184
213
|
return rest;
|
|
185
214
|
}
|
|
186
215
|
|
|
216
|
+
/** Shared Desktop profile DTO builder for the management API and CLI. */
|
|
217
|
+
export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxClaudeDesktopProfile) {
|
|
218
|
+
const { filterCatalogVisibleModels, nativeOpenAiContextWindow, visibleNativeSlugs } = await import("../../codex/catalog");
|
|
219
|
+
const { DESKTOP_SUPPORTS_1M_THRESHOLD } = await import("../../claude/desktop-3p");
|
|
220
|
+
const { reconcileDesktopProfile, renderDesktopProfile } = await import("../../claude/desktop-profile");
|
|
221
|
+
const routed = filterCatalogVisibleModels(await fetchAllModels(config), config);
|
|
222
|
+
const profileModels: DesktopProfileModel[] = [
|
|
223
|
+
// Native rows carry their real context window from the same accessor the Grok sync
|
|
224
|
+
// uses — otherwise Sol's 372k and gpt-5.5's 272k render as blank on Desktop.
|
|
225
|
+
...visibleNativeSlugs(config).map(id => {
|
|
226
|
+
const contextWindow = nativeOpenAiContextWindow(id);
|
|
227
|
+
return { route: `native/${id}`, label: `${id} (native)`,
|
|
228
|
+
...(contextWindow !== undefined ? { contextWindow } : {}) };
|
|
229
|
+
}),
|
|
230
|
+
...routed.map(model => ({
|
|
231
|
+
route: `${model.provider}/${model.id}`,
|
|
232
|
+
label: `${model.id} (${model.provider})`,
|
|
233
|
+
...(typeof model.contextWindow === "number" ? { contextWindow: model.contextWindow } : {}),
|
|
234
|
+
})),
|
|
235
|
+
];
|
|
236
|
+
const profile = reconcileDesktopProfile(stored ?? config.claudeCode?.desktopProfile, profileModels);
|
|
237
|
+
const available = new Set(profileModels.map(model => model.route));
|
|
238
|
+
const modelByRoute = new Map(profileModels.map(model => [model.route, model]));
|
|
239
|
+
// Effort support: routed models with a non-empty reasoningEfforts ladder support effort;
|
|
240
|
+
// native models always support it (Anthropic native effort).
|
|
241
|
+
const effortByRoute = new Map<string, boolean>();
|
|
242
|
+
for (const m of routed) {
|
|
243
|
+
effortByRoute.set(`${m.provider}/${m.id}`, Array.isArray(m.reasoningEfforts) && m.reasoningEfforts.length > 0);
|
|
244
|
+
}
|
|
245
|
+
for (const id of visibleNativeSlugs(config)) {
|
|
246
|
+
effortByRoute.set(`native/${id}`, true);
|
|
247
|
+
}
|
|
248
|
+
const models = Object.keys(profile.assignments).sort().map(route => ({
|
|
249
|
+
route,
|
|
250
|
+
label: modelByRoute.get(route)?.label ?? route,
|
|
251
|
+
available: available.has(route),
|
|
252
|
+
...(modelByRoute.get(route)?.contextWindow ? { contextWindow: modelByRoute.get(route)!.contextWindow } : {}),
|
|
253
|
+
effortSupported: effortByRoute.get(route) ?? false,
|
|
254
|
+
// Read-only view of the 1M capability the written Desktop config already emits,
|
|
255
|
+
// derived from the SAME threshold so the dashboard chip can never disagree.
|
|
256
|
+
supports1m: (modelByRoute.get(route)?.contextWindow ?? 0) >= DESKTOP_SUPPORTS_1M_THRESHOLD,
|
|
257
|
+
assignment: profile.assignments[route]!,
|
|
258
|
+
}));
|
|
259
|
+
return {
|
|
260
|
+
profile,
|
|
261
|
+
models,
|
|
262
|
+
rendered: renderDesktopProfile(profile, profileModels),
|
|
263
|
+
port: config.port,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
* The payload is scalar-only (numbers, enum strings): no paths, no tokens, no
|
|
10
10
|
* account identifiers. `jscHeap` (bun:jsc heapStats) is the js-vs-native
|
|
11
11
|
* discriminator: a flat JS heap under a growing RSS points at native runtime
|
|
12
|
-
* memory (the #314 shape), not an app-level JS leak.
|
|
12
|
+
* memory (the #314 shape), not an app-level JS leak. `responseState` attributes
|
|
13
|
+
* JS-heap growth further: it is the proxy's previous_response_id continuation
|
|
14
|
+
* store, so a growing responseState.totalBytes under a growing heap points the
|
|
15
|
+
* finger at conversation retention rather than the runtime allocator.
|
|
13
16
|
*/
|
|
14
17
|
import { decideEagerRelay } from "../../lib/bun-stream-caps";
|
|
15
18
|
import { getActiveMemoryWatchdog } from "../memory-watchdog";
|
|
19
|
+
import { responseStateMetrics } from "../../responses/state";
|
|
16
20
|
import { jsonResponse } from "../auth-cors";
|
|
17
21
|
import type { ManagementContext } from "./context";
|
|
18
22
|
|
|
@@ -56,6 +60,7 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
56
60
|
heapUsed: usage.heapUsed,
|
|
57
61
|
heapTotal: usage.heapTotal,
|
|
58
62
|
jscHeap,
|
|
63
|
+
responseState: responseStateMetrics(),
|
|
59
64
|
streamMode,
|
|
60
65
|
eagerRelay: process.platform === "win32" ? decideEagerRelay(streamMode) : null,
|
|
61
66
|
watchdog,
|