@yansigit/opencodex 2.33.1 → 2.35.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 +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -3
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +40 -3
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +9 -9
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-shell.ts +103 -6
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +74 -39
- package/src/adapters/cursor/request-builder.ts +32 -10
- package/src/adapters/cursor/tool-definitions.ts +13 -10
- package/src/adapters/cursor/tool-result-normalize.ts +35 -3
- package/src/adapters/cursor/types.ts +16 -4
- package/src/adapters/cursor.ts +97 -70
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +156 -65
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +334 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -37
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +229 -153
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +54 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +19 -20
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +73 -14
- package/src/providers/registry.ts +160 -68
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +23 -0
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +152 -80
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +21 -0
- package/src/server/request-log.ts +46 -20
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +571 -137
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +15 -4
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +77 -0
- package/src/types/request.ts +16 -10
- package/src/types/tools.ts +20 -25
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +17 -7
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-CIDo4y4k.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
- package/src/server/aistudio-ws-hub.ts +0 -295
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
const pendingTopLevelDeletions = new WeakMap<OcxConfig, Set<string>>();
|
|
4
|
+
export const CONFIG_REBASE_PROVENANCE_KEY = "configRebaseProvenance";
|
|
5
|
+
|
|
6
|
+
export function parsedConfigRebaseDeletionKeys(config: OcxConfig): Set<string> | null {
|
|
7
|
+
const value = config.configRebaseProvenance;
|
|
8
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
9
|
+
const record = value as Record<string, unknown>;
|
|
10
|
+
if (record.version !== 1 || !Array.isArray(record.deletedTopLevelKeys)) return null;
|
|
11
|
+
if (!record.deletedTopLevelKeys.every(key => typeof key === "string" && key !== CONFIG_REBASE_PROVENANCE_KEY)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return new Set(record.deletedTopLevelKeys as string[]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function configRebaseDeletionKeys(config: OcxConfig): Set<string> {
|
|
18
|
+
const deleted = new Set([
|
|
19
|
+
...(parsedConfigRebaseDeletionKeys(config) ?? []),
|
|
20
|
+
...(pendingTopLevelDeletions.get(config) ?? []),
|
|
21
|
+
]);
|
|
22
|
+
const record = config as unknown as Record<string, unknown>;
|
|
23
|
+
for (const key of [...deleted]) {
|
|
24
|
+
if (Object.hasOwn(record, key) && record[key] !== undefined) deleted.delete(key);
|
|
25
|
+
}
|
|
26
|
+
return deleted;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function configHasRebaseProvenance(config: OcxConfig): boolean {
|
|
30
|
+
return parsedConfigRebaseDeletionKeys(config) !== null || pendingTopLevelDeletions.has(config);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function projectConfigRebaseProvenance(config: OcxConfig): OcxConfig {
|
|
34
|
+
const pending = pendingTopLevelDeletions.get(config);
|
|
35
|
+
const parsed = parsedConfigRebaseDeletionKeys(config);
|
|
36
|
+
// Preserve unknown future metadata byte-for-value. It carries no authority here.
|
|
37
|
+
if (config.configRebaseProvenance !== undefined && parsed === null) return config;
|
|
38
|
+
const deleted = new Set([...(parsed ?? []), ...(pending ?? [])]);
|
|
39
|
+
const record = config as unknown as Record<string, unknown>;
|
|
40
|
+
for (const key of [...deleted]) {
|
|
41
|
+
if (Object.hasOwn(record, key) && record[key] !== undefined) deleted.delete(key);
|
|
42
|
+
}
|
|
43
|
+
// A SHALLOW copy, deliberately. A provider entry may carry a non-cloneable value — a test
|
|
44
|
+
// fixture injects its own `fetch`, and `structuredClone` throws DataCloneError on a function,
|
|
45
|
+
// which took every provider-probe and CLI-parity suite down. Only the provenance key is
|
|
46
|
+
// rewritten here, so nothing below the top level needs to be copied at all; the deep clone
|
|
47
|
+
// was doing work this projection never asked for.
|
|
48
|
+
const projected = { ...config };
|
|
49
|
+
if (deleted.size === 0) delete projected.configRebaseProvenance;
|
|
50
|
+
else projected.configRebaseProvenance = {
|
|
51
|
+
version: 1,
|
|
52
|
+
deletedTopLevelKeys: [...deleted].sort(),
|
|
53
|
+
};
|
|
54
|
+
return projected;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Delete one top-level config key and retain the writer's explicit intent for rebasing. */
|
|
58
|
+
export function deleteConfigTopLevelKey<K extends keyof OcxConfig>(config: OcxConfig, key: K): void {
|
|
59
|
+
delete config[key];
|
|
60
|
+
if (key === CONFIG_REBASE_PROVENANCE_KEY) return;
|
|
61
|
+
const deleted = pendingTopLevelDeletions.get(config) ?? new Set<string>();
|
|
62
|
+
deleted.add(key);
|
|
63
|
+
pendingTopLevelDeletions.set(config, deleted);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function clearPendingConfigTopLevelDeletions(config: OcxConfig): void {
|
|
67
|
+
pendingTopLevelDeletions.delete(config);
|
|
68
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
providerBaseUrlConfigError,
|
|
17
17
|
providerHeadersConfigError,
|
|
18
18
|
reasoningSummaryDeliveryRecordConfigError,
|
|
19
|
+
maxWsFrameBytesConfigError,
|
|
19
20
|
upstreamHttpVersionConfigError,
|
|
21
|
+
wsUpstreamConfigError,
|
|
20
22
|
} from "./config/provider-validation";
|
|
21
23
|
import {
|
|
22
24
|
bumpConfigGenerationAtPath,
|
|
@@ -62,6 +64,7 @@ import { providerDestinationConfigError } from "./lib/destination-policy";
|
|
|
62
64
|
import { redactSecretString } from "./lib/redact";
|
|
63
65
|
import { antigravityOAuthDestinationConfigError, providerTlsProfileConfigError } from "./lib/provider-tls-profile";
|
|
64
66
|
import { openRouterRoutingConfigError } from "./providers/openrouter-routing";
|
|
67
|
+
import { MODEL_ALIAS_PATTERN } from "./providers/default-aliases";
|
|
65
68
|
import {
|
|
66
69
|
MODEL_ADAPTER_OVERRIDE_ALLOWED,
|
|
67
70
|
OPENAI_PROVIDER_TIER_VERSION,
|
|
@@ -84,6 +87,7 @@ import {
|
|
|
84
87
|
registryModelServiceTierCapabilityApplies,
|
|
85
88
|
} from "./providers/registry";
|
|
86
89
|
import { resolveOpenAiVirtualModel } from "./providers/openai-virtual-models";
|
|
90
|
+
import { slugEquivalenceKey, slugsEquivalent } from "./providers/slug-codec";
|
|
87
91
|
import { parseDesktopProfile } from "./claude/desktop-profile";
|
|
88
92
|
import { isCodexReasoningEffort } from "./reasoning-effort";
|
|
89
93
|
import { parseSubagentRoles, salvageSubagentRoles } from "./codex/agent-roles";
|
|
@@ -144,6 +148,15 @@ export {
|
|
|
144
148
|
writeRuntimePort,
|
|
145
149
|
type RuntimePortState,
|
|
146
150
|
} from "./config/process-state";
|
|
151
|
+
import {
|
|
152
|
+
clearPendingConfigTopLevelDeletions,
|
|
153
|
+
configHasRebaseProvenance,
|
|
154
|
+
configRebaseDeletionKeys,
|
|
155
|
+
CONFIG_REBASE_PROVENANCE_KEY,
|
|
156
|
+
deleteConfigTopLevelKey,
|
|
157
|
+
projectConfigRebaseProvenance,
|
|
158
|
+
} from "./config/rebase-provenance";
|
|
159
|
+
export { deleteConfigTopLevelKey } from "./config/rebase-provenance";
|
|
147
160
|
|
|
148
161
|
export class OpenAiTierBackupCleanupError extends Error {
|
|
149
162
|
constructor() { super("OpenAI tier backup temporary cleanup failed"); this.name = "OpenAiTierBackupCleanupError"; }
|
|
@@ -491,8 +504,10 @@ const providerConfigSchema = z.object({
|
|
|
491
504
|
}).strict().transform(value => value.managedIdentityClientId === undefined
|
|
492
505
|
? value
|
|
493
506
|
: { ...value, managedIdentityClientId: value.managedIdentityClientId.trim() }).optional(),
|
|
507
|
+
alias: z.string().optional(),
|
|
508
|
+
modelAliases: z.record(z.string(), z.string()).optional(),
|
|
509
|
+
defaultAliases: z.boolean().optional(),
|
|
494
510
|
requestPacing: requestPacingSchema.optional().catch(undefined),
|
|
495
|
-
tlsProfile: z.literal("antigravity-browser").optional(),
|
|
496
511
|
mcpMaxTools: z.number().int().positive().optional(),
|
|
497
512
|
mcpMaxSchemaBytes: z.number().int().positive().optional(),
|
|
498
513
|
mcpMaxResultBytes: z.number().int().positive().optional(),
|
|
@@ -506,6 +521,8 @@ const providerConfigSchema = z.object({
|
|
|
506
521
|
preserveResponsesReasoningContent: z.boolean().optional(),
|
|
507
522
|
decodesNativeCompactionBlobs: z.boolean().optional(),
|
|
508
523
|
allowPrivateNetwork: z.boolean().optional(),
|
|
524
|
+
wsUpstream: z.boolean().nullish().transform(value => value ?? undefined),
|
|
525
|
+
maxWsFrameBytes: z.number().int().positive().nullish().transform(value => value ?? undefined),
|
|
509
526
|
// The management API accepts `null` as "clear this", so a config written before the POST
|
|
510
527
|
// canonicalization below can hold one on disk. Rejecting it here would send the operator
|
|
511
528
|
// through invalid-config recovery for a value the API told them was fine.
|
|
@@ -523,8 +540,8 @@ const providerConfigSchema = z.object({
|
|
|
523
540
|
// accepted, persisted, and then silently resolved to the `code_mode_only` default — the
|
|
524
541
|
// operator asked for shell mode, got code mode, and was told nothing (#2106).
|
|
525
542
|
codexToolMode: z.enum(["code_mode_only", "shell"]).optional(),
|
|
526
|
-
// Validated rather than passed through: same rationale as codexToolMode above.
|
|
527
543
|
projectContext: z.enum(["off", "on"]).optional(),
|
|
544
|
+
tlsProfile: z.literal("antigravity-browser").optional(),
|
|
528
545
|
responsesItemIdRepair: z.object({
|
|
529
546
|
message: z.array(z.string().min(1)).optional(),
|
|
530
547
|
reasoning: z.array(z.string().min(1)).optional(),
|
|
@@ -548,7 +565,9 @@ export {
|
|
|
548
565
|
providerBaseUrlConfigError,
|
|
549
566
|
providerHeadersConfigError,
|
|
550
567
|
reasoningSummaryDeliveryRecordConfigError,
|
|
568
|
+
maxWsFrameBytesConfigError,
|
|
551
569
|
upstreamHttpVersionConfigError,
|
|
570
|
+
wsUpstreamConfigError,
|
|
552
571
|
} from "./config/provider-validation";
|
|
553
572
|
|
|
554
573
|
function providerResponsesPathConfigError(responsesPath: string | undefined): string | null {
|
|
@@ -858,6 +877,12 @@ const v2NativeParentOverrideSchema = z.object({
|
|
|
858
877
|
|
|
859
878
|
const configSchema = z.object({
|
|
860
879
|
port: z.number().int().min(0).max(65535).default(10100),
|
|
880
|
+
autonomousRemediation: z.object({
|
|
881
|
+
enabled: z.boolean().optional(),
|
|
882
|
+
instanceId: z.string().trim().min(1).optional(),
|
|
883
|
+
threshold: z.number().int().positive().optional(),
|
|
884
|
+
rollingWindowMs: z.number().int().positive().optional(),
|
|
885
|
+
}).strict().optional().catch(undefined),
|
|
861
886
|
managementUsageMaxReadBytes: z.number().int().positive().default(64 * 1024 * 1024),
|
|
862
887
|
// Invalid hand edits disable only this opt-in circuit. Live writes remain strict.
|
|
863
888
|
upstreamHostCircuitThreshold: z.number().int()
|
|
@@ -887,6 +912,10 @@ const configSchema = z.object({
|
|
|
887
912
|
]).optional().catch(undefined),
|
|
888
913
|
providers: z.record(z.string(), providerConfigSchema),
|
|
889
914
|
defaultProvider: z.string().min(1).default("openai"),
|
|
915
|
+
defaultModelAliases: z.boolean().optional(),
|
|
916
|
+
// Future versions remain opaque through passthrough-compatible whole-config saves.
|
|
917
|
+
// Only version 1 grants deletion authority in the rebase path.
|
|
918
|
+
configRebaseProvenance: z.unknown().optional(),
|
|
890
919
|
// A retry can be billable, so absence and malformed hand edits both stay off.
|
|
891
920
|
emptyCompletionRetry: z.boolean().optional().catch(false),
|
|
892
921
|
// A malformed hand edit must not silently stop opening the browser: fall back
|
|
@@ -899,7 +928,6 @@ const configSchema = z.object({
|
|
|
899
928
|
providerContextCaps: z.record(z.string(), z.number().int().positive()).optional(),
|
|
900
929
|
contextCapValue: z.number().int().positive().optional(),
|
|
901
930
|
multiAgentGuidanceEnabled: z.boolean().optional(),
|
|
902
|
-
// Invalid hand edits disable only this experimental opt-in subtree.
|
|
903
931
|
v2NativeParentOverride: v2NativeParentOverrideSchema.optional().catch(undefined),
|
|
904
932
|
// Invalid optional recovery config must not discard unrelated provider/account state.
|
|
905
933
|
agentTaskRecovery: agentTaskRecoverySchema.optional().catch(undefined),
|
|
@@ -911,7 +939,6 @@ const configSchema = z.object({
|
|
|
911
939
|
injectionEffort: z.string().optional().catch(undefined),
|
|
912
940
|
syncCodexSubagentDefaults: z.boolean().optional().catch(undefined),
|
|
913
941
|
syncCodexAgentRoles: z.boolean().optional().catch(undefined),
|
|
914
|
-
// Salvage element by element. A malformed neighbour must not wipe the catalog.
|
|
915
942
|
subagentRoles: z.unknown().optional(),
|
|
916
943
|
// Per-primary-model fallback chains. Values must be non-empty string arrays;
|
|
917
944
|
// malformed entries degrade to undefined rather than rejecting the whole config.
|
|
@@ -919,6 +946,10 @@ const configSchema = z.object({
|
|
|
919
946
|
z.string(),
|
|
920
947
|
z.array(z.string().trim().min(1)).min(1),
|
|
921
948
|
).optional().catch(undefined),
|
|
949
|
+
subagentCandidates: z.union([
|
|
950
|
+
z.array(z.string().trim().min(1)).min(1),
|
|
951
|
+
z.record(z.string().trim().min(1), z.array(z.string().trim().min(1)).min(1)),
|
|
952
|
+
]).optional().catch(undefined),
|
|
922
953
|
codexShimAutoRestore: z.boolean().optional(),
|
|
923
954
|
pausedCodexAccountIds: z.array(z.string().regex(/^[a-zA-Z0-9._-]{1,64}$/)).optional(),
|
|
924
955
|
codexAccountNamespaces: codexAccountNamespacesSchema.optional(),
|
|
@@ -931,6 +962,9 @@ const configSchema = z.object({
|
|
|
931
962
|
// A malformed hand edit must degrade to false without discarding providers, accounts,
|
|
932
963
|
// or the exact selector map. Live writes remain strict.
|
|
933
964
|
codexAccountPickerEnabled: z.boolean().optional().catch(false),
|
|
965
|
+
// Same degrade-not-reject rule: a malformed hand edit hides Spark rather than discarding the
|
|
966
|
+
// whole config. Hidden is also the default, so `catch(false)` and the default agree.
|
|
967
|
+
showCodexSparkQuota: z.boolean().optional().catch(false),
|
|
934
968
|
// Model ids excluded from the Grok Build managed block (dashboard switches).
|
|
935
969
|
grokExcludedModels: z.array(z.string()).optional(),
|
|
936
970
|
// Invalid values degrade to undefined ("auto") instead of failing the whole
|
|
@@ -1069,6 +1103,22 @@ const configSchema = z.object({
|
|
|
1069
1103
|
message: responsesPathError,
|
|
1070
1104
|
});
|
|
1071
1105
|
}
|
|
1106
|
+
const wsUpstreamError = wsUpstreamConfigError((provider as { wsUpstream?: unknown }).wsUpstream);
|
|
1107
|
+
if (wsUpstreamError) {
|
|
1108
|
+
ctx.addIssue({
|
|
1109
|
+
code: "custom",
|
|
1110
|
+
path: ["providers", redactSecretString(name), "wsUpstream"],
|
|
1111
|
+
message: wsUpstreamError,
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
const maxWsFrameBytesError = maxWsFrameBytesConfigError((provider as { maxWsFrameBytes?: unknown }).maxWsFrameBytes);
|
|
1115
|
+
if (maxWsFrameBytesError) {
|
|
1116
|
+
ctx.addIssue({
|
|
1117
|
+
code: "custom",
|
|
1118
|
+
path: ["providers", redactSecretString(name), "maxWsFrameBytes"],
|
|
1119
|
+
message: maxWsFrameBytesError,
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1072
1122
|
const tlsProfileError = providerTlsProfileConfigError(name, provider);
|
|
1073
1123
|
if (tlsProfileError) {
|
|
1074
1124
|
ctx.addIssue({
|
|
@@ -1170,6 +1220,17 @@ const configSchema = z.object({
|
|
|
1170
1220
|
message: reasoningSummariesError,
|
|
1171
1221
|
});
|
|
1172
1222
|
}
|
|
1223
|
+
const verbositySupportError = booleanRecordConfigError(
|
|
1224
|
+
(provider as { modelSupportsVerbosity?: unknown }).modelSupportsVerbosity,
|
|
1225
|
+
"modelSupportsVerbosity",
|
|
1226
|
+
);
|
|
1227
|
+
if (verbositySupportError) {
|
|
1228
|
+
ctx.addIssue({
|
|
1229
|
+
code: "custom",
|
|
1230
|
+
path: ["providers", redactSecretString(name), "modelSupportsVerbosity"],
|
|
1231
|
+
message: verbositySupportError,
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1173
1234
|
const serviceTierModelsError = booleanRecordConfigError(
|
|
1174
1235
|
(provider as { modelSupportsServiceTier?: unknown }).modelSupportsServiceTier,
|
|
1175
1236
|
"modelSupportsServiceTier",
|
|
@@ -1682,8 +1743,9 @@ function malformedSyncCodexAgentRolesWarning(rawParsed: unknown): string | null
|
|
|
1682
1743
|
}
|
|
1683
1744
|
|
|
1684
1745
|
function normalizeSyncCodexAgentRoles(config: OcxConfig, rawParsed: unknown): OcxConfig {
|
|
1685
|
-
|
|
1686
|
-
|
|
1746
|
+
return malformedSyncCodexAgentRolesWarning(rawParsed)
|
|
1747
|
+
? { ...config, syncCodexAgentRoles: false }
|
|
1748
|
+
: config;
|
|
1687
1749
|
}
|
|
1688
1750
|
|
|
1689
1751
|
function warnDegradedSyncCodexAgentRoles(rawParsed: unknown): void {
|
|
@@ -1701,8 +1763,7 @@ function subagentRolesError(value: unknown): string | null {
|
|
|
1701
1763
|
const raw = rawConfigRecord(value);
|
|
1702
1764
|
if (!raw || !Object.hasOwn(raw, "subagentRoles")) return null;
|
|
1703
1765
|
const parsed = parseSubagentRoles(raw.subagentRoles);
|
|
1704
|
-
|
|
1705
|
-
return `schema_invalid: ${parsed.error}`;
|
|
1766
|
+
return parsed.ok ? null : `schema_invalid: ${parsed.error}`;
|
|
1706
1767
|
}
|
|
1707
1768
|
|
|
1708
1769
|
function malformedUpstreamHostCircuitThresholdWarning(rawParsed: unknown): string | null {
|
|
@@ -1873,6 +1934,7 @@ export function loadConfig(): OcxConfig {
|
|
|
1873
1934
|
try {
|
|
1874
1935
|
const raw = readFileSync(configPath, "utf-8").replace(/^\uFEFF/, "");
|
|
1875
1936
|
const parsed = JSON.parse(raw);
|
|
1937
|
+
sanitizeAliasesForLoad(parsed);
|
|
1876
1938
|
sanitizeRetryOn429ForLoad(parsed);
|
|
1877
1939
|
sanitizeModelCostsForLoad(parsed);
|
|
1878
1940
|
const result = configSchema.safeParse(parsed);
|
|
@@ -1950,6 +2012,43 @@ export function loadConfig(): OcxConfig {
|
|
|
1950
2012
|
}
|
|
1951
2013
|
}
|
|
1952
2014
|
|
|
2015
|
+
/** Hand-edited alias mistakes disable only the bad alias; providers and routing survive. */
|
|
2016
|
+
function sanitizeAliasesForLoad(raw: unknown): void {
|
|
2017
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
2018
|
+
const root = raw as Record<string, unknown>;
|
|
2019
|
+
if (!root.providers || typeof root.providers !== "object" || Array.isArray(root.providers)) return;
|
|
2020
|
+
const providers = root.providers as Record<string, Record<string, unknown>>;
|
|
2021
|
+
const providerNames = new Set(Object.keys(providers).map(name => name.toLowerCase()));
|
|
2022
|
+
const claimedProviders = new Set<string>();
|
|
2023
|
+
const comboAliases = new Set(Object.values((root.combos as Record<string, { alias?: unknown }> | undefined) ?? {})
|
|
2024
|
+
.map(combo => typeof combo?.alias === "string" ? combo.alias.toLowerCase() : "").filter(Boolean));
|
|
2025
|
+
const accountNamespaces = new Set(Object.keys((root.codexAccountNamespaces as Record<string, unknown> | undefined) ?? {}).map(name => name.toLowerCase()));
|
|
2026
|
+
for (const provider of Object.values(providers)) {
|
|
2027
|
+
const alias = provider.alias;
|
|
2028
|
+
if (typeof alias !== "string" || !isValidProviderName(alias)
|
|
2029
|
+
|| providerNames.has(alias.toLowerCase()) || claimedProviders.has(alias.toLowerCase())
|
|
2030
|
+
|| comboAliases.has(alias.toLowerCase()) || accountNamespaces.has(alias.toLowerCase())) {
|
|
2031
|
+
if (alias !== undefined) console.warn("Ignoring invalid or colliding provider alias in config.json");
|
|
2032
|
+
delete provider.alias;
|
|
2033
|
+
} else claimedProviders.add(alias.toLowerCase());
|
|
2034
|
+
if (!provider.modelAliases || typeof provider.modelAliases !== "object" || Array.isArray(provider.modelAliases)) {
|
|
2035
|
+
if (provider.modelAliases !== undefined) delete provider.modelAliases;
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
const aliases = provider.modelAliases as Record<string, unknown>;
|
|
2039
|
+
const nativeIds = new Set((Array.isArray(provider.models) ? provider.models : []).filter((id): id is string => typeof id === "string").map(id => id.toLowerCase()));
|
|
2040
|
+
const claimed = new Set<string>();
|
|
2041
|
+
for (const [id, value] of Object.entries(aliases)) {
|
|
2042
|
+
const lower = typeof value === "string" ? value.toLowerCase() : "";
|
|
2043
|
+
if (typeof value !== "string" || !MODEL_ALIAS_PATTERN.test(value) || claimed.has(lower)
|
|
2044
|
+
|| nativeIds.has(lower) || comboAliases.has(lower) || /^(?:gpt-|o1-|o3-|o4-|codex-)/i.test(value)) {
|
|
2045
|
+
console.warn(`Ignoring invalid or colliding model alias for ${id} in config.json`);
|
|
2046
|
+
delete aliases[id];
|
|
2047
|
+
} else claimed.add(lower);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
1953
2052
|
/** Refresh the user cost-overlay registry from `config` and return it unchanged. */
|
|
1954
2053
|
function withRefreshedCostOverlays(config: OcxConfig): OcxConfig {
|
|
1955
2054
|
refreshUserCostOverlays(config);
|
|
@@ -2021,6 +2120,52 @@ export function subagentDefaultSyncEffective(
|
|
|
2021
2120
|
return config.syncCodexSubagentDefaults === true && Boolean(config.injectionModel?.trim());
|
|
2022
2121
|
}
|
|
2023
2122
|
|
|
2123
|
+
/**
|
|
2124
|
+
* Resolve and normalize configured candidates for one spawned sub-agent.
|
|
2125
|
+
* Supports a global ordered list or a record keyed by role/model, with
|
|
2126
|
+
* `default` and `*` fallbacks for unmatched requests.
|
|
2127
|
+
*/
|
|
2128
|
+
export function resolveSubagentCandidates(
|
|
2129
|
+
config: Pick<OcxConfig, "subagentCandidates"> | OcxConfig,
|
|
2130
|
+
roleOrModel?: string,
|
|
2131
|
+
): string[] {
|
|
2132
|
+
const candidates = config?.subagentCandidates;
|
|
2133
|
+
if (!candidates) return [];
|
|
2134
|
+
|
|
2135
|
+
const normalizeList = (raw: unknown): string[] => {
|
|
2136
|
+
if (!Array.isArray(raw)) return [];
|
|
2137
|
+
const result: string[] = [];
|
|
2138
|
+
const seen = new Set<string>();
|
|
2139
|
+
for (const item of raw) {
|
|
2140
|
+
if (typeof item !== "string") continue;
|
|
2141
|
+
const trimmed = item.trim();
|
|
2142
|
+
if (!trimmed) continue;
|
|
2143
|
+
const key = slugEquivalenceKey(trimmed);
|
|
2144
|
+
if (seen.has(key)) continue;
|
|
2145
|
+
seen.add(key);
|
|
2146
|
+
result.push(trimmed);
|
|
2147
|
+
}
|
|
2148
|
+
return result;
|
|
2149
|
+
};
|
|
2150
|
+
|
|
2151
|
+
if (Array.isArray(candidates)) return normalizeList(candidates);
|
|
2152
|
+
if (typeof candidates !== "object") return [];
|
|
2153
|
+
|
|
2154
|
+
const record = candidates as Record<string, unknown>;
|
|
2155
|
+
const trimmed = roleOrModel?.trim();
|
|
2156
|
+
let selected: unknown;
|
|
2157
|
+
if (trimmed) {
|
|
2158
|
+
if (Object.hasOwn(record, trimmed)) {
|
|
2159
|
+
selected = record[trimmed];
|
|
2160
|
+
} else {
|
|
2161
|
+
const matchingKey = Object.keys(record).find(key => slugsEquivalent(key, trimmed));
|
|
2162
|
+
if (matchingKey) selected = record[matchingKey];
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
if (!selected) selected = record.default ?? record["*"];
|
|
2166
|
+
return normalizeList(selected);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2024
2169
|
function mergeConfigDefaults(parsed: unknown): unknown {
|
|
2025
2170
|
if (!parsed || typeof parsed !== "object") return parsed;
|
|
2026
2171
|
const defaults = getDefaultConfig();
|
|
@@ -2565,7 +2710,12 @@ function persistConfigUnlocked(config: OcxConfig): boolean {
|
|
|
2565
2710
|
// External editors can add provider rows the live config deliberately does
|
|
2566
2711
|
// not route with yet; merge them at the serialization boundary so an
|
|
2567
2712
|
// unrelated in-process save cannot erase the provider or its overlay.
|
|
2713
|
+
// Provider preservation reads symbol-keyed live-owner state, which structuredClone
|
|
2714
|
+
// intentionally drops. Resolve that ownership before projecting JSON provenance.
|
|
2715
|
+
const provenanceProjection = projectConfigRebaseProvenance(config);
|
|
2568
2716
|
const persisted = withPreservedDiskOnlyProviders(config);
|
|
2717
|
+
if (provenanceProjection.configRebaseProvenance === undefined) delete persisted.configRebaseProvenance;
|
|
2718
|
+
else persisted.configRebaseProvenance = provenanceProjection.configRebaseProvenance;
|
|
2569
2719
|
const bytes = JSON.stringify(persisted, null, 2) + "\n";
|
|
2570
2720
|
let unchanged = false;
|
|
2571
2721
|
try {
|
|
@@ -2593,9 +2743,15 @@ export function saveConfig(config: OcxConfig): void {
|
|
|
2593
2743
|
// Keep the real-home assertion ahead of even lock-directory preparation.
|
|
2594
2744
|
assertNotRealHomeUnderTest(getConfigDir());
|
|
2595
2745
|
withConfigMutationLockSync(() => {
|
|
2596
|
-
const
|
|
2597
|
-
|
|
2598
|
-
|
|
2746
|
+
const withProvenance = projectCustomModelCatalogMigration(
|
|
2747
|
+
readRawConfigJson(),
|
|
2748
|
+
projectConfigRebaseProvenance(config),
|
|
2749
|
+
);
|
|
2750
|
+
if (persistConfigUnlocked(withProvenance)) bumpGenerationForCooperatingConfigWrite();
|
|
2751
|
+
adoptCustomModelCatalogMigration(config, withProvenance);
|
|
2752
|
+
if (withProvenance.configRebaseProvenance === undefined) delete config.configRebaseProvenance;
|
|
2753
|
+
else config.configRebaseProvenance = structuredClone(withProvenance.configRebaseProvenance);
|
|
2754
|
+
clearPendingConfigTopLevelDeletions(config);
|
|
2599
2755
|
});
|
|
2600
2756
|
}
|
|
2601
2757
|
|
|
@@ -2714,7 +2870,6 @@ const claudeCodeBaseline = new WeakMap<OcxConfig, unknown>();
|
|
|
2714
2870
|
* reconciliation paths below.
|
|
2715
2871
|
*/
|
|
2716
2872
|
const liveConfigBaseline = new WeakMap<OcxConfig, OcxConfig>();
|
|
2717
|
-
|
|
2718
2873
|
/**
|
|
2719
2874
|
* The live config retains the address of the socket Bun actually opened, while
|
|
2720
2875
|
* this map retains the operator's desired address for the next process start.
|
|
@@ -3018,6 +3173,8 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
3018
3173
|
if (baseline && onDisk !== undefined) {
|
|
3019
3174
|
const persistedDiagnostics = configDiagnosticsFromRaw(JSON.stringify(onDisk));
|
|
3020
3175
|
if (persistedDiagnostics.source === "file") {
|
|
3176
|
+
const deletedKeys = configRebaseDeletionKeys(config);
|
|
3177
|
+
const provenanceExists = configHasRebaseProvenance(config);
|
|
3021
3178
|
// Only keys this live config is actually known to have diverged on may be
|
|
3022
3179
|
// rebased. The baseline is captured once when the server arms it, so any key
|
|
3023
3180
|
// that appeared on disk afterwards — through saveConfig(), a hand edit, or
|
|
@@ -3031,8 +3188,11 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
3031
3188
|
const rebaseableKeys = new Set([
|
|
3032
3189
|
...Object.keys(baseline as unknown as Record<string, unknown>),
|
|
3033
3190
|
...Object.keys(config as unknown as Record<string, unknown>),
|
|
3191
|
+
...(provenanceExists
|
|
3192
|
+
? Object.keys(persistedDiagnostics.config as unknown as Record<string, unknown>)
|
|
3193
|
+
: []),
|
|
3034
3194
|
]);
|
|
3035
|
-
const skipped = new Set(["hostname", "port", "claudeCode"]);
|
|
3195
|
+
const skipped = new Set(["hostname", "port", "claudeCode", CONFIG_REBASE_PROVENANCE_KEY]);
|
|
3036
3196
|
for (const key of Object.keys(persistedDiagnostics.config as unknown as Record<string, unknown>)) {
|
|
3037
3197
|
if (!rebaseableKeys.has(key)) skipped.add(key);
|
|
3038
3198
|
}
|
|
@@ -3042,6 +3202,7 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
3042
3202
|
persistedDiagnostics.config as unknown as Record<string, unknown>,
|
|
3043
3203
|
skipped,
|
|
3044
3204
|
);
|
|
3205
|
+
for (const key of deletedKeys) delete (config as unknown as Record<string, unknown>)[key];
|
|
3045
3206
|
}
|
|
3046
3207
|
}
|
|
3047
3208
|
if (claudeCodeBaseline.has(config)) {
|
|
@@ -3055,10 +3216,13 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
3055
3216
|
}
|
|
3056
3217
|
}
|
|
3057
3218
|
}
|
|
3219
|
+
const provenanceProjection = projectConfigRebaseProvenance(config);
|
|
3058
3220
|
const projectedConfig = projectCustomModelCatalogMigration(
|
|
3059
3221
|
onDisk,
|
|
3060
3222
|
config,
|
|
3061
3223
|
);
|
|
3224
|
+
if (provenanceProjection.configRebaseProvenance === undefined) delete projectedConfig.configRebaseProvenance;
|
|
3225
|
+
else projectedConfig.configRebaseProvenance = provenanceProjection.configRebaseProvenance;
|
|
3062
3226
|
const persistedBinding = bindingBaseline && onDisk
|
|
3063
3227
|
? readPersistedServerBinding(onDisk, bindingBaseline)
|
|
3064
3228
|
: bindingBaseline;
|
|
@@ -3076,8 +3240,11 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
3076
3240
|
claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
|
|
3077
3241
|
}
|
|
3078
3242
|
if (liveConfigBaseline.has(config)) {
|
|
3079
|
-
|
|
3243
|
+
if (projectedConfig.configRebaseProvenance === undefined) delete config.configRebaseProvenance;
|
|
3244
|
+
else config.configRebaseProvenance = structuredClone(projectedConfig.configRebaseProvenance);
|
|
3245
|
+
liveConfigBaseline.set(config, structuredClone(projectedConfig));
|
|
3080
3246
|
}
|
|
3247
|
+
clearPendingConfigTopLevelDeletions(config);
|
|
3081
3248
|
});
|
|
3082
3249
|
}
|
|
3083
3250
|
|
|
@@ -3153,10 +3320,17 @@ export function applyProxyEnv(config: OcxConfig): void {
|
|
|
3153
3320
|
const existing = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
3154
3321
|
const entries = existing.split(",").map(s => s.trim()).filter(Boolean);
|
|
3155
3322
|
const seen = new Set(entries.map(e => e.toLowerCase()));
|
|
3156
|
-
|
|
3157
|
-
|
|
3323
|
+
// Configured entries first, then loopback: loopback is unconditional, so appending it last
|
|
3324
|
+
// keeps it present even when the operator lists a loopback host themselves.
|
|
3325
|
+
const raw = config.noProxy;
|
|
3326
|
+
const configured = (Array.isArray(raw) ? raw : (resolveEnvValue(raw) ?? "").split(","))
|
|
3327
|
+
.map(entry => entry.trim())
|
|
3328
|
+
.filter(Boolean);
|
|
3329
|
+
for (const host of [...configured, "localhost", "127.0.0.1", "::1", "[::1]"]) {
|
|
3330
|
+
const key = host.toLowerCase();
|
|
3331
|
+
if (!seen.has(key)) {
|
|
3158
3332
|
entries.push(host);
|
|
3159
|
-
seen.add(
|
|
3333
|
+
seen.add(key);
|
|
3160
3334
|
}
|
|
3161
3335
|
}
|
|
3162
3336
|
process.env.NO_PROXY = entries.join(",");
|