@bitkyc08/opencodex 2.7.42 → 2.7.43
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 +8 -1
- package/bin/ocx.mjs +13 -14
- package/gui/dist/assets/index-Czw-jpTU.css +1 -0
- package/gui/dist/assets/index-cmds12BG.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +49 -6
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/inbound.ts +26 -9
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/claude.ts +3 -3
- package/src/cli/doctor.ts +23 -18
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +88 -19
- package/src/cli/interactive-confirm.ts +129 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +55 -16
- package/src/cli/status.ts +47 -13
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/parsing.ts +16 -1
- package/src/codex/catalog/provider-fetch.ts +48 -12
- package/src/codex/catalog/sync.ts +18 -3
- package/src/codex/catalog.ts +1 -1
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +15 -0
- package/src/codex/routing.ts +695 -106
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +566 -25
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +122 -4
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +164 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +283 -72
- package/src/oauth/key-providers.ts +17 -6
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +322 -10
- package/src/oauth/kiro.ts +423 -43
- package/src/oauth/login-cli.ts +32 -5
- package/src/oauth/store.ts +54 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +11 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +14 -4
- package/src/server/auth-cors.ts +110 -22
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/management/agent-settings-routes.ts +28 -4
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +5 -2
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +35 -7
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +2 -2
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +583 -152
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +49 -2
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +366 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +242 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/tray/windows.ts +2 -0
- package/src/types.ts +96 -1
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +103 -12
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/log.ts +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
package/src/codex/inject.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { atomicWriteFile, loadConfig, websocketsEnabled } from "../config";
|
|
2
|
+
import { atomicWriteFile, loadConfig, subagentDefaultSyncEffective, websocketsEnabled } from "../config";
|
|
3
3
|
import { markJournalInjectedState, removeJournal, restoreJournalState, writeJournal } from "./journal";
|
|
4
4
|
import { restoreCodexCatalog } from "./catalog";
|
|
5
5
|
import { migrateHistoryToOpenai, syncCodexHistoryProvider } from "./history-provider";
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
} from "./injected-marker";
|
|
16
16
|
import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, DEFAULT_CATALOG_PATH, parseTomlString, readRootTomlString, resolveCodexConfigPath, tomlString } from "./paths";
|
|
17
17
|
import { resolveEffectiveProjectModelProvider } from "./project-config-warnings";
|
|
18
|
+
import {
|
|
19
|
+
transformManagedSubagentDefaults,
|
|
20
|
+
type ManagedSubagentDefaults,
|
|
21
|
+
} from "./subagent-defaults";
|
|
18
22
|
import type { OcxConfig } from "../types";
|
|
19
23
|
|
|
20
24
|
// Ownership predicates live in `./injected-marker` so `journal.ts` can reach them
|
|
@@ -68,6 +72,16 @@ export interface InjectCodexOptions {
|
|
|
68
72
|
catalogPath?: string | null;
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
function configuredManagedSubagentDefaults(
|
|
76
|
+
config: Pick<OcxConfig, "injectionModel" | "injectionEffort" | "syncCodexSubagentDefaults"> | undefined,
|
|
77
|
+
): ManagedSubagentDefaults | null {
|
|
78
|
+
if (!subagentDefaultSyncEffective(config ?? {})) return null;
|
|
79
|
+
return {
|
|
80
|
+
model: config!.injectionModel!.trim(),
|
|
81
|
+
...(config!.injectionEffort?.trim() ? { reasoningEffort: config!.injectionEffort.trim() } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
/**
|
|
72
86
|
* The `[model_providers.opencodex]` TABLE only. A table is position-independent in TOML, so it is
|
|
73
87
|
* safe to append at EOF. The bare root key `model_provider = "opencodex"` is NOT included here —
|
|
@@ -458,7 +472,13 @@ export function chooseCatalogPathForInjection(content: string, requested?: strin
|
|
|
458
472
|
return existsSync(DEFAULT_CATALOG_PATH) ? DEFAULT_CATALOG_PATH : null;
|
|
459
473
|
}
|
|
460
474
|
|
|
461
|
-
export
|
|
475
|
+
export interface CodexInjectResult {
|
|
476
|
+
success: boolean;
|
|
477
|
+
message: string;
|
|
478
|
+
nativeSubagentDefaultsWarning?: string;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export async function injectCodexConfig(port: number, config?: OcxConfig, options: InjectCodexOptions = {}): Promise<CodexInjectResult> {
|
|
462
482
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
463
483
|
return { success: false, message: `Codex config not found at ${CODEX_CONFIG_PATH}. Is Codex installed?` };
|
|
464
484
|
}
|
|
@@ -469,8 +489,12 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
469
489
|
// A launcher may have journaled before the provider manager took ownership. Never let shutdown
|
|
470
490
|
// replay that stale snapshot over externally managed config.
|
|
471
491
|
removeJournal();
|
|
492
|
+
const nativeSubagentDefaultsWarning = configuredManagedSubagentDefaults(config)
|
|
493
|
+
? `Native Codex sub-agent defaults were not injected: external model_provider ${tomlString(activeProvider)} owns config.toml.`
|
|
494
|
+
: undefined;
|
|
472
495
|
return {
|
|
473
496
|
success: true,
|
|
497
|
+
...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
|
|
474
498
|
message: `⚠️ Codex routing NOT injected: config.toml selects the external model_provider ${tomlString(activeProvider)}.\n` +
|
|
475
499
|
` OpenCodex preserves external provider configuration so existing ${tomlString(activeProvider)} session history stays visible.\n` +
|
|
476
500
|
` Configure that provider for Responses passthrough at http://${providerBaseHost(config?.hostname)}:${port}/v1` +
|
|
@@ -479,16 +503,31 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
479
503
|
};
|
|
480
504
|
}
|
|
481
505
|
|
|
506
|
+
// Marker-owned native defaults are OpenCodex residue, never part of the
|
|
507
|
+
// user's journal baseline. Clean them before either snapshotting or adding a
|
|
508
|
+
// root routing key: inserting that key ahead of a marker-owned first table
|
|
509
|
+
// would otherwise separate the table marker from its header. Ambiguous
|
|
510
|
+
// markers fail closed without writing config, profile, or journal state.
|
|
511
|
+
const nativeDefaultsBaseline = transformManagedSubagentDefaults(rawContent, null);
|
|
512
|
+
if (!nativeDefaultsBaseline.ok) {
|
|
513
|
+
return {
|
|
514
|
+
success: false,
|
|
515
|
+
message: `Codex config injection refused: existing OpenCodex-managed native sub-agent defaults are ambiguous: ${nativeDefaultsBaseline.error}. `
|
|
516
|
+
+ `No files were changed; inspect ${CODEX_CONFIG_PATH}.`,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const baselineContent = nativeDefaultsBaseline.content;
|
|
520
|
+
|
|
482
521
|
// Classify and journal the same bytes: a native config is a valid original and
|
|
483
522
|
// supersedes a stale snapshot (#477), while an injected one must never become
|
|
484
523
|
// one — that is how opencodex routing would survive `ocx stop`.
|
|
485
524
|
writeJournal({
|
|
486
525
|
currentStateIsNative: !hasInjectedCodexRouting(rawContent),
|
|
487
|
-
configContent:
|
|
526
|
+
configContent: baselineContent,
|
|
488
527
|
});
|
|
489
528
|
// EOL boundary: transforms below are LF-pure; preserve the file's dominant ending on write.
|
|
490
529
|
const eol = dominantEol(rawContent);
|
|
491
|
-
let content = applyEol(
|
|
530
|
+
let content = applyEol(baselineContent, "\n");
|
|
492
531
|
|
|
493
532
|
// Idempotent clean-up of any prior injection: drop the provider table (marker-based) and every
|
|
494
533
|
// stray/mis-nested model_provider line, so re-injecting can't duplicate keys or leave the buggy
|
|
@@ -526,6 +565,31 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
526
565
|
keptUserBaseUrl = result.keptUserBaseUrl;
|
|
527
566
|
}
|
|
528
567
|
|
|
568
|
+
const desiredSubagentDefaults = configuredManagedSubagentDefaults(config);
|
|
569
|
+
const routingOwnershipWarning = keptUserBaseUrl && desiredSubagentDefaults
|
|
570
|
+
? "Native Codex sub-agent defaults were not injected: a user-owned root openai_base_url prevents OpenCodex from managing active Codex routing."
|
|
571
|
+
: undefined;
|
|
572
|
+
const managedDefaults = transformManagedSubagentDefaults(
|
|
573
|
+
content,
|
|
574
|
+
keptUserBaseUrl ? null : desiredSubagentDefaults,
|
|
575
|
+
);
|
|
576
|
+
let nativeSubagentDefaultsWarning = routingOwnershipWarning;
|
|
577
|
+
let managedDefaultsMessage = routingOwnershipWarning ? ` ⚠️ ${routingOwnershipWarning}\n` : "";
|
|
578
|
+
if (managedDefaults.ok) {
|
|
579
|
+
content = managedDefaults.content;
|
|
580
|
+
if (desiredSubagentDefaults && managedDefaults.conflicts.length > 0) {
|
|
581
|
+
const keys = managedDefaults.conflicts.map(conflict => `agents.${conflict.key}`).join(", ");
|
|
582
|
+
nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults were not injected: user-owned ${keys} preserved.`;
|
|
583
|
+
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
const action = desiredSubagentDefaults && !keptUserBaseUrl
|
|
587
|
+
? "were not injected"
|
|
588
|
+
: "could not be safely removed";
|
|
589
|
+
nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults ${action}: ${managedDefaults.error}.`;
|
|
590
|
+
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
591
|
+
}
|
|
592
|
+
|
|
529
593
|
const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname);
|
|
530
594
|
content = applyEol(content, eol);
|
|
531
595
|
atomicWriteFile(CODEX_CONFIG_PATH, content);
|
|
@@ -560,9 +624,11 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
560
624
|
if (keptUserBaseUrl) {
|
|
561
625
|
return {
|
|
562
626
|
success: true,
|
|
627
|
+
...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
|
|
563
628
|
message: `⚠️ Codex routing NOT injected: your config already sets a root openai_base_url, and opencodex never overwrites a user-owned override.\n` +
|
|
564
629
|
catalogMessage +
|
|
565
630
|
historyMessage +
|
|
631
|
+
managedDefaultsMessage +
|
|
566
632
|
` To route plain codex through the proxy, remove your openai_base_url line from ~/.codex/config.toml and rerun 'ocx start'.\n` +
|
|
567
633
|
` Reference config: ${CODEX_PROFILE_PATH}`,
|
|
568
634
|
};
|
|
@@ -572,9 +638,11 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
572
638
|
: `Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url).\n`;
|
|
573
639
|
return {
|
|
574
640
|
success: true,
|
|
641
|
+
...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
|
|
575
642
|
message: headline +
|
|
576
643
|
catalogMessage +
|
|
577
644
|
historyMessage +
|
|
645
|
+
managedDefaultsMessage +
|
|
578
646
|
` All models now route through opencodex proxy (like OpenRouter).\n` +
|
|
579
647
|
` OpenAI models (gpt-5.5, etc.) are passed through to OpenAI.\n` +
|
|
580
648
|
` Custom models route to their configured providers.\n` +
|
|
@@ -607,8 +675,17 @@ function removeOcxSection(content: string): string {
|
|
|
607
675
|
return filtered.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
608
676
|
}
|
|
609
677
|
|
|
610
|
-
|
|
611
|
-
|
|
678
|
+
interface StripOpencodexConfigResult {
|
|
679
|
+
content: string;
|
|
680
|
+
managedDefaultsError: string | null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Detailed form used by the on-disk restore path. A damaged ownership marker is
|
|
685
|
+
* ambiguous: keep the associated value, but return the transform error so the
|
|
686
|
+
* caller cannot report a complete restore.
|
|
687
|
+
*/
|
|
688
|
+
function stripOpencodexConfigResult(content: string): StripOpencodexConfigResult {
|
|
612
689
|
let out = content;
|
|
613
690
|
const hadRootOcxProvider = readRootTomlString(out, "model_provider") === "opencodex";
|
|
614
691
|
const hadInjectedBaseUrl = hasInjectedOpenaiBaseUrl(out);
|
|
@@ -623,8 +700,18 @@ export function stripOpencodexConfig(content: string): string {
|
|
|
623
700
|
// Routed root model ids (`model = "provider/slug"`) only make sense while the proxy serves
|
|
624
701
|
// them — strip on both the legacy re-tag form and the Design B injected-base-url form.
|
|
625
702
|
if (hadRootOcxProvider || hadInjectedBaseUrl) out = stripRootRoutedModel(out);
|
|
703
|
+
const managedDefaults = transformManagedSubagentDefaults(out, null);
|
|
704
|
+
if (managedDefaults.ok) out = managedDefaults.content;
|
|
626
705
|
out = stripOpencodexCatalogPath(out);
|
|
627
|
-
return
|
|
706
|
+
return {
|
|
707
|
+
content: out.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n",
|
|
708
|
+
managedDefaultsError: !managedDefaults.ok ? managedDefaults.error : null,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Pure transform: strip the opencodex provider block + `model_provider = "opencodex"` lines. */
|
|
713
|
+
export function stripOpencodexConfig(content: string): string {
|
|
714
|
+
return stripOpencodexConfigResult(content).content;
|
|
628
715
|
}
|
|
629
716
|
|
|
630
717
|
function hasOpencodexRouting(content: string): boolean {
|
|
@@ -635,7 +722,11 @@ function hasOpencodexRouting(content: string): boolean {
|
|
|
635
722
|
|
|
636
723
|
export function removeCodexConfig(options: { preserveProfile?: boolean } = {}): { success: boolean; message: string } {
|
|
637
724
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
638
|
-
|
|
725
|
+
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH)) unlinkSync(CODEX_PROFILE_PATH);
|
|
726
|
+
return {
|
|
727
|
+
success: true,
|
|
728
|
+
message: `Codex config not found; no native restore was needed${options.preserveProfile ? "." : ", and the opencodex profile was removed if present."}`,
|
|
729
|
+
};
|
|
639
730
|
}
|
|
640
731
|
const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
|
|
641
732
|
// Same EOL boundary as inject: strip in LF space, write back in the file's own ending.
|
|
@@ -643,16 +734,25 @@ export function removeCodexConfig(options: { preserveProfile?: boolean } = {}):
|
|
|
643
734
|
const eol = dominantEol(rawContent);
|
|
644
735
|
const content = applyEol(rawContent, "\n");
|
|
645
736
|
const had = hasOpencodexRouting(content);
|
|
646
|
-
const stripped =
|
|
647
|
-
if (had || stripped !== content) {
|
|
648
|
-
atomicWriteFile(CODEX_CONFIG_PATH, applyEol(stripped, eol));
|
|
737
|
+
const stripped = stripOpencodexConfigResult(content);
|
|
738
|
+
if (had || stripped.content !== content) {
|
|
739
|
+
atomicWriteFile(CODEX_CONFIG_PATH, applyEol(stripped.content, eol));
|
|
649
740
|
}
|
|
650
741
|
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH)) unlinkSync(CODEX_PROFILE_PATH);
|
|
742
|
+
const removedMessage = had
|
|
743
|
+
? `Removed opencodex routing from Codex config${options.preserveProfile ? "." : " + profile."}`
|
|
744
|
+
: "opencodex not present in Codex config.";
|
|
745
|
+
if (stripped.managedDefaultsError) {
|
|
746
|
+
const routingMessage = had ? removedMessage : "No opencodex routing was present in Codex config.";
|
|
747
|
+
return {
|
|
748
|
+
success: false,
|
|
749
|
+
message: `${routingMessage} Native Codex sub-agent defaults could not be safely removed: ${stripped.managedDefaultsError}. ` +
|
|
750
|
+
"The ambiguous marker and adjacent value were preserved; inspect $CODEX_HOME/config.toml before using native Codex.",
|
|
751
|
+
};
|
|
752
|
+
}
|
|
651
753
|
return {
|
|
652
754
|
success: true,
|
|
653
|
-
message:
|
|
654
|
-
? `Removed opencodex routing from Codex config${options.preserveProfile ? "." : " + profile."}`
|
|
655
|
-
: "opencodex not present in Codex config.",
|
|
755
|
+
message: removedMessage,
|
|
656
756
|
};
|
|
657
757
|
}
|
|
658
758
|
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { readCodexTokens } from "./auth-collision";
|
|
2
2
|
import { decodeJwtPayload } from "../oauth/chatgpt";
|
|
3
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
* Stable id under which the "main" Codex account (the Codex CLI login stored in
|
|
6
|
-
* ~/.codex/auth.json) participates in opencodex's account rotation. The main account is
|
|
7
|
-
* NOT imported into the managed credential store (Option A): its token is read-only from
|
|
8
|
-
* auth.json, so opencodex never refreshes it — an expired token surfaces as a reauth
|
|
9
|
-
* notice (re-login via the Codex CLI) rather than a background refresh.
|
|
10
|
-
*/
|
|
11
|
-
export const MAIN_CODEX_ACCOUNT_ID = "__main__";
|
|
5
|
+
export { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
12
6
|
|
|
13
7
|
/**
|
|
14
8
|
* Main account plan (e.g. "plus", "go", "free", "team"), populated from the WHAM usage
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { OcxAccountPoolRotationStrategy } from "../types";
|
|
2
|
+
|
|
3
|
+
export const POOL_KEY_CODEX = "codex";
|
|
4
|
+
export const POOL_KEY_ANTHROPIC = "anthropic";
|
|
5
|
+
|
|
6
|
+
interface SelectionState {
|
|
7
|
+
activeKey?: string;
|
|
8
|
+
successes: number;
|
|
9
|
+
currentWeights: Map<string, number>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const selectionState = new Map<string, SelectionState>();
|
|
13
|
+
|
|
14
|
+
const DEFAULT_STICKY_LIMIT = 1;
|
|
15
|
+
const MIN_STICKY_LIMIT = 1;
|
|
16
|
+
const MAX_STICKY_LIMIT = 100;
|
|
17
|
+
const DEFAULT_STRATEGY: OcxAccountPoolRotationStrategy = "quota";
|
|
18
|
+
const VALID_STRATEGIES = new Set<OcxAccountPoolRotationStrategy>(["quota", "round-robin", "fill-first"]);
|
|
19
|
+
|
|
20
|
+
/** Strict parse for management APIs — returns null instead of defaulting. */
|
|
21
|
+
export function parseAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationStrategy | null {
|
|
22
|
+
if (typeof raw === "string" && VALID_STRATEGIES.has(raw as OcxAccountPoolRotationStrategy)) {
|
|
23
|
+
return raw as OcxAccountPoolRotationStrategy;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Strict parse for management APIs — returns null instead of defaulting. */
|
|
29
|
+
export function parseAccountPoolStickyLimit(raw: unknown): number | null {
|
|
30
|
+
if (typeof raw === "number" && Number.isInteger(raw) && raw >= MIN_STICKY_LIMIT && raw <= MAX_STICKY_LIMIT) {
|
|
31
|
+
return raw;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function normalizeAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationStrategy {
|
|
37
|
+
return parseAccountPoolStrategy(raw) ?? DEFAULT_STRATEGY;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function normalizeAccountPoolStickyLimit(raw: unknown): number {
|
|
41
|
+
return parseAccountPoolStickyLimit(raw) ?? DEFAULT_STICKY_LIMIT;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getOrCreateState(poolKey: string): SelectionState {
|
|
45
|
+
let state = selectionState.get(poolKey);
|
|
46
|
+
if (!state) {
|
|
47
|
+
state = { successes: 0, currentWeights: new Map() };
|
|
48
|
+
selectionState.set(poolKey, state);
|
|
49
|
+
}
|
|
50
|
+
return state;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function cloneSelectionState(state: SelectionState): SelectionState {
|
|
54
|
+
return {
|
|
55
|
+
activeKey: state.activeKey,
|
|
56
|
+
successes: state.successes,
|
|
57
|
+
currentWeights: new Map(state.currentWeights),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function smoothWeightedIndex(ids: string[], state: SelectionState): number {
|
|
62
|
+
let best = -1;
|
|
63
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
64
|
+
let total = 0;
|
|
65
|
+
const weight = 1;
|
|
66
|
+
for (let i = 0; i < ids.length; i++) {
|
|
67
|
+
const id = ids[i]!;
|
|
68
|
+
const score = (state.currentWeights.get(id) ?? 0) + weight;
|
|
69
|
+
state.currentWeights.set(id, score);
|
|
70
|
+
total += weight;
|
|
71
|
+
if (score > bestScore) {
|
|
72
|
+
best = i;
|
|
73
|
+
bestScore = score;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (best >= 0) {
|
|
77
|
+
const key = ids[best]!;
|
|
78
|
+
state.currentWeights.set(key, (state.currentWeights.get(key) ?? 0) - total);
|
|
79
|
+
}
|
|
80
|
+
return best;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Shared pick core. Mutates `state` the same way live resolve does; callers pass
|
|
85
|
+
* either the live map entry or a scratch/clone for dry-run peek.
|
|
86
|
+
*/
|
|
87
|
+
function pickRoundRobinFromState(
|
|
88
|
+
eligibleIds: string[],
|
|
89
|
+
stickyLimit: number,
|
|
90
|
+
state: SelectionState,
|
|
91
|
+
commitSticky: boolean,
|
|
92
|
+
): string | null {
|
|
93
|
+
if (eligibleIds.length === 0) return null;
|
|
94
|
+
|
|
95
|
+
const limit = normalizeAccountPoolStickyLimit(stickyLimit);
|
|
96
|
+
|
|
97
|
+
if (state.activeKey && eligibleIds.includes(state.activeKey)) {
|
|
98
|
+
return state.activeKey;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (state.activeKey) {
|
|
102
|
+
delete state.activeKey;
|
|
103
|
+
state.successes = 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const index = smoothWeightedIndex(eligibleIds, state);
|
|
107
|
+
if (index < 0) return null;
|
|
108
|
+
|
|
109
|
+
const picked = eligibleIds[index]!;
|
|
110
|
+
if (commitSticky && limit > 1) {
|
|
111
|
+
state.activeKey = picked;
|
|
112
|
+
state.successes = 0;
|
|
113
|
+
}
|
|
114
|
+
return picked;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function pickRoundRobinAccount(
|
|
118
|
+
poolKey: string,
|
|
119
|
+
eligibleIds: string[],
|
|
120
|
+
stickyLimit: number,
|
|
121
|
+
): string | null {
|
|
122
|
+
return pickRoundRobinFromState(eligibleIds, stickyLimit, getOrCreateState(poolKey), true);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Dry-run of {@link pickRoundRobinAccount}: returns the same account resolve would
|
|
127
|
+
* pick without advancing ring weights, activeKey, or successes.
|
|
128
|
+
*/
|
|
129
|
+
export function peekRoundRobinAccount(
|
|
130
|
+
poolKey: string,
|
|
131
|
+
eligibleIds: string[],
|
|
132
|
+
stickyLimit: number,
|
|
133
|
+
): string | null {
|
|
134
|
+
const live = selectionState.get(poolKey);
|
|
135
|
+
const scratch = live
|
|
136
|
+
? cloneSelectionState(live)
|
|
137
|
+
: { successes: 0, currentWeights: new Map<string, number>() };
|
|
138
|
+
return pickRoundRobinFromState(eligibleIds, stickyLimit, scratch, false);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function notePoolRotationSuccess(
|
|
142
|
+
poolKey: string,
|
|
143
|
+
accountId: string,
|
|
144
|
+
stickyLimit: number,
|
|
145
|
+
): void {
|
|
146
|
+
const limit = normalizeAccountPoolStickyLimit(stickyLimit);
|
|
147
|
+
const state = selectionState.get(poolKey);
|
|
148
|
+
if (!state) return;
|
|
149
|
+
if (state.activeKey !== accountId) {
|
|
150
|
+
state.activeKey = accountId;
|
|
151
|
+
state.successes = 0;
|
|
152
|
+
}
|
|
153
|
+
state.successes += 1;
|
|
154
|
+
if (state.successes >= limit) {
|
|
155
|
+
delete state.activeKey;
|
|
156
|
+
state.successes = 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function notePoolRotationFailure(poolKey: string, accountId: string): void {
|
|
161
|
+
const state = selectionState.get(poolKey);
|
|
162
|
+
if (state?.activeKey === accountId) {
|
|
163
|
+
delete state.activeKey;
|
|
164
|
+
state.successes = 0;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Force the next sticky/RR pick onto `accountId` (manual dashboard selection).
|
|
170
|
+
* Clears sticky success counters and ring weights so the seeded account is held
|
|
171
|
+
* for the next new-session pick before ordinary rotation resumes.
|
|
172
|
+
*/
|
|
173
|
+
export function seedPoolRotationAccount(poolKey: string, accountId: string): void {
|
|
174
|
+
const state = getOrCreateState(poolKey);
|
|
175
|
+
state.activeKey = accountId;
|
|
176
|
+
state.successes = 0;
|
|
177
|
+
state.currentWeights.clear();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function clearPoolRotationState(poolKey?: string): void {
|
|
181
|
+
if (poolKey === undefined) {
|
|
182
|
+
selectionState.clear();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
selectionState.delete(poolKey);
|
|
186
|
+
}
|
package/src/codex/quota.ts
CHANGED
|
@@ -33,6 +33,21 @@ const MONTHLY_WINDOW_MIN_MINUTES = MONTHLY_WINDOW_MIN_SECONDS / 60;
|
|
|
33
33
|
const accountQuota = new Map<string, StoredAccountQuota>();
|
|
34
34
|
|
|
35
35
|
export const CODEX_UNKNOWN_USAGE_SCORE = 100;
|
|
36
|
+
export const CODEX_EXHAUSTED_USAGE_PERCENT = 100;
|
|
37
|
+
|
|
38
|
+
export function isCodexQuotaExhausted(
|
|
39
|
+
quota: Pick<StoredAccountQuota, "weeklyPercent" | "monthlyPercent"> | null,
|
|
40
|
+
plan?: string | null,
|
|
41
|
+
): boolean {
|
|
42
|
+
if (!quota) return false;
|
|
43
|
+
const normalizedPlan = plan?.trim().toLowerCase();
|
|
44
|
+
const values = normalizedPlan === "go" || normalizedPlan === "free"
|
|
45
|
+
? [quota.monthlyPercent]
|
|
46
|
+
: [quota.weeklyPercent, quota.monthlyPercent];
|
|
47
|
+
return values.some(value => typeof value === "number"
|
|
48
|
+
&& Number.isFinite(value)
|
|
49
|
+
&& value >= CODEX_EXHAUSTED_USAGE_PERCENT);
|
|
50
|
+
}
|
|
36
51
|
|
|
37
52
|
export function normalizeUsagePercent(value: unknown): number | undefined {
|
|
38
53
|
const numeric = typeof value === "number"
|