@bitkyc08/opencodex 2.76.0 → 2.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/App-COdEeqSj.css +1 -0
- package/gui/dist/assets/App-DEpCJEP_.js +54 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
- package/gui/dist/assets/index-Cd7ittMv.css +1 -0
- package/gui/dist/assets/index-DwJKFJm5.js +94 -0
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/ollama-native.ts +29 -2
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/account-api.ts +3 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/agent.ts +10 -0
- package/src/cli/capabilities.ts +9 -0
- package/src/cli/chatgpt-command.ts +10 -7
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-readiness.ts +89 -0
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +84 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +52 -8
- package/src/cli/models-runtime.ts +2 -1
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider.ts +3 -29
- package/src/cli/registry.ts +29 -5
- package/src/cli/root.ts +30 -9
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/tray-proxy.ts +3 -2
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +46 -66
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/main-account.ts +144 -11
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +33 -1
- package/src/codex/quota.ts +56 -15
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/shim-path-resolution.ts +133 -0
- package/src/codex/shim.ts +39 -39
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/combos/failover.ts +82 -1
- package/src/combos/jev.ts +13 -3
- package/src/config/diagnostics.ts +4 -0
- package/src/config/load-degrade.ts +4 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +11 -0
- package/src/config/schema/leaf-validators.ts +5 -1
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +245 -153
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +4 -2
- package/src/integrations/state.ts +68 -6
- package/src/integrations/writer.ts +18 -4
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +3 -3
- package/src/oauth/anthropic.ts +7 -1
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +36 -27
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +18 -11
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +6 -129
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/integration-routes.ts +64 -6
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/route-registry.ts +4 -3
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +100 -1
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +57 -26
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +18 -1
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +18 -0
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -61,6 +61,7 @@ import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
|
61
61
|
import type { OcxConfig } from "../types";
|
|
62
62
|
import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
|
|
63
63
|
import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext } from "./request-log";
|
|
64
|
+
import { recordGenerationEvent } from "./request-log-generation-window";
|
|
64
65
|
import { createFinalRequestLog } from "./inference/final-log";
|
|
65
66
|
import {
|
|
66
67
|
conversationIdFromClaudeMetadata,
|
|
@@ -143,10 +144,10 @@ function decodeClaudeFastSelector(raw: string, cc?: OcxConfig["claudeCode"]): st
|
|
|
143
144
|
return decodedBase === bare ? exact : `${decodedBase}--fast`;
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
/** Restore
|
|
147
|
-
function
|
|
147
|
+
/** Restore reversible native Claude picker aliases before Anthropic passthrough checks. */
|
|
148
|
+
function decodeNativeClaudePickerAlias(raw: string, cc?: OcxConfig["claudeCode"]): string {
|
|
148
149
|
const decoded = resolveInboundModel(raw, cc);
|
|
149
|
-
if (!decoded.startsWith("claude-
|
|
150
|
+
if (!decoded.startsWith("claude-")) return raw;
|
|
150
151
|
// A picker value saved before the ocx-claude spelling keeps the native passthrough too.
|
|
151
152
|
return claudeCodeNativeAlias(decoded) === raw || legacyAliasForNative(decoded) === raw ? decoded : raw;
|
|
152
153
|
}
|
|
@@ -334,6 +335,7 @@ export function tapAnthropicSseForLog(
|
|
|
334
335
|
let data: unknown;
|
|
335
336
|
try { data = JSON.parse(dataLine); } catch { return; }
|
|
336
337
|
if (!isRec(data)) return;
|
|
338
|
+
recordGenerationEvent(logCtx, data.type);
|
|
337
339
|
if (data.type === "message_start" && isRec(data.message) && isRec(data.message.usage)) {
|
|
338
340
|
usageAcc = { ...usageAcc, ...data.message.usage };
|
|
339
341
|
} else if (data.type === "message_delta" && isRec(data.usage)) {
|
|
@@ -894,7 +896,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
894
896
|
}
|
|
895
897
|
}
|
|
896
898
|
if (isRec(anthropicBody) && typeof anthropicBody.model === "string") {
|
|
897
|
-
anthropicBody.model =
|
|
899
|
+
anthropicBody.model = decodeNativeClaudePickerAlias(anthropicBody.model, cc);
|
|
898
900
|
}
|
|
899
901
|
if (isRec(anthropicBody) && typeof anthropicBody.model === "string") {
|
|
900
902
|
requestedModel = anthropicBody.model;
|
|
@@ -1313,10 +1315,12 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1313
1315
|
const replayRefusal = isReplayRefusalResponse(response);
|
|
1314
1316
|
// Re-shape the OpenAI-style error envelope into the Anthropic one, preserving status.
|
|
1315
1317
|
let message = `upstream error (${response.status})`;
|
|
1318
|
+
let contextError = false;
|
|
1316
1319
|
try {
|
|
1317
1320
|
const text = await response.text();
|
|
1318
1321
|
try {
|
|
1319
|
-
const parsed = JSON.parse(text) as { error?: { message?: string; type?: string } | string; message?: string };
|
|
1322
|
+
const parsed = JSON.parse(text) as { error?: { message?: string; type?: string; code?: unknown } | string; message?: string };
|
|
1323
|
+
contextError = typeof parsed?.error === "object" && parsed.error?.code === "context_length_exceeded";
|
|
1320
1324
|
const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error.message : undefined;
|
|
1321
1325
|
const flat = typeof parsed?.error === "string" ? parsed.error : parsed?.message;
|
|
1322
1326
|
message = nested || flat || (text ? `upstream error (${response.status}): ${text.slice(0, 400)}` : message);
|
|
@@ -1325,7 +1329,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1325
1329
|
}
|
|
1326
1330
|
} catch { /* keep fallback message */ }
|
|
1327
1331
|
const upstreamRetryAfter = response.headers.get("retry-after");
|
|
1328
|
-
const retryAfter = replayRefusal
|
|
1332
|
+
const retryAfter = replayRefusal || contextError
|
|
1329
1333
|
? undefined
|
|
1330
1334
|
: resolveClientRetryAfter({
|
|
1331
1335
|
status: response.status,
|
|
@@ -1345,10 +1349,10 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1345
1349
|
const nativeMainFence = response.status === 503
|
|
1346
1350
|
&& upstreamRetryAfter?.trim() === "1"
|
|
1347
1351
|
&& message === CODEX_MAIN_PROFILE_MAINTENANCE_MESSAGE;
|
|
1348
|
-
const transient = !replayRefusal && !nativeMainFence && isTransientUpstreamStatus(response.status);
|
|
1352
|
+
const transient = !replayRefusal && !nativeMainFence && !contextError && isTransientUpstreamStatus(response.status);
|
|
1349
1353
|
const outStatus = replayRefusal
|
|
1350
1354
|
? REPLAY_REFUSED_STATUS
|
|
1351
|
-
: nativeMainFence ? 503 : transient ? 529 : response.status;
|
|
1355
|
+
: nativeMainFence ? 503 : contextError ? 400 : transient ? 529 : response.status;
|
|
1352
1356
|
const outHeaders = new Headers({ "Content-Type": "application/json" });
|
|
1353
1357
|
if (retryAfter) outHeaders.set("Retry-After", retryAfter);
|
|
1354
1358
|
else if (transient) outHeaders.set("Retry-After", "2");
|
|
@@ -1357,7 +1361,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1357
1361
|
outStatus,
|
|
1358
1362
|
message,
|
|
1359
1363
|
undefined,
|
|
1360
|
-
replayRefusal ? UPSTREAM_RESET_REPLAY_REFUSED_CODE : undefined,
|
|
1364
|
+
replayRefusal ? UPSTREAM_RESET_REPLAY_REFUSED_CODE : contextError ? "context_length_exceeded" : undefined,
|
|
1361
1365
|
)), {
|
|
1362
1366
|
status: outStatus,
|
|
1363
1367
|
headers: outHeaders,
|
|
@@ -1408,7 +1412,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1408
1412
|
);
|
|
1409
1413
|
}
|
|
1410
1414
|
return new Response(JSON.stringify(message), {
|
|
1411
|
-
status: isError ? 502 : 200,
|
|
1415
|
+
status: isError ? (translatedError?.code === "context_length_exceeded" ? 400 : 502) : 200,
|
|
1412
1416
|
headers: { "Content-Type": "application/json" },
|
|
1413
1417
|
});
|
|
1414
1418
|
}
|
|
@@ -1431,6 +1435,9 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1431
1435
|
"translation_buffer_limit",
|
|
1432
1436
|
);
|
|
1433
1437
|
}
|
|
1438
|
+
if (error?.code === "context_length_exceeded") {
|
|
1439
|
+
return anthropicErrorResponse(400, error.message ?? "upstream context limit exceeded", "invalid_request_error", error.code);
|
|
1440
|
+
}
|
|
1434
1441
|
return anthropicErrorResponse(502, error?.message ?? "upstream request failed", "api_error");
|
|
1435
1442
|
}
|
|
1436
1443
|
let message: Rec;
|
|
@@ -1659,7 +1666,7 @@ export async function handleClaudeCountTokens(
|
|
|
1659
1666
|
model = stripOneMillionMarker(countRoute);
|
|
1660
1667
|
raw.model = model;
|
|
1661
1668
|
}
|
|
1662
|
-
model =
|
|
1669
|
+
model = decodeNativeClaudePickerAlias(model, cc);
|
|
1663
1670
|
raw.model = model;
|
|
1664
1671
|
// Fast-only: count_tokens never parsed an effort row, so it must not start. It returns a
|
|
1665
1672
|
// token estimate and sends no tier, so only the IDENTITY is corrected - without this the
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { CODEX_REASONING_LEVELS, configuredReasoningEfforts, isDeclaredReasoningEffort } from "../reasoning-effort";
|
|
2
|
+
import { isPlainObject } from "../lib/plain-data";
|
|
3
|
+
import type { OcxProviderConfig } from "../types";
|
|
4
|
+
|
|
5
|
+
export { DROID_DEFAULT_EFFORT_HEADER } from "../clients/config-export/contracts";
|
|
6
|
+
|
|
7
|
+
type ChatRequestBody = Record<string, unknown>;
|
|
8
|
+
type DroidReasoningTarget = { provider: OcxProviderConfig; modelId: string };
|
|
9
|
+
|
|
10
|
+
function hasExplicitEffort(body: ChatRequestBody): boolean {
|
|
11
|
+
return Object.hasOwn(body, "reasoning_effort")
|
|
12
|
+
|| (isPlainObject(body.reasoning) && Object.hasOwn(body.reasoning, "effort"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Read before the Chat body is bridged: the bridge drops null and unknown efforts, which still count as explicit. */
|
|
16
|
+
export function droidReasoningDefault(header: string | null, chatBody: ChatRequestBody): string | undefined {
|
|
17
|
+
return header && isDeclaredReasoningEffort(header) && !hasExplicitEffort(chatBody) ? header : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pendingDefault(
|
|
21
|
+
body: ChatRequestBody,
|
|
22
|
+
defaultEffort: string | undefined,
|
|
23
|
+
target: DroidReasoningTarget,
|
|
24
|
+
): string | undefined {
|
|
25
|
+
if (!defaultEffort || hasExplicitEffort(body)) return undefined;
|
|
26
|
+
const supportedEfforts = configuredReasoningEfforts(target.provider, target.modelId)
|
|
27
|
+
?? CODEX_REASONING_LEVELS.map(level => level.effort);
|
|
28
|
+
return supportedEfforts.includes(defaultEffort) ? defaultEffort : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function applyDroidReasoningDefault(
|
|
32
|
+
body: ChatRequestBody,
|
|
33
|
+
defaultEffort: string | undefined,
|
|
34
|
+
target: DroidReasoningTarget,
|
|
35
|
+
): void {
|
|
36
|
+
const effort = pendingDefault(body, defaultEffort, target);
|
|
37
|
+
if (effort) body.reasoning_effort = effort;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function applyDroidResponsesReasoningDefault(
|
|
41
|
+
body: unknown,
|
|
42
|
+
defaultEffort: string | undefined,
|
|
43
|
+
target: DroidReasoningTarget,
|
|
44
|
+
): boolean {
|
|
45
|
+
if (!isPlainObject(body)) return false;
|
|
46
|
+
const effort = pendingDefault(body, defaultEffort, target);
|
|
47
|
+
if (!effort) return false;
|
|
48
|
+
body.reasoning = { ...(isPlainObject(body.reasoning) ? body.reasoning : {}), effort };
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
} from "../lib/translator-budget";
|
|
15
15
|
import {
|
|
16
16
|
replaceSseDataPayload,
|
|
17
|
+
splitSseBlock,
|
|
18
|
+
sseLineEnding as blockNewline,
|
|
17
19
|
sseDataPayload,
|
|
18
20
|
type SseBlockRewrite,
|
|
19
21
|
} from "./sse-payload-rewrite";
|
|
@@ -46,14 +48,9 @@ function byteLength(value: unknown): number {
|
|
|
46
48
|
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
function blockNewline(block: string): "\r\n" | "\n" {
|
|
50
|
-
return block.includes("\r\n") ? "\r\n" : "\n";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
51
|
/** Keep a named SSE block's `event:` field aligned with its JSON `type`. */
|
|
54
52
|
function withEventName(block: string, eventName: string): string {
|
|
55
|
-
const newline =
|
|
56
|
-
const lines = block.split(/\r?\n/);
|
|
53
|
+
const { newline, lines } = splitSseBlock(block);
|
|
57
54
|
let replaced = false;
|
|
58
55
|
const rewritten = lines.flatMap((line) => {
|
|
59
56
|
if (!line.startsWith("event:")) return [line];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { replaceSseDataPayload, sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
1
|
+
import { replaceSseDataPayload, splitSseBlock, sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
2
2
|
|
|
3
3
|
const GROK_CONTROL_FRAME_TYPES: Record<string, true> = {
|
|
4
4
|
"codex.rate_limits": true,
|
|
@@ -144,7 +144,7 @@ export function createGrokResponsesControlFrameBlockRewrite(): SseBlockRewrite {
|
|
|
144
144
|
let eventName = "";
|
|
145
145
|
// SSE overwrites the event type on every event field, including empty resets.
|
|
146
146
|
// Like sseDataPayload, remove only one optional ASCII space after the colon.
|
|
147
|
-
for (const line of block.
|
|
147
|
+
for (const line of splitSseBlock(block).lines) {
|
|
148
148
|
if (line === "event") eventName = "";
|
|
149
149
|
else if (line.startsWith("event:")) {
|
|
150
150
|
const value = line.slice("event:".length);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Strict terminal reconstruction selected by the Grok compatibility marker. */
|
|
2
2
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
3
3
|
import { MAX_COMPLETED_OUTPUT_ITEMS, MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES } from "./relay";
|
|
4
|
-
import { replaceSseDataPayload, sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
4
|
+
import { replaceSseDataPayload, splitSseBlock, sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
5
5
|
import { isPlainObject, jsonBlock, type RetainedOutputItem } from "./responses-snapshot-codec";
|
|
6
6
|
import {
|
|
7
7
|
requestToolScope,
|
|
@@ -215,14 +215,13 @@ function refusedTerminalBlock(
|
|
|
215
215
|
},
|
|
216
216
|
});
|
|
217
217
|
const rewritten = replaceSseDataPayload(block, payload);
|
|
218
|
-
const newline =
|
|
218
|
+
const { newline, lines } = splitSseBlock(rewritten);
|
|
219
219
|
let eventRewritten = false;
|
|
220
|
-
|
|
220
|
+
return lines.map(line => {
|
|
221
221
|
if (eventRewritten || !line.startsWith("event:")) return line;
|
|
222
222
|
eventRewritten = true;
|
|
223
223
|
return `event: ${GROK_REFUSED_TERMINAL_EVENT_TYPE}`;
|
|
224
|
-
});
|
|
225
|
-
return lines.join(newline);
|
|
224
|
+
}).join(newline);
|
|
226
225
|
}
|
|
227
226
|
|
|
228
227
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import type { OcxConfig } from "../types";
|
|
3
|
-
import { canonicalGuiBrowserOrigin } from "../lib/gui-pair-capability";
|
|
3
|
+
import { canonicalGuiBrowserOrigin, standaloneGuiPairingOrigin } from "../lib/gui-pair-capability";
|
|
4
4
|
import {
|
|
5
5
|
isAllowedManagementOrigin,
|
|
6
6
|
isApiAuthRequired,
|
|
@@ -20,6 +20,8 @@ export interface GuiSessionRecord {
|
|
|
20
20
|
csrfToken: string;
|
|
21
21
|
expiresAt: number;
|
|
22
22
|
issuance: GuiSessionIssuance;
|
|
23
|
+
/** Process-local marker; never accepted from a browser. */
|
|
24
|
+
localPairing?: true;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface GuiSessionBootstrap extends GuiSessionRecord {
|
|
@@ -31,6 +33,7 @@ export interface GuiPairingGrantRecord {
|
|
|
31
33
|
browserOrigin: string;
|
|
32
34
|
expiresAt: number;
|
|
33
35
|
failedAttempts?: number;
|
|
36
|
+
localPairing?: true;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
export interface PairingAttemptContext {
|
|
@@ -131,6 +134,7 @@ function mintSession(
|
|
|
131
134
|
issuance: GuiSessionIssuance,
|
|
132
135
|
state: GuiSessionState,
|
|
133
136
|
now: number,
|
|
137
|
+
localPairing = false,
|
|
134
138
|
): GuiSessionBootstrap {
|
|
135
139
|
pruneExpired(state, now);
|
|
136
140
|
evictOldestSession(state);
|
|
@@ -142,8 +146,9 @@ function mintSession(
|
|
|
142
146
|
serverOrigin,
|
|
143
147
|
browserOrigin,
|
|
144
148
|
csrfToken: randomBytes(32).toString("base64url"),
|
|
145
|
-
expiresAt: now + (issuance === "loopback" ? LOOPBACK_GUI_SESSION_TTL_MS : REMOTE_GUI_SESSION_TTL_MS),
|
|
149
|
+
expiresAt: now + (issuance === "loopback" || localPairing ? LOOPBACK_GUI_SESSION_TTL_MS : REMOTE_GUI_SESSION_TTL_MS),
|
|
146
150
|
issuance,
|
|
151
|
+
...(localPairing ? { localPairing: true as const } : {}),
|
|
147
152
|
};
|
|
148
153
|
state.sessions.set(token, session);
|
|
149
154
|
return {
|
|
@@ -258,13 +263,16 @@ export function createGuiPairingGrant(
|
|
|
258
263
|
now = Date.now(),
|
|
259
264
|
): { grant: string; browserOrigin: string; serverOrigin: string; expiresAt: number } {
|
|
260
265
|
const canonicalBrowserOrigin = canonicalGuiBrowserOrigin(browserOrigin);
|
|
261
|
-
const
|
|
266
|
+
const localOrigin = standaloneGuiPairingOrigin(config);
|
|
267
|
+
const localPairing = localOrigin !== null && canonicalBrowserOrigin === localOrigin;
|
|
268
|
+
const serverOrigin = config.runtimeRole === "hub"
|
|
269
|
+
? canonicalHttpOrigin(config.hub?.managementPublicOrigin) : localOrigin;
|
|
262
270
|
if (
|
|
263
|
-
|
|
264
|
-
|| !canonicalBrowserOrigin
|
|
271
|
+
!canonicalBrowserOrigin
|
|
265
272
|
|| canonicalBrowserOrigin !== browserOrigin
|
|
266
273
|
|| !serverOrigin
|
|
267
|
-
||
|
|
274
|
+
|| (config.runtimeRole === "hub"
|
|
275
|
+
? !isRemoteGuiBrowserOriginAllowed(canonicalBrowserOrigin, config) : !localPairing)
|
|
268
276
|
) throw new TypeError("remote GUI origin is not allowed");
|
|
269
277
|
pruneExpired(state, now);
|
|
270
278
|
consumeGrantRateSlot(state, now);
|
|
@@ -276,7 +284,8 @@ export function createGuiPairingGrant(
|
|
|
276
284
|
digest = pairingGrantDigest(grant);
|
|
277
285
|
} while (state.pairingGrants.has(digest));
|
|
278
286
|
const expiresAt = now + GUI_PAIRING_GRANT_TTL_MS;
|
|
279
|
-
state.pairingGrants.set(digest, { browserOrigin: canonicalBrowserOrigin, serverOrigin, expiresAt
|
|
287
|
+
state.pairingGrants.set(digest, { browserOrigin: canonicalBrowserOrigin, serverOrigin, expiresAt,
|
|
288
|
+
...(localPairing ? { localPairing: true as const } : {}) });
|
|
280
289
|
return { grant, browserOrigin: canonicalBrowserOrigin, serverOrigin, expiresAt };
|
|
281
290
|
}
|
|
282
291
|
|
|
@@ -316,7 +325,8 @@ export function consumeGuiPairingGrant(
|
|
|
316
325
|
now = Date.now(),
|
|
317
326
|
attemptContext?: PairingAttemptContext,
|
|
318
327
|
): GuiSessionBootstrap | PairingAttemptRefusal | null {
|
|
319
|
-
|
|
328
|
+
const localOrigin = standaloneGuiPairingOrigin(config);
|
|
329
|
+
if (req.method !== "POST" || hasAlternateCredential(req) || (config.runtimeRole !== "hub" && !localOrigin)) return null;
|
|
320
330
|
// Scheme check FIRST, before the grant is parsed or looked up.
|
|
321
331
|
//
|
|
322
332
|
// A grant is single-use, so consuming one and then refusing to mint would burn the
|
|
@@ -333,6 +343,11 @@ export function consumeGuiPairingGrant(
|
|
|
333
343
|
const grant = strictPairingGrantBody(body);
|
|
334
344
|
const browserOrigin = canonicalGuiBrowserOrigin(req.headers.get("Origin"));
|
|
335
345
|
if (!grant || !browserOrigin) return null;
|
|
346
|
+
// Unlike the hub path, standalone redemption never accepts a proxy-origin claim:
|
|
347
|
+
// both origins must be the configured loopback origin and the kernel peer must be local.
|
|
348
|
+
if (localOrigin && (destination !== localOrigin || browserOrigin !== localOrigin
|
|
349
|
+
|| attemptContext?.ingress !== "public"
|
|
350
|
+
|| !["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(attemptContext.peerAddress ?? ""))) return null;
|
|
336
351
|
const context = attemptContext ?? {
|
|
337
352
|
ingress: "public",
|
|
338
353
|
peerAddress: null,
|
|
@@ -344,11 +359,14 @@ export function consumeGuiPairingGrant(
|
|
|
344
359
|
const found = findPairingGrant(grant, state);
|
|
345
360
|
if (!found) {
|
|
346
361
|
// Cross-origin browser requests must not create limiter state as a side effect.
|
|
347
|
-
if (!isRemoteGuiBrowserOriginAllowed(browserOrigin, config)) return null;
|
|
362
|
+
if (localOrigin ? browserOrigin !== localOrigin : !isRemoteGuiBrowserOriginAllowed(browserOrigin, config)) return null;
|
|
348
363
|
const source = recordSourceFailure(state, context, now);
|
|
349
364
|
return attemptContext && !source.allowed ? source : null;
|
|
350
365
|
}
|
|
351
366
|
const [digest, record] = found;
|
|
367
|
+
// A live role/config change must not turn a hub grant into local operator consent, or vice versa.
|
|
368
|
+
if (record.localPairing ? !localOrigin || record.serverOrigin !== localOrigin
|
|
369
|
+
: config.runtimeRole !== "hub") return null;
|
|
352
370
|
if (record.expiresAt <= now) {
|
|
353
371
|
state.pairingGrants.delete(digest);
|
|
354
372
|
return null;
|
|
@@ -369,7 +387,7 @@ export function consumeGuiPairingGrant(
|
|
|
369
387
|
// the session is actually minted from.
|
|
370
388
|
if (!isPairingTransportPermitted(record.serverOrigin)) return null;
|
|
371
389
|
state.pairingGrants.delete(digest);
|
|
372
|
-
return mintSession(record.serverOrigin, record.browserOrigin, "pairing", state, now);
|
|
390
|
+
return mintSession(record.serverOrigin, record.browserOrigin, "pairing", state, now, record.localPairing === true);
|
|
373
391
|
}
|
|
374
392
|
|
|
375
393
|
/**
|
|
@@ -420,6 +438,10 @@ export function authorizeGuiSessionRequest(
|
|
|
420
438
|
state.sessions.delete(token);
|
|
421
439
|
return { ok: false, reason: "expired" };
|
|
422
440
|
}
|
|
441
|
+
if (session.localPairing && standaloneGuiPairingOrigin(config) !== session.serverOrigin) {
|
|
442
|
+
state.sessions.delete(token);
|
|
443
|
+
return { ok: false, reason: "server-origin" };
|
|
444
|
+
}
|
|
423
445
|
if (managementRequestOrigin(req, config) !== session.serverOrigin) {
|
|
424
446
|
return { ok: false, reason: "server-origin" };
|
|
425
447
|
}
|
|
@@ -435,6 +457,6 @@ export function authorizeGuiSessionRequest(
|
|
|
435
457
|
const csrf = req.headers.get("x-opencodex-csrf-token")?.trim();
|
|
436
458
|
if (!csrf || !equalSecret(csrf, session.csrfToken)) return { ok: false, reason: "csrf" };
|
|
437
459
|
}
|
|
438
|
-
if (session.issuance !== "loopback") session.expiresAt = now + REMOTE_GUI_SESSION_TTL_MS;
|
|
460
|
+
if (session.issuance !== "loopback" && !session.localPairing) session.expiresAt = now + REMOTE_GUI_SESSION_TTL_MS;
|
|
439
461
|
return { ok: true, principal: "gui-session", session };
|
|
440
462
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Server } from "bun";
|
|
2
2
|
import type { OcxConfig } from "../../types";
|
|
3
|
+
import { getConfigDir } from "../../config/paths";
|
|
4
|
+
import { discardPickerCaKey } from "../../claude/intercept/picker-ca";
|
|
3
5
|
import type { PickerRouteInput } from "../../claude/intercept/picker-models";
|
|
4
6
|
import { observeClaudeDesktopMode, type ClaudeDesktopModeObservation } from "../../claude/desktop-first-party";
|
|
5
7
|
import { firstPartyDesired, type ClaudeFirstPartyDesired } from "../../claude/first-party-settings";
|
|
@@ -79,6 +81,8 @@ export function createClaudeInterceptLifecycle<T>(): ClaudeInterceptLifecycle<T>
|
|
|
79
81
|
if (!options) return Promise.resolve(outcome = { ok: false, reason: "failed" });
|
|
80
82
|
const opts = options;
|
|
81
83
|
const run = async (): Promise<ClaudeInterceptOutcome> => {
|
|
84
|
+
// Legacy key cleanup is independent of listener eligibility, including on-demand retries.
|
|
85
|
+
try { discardPickerCaKey(opts.configDir ?? getConfigDir()); } catch { /* best-effort; retried on the next attempt */ }
|
|
82
86
|
if (opts.config.runtimeRole === "client") return { ok: false, reason: "client_role" };
|
|
83
87
|
if (!claudeInterceptEnabled(opts.config)) return { ok: false, reason: "disabled" };
|
|
84
88
|
if (opts.requestedPort === 0 && typeof opts.config.claudeCode?.intercept?.port !== "number")
|
|
@@ -202,6 +202,7 @@ import type { WorkflowRefusalLog } from "../workflow-refusal";
|
|
|
202
202
|
import { readyProtocolMetadata } from "../../remote/protocol";
|
|
203
203
|
import { modelCapabilityFields } from "../models-capabilities";
|
|
204
204
|
import { createWebsocketHandler } from "./websocket-handler";
|
|
205
|
+
import { withGrokSessionIdentity } from "../../grok/session-identity";
|
|
205
206
|
|
|
206
207
|
export type ServerIngress = "public" | "unauthenticated-loopback" | "hub-management" | "claude-intercept" | "hub-link";
|
|
207
208
|
|
|
@@ -1476,7 +1477,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1476
1477
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1477
1478
|
let response: Response;
|
|
1478
1479
|
try {
|
|
1479
|
-
response = await handleResponses(req, config, logCtx, {
|
|
1480
|
+
response = await handleResponses(withGrokSessionIdentity(req), config, logCtx, {
|
|
1480
1481
|
turnAdmissionLease,
|
|
1481
1482
|
admission,
|
|
1482
1483
|
onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
|
|
@@ -761,135 +761,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
761
761
|
});
|
|
762
762
|
}
|
|
763
763
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const models = await (deps.fetchAllModels ?? fetchAllModels)(config);
|
|
768
|
-
// Native gpt (passthrough) are also valid subagent picks — they're picker-visible models in the
|
|
769
|
-
// catalog, just buried by priority. List them first so the user can feature them over routed.
|
|
770
|
-
const [{ listCatalogNativeSlugs }, { subagentSelectableModels }] = await Promise.all([
|
|
771
|
-
import("../../codex/catalog"),
|
|
772
|
-
import("../../codex/subagent-selectable-models"),
|
|
773
|
-
]);
|
|
774
|
-
const chosen = config.subagentModels ?? [];
|
|
775
|
-
// A saved roster slot must stay representable even after its model is disabled
|
|
776
|
-
// elsewhere (Models page, provider allowlist, a provider row going away). The
|
|
777
|
-
// dashboard treats `available` as the set of rows it can render, so a chosen id
|
|
778
|
-
// missing from it disappears from the roster UI and the next Save — which PUTs
|
|
779
|
-
// exactly what the UI holds — silently truncates the persisted list. Losing a
|
|
780
|
-
// deliberate 5-model roster to an unrelated visibility toggle is data loss, not a
|
|
781
|
-
// filter. Same reasoning as `fetchGrokCandidateModels`, which deliberately lists a
|
|
782
|
-
// model the user already excluded so its switch remains reachable.
|
|
783
|
-
const available = subagentSelectableModels(config, models, listCatalogNativeSlugs());
|
|
784
|
-
// #857: let CLI/GUI show when a running Codex app-server keeps an older
|
|
785
|
-
// in-memory catalog than the one on disk. Bounded request-path read: the synchronous
|
|
786
|
-
// collector blocked the event loop for the whole Windows CIM walk (4-7s measured).
|
|
787
|
-
const {
|
|
788
|
-
collectCodexAppServerCatalogStateWithin,
|
|
789
|
-
DASHBOARD_CATALOG_STATE_DEADLINE_MS,
|
|
790
|
-
} = await import("../../codex/app-server-processes");
|
|
791
|
-
const catalogState = await collectCodexAppServerCatalogStateWithin(DASHBOARD_CATALOG_STATE_DEADLINE_MS);
|
|
792
|
-
return jsonResponse({
|
|
793
|
-
chosen, available, catalogState,
|
|
794
|
-
pickerAvailable: [...new Set(filterCatalogVisibleModels(models, config).map(catalogModelSlug).filter(slug => slug.includes("/")))],
|
|
795
|
-
pickerOrder: config.modelPickerOrder ?? [],
|
|
796
|
-
pickerOrderMode: config.modelPickerOrderMode ?? null,
|
|
797
|
-
});
|
|
798
|
-
}
|
|
799
|
-
if (url.pathname === "/api/subagent-models" && req.method === "PUT") {
|
|
800
|
-
let rawBody: unknown;
|
|
801
|
-
try { rawBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
802
|
-
if (!isPlainRecord(rawBody)) return jsonResponse({ error: "JSON body must be an object" }, 400);
|
|
803
|
-
const body = rawBody as { models?: unknown; pickerOrder?: unknown; pickerOrderMode?: unknown };
|
|
804
|
-
const updatesRoster = body.models !== undefined;
|
|
805
|
-
const updatesPicker = body.pickerOrder !== undefined;
|
|
806
|
-
if (!updatesRoster && !updatesPicker) return jsonResponse({ error: "models or pickerOrder is required" }, 400);
|
|
807
|
-
let chosen: string[] | undefined;
|
|
808
|
-
if (updatesRoster) {
|
|
809
|
-
if (!Array.isArray(body.models) || body.models.some(model => typeof model !== "string")) {
|
|
810
|
-
return jsonResponse({ error: "models must be an array of strings" }, 400);
|
|
811
|
-
}
|
|
812
|
-
// Keep the original valid roster contract: no discovery validation, trimming or deduping.
|
|
813
|
-
chosen = body.models.slice(0, 5);
|
|
814
|
-
}
|
|
815
|
-
const mode = body.pickerOrderMode;
|
|
816
|
-
if (mode !== undefined && (!updatesPicker || (mode !== null
|
|
817
|
-
&& mode !== "alphabetical" && mode !== "provider" && mode !== "most-used"))) {
|
|
818
|
-
return jsonResponse({ error: "pickerOrderMode requires pickerOrder and must be alphabetical, provider, most-used, or null" }, 400);
|
|
819
|
-
}
|
|
820
|
-
let pickerOrder: string[] | undefined;
|
|
821
|
-
if (updatesPicker) {
|
|
822
|
-
if (body.pickerOrder !== null && (!Array.isArray(body.pickerOrder)
|
|
823
|
-
|| body.pickerOrder.some(model => typeof model !== "string" || model.trim() === ""))) {
|
|
824
|
-
return jsonResponse({ error: "pickerOrder must be an array of non-empty routed model ids, or null" }, 400);
|
|
825
|
-
}
|
|
826
|
-
pickerOrder = body.pickerOrder === null ? [] : (body.pickerOrder as string[]).map(model => model.trim());
|
|
827
|
-
if (new Set(pickerOrder).size !== pickerOrder.length) {
|
|
828
|
-
return jsonResponse({ error: "pickerOrder must not contain duplicate ids" }, 400);
|
|
829
|
-
}
|
|
830
|
-
if (pickerOrder.length > 0) {
|
|
831
|
-
const models = await (deps.fetchAllModels ?? fetchAllModels)(config);
|
|
832
|
-
// Evaluate visibility AFTER discovery: a concurrent visibility write may have completed.
|
|
833
|
-
const visible = new Set(filterCatalogVisibleModels(models, config).map(catalogModelSlug).filter(slug => slug.includes("/")));
|
|
834
|
-
// A bare native id orders the complete picker (docs: guides/model-ordering.md).
|
|
835
|
-
for (const row of nativeModelRows(config)) if (!row.disabled) visible.add(row.slug);
|
|
836
|
-
if (pickerOrder.some(model => !visible.has(model))) {
|
|
837
|
-
return jsonResponse({ error: "pickerOrder must contain visible routed or native model ids, each at most once" }, 400);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// Everything above can await. From this snapshot through persistence there is no yield.
|
|
843
|
-
// Stage deletion intent before adopting the touched fields through the canonical
|
|
844
|
-
// live deletion owner. A failed save restores both fields and pending intent.
|
|
845
|
-
if (updatesPicker && config.configRebaseProvenance !== undefined
|
|
846
|
-
&& parsedConfigRebaseDeletionKeys(config) === null) {
|
|
847
|
-
// A newer provenance format must not silently discard this clear's intent on rebase.
|
|
848
|
-
return jsonResponse({ error: "unsupported config deletion provenance" }, 409);
|
|
849
|
-
}
|
|
850
|
-
const draft = { ...projectConfigRebaseProvenance(config) };
|
|
851
|
-
if (chosen !== undefined) draft.subagentModels = chosen;
|
|
852
|
-
if (pickerOrder !== undefined) {
|
|
853
|
-
if (pickerOrder.length === 0) {
|
|
854
|
-
deleteConfigTopLevelKey(draft, "modelPickerOrder");
|
|
855
|
-
deleteConfigTopLevelKey(draft, "modelPickerOrderMode");
|
|
856
|
-
} else {
|
|
857
|
-
draft.modelPickerOrder = pickerOrder;
|
|
858
|
-
if (mode === "alphabetical" || mode === "provider" || mode === "most-used") draft.modelPickerOrderMode = mode;
|
|
859
|
-
else deleteConfigTopLevelKey(draft, "modelPickerOrderMode");
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
const projected = projectConfigRebaseProvenance(draft);
|
|
863
|
-
const touched = [
|
|
864
|
-
...(updatesRoster ? ["subagentModels" as const] : []),
|
|
865
|
-
...(updatesPicker ? ["modelPickerOrder" as const, "modelPickerOrderMode" as const] : []),
|
|
866
|
-
"configRebaseProvenance" as const,
|
|
867
|
-
];
|
|
868
|
-
const rollback = captureConfigTopLevelRollback(config, touched);
|
|
869
|
-
try {
|
|
870
|
-
for (const key of touched) {
|
|
871
|
-
if (Object.hasOwn(projected, key)) Object.defineProperty(config, key, {
|
|
872
|
-
value: projected[key], writable: true, enumerable: true, configurable: true,
|
|
873
|
-
});
|
|
874
|
-
else deleteConfigTopLevelKey(config, key);
|
|
875
|
-
}
|
|
876
|
-
(deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
|
|
877
|
-
} catch (error) {
|
|
878
|
-
rollback();
|
|
879
|
-
throw error;
|
|
880
|
-
}
|
|
881
|
-
// Capture the result before convergence yields to another settings mutation.
|
|
882
|
-
const saved = {
|
|
883
|
-
applied: [...(config.subagentModels ?? [])],
|
|
884
|
-
pickerOrder: [...(config.modelPickerOrder ?? [])],
|
|
885
|
-
pickerOrderMode: config.modelPickerOrderMode ?? null,
|
|
886
|
-
};
|
|
887
|
-
const catalogRefresh = await convergeCodexCatalog();
|
|
888
|
-
if (updatesRoster) {
|
|
889
|
-
await syncClaudeAgentDefsBestEffort();
|
|
890
|
-
await autoApplyDesktopBestEffort();
|
|
891
|
-
}
|
|
892
|
-
return jsonResponse({ ok: true, ...saved, catalogRefresh });
|
|
764
|
+
if (url.pathname === "/api/subagent-models") {
|
|
765
|
+
const { handleSubagentModelRoutes } = await import("./subagent-model-routes");
|
|
766
|
+
return handleSubagentModelRoutes(ctx, autoApplyDesktopBestEffort);
|
|
893
767
|
}
|
|
894
768
|
|
|
895
769
|
// Priority-ordered subagent model fallback chain for quota-aware spawn routing.
|
|
@@ -1194,6 +1068,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1194
1068
|
const picker = modeSaved.ok && pickerPreferenceOn(loadConfig())
|
|
1195
1069
|
? await ops.enableLocked({ persist: false, context: "server" })
|
|
1196
1070
|
: await pickerStatusFor(loadConfig());
|
|
1071
|
+
if (modeSaved.ok) await syncClaudeAgentDefsBestEffort();
|
|
1197
1072
|
return jsonResponse({
|
|
1198
1073
|
ok: true,
|
|
1199
1074
|
mode: "first-party",
|
|
@@ -1542,6 +1417,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1542
1417
|
? { visionSidecar: { backend: visionOverride.backend, model: visionOverride.model } }
|
|
1543
1418
|
: {}),
|
|
1544
1419
|
fastMode: config.fastMode,
|
|
1420
|
+
forceAvailable: [...visibleNativeSlugs(config), ...filterCatalogVisibleModels(models, config).map(catalogModelSlug)],
|
|
1545
1421
|
contextWindows,
|
|
1546
1422
|
effectiveModelEnv: effectiveModelEnv(config.claudeCode, contextWindows),
|
|
1547
1423
|
available,
|
|
@@ -1678,6 +1554,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1678
1554
|
const finalDesired = firstPartyDesired(config, observeClaudeDesktopMode(config));
|
|
1679
1555
|
const residual = !finalDesired.desktop && !finalDesired.cli
|
|
1680
1556
|
&& readFirstPartyProxyStatus(config, bound?.proxyPort ?? null) !== "none";
|
|
1557
|
+
await syncClaudeAgentDefsBestEffort();
|
|
1681
1558
|
return jsonResponse({ ok: true, enabled: config.claudeCode?.enabled !== false,
|
|
1682
1559
|
cliFirstParty: body.cliFirstParty,
|
|
1683
1560
|
warnings: [
|
|
@@ -276,7 +276,7 @@ export async function syncEnabledClientIntegrations(
|
|
|
276
276
|
},
|
|
277
277
|
config,
|
|
278
278
|
port,
|
|
279
|
-
}, ["mcode", "pi", "aside", "raycast", "omo", "cline", "droid"]));
|
|
279
|
+
}, ["mcode", "pi", "aside", "raycast", "omo", "cline", "droid", "opencode", "kilo"]));
|
|
280
280
|
|
|
281
281
|
return out;
|
|
282
282
|
}
|