@bitkyc08/opencodex 2.75.0 → 2.76.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 +6 -1
- package/bin/ocx.mjs +12 -1
- package/gui/dist/assets/App-CddorHdA.js +51 -0
- package/gui/dist/assets/App-DMudtISW.css +1 -0
- package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
- package/gui/dist/assets/index-Btze1I3T.css +1 -0
- package/gui/dist/assets/index-CvnY7E6i.js +86 -0
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/opengateway.svg +12 -0
- package/package.json +1 -1
- package/src/adapters/anthropic/account-metadata.ts +17 -0
- package/src/adapters/anthropic/client-identity.ts +69 -0
- package/src/adapters/anthropic/passthrough.ts +23 -4
- package/src/adapters/anthropic.ts +18 -1
- package/src/adapters/command-code-project-context.ts +7 -11
- package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
- package/src/adapters/kiro-retry.ts +23 -8
- package/src/adapters/openai-chat/messages.ts +1 -1
- package/src/adapters/registry.ts +8 -1
- package/src/adapters/zed.ts +377 -0
- package/src/bridge/sse.ts +4 -2
- package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
- package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
- package/src/chatgpt/app-server-shim/filter.ts +157 -0
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
- package/src/chatgpt/app-server-shim/launcher.ts +91 -0
- package/src/claude/desktop-3p-startup.ts +62 -0
- package/src/claude/desktop-3p.ts +5 -0
- package/src/claude/first-party-settings.ts +14 -0
- package/src/claude/intercept/cli-catalog.ts +150 -0
- package/src/claude/intercept/cli-picker.ts +82 -0
- package/src/claude/intercept/listener.ts +21 -6
- package/src/claude/intercept/picker-bootstrap.ts +23 -5
- package/src/claude/intercept/picker-models.ts +57 -13
- package/src/claude/intercept/runtime.ts +30 -2
- package/src/claude/intercept/settings.ts +3 -0
- package/src/cli/agent.ts +172 -0
- package/src/cli/alias.ts +2 -1
- package/src/cli/capabilities-command.ts +3 -2
- package/src/cli/capabilities.ts +87 -0
- package/src/cli/chatgpt-command.ts +140 -0
- package/src/cli/combo.ts +144 -19
- package/src/cli/config-command.ts +3 -1
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +5 -24
- package/src/cli/init.ts +16 -2
- package/src/cli/integrations.ts +13 -0
- package/src/cli/internal-command.ts +12 -5
- package/src/cli/observe.ts +6 -1
- package/src/cli/registry.ts +6 -0
- package/src/cli/route-policy.ts +3 -1
- package/src/cli/runtime-api.ts +5 -2
- package/src/cli/status-oauth.ts +5 -0
- package/src/cli/status.ts +8 -1
- package/src/cli/stop-approval.ts +55 -16
- package/src/cli/stop-report.ts +5 -0
- package/src/clients/lazycodex.ts +48 -0
- package/src/clients/omo-role-models.ts +111 -0
- package/src/codex/account-credit-use.ts +99 -0
- package/src/codex/account-lifecycle.ts +2 -0
- package/src/codex/agent-role-models.ts +171 -0
- package/src/codex/auth-api/account-list.ts +10 -3
- package/src/codex/auth-api/routes.ts +34 -0
- package/src/codex/auth-context.ts +52 -11
- package/src/codex/catalog/discovered-natives.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +6 -1
- package/src/codex/catalog/model-hints.ts +9 -1
- package/src/codex/catalog/provider-models.ts +60 -0
- package/src/codex/catalog/routed-gather.ts +30 -5
- package/src/codex/convergence-types.ts +2 -1
- package/src/codex/main-account-external-usage.ts +62 -0
- package/src/codex/main-account-hard-lock.ts +24 -13
- package/src/codex/native-profile-processes.ts +53 -16
- package/src/codex/prompt-layers/toml-edit.ts +1 -1
- package/src/codex/quota-types.ts +3 -7
- package/src/codex/quota.ts +19 -4
- package/src/codex/role-auto-assign.ts +159 -0
- package/src/codex/role-sizing-limits.ts +2 -0
- package/src/codex/role-sizing.ts +216 -0
- package/src/codex/routing/selection.ts +36 -0
- package/src/codex/routing/thread-affinity.ts +2 -0
- package/src/codex/routing.ts +4 -4
- package/src/codex/subagent-model-fallback.ts +82 -26
- package/src/codex/subagent-selectable-models.ts +32 -0
- package/src/combos/index.ts +18 -0
- package/src/combos/jev-decision-contract.ts +20 -0
- package/src/combos/jev-dispatch.ts +18 -0
- package/src/combos/jev-model-backend.ts +127 -0
- package/src/combos/jev.ts +249 -33
- package/src/combos/types.ts +125 -2
- package/src/config/diagnostics.ts +18 -0
- package/src/config/macos-system-proxy.ts +72 -15
- package/src/config/owner-registry.ts +13 -4
- package/src/config/process-state.ts +17 -8
- package/src/config/proxy-env.ts +35 -5
- package/src/config/schema/config-schema.ts +30 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config/serving-runtimes.ts +95 -1
- package/src/generated/compatibility-version.json +336 -156
- package/src/lib/errors.ts +7 -7
- package/src/lib/jsonc.ts +13 -0
- package/src/lib/local-chat-completion.ts +144 -0
- package/src/lib/provider-outbound.ts +87 -1
- package/src/lib/provider-runtime-fetch.ts +23 -0
- package/src/lib/provider-tls-profile.ts +221 -0
- package/src/lib/request-execution-budget.ts +16 -6
- package/src/lib/spend-reservation-ledger.ts +54 -20
- package/src/lib/sse-decoder.ts +20 -17
- package/src/lib/synced-state-location.ts +96 -0
- package/src/lib/winsw.ts +14 -5
- package/src/oauth/anthropic-account-refusal.ts +53 -4
- package/src/oauth/anthropic-model-quota.ts +83 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
- package/src/oauth/anthropic-routing.ts +112 -58
- package/src/oauth/health.ts +39 -3
- package/src/oauth/index.ts +14 -0
- package/src/oauth/kiro-account-load.ts +10 -0
- package/src/oauth/zed.ts +134 -0
- package/src/providers/model-discovery.ts +19 -8
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/provider-id-rewrite.ts +9 -2
- package/src/providers/quota/account-cache.ts +29 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
- package/src/providers/quota/anthropic-family-headers.ts +25 -0
- package/src/providers/quota/vendor-probes-oauth.ts +5 -1
- package/src/providers/quota-types.ts +4 -0
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +55 -0
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/xai-fast-model.ts +12 -4
- package/src/providers/zed.ts +598 -0
- package/src/responses/state/snapshot-select.ts +22 -9
- package/src/responses/state.ts +2 -3
- package/src/router.ts +9 -0
- package/src/server/anthropic-rate-limit-headers.ts +7 -0
- package/src/server/auth-cors.ts +7 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/claude-messages.ts +59 -21
- package/src/server/index/claude-intercept-lifecycle.ts +71 -25
- package/src/server/index/optional-listeners.ts +4 -0
- package/src/server/index/spend-ledger-lifecycle.ts +4 -0
- package/src/server/management/agent-settings-routes.ts +74 -37
- package/src/server/management/claude-desktop-picker-routes.ts +7 -0
- package/src/server/management/claude-intercept-routes.ts +35 -0
- package/src/server/management/codex-agent-role-routes.ts +122 -0
- package/src/server/management/codex-role-auto-assign.ts +208 -0
- package/src/server/management/combo-routes.ts +62 -5
- package/src/server/management/config-routes.ts +35 -3
- package/src/server/management/context.ts +4 -1
- package/src/server/management/decision-discovery.ts +74 -0
- package/src/server/management/decision-model-validation.ts +80 -0
- package/src/server/management/decision-routes.ts +160 -0
- package/src/server/management/native-integration-routes.ts +16 -0
- package/src/server/management/provider-routes.ts +30 -34
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management/zed-provider-probe.ts +38 -0
- package/src/server/management-api.ts +12 -1
- package/src/server/messages-native-oauth.ts +7 -2
- package/src/server/messages-native.ts +7 -3
- package/src/server/relay.ts +3 -1
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +18 -3
- package/src/server/responses/compact.ts +14 -1
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-combo.ts +67 -11
- package/src/server/responses/core-normalize.ts +21 -1
- package/src/server/responses/core-options.ts +11 -1
- package/src/server/responses/core.ts +5 -3
- package/src/server/responses/fetch-helpers.ts +6 -3
- package/src/server/responses/jev-model-invoke.ts +190 -0
- package/src/server/responses/passthrough-delivery.ts +16 -0
- package/src/server/responses/policy-fallback.ts +55 -3
- package/src/server/responses/policy-request-scope.ts +80 -0
- package/src/server/responses/request-prepare.ts +70 -18
- package/src/server/responses/request-transport.ts +54 -23
- package/src/server/responses/run-turn-execution.ts +1 -0
- package/src/server/responses/sidecar-execution.ts +9 -3
- package/src/server/responses-hosted-image-display.ts +225 -0
- package/src/service/managing-cli.ts +34 -4
- package/src/service/systemd-env.ts +75 -0
- package/src/service/windows-guarded-manager.ts +60 -19
- package/src/service/windows-scheduler.ts +22 -3
- package/src/service/windows-taskxml.ts +32 -10
- package/src/service-manager-probe.ts +72 -19
- package/src/types/config.ts +31 -1
- package/src/types/provider.ts +2 -0
- package/src/types/request.ts +2 -0
- package/src/update/index.ts +39 -6
- package/src/update/job.ts +63 -63
- package/src/update/npm-cache-preflight.d.mts +33 -1
- package/src/update/npm-cache-preflight.mjs +131 -18
- package/src/update/ownership-transaction.ts +19 -1
- package/src/update/restart-ownership.ts +80 -6
- package/src/update/transactional-install.d.mts +2 -0
- package/src/update/transactional-install.mjs +6 -1
- package/src/usage/jev-stats.ts +28 -1
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +5 -3
- package/src/vision/index.ts +1 -0
- package/src/vision/plan.ts +2 -1
- package/src/vision/routed-describe.ts +29 -121
- package/src/web-search/alpha-search.ts +3 -3
- package/src/web-search/anthropic-executor.ts +6 -4
- package/src/web-search/index.ts +1 -1
- package/src/web-search/loop.ts +3 -3
- package/src/web-search/passthrough-bridge.ts +2 -1
- package/src/web-search/run-turn-loop.ts +1 -1
- package/src/web-search/sidecar-providers.ts +2 -1
- package/gui/dist/assets/App-ia4U0ufY.css +0 -1
- package/gui/dist/assets/App-xm0zGNOe.js +0 -51
- package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
- package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
- package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ensureManagementClaudeIntercept, interceptStartRefusal } from "./claude-intercept-routes";
|
|
1
2
|
/**
|
|
2
3
|
* Claude Desktop picker mode over the management API, and the transition helpers every Desktop
|
|
3
4
|
* mode change uses.
|
|
@@ -76,6 +77,12 @@ export async function handleClaudeDesktopPickerRoutes(ctx: ManagementContext): P
|
|
|
76
77
|
}
|
|
77
78
|
const body = parsePickerBody(raw);
|
|
78
79
|
if (typeof body === "string") return jsonResponse({ error: body }, 400);
|
|
80
|
+
if (body.enabled) {
|
|
81
|
+
const refusal = interceptStartRefusal(ctx);
|
|
82
|
+
if (refusal) return refusal;
|
|
83
|
+
const started = await ensureManagementClaudeIntercept(ctx);
|
|
84
|
+
if (!started.ok) return jsonResponse({ ...started, code: started.reason }, 409);
|
|
85
|
+
}
|
|
79
86
|
const controller = await currentPickerController();
|
|
80
87
|
if (body.enabled) {
|
|
81
88
|
if (!controller) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ManagementContext } from "./context";
|
|
2
|
+
import { jsonResponse } from "../auth-cors";
|
|
3
|
+
import { claudeInterceptEnabled, getClaudeInterceptState, type ClaudeInterceptOutcome } from "../../claude/intercept/runtime";
|
|
4
|
+
|
|
5
|
+
/** Local management authority only; data-plane credentials never grant this capability. */
|
|
6
|
+
export function interceptStartRefusal(ctx: ManagementContext): Response | null {
|
|
7
|
+
return ctx.trustedLoopbackIngress && (ctx.principal === "gui-session" || ctx.principal === "admin-token")
|
|
8
|
+
? null : jsonResponse({ ok: false, code: "intercept_start_forbidden", error: "Local management authority is required." }, 403);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function ensureManagementClaudeIntercept(ctx: ManagementContext): Promise<ClaudeInterceptOutcome> {
|
|
12
|
+
if (ctx.config.runtimeRole === "client") return { ok: false, reason: "client_role" };
|
|
13
|
+
if (!claudeInterceptEnabled(ctx.config)) return { ok: false, reason: "disabled" };
|
|
14
|
+
if (ctx.deps.ensureClaudeIntercept) return ctx.deps.ensureClaudeIntercept();
|
|
15
|
+
const injected = ctx.deps.getClaudeInterceptState?.();
|
|
16
|
+
if (injected) return { ok: true, state: injected };
|
|
17
|
+
const listener = ctx.deps.linkListener?.();
|
|
18
|
+
if (listener?.ensureClaudeIntercept) return listener.ensureClaudeIntercept();
|
|
19
|
+
const state = (ctx.deps.getClaudeInterceptState ?? getClaudeInterceptState)();
|
|
20
|
+
return state ? { ok: true, state } : { ok: false, reason: "failed" };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function interceptStatus(ctx: ManagementContext): { interceptReason: string | null; pickerReason: string | null; pickerFailurePort?: number; interceptFailurePort?: number } {
|
|
24
|
+
const outcome = ctx.deps.linkListener?.()?.claudeInterceptOutcome?.();
|
|
25
|
+
const state = (ctx.deps.getClaudeInterceptState ?? getClaudeInterceptState)();
|
|
26
|
+
return { ...(outcome && !outcome.ok ? { interceptFailurePort: outcome.port } : {}), interceptReason: outcome && !outcome.ok ? outcome.reason : null, pickerReason: state?.pickerReason ?? null, pickerFailurePort: state?.pickerFailurePort };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function handleClaudeInterceptRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
30
|
+
if (ctx.url.pathname !== "/api/claude-intercept/start" || ctx.req.method !== "POST") return null;
|
|
31
|
+
const refusal = interceptStartRefusal(ctx);
|
|
32
|
+
if (refusal) return refusal;
|
|
33
|
+
const outcome = await ensureManagementClaudeIntercept(ctx);
|
|
34
|
+
return jsonResponse(outcome, outcome.ok ? 200 : 409);
|
|
35
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-role model for omo (Codex / LazyCodex): `$CODEX_HOME/agents/<role>.toml` plus LazyCodex's
|
|
3
|
+
* `codex.agents.<role>.model` mirror in omo.jsonc. Both halves exist only when LazyCodex is
|
|
4
|
+
* detected; Pi-based and OpenCode-based omo are never read here.
|
|
5
|
+
*
|
|
6
|
+
* Loaded on demand from `src/server/management-api.ts`, like the quota-reset handler, so a
|
|
7
|
+
* dashboard request that never opens the omo tab loads neither writer.
|
|
8
|
+
*/
|
|
9
|
+
import { jsonResponse } from "../auth-cors";
|
|
10
|
+
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
11
|
+
import type { ManagementContext } from "./context";
|
|
12
|
+
|
|
13
|
+
const ROLE_PATH_PREFIX = "/api/codex-agent-roles/";
|
|
14
|
+
const AUTO_ASSIGN_PATH = "/api/codex-agent-roles/auto-assign";
|
|
15
|
+
|
|
16
|
+
function lazycodexNotDetected(ctx: ManagementContext): Response {
|
|
17
|
+
return jsonResponse({
|
|
18
|
+
error: "omo (Codex / LazyCodex) is not installed in this CODEX_HOME",
|
|
19
|
+
code: "lazycodex_not_detected",
|
|
20
|
+
}, 409, ctx.req, ctx.config);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
24
|
+
const { req, url, config, deps } = ctx;
|
|
25
|
+
const [{ getCodexHome }, roles, omo, { detectLazyCodex }] = await Promise.all([
|
|
26
|
+
import("../../codex/paths"),
|
|
27
|
+
import("../../codex/agent-role-models"),
|
|
28
|
+
import("../../clients/omo-role-models"),
|
|
29
|
+
import("../../clients/lazycodex"),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
if (url.pathname === "/api/codex-agent-roles" && req.method === "GET") {
|
|
33
|
+
const codexHome = getCodexHome();
|
|
34
|
+
const lazycodex = detectLazyCodex(codexHome);
|
|
35
|
+
if (!lazycodex.detected) return jsonResponse({ lazycodex, omoJsonc: null, roles: [] }, 200, req, config);
|
|
36
|
+
const omoState = omo.readOmoRoleModels(omo.omoJsoncPath());
|
|
37
|
+
const omoModels = omoState.state === "present" ? omoState.models : {};
|
|
38
|
+
return jsonResponse({
|
|
39
|
+
lazycodex,
|
|
40
|
+
omoJsonc: { state: omoState.state },
|
|
41
|
+
roles: roles.listCodexAgentRoleModels(codexHome).map(entry => ({
|
|
42
|
+
...entry,
|
|
43
|
+
omoJsoncModel: omoModels[entry.role] ?? null,
|
|
44
|
+
})),
|
|
45
|
+
}, 200, req, config);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (url.pathname === AUTO_ASSIGN_PATH && req.method === "POST") {
|
|
49
|
+
const codexHome = getCodexHome();
|
|
50
|
+
if (!detectLazyCodex(codexHome).detected) return lazycodexNotDetected(ctx);
|
|
51
|
+
let body: { model?: unknown };
|
|
52
|
+
try {
|
|
53
|
+
body = await readManagementJsonBody(req);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
rethrowManagementBodyTooLarge(error);
|
|
56
|
+
return jsonResponse({ error: "invalid JSON body" }, 400, req, config);
|
|
57
|
+
}
|
|
58
|
+
if (body?.model !== undefined && typeof body.model !== "string") {
|
|
59
|
+
return jsonResponse({ error: "model must be a string", code: "invalid_model" }, 400, req, config);
|
|
60
|
+
}
|
|
61
|
+
const [{ proposeCodexRoleModels, NoSizingModelError }, { fetchAllModels }] = await Promise.all([
|
|
62
|
+
import("./codex-role-auto-assign"),
|
|
63
|
+
import("./shared"),
|
|
64
|
+
]);
|
|
65
|
+
try {
|
|
66
|
+
return jsonResponse(await proposeCodexRoleModels({
|
|
67
|
+
config,
|
|
68
|
+
codexHome,
|
|
69
|
+
...(typeof body?.model === "string" ? { sizingModel: body.model } : {}),
|
|
70
|
+
fetchAllModels: deps.fetchAllModels ?? fetchAllModels,
|
|
71
|
+
...(deps.completeCodexRoleSizing ? { completeRoleSizing: deps.completeCodexRoleSizing } : {}),
|
|
72
|
+
}), 200, req, config);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error instanceof NoSizingModelError) {
|
|
75
|
+
return jsonResponse({ error: error.message, code: "no_sizing_model" }, 409, req, config);
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!url.pathname.startsWith(ROLE_PATH_PREFIX) || req.method !== "PUT") return null;
|
|
82
|
+
const codexHome = getCodexHome();
|
|
83
|
+
if (!detectLazyCodex(codexHome).detected) return lazycodexNotDetected(ctx);
|
|
84
|
+
let role: string;
|
|
85
|
+
try {
|
|
86
|
+
role = decodeURIComponent(url.pathname.slice(ROLE_PATH_PREFIX.length));
|
|
87
|
+
} catch {
|
|
88
|
+
return jsonResponse({ error: "role must be URL-encoded", code: "invalid_role" }, 400, req, config);
|
|
89
|
+
}
|
|
90
|
+
let body: { model?: unknown; effort?: unknown };
|
|
91
|
+
try {
|
|
92
|
+
body = await readManagementJsonBody(req);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
rethrowManagementBodyTooLarge(error);
|
|
95
|
+
return jsonResponse({ error: "invalid JSON body" }, 400, req, config);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let model: string;
|
|
99
|
+
let effort: string | undefined;
|
|
100
|
+
let toml: { status: "written" | "unchanged" };
|
|
101
|
+
try {
|
|
102
|
+
model = roles.validateAgentRoleModel(body?.model);
|
|
103
|
+
effort = body?.effort === undefined || body.effort === null ? undefined : roles.validateAgentRoleEffort(body.effort);
|
|
104
|
+
toml = roles.writeCodexAgentRoleModel(role, model, codexHome, effort);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof roles.AgentRoleModelError) {
|
|
107
|
+
const status = error.code === "unknown_role" ? 404 : error.code === "invalid_model" || error.code === "invalid_effort" ? 400 : 409;
|
|
108
|
+
return jsonResponse({ error: error.message, code: error.code }, status, req, config);
|
|
109
|
+
}
|
|
110
|
+
// Filesystem and ownership errors carry absolute paths and UIDs; keep them out of the response.
|
|
111
|
+
return jsonResponse({ error: "could not write the role file", code: "write_failed" }, 500, req, config);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The role TOML is what Codex obeys, so its write stands even when the omo mirror cannot follow.
|
|
115
|
+
let omoStatus: ReturnType<typeof omo.writeOmoRoleModel> | "write_failed";
|
|
116
|
+
try {
|
|
117
|
+
omoStatus = omo.writeOmoRoleModel(role, model, omo.omoJsoncPath());
|
|
118
|
+
} catch {
|
|
119
|
+
omoStatus = "write_failed";
|
|
120
|
+
}
|
|
121
|
+
return jsonResponse({ ok: true, role, model, ...(effort ? { effort } : {}), toml, omoJsonc: { status: omoStatus } }, 200, req, config);
|
|
122
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read-only half of role auto-assign: size every Codex agent role with one model call, map
|
|
3
|
+
* each size to a concrete model and effort, and return the proposals. Nothing is written here;
|
|
4
|
+
* the dashboard and "ocx agent roles suggest --apply" apply a proposal through the ordinary
|
|
5
|
+
* PUT /api/codex-agent-roles/{role}.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import type { OcxConfig } from "../../types";
|
|
10
|
+
import type { CatalogModel } from "../../codex/catalog";
|
|
11
|
+
import type { RoleModelCandidate } from "../../codex/role-auto-assign";
|
|
12
|
+
|
|
13
|
+
export interface RoleSizingCall {
|
|
14
|
+
readonly model: string;
|
|
15
|
+
readonly system: string;
|
|
16
|
+
readonly user: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CompleteRoleSizing = (call: RoleSizingCall, config: OcxConfig) => Promise<{ text: string; error?: string }>;
|
|
20
|
+
|
|
21
|
+
const SIZING_TIMEOUT_MS = 180_000;
|
|
22
|
+
const MAX_SIZING_RESPONSE_BYTES = 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The completion error can carry upstream response text and exception messages, which may
|
|
26
|
+
* include paths, account identifiers or secrets the redactor does not recognise. Only a fixed
|
|
27
|
+
* category, plus a bare HTTP status, leaves the management response and the CLI.
|
|
28
|
+
*/
|
|
29
|
+
export function publicSizingError(error: string): string {
|
|
30
|
+
const http = /^[\w .-]* HTTP (\d{3}):/.exec(error);
|
|
31
|
+
if (http) return `HTTP ${http[1]}`;
|
|
32
|
+
if (error.endsWith("response exceeded byte bound")) return "the response was too large";
|
|
33
|
+
if (error.endsWith("returned non-JSON")) return "the response was not JSON";
|
|
34
|
+
if (error.endsWith("returned no text")) return "the response had no text";
|
|
35
|
+
return "the request could not be completed";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function completeThroughProxy(call: RoleSizingCall, config: OcxConfig) {
|
|
39
|
+
const { postLocalChatCompletion } = await import("../../lib/local-chat-completion");
|
|
40
|
+
return postLocalChatCompletion({
|
|
41
|
+
config,
|
|
42
|
+
label: "role sizing",
|
|
43
|
+
logTag: "role-sizing",
|
|
44
|
+
timeoutMs: SIZING_TIMEOUT_MS,
|
|
45
|
+
maxResponseBytes: MAX_SIZING_RESPONSE_BYTES,
|
|
46
|
+
boundWhileStreaming: true,
|
|
47
|
+
body: {
|
|
48
|
+
model: call.model,
|
|
49
|
+
messages: [{ role: "system", content: call.system }, { role: "user", content: call.user }],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function modelCandidates(slugs: readonly string[], models: readonly CatalogModel[]): Promise<RoleModelCandidate[]> {
|
|
55
|
+
const [catalog, { resolveMatchedPrice }] = await Promise.all([
|
|
56
|
+
import("../../codex/catalog"),
|
|
57
|
+
import("../../usage/cost"),
|
|
58
|
+
]);
|
|
59
|
+
const nativeSlugs = catalog.listCatalogNativeSlugs();
|
|
60
|
+
const routed = new Map(models.map(model => [catalog.catalogModelSlug(model), model]));
|
|
61
|
+
const unitPrice = (provider: string, modelId: string): number | null => {
|
|
62
|
+
const cost = resolveMatchedPrice(provider, modelId)?.cost4;
|
|
63
|
+
return cost ? cost.input + cost.output : null;
|
|
64
|
+
};
|
|
65
|
+
return slugs.map(slug => {
|
|
66
|
+
const row = routed.get(slug);
|
|
67
|
+
if (row) {
|
|
68
|
+
return {
|
|
69
|
+
model: slug,
|
|
70
|
+
unitPrice: unitPrice(row.provider, row.id),
|
|
71
|
+
efforts: row.reasoningEfforts ?? [],
|
|
72
|
+
...(row.defaultReasoningEffort ? { defaultEffort: row.defaultReasoningEffort } : {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (nativeSlugs.includes(slug)) {
|
|
76
|
+
const defaultEffort = catalog.nativeDefaultReasoningEffort(slug);
|
|
77
|
+
return {
|
|
78
|
+
model: slug,
|
|
79
|
+
unitPrice: unitPrice("openai", slug),
|
|
80
|
+
efforts: catalog.nativeReasoningEfforts(slug),
|
|
81
|
+
...(defaultEffort ? { defaultEffort } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return { model: slug, unitPrice: null, efforts: [] };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function roleModelCandidates(config: OcxConfig, models: readonly CatalogModel[]): Promise<RoleModelCandidate[]> {
|
|
89
|
+
const [{ listCatalogNativeSlugs }, { subagentSelectableModels }] = await Promise.all([
|
|
90
|
+
import("../../codex/catalog"),
|
|
91
|
+
import("../../codex/subagent-selectable-models"),
|
|
92
|
+
]);
|
|
93
|
+
return modelCandidates(subagentSelectableModels(config, models, listCatalogNativeSlugs()), models);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class NoSizingModelError extends Error {}
|
|
97
|
+
|
|
98
|
+
function resolveSizingModel(sizingModel: string | undefined, readConfiguredDefaultModel: () => string | null): string {
|
|
99
|
+
const model = sizingModel?.trim() || readConfiguredDefaultModel();
|
|
100
|
+
if (!model) throw new NoSizingModelError("no default model is set in Codex config.toml; pass a sizing model");
|
|
101
|
+
return model;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function proposeCodexRoleModels(options: {
|
|
105
|
+
config: OcxConfig;
|
|
106
|
+
codexHome: string;
|
|
107
|
+
sizingModel?: string;
|
|
108
|
+
fetchAllModels: (config: OcxConfig) => Promise<CatalogModel[]>;
|
|
109
|
+
completeRoleSizing?: CompleteRoleSizing;
|
|
110
|
+
}) {
|
|
111
|
+
const [roles, sizing, assign, { readConfiguredDefaultModel }] = await Promise.all([
|
|
112
|
+
import("../../codex/agent-role-models"),
|
|
113
|
+
import("../../codex/role-sizing"),
|
|
114
|
+
import("../../codex/role-auto-assign"),
|
|
115
|
+
import("../../codex/catalog/parsing"),
|
|
116
|
+
]);
|
|
117
|
+
const sizingModel = resolveSizingModel(options.sizingModel, readConfiguredDefaultModel);
|
|
118
|
+
|
|
119
|
+
const roleRows = roles.listCodexAgentRoleModels(options.codexHome).map(row => ({
|
|
120
|
+
role: row.role,
|
|
121
|
+
model: row.model,
|
|
122
|
+
effort: roles.readCodexAgentRoleEffort(row.role, options.codexHome),
|
|
123
|
+
}));
|
|
124
|
+
const inputs: { role: string; instructions: string }[] = [];
|
|
125
|
+
const outcomes = new Map<string, import("../../codex/role-sizing").RoleSizingOutcome>();
|
|
126
|
+
for (const row of roleRows) {
|
|
127
|
+
let excerpt: string | null = null;
|
|
128
|
+
try {
|
|
129
|
+
excerpt = sizing.roleInstructionsExcerpt(readFileSync(join(options.codexHome, "agents", `${row.role}.toml`), "utf8"));
|
|
130
|
+
} catch { /* an unreadable role is reported unsized below */ }
|
|
131
|
+
if (excerpt === null) outcomes.set(row.role, { unsized: "the role file has no description or developer_instructions to size from" });
|
|
132
|
+
else inputs.push({ role: row.role, instructions: excerpt });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let sizingError: string | null = null;
|
|
136
|
+
if (inputs.length > 0) {
|
|
137
|
+
const answer = await (options.completeRoleSizing ?? completeThroughProxy)({
|
|
138
|
+
model: sizingModel,
|
|
139
|
+
system: sizing.ROLE_SIZING_SYSTEM_PROMPT,
|
|
140
|
+
user: sizing.buildRoleSizingUserMessage(inputs),
|
|
141
|
+
}, options.config);
|
|
142
|
+
const names = inputs.map(input => input.role);
|
|
143
|
+
if (answer.error) {
|
|
144
|
+
sizingError = publicSizingError(answer.error);
|
|
145
|
+
for (const role of names) outcomes.set(role, { unsized: `the sizing call failed: ${sizingError}` });
|
|
146
|
+
} else {
|
|
147
|
+
for (const [role, outcome] of sizing.parseRoleSizingResponse(answer.text, names)) outcomes.set(role, outcome);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const classified = assign.classifyRoleModelCandidates(
|
|
152
|
+
await roleModelCandidates(options.config, await options.fetchAllModels(options.config)),
|
|
153
|
+
options.config.codexRoleTiers,
|
|
154
|
+
);
|
|
155
|
+
return {
|
|
156
|
+
sizingModel,
|
|
157
|
+
sizingError,
|
|
158
|
+
proposals: assign.buildRoleProposals(roleRows, outcomes, classified),
|
|
159
|
+
candidates: classified.map(({ model, tier, tierSource, unitPrice }) => ({ model, tier, tierSource, unitPrice })),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const DELEGATED_WORK_ROLE = "delegated-work";
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The same sizing for the Subagents page's delegation default: the work a parent hands its
|
|
167
|
+
* preferred subagent is sized as one standing role, and the pick comes from the models that page
|
|
168
|
+
* offers. Effort is always proposed, restricted to the Codex levels the page can save.
|
|
169
|
+
*/
|
|
170
|
+
export async function proposeDelegationModel(options: {
|
|
171
|
+
config: OcxConfig;
|
|
172
|
+
work: string;
|
|
173
|
+
offered: readonly string[];
|
|
174
|
+
sizingModel?: string;
|
|
175
|
+
models: readonly CatalogModel[];
|
|
176
|
+
completeRoleSizing?: CompleteRoleSizing;
|
|
177
|
+
}) {
|
|
178
|
+
const [sizing, assign, { readConfiguredDefaultModel }, { isCodexReasoningEffort }] = await Promise.all([
|
|
179
|
+
import("../../codex/role-sizing"),
|
|
180
|
+
import("../../codex/role-auto-assign"),
|
|
181
|
+
import("../../codex/catalog/parsing"),
|
|
182
|
+
import("../../reasoning-effort"),
|
|
183
|
+
]);
|
|
184
|
+
const sizingModel = resolveSizingModel(options.sizingModel, readConfiguredDefaultModel);
|
|
185
|
+
const answer = await (options.completeRoleSizing ?? completeThroughProxy)({
|
|
186
|
+
model: sizingModel,
|
|
187
|
+
system: sizing.DELEGATED_WORK_SIZING_SYSTEM_PROMPT,
|
|
188
|
+
user: sizing.buildRoleSizingUserMessage([{ role: DELEGATED_WORK_ROLE, instructions: options.work }]),
|
|
189
|
+
}, options.config);
|
|
190
|
+
const sizingError = answer.error ? publicSizingError(answer.error) : null;
|
|
191
|
+
const outcomes = sizingError
|
|
192
|
+
? new Map([[DELEGATED_WORK_ROLE, { unsized: `the sizing call failed: ${sizingError}` }]])
|
|
193
|
+
: sizing.parseRoleSizingResponse(answer.text, [DELEGATED_WORK_ROLE]);
|
|
194
|
+
const candidates = (await modelCandidates(options.offered, options.models))
|
|
195
|
+
.map(candidate => ({ ...candidate, efforts: candidate.efforts.filter(isCodexReasoningEffort) }));
|
|
196
|
+
const classified = assign.classifyRoleModelCandidates(candidates, options.config.codexRoleTiers);
|
|
197
|
+
const [proposal] = assign.buildRoleProposals([{
|
|
198
|
+
role: DELEGATED_WORK_ROLE,
|
|
199
|
+
model: options.config.injectionModel ?? null,
|
|
200
|
+
effort: options.config.injectionEffort ?? null,
|
|
201
|
+
}], outcomes, classified, { alwaysProposeEffort: true });
|
|
202
|
+
return {
|
|
203
|
+
sizingModel,
|
|
204
|
+
sizingError,
|
|
205
|
+
proposal: proposal!,
|
|
206
|
+
candidates: classified.map(({ model, tier, tierSource, unitPrice }) => ({ model, tier, tierSource, unitPrice })),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -66,6 +66,8 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
|
|
|
66
66
|
import type { ManagementContext } from "./context";
|
|
67
67
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
68
68
|
import { shadowCallTargetError } from "./shadow-call-validation";
|
|
69
|
+
import { decisionModelRouteError, normalizeDecisionModelSelector } from "./decision-model-validation";
|
|
70
|
+
import { comboConfigIssues, resolveComboId } from "../../combos/types";
|
|
69
71
|
import { COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS } from "../../combos";
|
|
70
72
|
|
|
71
73
|
|
|
@@ -221,13 +223,62 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
221
223
|
...(!Object.hasOwn(requestedCombo, "defaultEffortMode") && previous?.defaultEffortMode !== undefined
|
|
222
224
|
? { defaultEffortMode: previous.defaultEffortMode }
|
|
223
225
|
: {}),
|
|
226
|
+
// An API or CLI round-trip of a JEV combo that omits these keeps them; the dashboard sends
|
|
227
|
+
// both explicitly (null = default). Switching to another strategy drops them rather than
|
|
228
|
+
// failing validation.
|
|
229
|
+
...(!Object.hasOwn(requestedCombo, "decisionProvider")
|
|
230
|
+
&& !Object.hasOwn(requestedCombo, "decisionModel")
|
|
231
|
+
&& previous?.decisionProvider !== undefined
|
|
232
|
+
&& requestedCombo.strategy === "jev"
|
|
233
|
+
? { decisionProvider: previous.decisionProvider }
|
|
234
|
+
: {}),
|
|
235
|
+
...(!Object.hasOwn(requestedCombo, "decisionModel")
|
|
236
|
+
&& !Object.hasOwn(requestedCombo, "decisionProvider")
|
|
237
|
+
&& previous?.decisionModel !== undefined
|
|
238
|
+
&& requestedCombo.strategy === "jev"
|
|
239
|
+
? { decisionModel: previous.decisionModel }
|
|
240
|
+
: {}),
|
|
241
|
+
...(!Object.hasOwn(requestedCombo, "decisionTimeoutMs")
|
|
242
|
+
&& previous?.decisionTimeoutMs !== undefined
|
|
243
|
+
&& requestedCombo.strategy === "jev"
|
|
244
|
+
? { decisionTimeoutMs: previous.decisionTimeoutMs }
|
|
245
|
+
: {}),
|
|
224
246
|
};
|
|
225
|
-
const
|
|
247
|
+
const nextCombos = { ...(config.combos ?? {}) };
|
|
248
|
+
if (renameFrom) delete nextCombos[renameFrom];
|
|
249
|
+
nextCombos[id] = effectiveCombo as unknown as OcxComboConfig;
|
|
250
|
+
const prospectiveConfig = { ...config, combos: nextCombos };
|
|
251
|
+
// Decision routes follow the same combo identity migration as agent/shadow references,
|
|
252
|
+
// retaining any synthetic suffix while resolving the old identity against the old map.
|
|
253
|
+
if (previous) {
|
|
254
|
+
const nextPublicModel = comboPublicModelId(id, nextCombos[id]!);
|
|
255
|
+
for (const [otherId, combo] of Object.entries(nextCombos)) {
|
|
256
|
+
if (otherId === id || typeof combo.decisionModel !== "string") continue;
|
|
257
|
+
const model = combo.decisionModel.trim();
|
|
258
|
+
const selector = normalizeDecisionModelSelector(config, model);
|
|
259
|
+
if (resolveComboId(config, selector) !== sourceId) continue;
|
|
260
|
+
const nextSelector = selector === comboModelId(sourceId) ? comboModelId(id) : nextPublicModel;
|
|
261
|
+
nextCombos[otherId] = { ...combo, decisionModel: nextSelector + model.slice(selector.length) };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const validationOptions = {
|
|
226
265
|
requireEnabledTarget: true,
|
|
227
|
-
|
|
266
|
+
requireUsableDecisionService: true,
|
|
267
|
+
combos: nextCombos,
|
|
228
268
|
excludeComboId: sourceId,
|
|
229
|
-
|
|
269
|
+
normalizeDecisionModel: (model: string) => normalizeDecisionModelSelector(prospectiveConfig, model),
|
|
270
|
+
};
|
|
271
|
+
const error = comboConfigError(id, effectiveCombo, config.providers, validationOptions);
|
|
230
272
|
if (error) return jsonResponse({ error }, 400);
|
|
273
|
+
for (const [otherId, combo] of Object.entries(nextCombos)) {
|
|
274
|
+
if (typeof combo.decisionModel !== "string") continue;
|
|
275
|
+
const issue = otherId === id ? undefined : comboConfigIssues(otherId, combo, config.providers, {
|
|
276
|
+
combos: nextCombos,
|
|
277
|
+
normalizeDecisionModel: validationOptions.normalizeDecisionModel,
|
|
278
|
+
}).find(issue => issue.path[0] === "decisionModel");
|
|
279
|
+
const routeError = issue?.message ?? decisionModelRouteError(prospectiveConfig, otherId, combo.decisionModel);
|
|
280
|
+
if (routeError) return jsonResponse({ error: issue ? `combo "${otherId}": ${routeError}` : routeError }, 400);
|
|
281
|
+
}
|
|
231
282
|
const normalized = normalizeComboConfig(effectiveCombo as unknown as OcxComboConfig);
|
|
232
283
|
// Persist only non-default identity/capability fields so config stays sparse.
|
|
233
284
|
// Capability defaults (`imageInput`, `reasoningEffortMode`) go through the same
|
|
@@ -267,8 +318,6 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
267
318
|
if (codexAccountNamespaceForModel(config.codexAccountNamespaces, newPublicModel)) {
|
|
268
319
|
return jsonResponse({ error: CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR }, 409);
|
|
269
320
|
}
|
|
270
|
-
const nextCombos = { ...(config.combos ?? {}) };
|
|
271
|
-
if (renameFrom) delete nextCombos[renameFrom];
|
|
272
321
|
nextCombos[id] = stored;
|
|
273
322
|
let shouldSyncClaudeAgentDefs = false;
|
|
274
323
|
const migratedModels = new Map<string, string>();
|
|
@@ -362,6 +411,14 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
362
411
|
if (!Object.hasOwn(config.combos ?? {}, id)) {
|
|
363
412
|
return jsonResponse({ error: "unknown combo" }, 404);
|
|
364
413
|
}
|
|
414
|
+
const dependents = Object.entries(config.combos ?? {}).filter(([otherId, combo]) =>
|
|
415
|
+
otherId !== id && typeof combo.decisionModel === "string"
|
|
416
|
+
&& resolveComboId(config, normalizeDecisionModelSelector(config, combo.decisionModel.trim())) === id,
|
|
417
|
+
).map(([otherId]) => otherId);
|
|
418
|
+
if (dependents.length) {
|
|
419
|
+
return jsonResponse({ error: `combo "${id}" is referenced by decisionModel in combos: ${dependents.join(", ")}`,
|
|
420
|
+
code: "combo_has_dependent_combos", combos: dependents }, 409);
|
|
421
|
+
}
|
|
365
422
|
const { clearComboSelectionState, clearComboTargetCooldowns } = await import("../../combos");
|
|
366
423
|
delete config.combos![id];
|
|
367
424
|
if (Object.keys(config.combos!).length === 0) deleteConfigTopLevelKey(config, "combos");
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT } from "../../codex/quota-types";
|
|
2
|
+
import { resolveMainAccountHardLockThresholds } from "../../codex/main-account-hard-lock";
|
|
3
|
+
import { getMainAccountExternalUsageWarning } from "../../codex/main-account-external-usage";
|
|
4
|
+
import { getObservedMainQuotaIdentityKey } from "../../codex/main-account-cache";
|
|
1
5
|
import { compactionRoutingSchema, memoryModelsSchema } from "../../config/schema/leaf-validators";
|
|
2
6
|
import { compactionRecoverySchema } from "../../config/schema/compaction-recovery";
|
|
3
7
|
import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
|
|
@@ -363,7 +367,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
363
367
|
// Absent means on by default: the GUI renders a switch enabled unless explicit false.
|
|
364
368
|
fastRows: config.fastRows !== false,
|
|
365
369
|
codexMainAccountHardLock: isMainAccountHardLockEnabled(config),
|
|
366
|
-
mainAccountHardLock: getMainAccountHardLockStatus(config),
|
|
370
|
+
mainAccountHardLock: { ...getMainAccountHardLockStatus(config),
|
|
371
|
+
externalUsage: getMainAccountExternalUsageWarning(getObservedMainQuotaIdentityKey()) },
|
|
367
372
|
// Absent means the historical auto-open, so the GUI can render the toggle
|
|
368
373
|
// without having to know that `undefined` and `true` mean the same thing.
|
|
369
374
|
oauthOpenBrowser: config.oauthOpenBrowser !== false,
|
|
@@ -467,6 +472,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
467
472
|
ultraFastTier?: unknown;
|
|
468
473
|
fastRows?: unknown;
|
|
469
474
|
codexMainAccountHardLock?: unknown;
|
|
475
|
+
codexMainAccountHardLockThresholds?: unknown;
|
|
470
476
|
codexDesktopAuthless?: unknown;
|
|
471
477
|
codexClientCompaction?: unknown;
|
|
472
478
|
compactionRouting?: unknown;
|
|
@@ -483,12 +489,13 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
483
489
|
&& body.ultraFastTier === undefined
|
|
484
490
|
&& body.fastRows === undefined
|
|
485
491
|
&& body.codexMainAccountHardLock === undefined
|
|
492
|
+
&& body.codexMainAccountHardLockThresholds === undefined
|
|
486
493
|
&& body.codexDesktopAuthless === undefined
|
|
487
494
|
&& body.codexClientCompaction === undefined
|
|
488
495
|
&& body.compactionRouting === undefined
|
|
489
496
|
&& body.compactionRecovery === undefined
|
|
490
497
|
&& body.memoryModels === undefined) {
|
|
491
|
-
return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, showCodexCredits, ultraFastTier, fastRows, codexMainAccountHardLock, codexDesktopAuthless, codexClientCompaction, compactionRouting, compactionRecovery, or memoryModels" }, 400);
|
|
498
|
+
return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, showCodexCredits, ultraFastTier, fastRows, codexMainAccountHardLock, codexMainAccountHardLockThresholds, codexDesktopAuthless, codexClientCompaction, compactionRouting, compactionRecovery, or memoryModels" }, 400);
|
|
492
499
|
}
|
|
493
500
|
if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
|
|
494
501
|
return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
|
|
@@ -515,6 +522,24 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
515
522
|
if (body.codexMainAccountHardLock !== undefined && typeof body.codexMainAccountHardLock !== "boolean") {
|
|
516
523
|
return jsonResponse({ error: "codexMainAccountHardLock boolean is required" }, 400);
|
|
517
524
|
}
|
|
525
|
+
let hardLockThresholds: OcxConfig["codexMainAccountHardLockThresholds"];
|
|
526
|
+
if (body.codexMainAccountHardLockThresholds !== undefined) {
|
|
527
|
+
const value = body.codexMainAccountHardLockThresholds;
|
|
528
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
529
|
+
|| Object.keys(value).length === 0
|
|
530
|
+
|| Object.keys(value).some(key => key !== "short" && key !== "long")
|
|
531
|
+
|| Object.values(value).some(percent => typeof percent !== "number" || !Number.isInteger(percent)
|
|
532
|
+
|| percent < MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT || percent > 100)) {
|
|
533
|
+
return jsonResponse({ error: "codexMainAccountHardLockThresholds requires integer short/long percentages from 80 to 100" }, 400);
|
|
534
|
+
}
|
|
535
|
+
// A partial body updates one window and keeps the other stored value.
|
|
536
|
+
hardLockThresholds = { ...config.codexMainAccountHardLockThresholds,
|
|
537
|
+
...(value as NonNullable<OcxConfig["codexMainAccountHardLockThresholds"]>) };
|
|
538
|
+
const defaults = resolveMainAccountHardLockThresholds(undefined);
|
|
539
|
+
if ((hardLockThresholds.short ?? defaults.short) > (hardLockThresholds.long ?? defaults.long)) {
|
|
540
|
+
return jsonResponse({ error: "codexMainAccountHardLockThresholds short must not exceed long" }, 400);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
518
543
|
if (body.codexDesktopAuthless !== undefined && typeof body.codexDesktopAuthless !== "boolean") {
|
|
519
544
|
return jsonResponse({ error: "codexDesktopAuthless boolean is required" }, 400);
|
|
520
545
|
}
|
|
@@ -586,6 +611,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
586
611
|
hasUltraFastTier: Object.hasOwn(config, "ultraFastTier"),
|
|
587
612
|
fastRows: config.fastRows,
|
|
588
613
|
hasFastRows: Object.hasOwn(config, "fastRows"),
|
|
614
|
+
codexMainAccountHardLockThresholds: config.codexMainAccountHardLockThresholds,
|
|
615
|
+
hasCodexMainAccountHardLockThresholds: Object.hasOwn(config, "codexMainAccountHardLockThresholds"),
|
|
589
616
|
codexMainAccountHardLock: config.codexMainAccountHardLock,
|
|
590
617
|
hasCodexMainAccountHardLock: Object.hasOwn(config, "codexMainAccountHardLock"),
|
|
591
618
|
codexDesktopAuthless: config.codexDesktopAuthless,
|
|
@@ -630,6 +657,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
630
657
|
else if (body.fastRows === true) deleteConfigTopLevelKey(config, "fastRows");
|
|
631
658
|
// Inverted from the pair above because the default is on (#5694): off is the persisted
|
|
632
659
|
// decision, so it writes `false`, while on deletes the key and returns to the default.
|
|
660
|
+
if (hardLockThresholds !== undefined) config.codexMainAccountHardLockThresholds = { ...hardLockThresholds };
|
|
633
661
|
if (body.codexMainAccountHardLock === false) config.codexMainAccountHardLock = false;
|
|
634
662
|
else if (body.codexMainAccountHardLock === true) deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
|
|
635
663
|
if (body.codexDesktopAuthless === true) config.codexDesktopAuthless = true;
|
|
@@ -684,6 +712,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
684
712
|
if (previousSettings.hasFastRows) {
|
|
685
713
|
config.fastRows = previousSettings.fastRows;
|
|
686
714
|
} else deleteConfigTopLevelKey(config, "fastRows");
|
|
715
|
+
if (previousSettings.hasCodexMainAccountHardLockThresholds) {
|
|
716
|
+
config.codexMainAccountHardLockThresholds = previousSettings.codexMainAccountHardLockThresholds;
|
|
717
|
+
} else deleteConfigTopLevelKey(config, "codexMainAccountHardLockThresholds");
|
|
687
718
|
if (previousSettings.hasCodexMainAccountHardLock) {
|
|
688
719
|
config.codexMainAccountHardLock = previousSettings.codexMainAccountHardLock;
|
|
689
720
|
} else deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
|
|
@@ -752,7 +783,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
752
783
|
// phases as "Off" while the server kept them.
|
|
753
784
|
memoryModels: config.memoryModels ?? null,
|
|
754
785
|
codexMainAccountHardLock: isMainAccountHardLockEnabled(config),
|
|
755
|
-
mainAccountHardLock: getMainAccountHardLockStatus(config),
|
|
786
|
+
mainAccountHardLock: { ...getMainAccountHardLockStatus(config),
|
|
787
|
+
externalUsage: getMainAccountExternalUsageWarning(getObservedMainQuotaIdentityKey()) },
|
|
756
788
|
startupHealth: await readStartupHealthSnapshot(config),
|
|
757
789
|
});
|
|
758
790
|
}
|
|
@@ -48,6 +48,7 @@ export interface ManagementApiDeps {
|
|
|
48
48
|
/** Bound to this server's lifecycle owner; absent in direct route tests. */
|
|
49
49
|
listLowQuotaEvents?: (limit?: number) => LowQuotaEvent[];
|
|
50
50
|
/** Bound Claude intercept state, injectable for isolated management-route tests. */
|
|
51
|
+
ensureClaudeIntercept?: () => Promise<import("../../claude/intercept/runtime").ClaudeInterceptOutcome>;
|
|
51
52
|
getClaudeInterceptState?: typeof import("../../claude/intercept/runtime").getClaudeInterceptState;
|
|
52
53
|
/** Reconciliation seam for field-scoped rollback tests. */
|
|
53
54
|
reconcileClaudeFirstPartySettings?: typeof import("../../claude/first-party-settings").reconcileClaudeFirstPartySettings;
|
|
@@ -84,6 +85,8 @@ export interface ManagementApiDeps {
|
|
|
84
85
|
* Tests stub it to orphan the fixture file mid-fetch (the r7 recheck test).
|
|
85
86
|
*/
|
|
86
87
|
fetchAllModels?: (config: OcxConfig) => Promise<CatalogModel[]>;
|
|
88
|
+
/** Codex role auto-assign's one sizing model call; route tests answer it without a provider. */
|
|
89
|
+
completeCodexRoleSizing?: import("./codex-role-auto-assign").CompleteRoleSizing;
|
|
87
90
|
/**
|
|
88
91
|
* Writer seam for the Grok toggle: lets a test place the file in any state
|
|
89
92
|
* between the pre-write recheck and the write itself (the r8 post-inspection
|
|
@@ -149,7 +152,7 @@ export interface ManagementApiDeps {
|
|
|
149
152
|
codexPromptPaths?: CodexPromptPaths;
|
|
150
153
|
/** Link seams are getters so the optional listener and supervisor are singletons. */
|
|
151
154
|
linkSupervisor?: () => LinkSupervisor;
|
|
152
|
-
linkListener?: () => Pick<LinkListenerLifecycle<unknown>, "ensureStarted" | "status" | "close" | "onAuthenticatedCatalog"
|
|
155
|
+
linkListener?: () => Pick<LinkListenerLifecycle<unknown>, "ensureStarted" | "status" | "close" | "onAuthenticatedCatalog"> & Partial<Pick<import("../index/optional-listeners").OptionalListenerSet<unknown>, "ensureClaudeIntercept" | "claudeInterceptOutcome">>;
|
|
153
156
|
readLinkStore?: () => LinkStore;
|
|
154
157
|
writeLinkStore?: (store: LinkStore) => void;
|
|
155
158
|
linkKnownHostsPath?: () => string;
|