@bitkyc08/opencodex 2.68.0 → 2.69.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/bin/ocx.mjs +25 -17
- package/gui/dist/assets/App-DDbgOTh6.js +51 -0
- package/gui/dist/assets/{Tray-Bn8WhKBH.js → Tray-uqKRqfvw.js} +1 -1
- package/gui/dist/assets/index-BF9QnKO_.css +1 -0
- package/gui/dist/assets/index-CuaBJkG4.js +86 -0
- package/gui/dist/assets/{quota-summary-DL-bNuNL.js → quota-summary-COppQSVq.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/factory-droid.svg +8 -0
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +109 -11
- package/src/adapters/coding-agent/protocol.ts +106 -11
- package/src/adapters/coding-agent/turn.ts +23 -18
- package/src/adapters/google-errors.ts +16 -1
- package/src/adapters/google-http.ts +14 -0
- package/src/adapters/openai-chat/passthrough.ts +2 -0
- package/src/adapters/openai-chat/summary-budget.ts +72 -0
- package/src/adapters/openai-chat/tool-call-id-remint.ts +11 -2
- package/src/adapters/openai-chat.ts +6 -9
- package/src/bridge/response-json.ts +3 -1
- package/src/bridge/sse.ts +3 -1
- package/src/claude/desktop-picker.ts +6 -2
- package/src/claude/desktop-system-proxy.ts +155 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/picker-ca-cleanup.ts +31 -0
- package/src/claude/intercept/picker-ca.ts +283 -83
- package/src/claude/intercept/picker-models.ts +4 -1
- package/src/claude/intercept/picker-trust.ts +15 -2
- package/src/claude/intercept/runtime.ts +99 -32
- package/src/cli/account-api.ts +5 -2
- package/src/cli/account-extended.ts +47 -11
- package/src/cli/account-target.ts +12 -7
- package/src/cli/account.ts +47 -3
- package/src/cli/capabilities.ts +33 -7
- package/src/cli/claude-agent-startup-sync.ts +2 -0
- package/src/cli/claude-desktop.ts +37 -13
- package/src/cli/cross-home-owner.ts +138 -0
- package/src/cli/dispatch.ts +1 -1
- package/src/cli/doctor.ts +18 -8
- package/src/cli/ensure-desired-integrations.ts +5 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +15 -18
- package/src/cli/integrations.ts +8 -1
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +106 -4
- package/src/cli/models.ts +6 -1
- package/src/cli/opencode.ts +2 -83
- package/src/cli/registry.ts +3 -2
- package/src/cli/status.ts +86 -7
- package/src/client/connect.ts +24 -10
- package/src/client/link-ingress.ts +3 -1
- package/src/client/link-join.ts +76 -9
- package/src/client/link-relay-transport.ts +184 -0
- package/src/client/link-relay.ts +16 -4
- package/src/client/machine-listener.ts +2 -0
- package/src/clients/config-export/constants.ts +8 -0
- package/src/clients/config-export/contracts.ts +13 -1
- package/src/clients/config-export/droid.ts +71 -0
- package/src/clients/config-export/kilo.ts +116 -0
- package/src/clients/config-export.ts +33 -2
- package/src/codex/auth-api/routes.ts +1 -1
- package/src/codex/catalog/access-programs.ts +14 -2
- package/src/codex/catalog/provider-models.ts +1 -1
- package/src/codex/catalog-auto-refresh.ts +122 -0
- package/src/codex/config-drift-heal.ts +80 -0
- package/src/codex/desktop-switches.ts +7 -5
- package/src/codex/inject/bounded-config-reader.ts +69 -0
- package/src/codex/inject/config-toml.ts +31 -3
- package/src/codex/inject/routing-target.ts +6 -1
- package/src/codex/inject.ts +4 -0
- package/src/codex/model-entitlements.ts +32 -5
- package/src/codex/native-profile-startup.ts +17 -5
- package/src/codex/project-config-warnings.ts +72 -21
- package/src/codex/quota-auto-refresh-state.ts +8 -2
- package/src/codex/quota-auto-refresh.ts +80 -9
- package/src/codex/sync.ts +10 -0
- package/src/combos/failover.ts +4 -1
- package/src/combos/jev.ts +15 -4
- package/src/combos/types.ts +16 -0
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +29 -0
- package/src/config/macos-system-proxy.ts +114 -0
- package/src/config/proxy-env.ts +67 -20
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +23 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +220 -144
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +12 -3
- package/src/integrations/droid-settings.ts +65 -0
- package/src/integrations/kilo-candidates.ts +30 -0
- package/src/integrations/mutation-plan.ts +62 -7
- package/src/integrations/native/ownership-preflight.ts +46 -12
- package/src/integrations/registry.ts +136 -7
- package/src/integrations/state.ts +52 -4
- package/src/integrations/target.ts +3 -0
- package/src/integrations/writer.ts +55 -14
- package/src/lib/account-selection-events.ts +14 -0
- package/src/lib/jsonc.ts +110 -0
- package/src/lib/standalone.ts +9 -1
- package/src/lib/windows-system-proxy.ts +101 -1
- package/src/link/relay-auth.ts +29 -0
- package/src/link/ssh-argv.ts +6 -3
- package/src/link/ssh-runner.ts +5 -5
- package/src/oauth/generic-account-failover.ts +52 -8
- package/src/oauth/index.ts +37 -14
- package/src/oauth/store.ts +96 -11
- package/src/oauth/token-guardian.ts +7 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +5 -3
- package/src/providers/fastwire.ts +3 -0
- package/src/providers/key-failover.ts +13 -5
- package/src/providers/kiro-model-catalog.ts +4 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-tiers-destination.ts +9 -0
- package/src/providers/quota/account-cache.ts +17 -3
- package/src/providers/quota/vendor-probes-oauth.ts +2 -0
- package/src/providers/quota.ts +2 -2
- package/src/responses/code-mode-helper-compat.ts +14 -1
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/images.ts +70 -20
- package/src/server/index/link-listener.ts +20 -3
- package/src/server/index/link-relay-sessions.ts +138 -0
- package/src/server/index/optional-listeners.ts +11 -1
- package/src/server/index/serve-options.ts +12 -0
- package/src/server/inference/client-encoder-delivery.ts +1 -0
- package/src/server/local-management-read-client.ts +21 -2
- package/src/server/management/agent-settings-routes.ts +19 -5
- package/src/server/management/combo-routes.ts +5 -3
- package/src/server/management/config-routes.ts +32 -6
- package/src/server/management/model-routes.ts +232 -7
- package/src/server/management/model-rows.ts +199 -1
- package/src/server/management/oauth-account-routes.ts +63 -3
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/port-reclaim.ts +170 -32
- package/src/server/responses/adapter-delivery.ts +6 -4
- package/src/server/responses/adapter-dispatch.ts +72 -2
- package/src/server/responses/core-combo.ts +6 -1
- package/src/server/responses/core-normalize.ts +19 -4
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/memory-models.ts +169 -0
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/passthrough-dispatch.ts +10 -0
- package/src/server/responses/request-prepare.ts +86 -4
- package/src/server/responses/request-transport.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +5 -2
- package/src/server/responses/shadow-target-availability.ts +17 -4
- package/src/server/responses/sidecar-execution.ts +18 -2
- package/src/server/responses/skills-snapshot.ts +239 -0
- package/src/server/responses-undeclared-tool-guard.ts +43 -6
- package/src/server/search.ts +1 -1
- package/src/service/guards.ts +4 -5
- package/src/service/state.ts +39 -2
- package/src/service.ts +2 -2
- package/src/types/config.ts +46 -4
- package/src/types/provider.ts +9 -0
- package/src/types/request.ts +6 -0
- package/src/types/tools.ts +38 -8
- package/src/types.ts +3 -0
- package/src/update/async-check.ts +5 -1
- package/src/update/index.ts +30 -12
- package/src/update/job.ts +3 -1
- package/src/update/pnpm-read-policy.d.mts +4 -0
- package/src/update/pnpm-read-policy.mjs +44 -0
- package/src/update/worker-launch.ts +152 -11
- package/src/usage/cost.ts +6 -3
- package/src/usage/log.ts +4 -0
- package/src/web-search/backends.ts +2 -2
- package/src/web-search/devin-executor.ts +1 -1
- package/src/web-search/sidecar-providers.ts +5 -5
- package/gui/dist/assets/App-mpsibsFj.js +0 -51
- package/gui/dist/assets/index-BDUBS8PW.css +0 -1
- package/gui/dist/assets/index-kUE8tuAK.js +0 -86
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
fstatSync,
|
|
6
6
|
lstatSync,
|
|
7
7
|
openSync,
|
|
8
|
-
readFileSync,
|
|
9
8
|
readSync,
|
|
10
9
|
realpathSync,
|
|
11
10
|
} from "node:fs";
|
|
12
11
|
import path, { dirname, join, resolve } from "node:path";
|
|
13
12
|
import { expandUserPath } from "../config";
|
|
14
13
|
import { defaultCodexHome } from "./home";
|
|
14
|
+
import { readBoundedCodexConfig } from "./inject/bounded-config-reader";
|
|
15
15
|
import { readRootTomlString } from "./paths";
|
|
16
16
|
import { truncateRetainedUtf8 } from "../lib/admission";
|
|
17
17
|
|
|
@@ -53,7 +53,8 @@ function resolveCodexConfigPath(): string {
|
|
|
53
53
|
return join(home, "config.toml");
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export type ProjectCodexConfigIssueCode = "model_providers_table" | "profile_selector" | "model_provider_root"
|
|
56
|
+
export type ProjectCodexConfigIssueCode = "model_providers_table" | "profile_selector" | "model_provider_root"
|
|
57
|
+
| "global_config_unreadable";
|
|
57
58
|
|
|
58
59
|
export interface ProjectCodexConfigWarning {
|
|
59
60
|
path: string;
|
|
@@ -265,17 +266,18 @@ export function resolveEffectiveProjectModelProvider(content: string): Effective
|
|
|
265
266
|
/** True when global Codex config routes through the opencodex proxy. */
|
|
266
267
|
export function isGlobalOpencodexRoutingActive(
|
|
267
268
|
codexConfigPath: string = resolveCodexConfigPath(),
|
|
268
|
-
content?: string,
|
|
269
|
+
content?: string | null,
|
|
269
270
|
): boolean {
|
|
270
271
|
let text = content;
|
|
271
272
|
if (text === undefined) {
|
|
272
|
-
if (!existsSync(codexConfigPath)) return false;
|
|
273
273
|
try {
|
|
274
|
-
text =
|
|
274
|
+
text = readBoundedCodexConfig(codexConfigPath) ?? undefined;
|
|
275
275
|
} catch {
|
|
276
276
|
return false;
|
|
277
277
|
}
|
|
278
|
+
if (text === undefined) return false;
|
|
278
279
|
}
|
|
280
|
+
if (text === null) return false;
|
|
279
281
|
if (hasInjectedOpenaiBaseUrl(text)) return true;
|
|
280
282
|
if (readRootTomlString(text, "model_provider") === "opencodex") return true;
|
|
281
283
|
return false;
|
|
@@ -379,6 +381,8 @@ export function discoverProjectCodexConfigPaths(options: {
|
|
|
379
381
|
cwd?: string;
|
|
380
382
|
codexConfigPath?: string;
|
|
381
383
|
maxWalkParents?: number;
|
|
384
|
+
/** Explicit null keeps an absent/unreadable observation; undefined permits a fresh read. */
|
|
385
|
+
globalContent?: string | null;
|
|
382
386
|
} = {}): string[] {
|
|
383
387
|
const found = new Set<string>();
|
|
384
388
|
const codexConfigPath = options.codexConfigPath ?? resolveCodexConfigPath();
|
|
@@ -416,15 +420,16 @@ export function discoverProjectCodexConfigPaths(options: {
|
|
|
416
420
|
cwd = parent;
|
|
417
421
|
}
|
|
418
422
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
423
|
+
try {
|
|
424
|
+
const global = options.globalContent === undefined
|
|
425
|
+
? readBoundedCodexConfig(codexConfigPath) : options.globalContent;
|
|
426
|
+
if (global !== null) {
|
|
422
427
|
for (const projectPath of parseTrustedProjectPathsFromCodexConfig(global)) {
|
|
423
428
|
addIfExists(projectPath);
|
|
424
429
|
}
|
|
425
|
-
} catch {
|
|
426
|
-
/* ignore unreadable global config */
|
|
427
430
|
}
|
|
431
|
+
} catch {
|
|
432
|
+
/* ignore unreadable global config */
|
|
428
433
|
}
|
|
429
434
|
|
|
430
435
|
return [...found];
|
|
@@ -437,10 +442,32 @@ export function collectProjectCodexConfigWarnings(options: {
|
|
|
437
442
|
} = {}): ProjectCodexConfigWarning[] {
|
|
438
443
|
const codexConfigPath = options.codexConfigPath ?? resolveCodexConfigPath();
|
|
439
444
|
const requireRouting = options.requireOpencodexRouting ?? true;
|
|
440
|
-
|
|
445
|
+
|
|
446
|
+
// The routing question has three answers: active, inactive, and unreadable. An oversized
|
|
447
|
+
// or swapped-underneath global config must not silently collapse to "inactive" — that
|
|
448
|
+
// would erase both project-bypass coverage and trusted-path discovery without a trace.
|
|
449
|
+
let globalContent: string | null = null;
|
|
450
|
+
let globalUnreadable = false;
|
|
451
|
+
try {
|
|
452
|
+
globalContent = readBoundedCodexConfig(codexConfigPath);
|
|
453
|
+
} catch {
|
|
454
|
+
globalUnreadable = true;
|
|
455
|
+
}
|
|
456
|
+
if (requireRouting && !globalUnreadable
|
|
457
|
+
&& !isGlobalOpencodexRoutingActive(codexConfigPath, globalContent)) {
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
441
460
|
|
|
442
461
|
const warnings: ProjectCodexConfigWarning[] = [];
|
|
443
|
-
|
|
462
|
+
if (globalUnreadable) {
|
|
463
|
+
warnings.push({
|
|
464
|
+
path: codexConfigPath,
|
|
465
|
+
code: "global_config_unreadable",
|
|
466
|
+
detail: "unreadable",
|
|
467
|
+
message: "The global Codex config could not be read within the 1 MiB bound — whether it routes through OpenCodex, and which projects it declares trusted, is undetermined.",
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
for (const path of discoverProjectCodexConfigPaths({ cwd: options.cwd, codexConfigPath, globalContent })) {
|
|
444
471
|
const content = readBoundedProjectConfig(path);
|
|
445
472
|
if (content !== null) warnings.push(...analyzeProjectCodexConfig(content, path));
|
|
446
473
|
}
|
|
@@ -480,6 +507,8 @@ export function summarizeProjectCodexIssue(warning: ProjectCodexConfigWarning):
|
|
|
480
507
|
return warning.profileName ? `profile="${warning.profileName}"` : `model_provider="${warning.detail}"`;
|
|
481
508
|
case "model_provider_root":
|
|
482
509
|
return `model_provider="${warning.detail}"`;
|
|
510
|
+
case "global_config_unreadable":
|
|
511
|
+
return "config.toml unreadable or oversized";
|
|
483
512
|
}
|
|
484
513
|
}
|
|
485
514
|
|
|
@@ -501,6 +530,8 @@ export interface ProjectCodexConfigWarningGroup {
|
|
|
501
530
|
path: string;
|
|
502
531
|
issues: string[];
|
|
503
532
|
bypass: string;
|
|
533
|
+
/** True when the group is the global-config-unreadable caveat, not a project bypass. */
|
|
534
|
+
globalUnreadable?: boolean;
|
|
504
535
|
}
|
|
505
536
|
|
|
506
537
|
export function groupProjectCodexConfigWarningsByPath(
|
|
@@ -512,22 +543,34 @@ export function groupProjectCodexConfigWarningsByPath(
|
|
|
512
543
|
list.push(warning);
|
|
513
544
|
grouped.set(warning.path, list);
|
|
514
545
|
}
|
|
515
|
-
return [...grouped.entries()].map(([path, pathWarnings]) =>
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
546
|
+
return [...grouped.entries()].map(([path, pathWarnings]) => {
|
|
547
|
+
const globalUnreadable = pathWarnings.every(warning => warning.code === "global_config_unreadable");
|
|
548
|
+
return {
|
|
549
|
+
path,
|
|
550
|
+
issues: pathWarnings.map(summarizeProjectCodexIssue),
|
|
551
|
+
bypass: globalUnreadable ? pathWarnings[0]!.message : explainProjectConfigBypass(pathWarnings),
|
|
552
|
+
...(globalUnreadable ? { globalUnreadable } : {}),
|
|
553
|
+
};
|
|
554
|
+
});
|
|
520
555
|
}
|
|
521
556
|
|
|
522
557
|
export function formatProjectCodexConfigWarningsForDoctor(warnings: ProjectCodexConfigWarning[]): string[] {
|
|
523
558
|
const grouped = groupProjectCodexConfigWarningsByPath(warnings);
|
|
524
559
|
if (grouped.length === 0) return [];
|
|
525
560
|
const lines: string[] = [];
|
|
526
|
-
|
|
561
|
+
let hasBypassEntries = false;
|
|
562
|
+
for (const { path, issues, bypass, globalUnreadable } of grouped) {
|
|
527
563
|
lines.push(` -- ${relPath(path)} — ${issues.join(", ")}`);
|
|
528
564
|
lines.push(` ${bypass}`);
|
|
565
|
+
if (globalUnreadable) {
|
|
566
|
+
lines.push(" fix: keep the global config.toml a readable regular file within the 1 MiB bound");
|
|
567
|
+
} else {
|
|
568
|
+
hasBypassEntries = true;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (hasBypassEntries) {
|
|
572
|
+
lines.push(" fix: remove those entries so OpenCodex proxy routing applies in this project");
|
|
529
573
|
}
|
|
530
|
-
lines.push(" fix: remove those entries so OpenCodex proxy routing applies in this project");
|
|
531
574
|
return lines;
|
|
532
575
|
}
|
|
533
576
|
|
|
@@ -535,11 +578,19 @@ export function formatProjectCodexConfigWarningsForConsole(warnings: ProjectCode
|
|
|
535
578
|
const grouped = groupProjectCodexConfigWarningsByPath(warnings);
|
|
536
579
|
if (grouped.length === 0) return [];
|
|
537
580
|
const lines = ["⚠️ Project Codex config bypasses OpenCodex:"];
|
|
538
|
-
|
|
581
|
+
let hasBypassEntries = false;
|
|
582
|
+
for (const { path, issues, bypass, globalUnreadable } of grouped) {
|
|
539
583
|
lines.push(` ${relPath(path)} — ${issues.join(", ")}`);
|
|
540
584
|
lines.push(` ${bypass}`);
|
|
585
|
+
if (globalUnreadable) {
|
|
586
|
+
lines.push(" fix: keep the global config.toml a readable regular file within the 1 MiB bound");
|
|
587
|
+
} else {
|
|
588
|
+
hasBypassEntries = true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (hasBypassEntries) {
|
|
592
|
+
lines.push(" fix: remove those entries so OpenCodex proxy routing applies in this project");
|
|
541
593
|
}
|
|
542
|
-
lines.push(" fix: remove those entries so OpenCodex proxy routing applies in this project");
|
|
543
594
|
return lines;
|
|
544
595
|
}
|
|
545
596
|
|
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
/** Completed/due markers use epoch milliseconds; persisted legacy markers may use seconds. */
|
|
3
3
|
export type CodexQuotaAutoRefreshWindows = { fiveHour?: number; weekly?: number };
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Backoff evidence for one account. `generation` names the credential the failure was observed
|
|
7
|
+
* under; a record from another generation says nothing about the credential in use now.
|
|
8
|
+
*/
|
|
9
|
+
export type CodexQuotaRetry = { after: number; delay: number; generation: string };
|
|
10
|
+
|
|
5
11
|
export const completedByAccount = new Map<string, CodexQuotaAutoRefreshWindows>();
|
|
6
|
-
export const retryAfterByAccount = new Map<string,
|
|
12
|
+
export const retryAfterByAccount = new Map<string, CodexQuotaRetry>();
|
|
7
13
|
export const scheduledByAccount = new Map<string, CodexQuotaAutoRefreshWindows>();
|
|
8
|
-
export const quotaRefreshAfterByAccount = new Map<string,
|
|
14
|
+
export const quotaRefreshAfterByAccount = new Map<string, CodexQuotaRetry>();
|
|
9
15
|
|
|
10
16
|
/** Drop every activation record when its account is removed. */
|
|
11
17
|
export function forgetCodexQuotaAutoRefreshAccount(accountId: string): void {
|
|
@@ -21,13 +21,14 @@ import { CodexWarmupError, codexWarmupFailureReason, warmCodexAccount } from "./
|
|
|
21
21
|
import {
|
|
22
22
|
completedByAccount, retryAfterByAccount, scheduledByAccount, quotaRefreshAfterByAccount,
|
|
23
23
|
resetCodexQuotaAutoRefreshStateForTests,
|
|
24
|
-
type CodexQuotaAutoRefreshWindows,
|
|
24
|
+
type CodexQuotaAutoRefreshWindows, type CodexQuotaRetry,
|
|
25
25
|
} from "./quota-auto-refresh-state";
|
|
26
26
|
export type { CodexQuotaAutoRefreshWindows } from "./quota-auto-refresh-state";
|
|
27
27
|
export { forgetCodexQuotaAutoRefreshAccount } from "./quota-auto-refresh-state";
|
|
28
28
|
|
|
29
29
|
export const FIVE_HOUR_WINDOW_SECONDS = 5 * 60 * 60;
|
|
30
30
|
const RETRY_MS = 5 * 60_000;
|
|
31
|
+
const MAX_RETRY_MS = 60 * 60_000;
|
|
31
32
|
const CONCURRENCY = 4;
|
|
32
33
|
|
|
33
34
|
export interface CodexQuotaAutoRefreshStatus {
|
|
@@ -51,6 +52,57 @@ export interface CodexQuotaAutoRefreshRunDeps {
|
|
|
51
52
|
|
|
52
53
|
let inFlight: Promise<void> | null = null;
|
|
53
54
|
|
|
55
|
+
const LOCAL_BUSY_RETRY_MS = 60_000;
|
|
56
|
+
|
|
57
|
+
/** The native main profile was claimed locally, so no upstream request was sent. */
|
|
58
|
+
export class NativeMainBusyError extends Error {
|
|
59
|
+
constructor() {
|
|
60
|
+
super("native main busy");
|
|
61
|
+
this.name = "NativeMainBusyError";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The credential a retry record describes: main's quota generation, or the pool record's. */
|
|
66
|
+
function credentialGeneration(accountId: string): string {
|
|
67
|
+
return accountId === MAIN_CODEX_ACCOUNT_ID
|
|
68
|
+
? `main:${getMainQuotaCredentialGeneration()}`
|
|
69
|
+
: `pool:${readCodexAccountRecord(accountId)?.generation ?? "none"}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A retry recorded under a replaced credential is spent; drop it rather than hold the new one. */
|
|
73
|
+
function liveRetry(
|
|
74
|
+
retries: Map<string, CodexQuotaRetry>,
|
|
75
|
+
accountId: string,
|
|
76
|
+
generation: string,
|
|
77
|
+
): CodexQuotaRetry | undefined {
|
|
78
|
+
const retry = retries.get(accountId);
|
|
79
|
+
if (retry && retry.generation !== generation) {
|
|
80
|
+
retries.delete(accountId);
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return retry;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Back off unsuccessful discovery/activation without adding another timer. */
|
|
87
|
+
function deferRetry(
|
|
88
|
+
retries: Map<string, CodexQuotaRetry>,
|
|
89
|
+
accountId: string,
|
|
90
|
+
now: number,
|
|
91
|
+
generation: string,
|
|
92
|
+
): number {
|
|
93
|
+
const delay = Math.min((liveRetry(retries, accountId, generation)?.delay ?? RETRY_MS / 2) * 2, MAX_RETRY_MS);
|
|
94
|
+
retries.set(accountId, { after: now + delay, delay, generation });
|
|
95
|
+
return delay;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Every enabled window needs a retained, uncompleted deadline, not fresh usage percentages. */
|
|
99
|
+
function hasScheduledWindows(config: OcxConfig, accountId: string): boolean {
|
|
100
|
+
const setting = config.codexQuotaAutoRefresh?.[accountId];
|
|
101
|
+
const scheduled = scheduledByAccount.get(accountId);
|
|
102
|
+
return (!setting?.fiveHour || scheduled?.fiveHour !== undefined)
|
|
103
|
+
&& (!setting?.weekly || scheduled?.weekly !== undefined);
|
|
104
|
+
}
|
|
105
|
+
|
|
54
106
|
/** Report upstream window availability separately from persisted spending intent. */
|
|
55
107
|
export function codexQuotaAutoRefreshStatus(
|
|
56
108
|
config: OcxConfig,
|
|
@@ -187,7 +239,7 @@ async function warmAccount(config: OcxConfig, accountId: string): Promise<void |
|
|
|
187
239
|
return;
|
|
188
240
|
}
|
|
189
241
|
const lease = tryAcquireNativeMainProfileClaim();
|
|
190
|
-
if (!lease) throw new
|
|
242
|
+
if (!lease) throw new NativeMainBusyError();
|
|
191
243
|
try {
|
|
192
244
|
reconcileMainCodexAccountRuntimeState();
|
|
193
245
|
if (mainWarmupRestricted(config)) return false;
|
|
@@ -309,14 +361,21 @@ export async function runCodexQuotaAutoRefresh(
|
|
|
309
361
|
// Capture before WHAM can move an idle window's reset into the future.
|
|
310
362
|
rememberWindows(config, accountId, quotaFor(accountId));
|
|
311
363
|
const quota = quotaFor(accountId);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
364
|
+
// A known deadline remains actionable even when its usage snapshot is old.
|
|
365
|
+
// Only discover missing windows; never poll merely to keep percentages fresh.
|
|
366
|
+
if (hasScheduledWindows(config, accountId)) {
|
|
367
|
+
quotaRefreshAfterByAccount.delete(accountId);
|
|
368
|
+
} else if ((!quota || now - quota.updatedAt >= RETRY_MS)
|
|
369
|
+
&& (liveRetry(quotaRefreshAfterByAccount, accountId, credentialGeneration(accountId))?.after ?? 0) <= now) {
|
|
370
|
+
deferRetry(quotaRefreshAfterByAccount, accountId, now, credentialGeneration(accountId));
|
|
371
|
+
try { await refresh(config, accountId); } catch { /* Retry missing metadata with backoff. */ }
|
|
316
372
|
}
|
|
317
373
|
if (!eligible(accountId)) return;
|
|
318
374
|
rememberWindows(config, accountId, quotaFor(accountId));
|
|
319
|
-
|
|
375
|
+
// Backoff from a replaced credential is dropped here, so reauthenticating or rotating an
|
|
376
|
+
// account never waits out the failures of the credential it replaced.
|
|
377
|
+
const generation = credentialGeneration(accountId);
|
|
378
|
+
if ((liveRetry(retryAfterByAccount, accountId, generation)?.after ?? 0) > now) return;
|
|
320
379
|
const windows = dueCodexQuotaAutoRefreshWindows(config, accountId, quotaFor(accountId), now);
|
|
321
380
|
if (!windows) return;
|
|
322
381
|
try {
|
|
@@ -327,11 +386,23 @@ export async function runCodexQuotaAutoRefresh(
|
|
|
327
386
|
persist(config, accountId, completed);
|
|
328
387
|
rememberWindows(config, accountId, quotaFor(accountId));
|
|
329
388
|
} catch (error) {
|
|
330
|
-
|
|
389
|
+
// A failure that raced a credential replacement describes the old credential; the next
|
|
390
|
+
// sweep evaluates the replacement on its own evidence.
|
|
391
|
+
if (credentialGeneration(accountId) !== generation) return;
|
|
392
|
+
if (error instanceof NativeMainBusyError) {
|
|
393
|
+
// Local admission refused before any upstream request: retry soon, and keep the
|
|
394
|
+
// upstream backoff where it was instead of doubling it.
|
|
395
|
+
const previous = liveRetry(retryAfterByAccount, accountId, generation);
|
|
396
|
+
retryAfterByAccount.set(accountId, {
|
|
397
|
+
after: now + LOCAL_BUSY_RETRY_MS, delay: previous?.delay ?? RETRY_MS / 2, generation,
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const delay = deferRetry(retryAfterByAccount, accountId, now, generation);
|
|
331
402
|
const account = config.codexAccounts?.find(candidate => candidate.id === accountId);
|
|
332
403
|
const label = account ? codexAccountLogLabel(account) : "main";
|
|
333
404
|
console.warn(`[codex-quota-auto-refresh] ${label}: ${codexWarmupFailureReason(error)}; ${
|
|
334
|
-
isAccountNeedsReauth(accountId) ? "reauthentication required" :
|
|
405
|
+
isAccountNeedsReauth(accountId) ? "reauthentication required" : `retry in ${delay / 60_000} minutes`
|
|
335
406
|
}`);
|
|
336
407
|
}
|
|
337
408
|
}));
|
package/src/codex/sync.ts
CHANGED
|
@@ -132,6 +132,16 @@ export async function syncModelsToCodex(
|
|
|
132
132
|
// the injector, before it gets a chance to create any artifact.
|
|
133
133
|
const admission = (deps.admitCodexWrite ?? admitCodexWrite)();
|
|
134
134
|
if (admission.kind === "refused" && admission.authority === "service-home") {
|
|
135
|
+
// Startup turns this result into a failed /readyz and records nothing else, and
|
|
136
|
+
// `ocx sync` only says the sync did not complete, so an unattended refusal must
|
|
137
|
+
// leave one line behind (#5782). The admission message can name private home and
|
|
138
|
+
// service-definition paths, so the log carries the authority only; POST /api/sync
|
|
139
|
+
// returns the specific reason to an authenticated operator.
|
|
140
|
+
log?.error(
|
|
141
|
+
"[opencodex] Codex write refused (service-home): this proxy could not prove it owns the "
|
|
142
|
+
+ "installed service homes, so catalog sync and Codex config injection were skipped. "
|
|
143
|
+
+ "POST /api/sync returns the specific reason.",
|
|
144
|
+
);
|
|
135
145
|
return {
|
|
136
146
|
status: "refused",
|
|
137
147
|
authority: "service-home",
|
package/src/combos/failover.ts
CHANGED
|
@@ -326,7 +326,10 @@ function normalizedFailureCode(code?: string | null): string {
|
|
|
326
326
|
// are quota-limit codes whose window length this gateway has no evidence for, and guessing long on
|
|
327
327
|
// them would hold a target that may clear sooner.
|
|
328
328
|
const ACCOUNT_EXHAUSTION_CODES = new Set(["usage_limit_exceeded", "usage_limit_reached", "1308"]);
|
|
329
|
-
|
|
329
|
+
// Token-plan windows (Alibaba's DeepSeek/Qwen plans) report "Your token-plan 1-week quota has been
|
|
330
|
+
// exhausted" (#5494). The match is anchored to that phrasing: a looser "quota ... exhausted" would
|
|
331
|
+
// also catch per-minute limits, and this arm outranks the transient rate-limit duration.
|
|
332
|
+
const ACCOUNT_EXHAUSTION_TEXT = /usage limit (?:has been )?reached|token-plan\s+\S+\s+quota has been exhausted/;
|
|
330
333
|
|
|
331
334
|
function isAccountWindowExhausted(message: string, code?: string | null): boolean {
|
|
332
335
|
return ACCOUNT_EXHAUSTION_CODES.has(normalizedFailureCode(code))
|
package/src/combos/jev.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { resolveProviderApiKey } from "../providers/api-key-resolve";
|
|
7
7
|
import { providerMatchesRegistryTransport } from "../providers/registry";
|
|
8
8
|
import type { OcxComboDefaultEffort, OcxConfig, OcxProviderConfig } from "../types";
|
|
9
|
+
import { JEV_MAX_CANDIDATE_FIELD_CHARS } from "./types";
|
|
9
10
|
|
|
10
11
|
export const JEV_PROVIDER_ID = "jev";
|
|
11
12
|
export const JEV_API_URL = "https://api.typesafe.ai/v1/systemone";
|
|
@@ -13,7 +14,6 @@ export const JEV_MODEL = "jev-latest";
|
|
|
13
14
|
|
|
14
15
|
const JEV_TIMEOUT_MS = 4_000;
|
|
15
16
|
const JEV_MAX_CANDIDATES = 64;
|
|
16
|
-
const JEV_MAX_CANDIDATE_FIELD_CHARS = 512;
|
|
17
17
|
const JEV_MAX_REQUEST_BYTES = 65_536;
|
|
18
18
|
const JEV_MAX_RESPONSE_BYTES = 65_536;
|
|
19
19
|
const JEV_OUTBOUND_DEPENDENCIES = {
|
|
@@ -70,6 +70,8 @@ export interface JevCandidate {
|
|
|
70
70
|
provider: string;
|
|
71
71
|
model: string;
|
|
72
72
|
reasoningEfforts: readonly OcxComboDefaultEffort[];
|
|
73
|
+
/** Optional operator note sent as decision evidence for this target only. */
|
|
74
|
+
modelProfile?: string;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
export interface JevDecision {
|
|
@@ -337,7 +339,7 @@ function hasImageContent(item: Record<string, unknown>): boolean {
|
|
|
337
339
|
return item.content.some(part => isRecord(part) && (part.type === "input_image" || part.type === "image_url"));
|
|
338
340
|
}
|
|
339
341
|
|
|
340
|
-
export function buildJevState(body: unknown): Record<string, unknown> {
|
|
342
|
+
export function buildJevState(body: unknown, candidates: readonly JevCandidate[] = []): Record<string, unknown> {
|
|
341
343
|
const input = isRecord(body) ? body.input : undefined;
|
|
342
344
|
let task = "";
|
|
343
345
|
let previousAssistant = "";
|
|
@@ -383,11 +385,18 @@ export function buildJevState(body: unknown): Record<string, unknown> {
|
|
|
383
385
|
}
|
|
384
386
|
}
|
|
385
387
|
|
|
388
|
+
const operatorNotes: Record<string, string> = {};
|
|
389
|
+
for (const candidate of candidates) {
|
|
390
|
+
const note = candidate.modelProfile?.trim();
|
|
391
|
+
if (note) operatorNotes[candidate.key] = note;
|
|
392
|
+
}
|
|
393
|
+
|
|
386
394
|
return {
|
|
387
395
|
task,
|
|
388
396
|
signals: { has_image: hasImage, tool_history: toolHistory },
|
|
389
397
|
step,
|
|
390
398
|
...(previousAssistant ? { previous_assistant: previousAssistant.slice(-ASSISTANT_TAIL_CHARS) } : {}),
|
|
399
|
+
...(Object.keys(operatorNotes).length ? { operator_notes: operatorNotes } : {}),
|
|
391
400
|
};
|
|
392
401
|
}
|
|
393
402
|
|
|
@@ -425,7 +434,9 @@ function candidateOptions(candidates: readonly JevCandidate[]): Map<string, JevR
|
|
|
425
434
|
function candidatesFitRequestBounds(candidates: readonly JevCandidate[]): boolean {
|
|
426
435
|
if (candidates.length > JEV_MAX_CANDIDATES) return false;
|
|
427
436
|
return candidates.every(candidate => [candidate.key, candidate.provider, candidate.model]
|
|
428
|
-
.every(value => value.length > 0 && value.length <= JEV_MAX_CANDIDATE_FIELD_CHARS)
|
|
437
|
+
.every(value => value.length > 0 && value.length <= JEV_MAX_CANDIDATE_FIELD_CHARS)
|
|
438
|
+
&& (candidate.modelProfile === undefined
|
|
439
|
+
|| (typeof candidate.modelProfile === "string" && candidate.modelProfile.length <= JEV_MAX_CANDIDATE_FIELD_CHARS)));
|
|
429
440
|
}
|
|
430
441
|
|
|
431
442
|
function modelProfile(candidate: JevCandidate): string {
|
|
@@ -566,7 +577,7 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
|
|
|
566
577
|
|
|
567
578
|
let requestBody: string;
|
|
568
579
|
try {
|
|
569
|
-
const state = buildJevState(options.body);
|
|
580
|
+
const state = buildJevState(options.body, options.candidates);
|
|
570
581
|
if (!hasJevDecisionState(state)) return failed("no_state");
|
|
571
582
|
requestBody = JSON.stringify({
|
|
572
583
|
model: JEV_MODEL,
|
package/src/combos/types.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { OcxComboConfig, OcxComboCooldownWaitPolicy, OcxComboDefaultEffort,
|
|
|
4
4
|
import { COMBO_NAMESPACE, isValidComboId, targetKey } from "./identifiers";
|
|
5
5
|
|
|
6
6
|
export const COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS = 0;
|
|
7
|
+
export const JEV_MAX_CANDIDATE_FIELD_CHARS = 512;
|
|
7
8
|
export { COMBO_NAMESPACE, preservesPhysicalComboProvider, isNativeAliasCombo, targetKey, parseComboModelId, comboModelId, comboPublicModelId, comboDisabledModelId, comboDisabledModelSelectors, resolveComboId, isValidComboId } from "./identifiers";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -27,6 +28,8 @@ export interface NormalizedComboTarget {
|
|
|
27
28
|
/** Emergency-only target, deferred under `cooldownWaitPolicy` (#5691). */
|
|
28
29
|
lastResort: boolean;
|
|
29
30
|
reasoningEfforts?: OcxComboDefaultEffort[];
|
|
31
|
+
/** Optional JEV decision description. */
|
|
32
|
+
modelProfile?: string;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export interface NormalizedComboConfig {
|
|
@@ -333,6 +336,16 @@ export function comboConfigIssues(
|
|
|
333
336
|
message: `targets[${i}].lastResort must be a boolean`,
|
|
334
337
|
});
|
|
335
338
|
}
|
|
339
|
+
if (target.modelProfile !== undefined
|
|
340
|
+
&& (typeof target.modelProfile !== "string"
|
|
341
|
+
|| target.modelProfile.trim().length === 0
|
|
342
|
+
|| target.modelProfile.length > JEV_MAX_CANDIDATE_FIELD_CHARS
|
|
343
|
+
|| /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(target.modelProfile))) {
|
|
344
|
+
issues.push({
|
|
345
|
+
path: ["targets", i, "modelProfile"],
|
|
346
|
+
message: `targets[${i}].modelProfile must be a non-empty string of at most ${JEV_MAX_CANDIDATE_FIELD_CHARS} characters; only tab, line feed and carriage return are allowed among control characters`,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
336
349
|
|
|
337
350
|
if (provider && model) {
|
|
338
351
|
const key = targetKey({ provider, model });
|
|
@@ -389,6 +402,9 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
|
|
|
389
402
|
...(target.reasoningEfforts !== undefined
|
|
390
403
|
? { reasoningEfforts: [...target.reasoningEfforts] }
|
|
391
404
|
: {}),
|
|
405
|
+
...(typeof target.modelProfile === "string" && target.modelProfile.trim()
|
|
406
|
+
? { modelProfile: target.modelProfile.trim() }
|
|
407
|
+
: {}),
|
|
392
408
|
lastResort: target.lastResort === true,
|
|
393
409
|
})),
|
|
394
410
|
};
|
|
@@ -63,6 +63,8 @@ import {
|
|
|
63
63
|
runtimeRoleSchema,
|
|
64
64
|
spendSchema,
|
|
65
65
|
compactionRoutingSchema,
|
|
66
|
+
skillsConfigSchema,
|
|
67
|
+
memoryModelsSchema,
|
|
66
68
|
} from "./schema/leaf-validators";
|
|
67
69
|
|
|
68
70
|
export type ConfigDiagnostics = {
|
|
@@ -594,11 +596,26 @@ export function metricsExportConfigError(value: unknown): string | null {
|
|
|
594
596
|
return null;
|
|
595
597
|
}
|
|
596
598
|
|
|
599
|
+
|
|
600
|
+
function skillsConfigError(value: unknown): string | null {
|
|
601
|
+
const raw = rawConfigRecord(value);
|
|
602
|
+
if (!raw || !Object.hasOwn(raw, "skills") || raw.skills === undefined) return null;
|
|
603
|
+
const result = skillsConfigSchema.safeParse(raw.skills);
|
|
604
|
+
if (result.success) return null;
|
|
605
|
+
const issue = result.error.issues[0];
|
|
606
|
+
const field = issue?.path.join(".");
|
|
607
|
+
return "schema_invalid: skills" + (field ? "." + field : "") + ": " + (issue?.message ?? "invalid configuration");
|
|
608
|
+
}
|
|
609
|
+
|
|
597
610
|
export function validateConfigCandidate(value: unknown): { ok: true; config: OcxConfig } | { ok: false; error: string } {
|
|
598
611
|
const compactionRouting = rawConfigRecord(value)?.compactionRouting;
|
|
599
612
|
if (compactionRouting !== undefined && !compactionRoutingSchema.safeParse(compactionRouting).success) {
|
|
600
613
|
return { ok: false, error: "schema_invalid: compactionRouting: requires a nonblank model, an optional valid reasoningEffort, and optional non-repeating triggers drawn from \"manual\" and \"auto\"" };
|
|
601
614
|
}
|
|
615
|
+
const memoryModels = rawConfigRecord(value)?.memoryModels;
|
|
616
|
+
if (memoryModels !== undefined && !memoryModelsSchema.safeParse(memoryModels).success) {
|
|
617
|
+
return { ok: false, error: "schema_invalid: memoryModels: requires a nonblank model and an optional declared reasoningEffort per configured phase, and no other fields" };
|
|
618
|
+
}
|
|
602
619
|
const boundaryError = compactionRecoveryConfigError(value) ?? configReasoningPinsConfigError(value)
|
|
603
620
|
?? blankHostnameError(value)
|
|
604
621
|
?? claudeSubagentEffortError(value)
|
|
@@ -625,7 +642,8 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
625
642
|
?? clientRolePairError(value)
|
|
626
643
|
?? loopbackListenerPortError(value)
|
|
627
644
|
?? managementIngressConfigError(value)
|
|
628
|
-
?? metricsExportConfigError(value)
|
|
645
|
+
?? metricsExportConfigError(value)
|
|
646
|
+
?? skillsConfigError(value);
|
|
629
647
|
if (boundaryError) return { ok: false, error: boundaryError };
|
|
630
648
|
const result = configSchema.safeParse(value);
|
|
631
649
|
if (result.success) {
|
|
@@ -122,6 +122,35 @@ export function warnDegradedTopLevelOptIns(rawParsed: unknown, validated: OcxCon
|
|
|
122
122
|
if (compactionRecoveryConfigError(rawParsed)) console.warn("⚠️ invalid compactionRecovery disabled; the original compaction failure is preserved");
|
|
123
123
|
warnDegradedStreamMode(rawParsed, validated);
|
|
124
124
|
warnDegradedCompactionRouting(rawParsed, validated);
|
|
125
|
+
warnDegradedMemoryModels(rawParsed, validated);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A malformed `memoryModels` phase disables that phase rather than failing the whole schema, so
|
|
130
|
+
* say so once: silently keeping whatever route the phase already had — which may be the shadow
|
|
131
|
+
* intercept rather than Codex's own model — is the outcome a typo must not produce quietly.
|
|
132
|
+
*/
|
|
133
|
+
export function warnDegradedMemoryModels(rawParsed: unknown, validated: OcxConfig): void {
|
|
134
|
+
if (!rawParsed || typeof rawParsed !== "object") return;
|
|
135
|
+
const raw = (rawParsed as Record<string, unknown>).memoryModels;
|
|
136
|
+
if (raw === undefined) return;
|
|
137
|
+
if (validated.memoryModels === undefined || raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
138
|
+
console.warn("\u26a0\ufe0f config.json memoryModels is invalid (expected { extract?: { model, reasoningEffort? }, consolidation?: { model, reasoningEffort? } } with a nonblank model and a declared effort per phase) \u2014 the memory pipeline keeps its existing route, which may include shadow-call interception");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// A misspelled phase key is stripped by the permissive load schema, so without this warning it
|
|
142
|
+
// disappears silently and the next settings save persists the sanitized map without it.
|
|
143
|
+
for (const key of Object.keys(raw as Record<string, unknown>)) {
|
|
144
|
+
if (key === "extract" || key === "consolidation") continue;
|
|
145
|
+
// Redact and JSON-escape the key name: a malformed hand-edit can place a secret in a property
|
|
146
|
+
// name, and a control character in one must not be able to forge a log line.
|
|
147
|
+
console.warn("\u26a0\ufe0f config.json memoryModels." + JSON.stringify(redactSecretString(key)) + " is not a recognized phase \u2014 ignoring it");
|
|
148
|
+
}
|
|
149
|
+
for (const phase of ["extract", "consolidation"] as const) {
|
|
150
|
+
if ((raw as Record<string, unknown>)[phase] !== undefined && validated.memoryModels[phase] === undefined) {
|
|
151
|
+
console.warn("\u26a0\ufe0f config.json memoryModels." + phase + " is invalid (expected { model, reasoningEffort? } with a nonblank model) \u2014 that phase keeps its existing route, which may include shadow-call interception");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
125
154
|
}
|
|
126
155
|
|
|
127
156
|
/**
|