@bitkyc08/opencodex 2.67.0 → 2.68.0-preview.20260927
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 +3 -0
- package/gui/dist/assets/App-B5-cAEd2.js +51 -0
- package/gui/dist/assets/App-DCBismRi.css +1 -0
- package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
- package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
- package/gui/dist/assets/index-BcXblvem.js +86 -0
- package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
- package/gui/dist/index.html +21 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-guard.ts +13 -1
- package/src/adapters/anthropic-image-normalize.ts +3 -2
- package/src/adapters/anthropic-output-schema.ts +36 -0
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/codebuddy/live-models.ts +180 -0
- package/src/adapters/coding-agent/protocol.ts +91 -12
- package/src/adapters/coding-agent/turn.ts +32 -27
- package/src/adapters/devin/cloud-direct/index.ts +1 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
- package/src/adapters/devin.ts +9 -6
- package/src/adapters/google-http.ts +4 -4
- package/src/adapters/google.ts +93 -3
- package/src/adapters/kiro/adapter.ts +6 -2
- package/src/adapters/kiro/stream.ts +12 -2
- package/src/adapters/kiro/usage.ts +3 -1
- package/src/adapters/kiro-errors.ts +11 -1
- package/src/adapters/kiro-events.ts +27 -1
- package/src/adapters/kiro-refusal.ts +30 -0
- package/src/adapters/kiro-retry.ts +71 -31
- package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
- package/src/adapters/openai-chat/messages.ts +31 -4
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
- package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
- package/src/adapters/openai-chat/tool-schema.ts +6 -2
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -3
- package/src/adapters/opencode-go-additional-tools.ts +19 -10
- package/src/adapters/physical-send.ts +10 -3
- package/src/adapters/registry.ts +2 -1
- package/src/adapters/responses-tool-schema.ts +31 -3
- package/src/adapters/run-turn-queue.ts +63 -23
- package/src/adapters/unique-tool-call-ids.ts +63 -0
- package/src/adapters/xai-web-search.ts +32 -2
- package/src/bridge/sse.ts +14 -2
- package/src/chat/inbound.ts +20 -6
- package/src/claude/agents-inject.ts +4 -0
- package/src/claude/desktop-3p.ts +53 -13
- package/src/claude/desktop-profile.ts +41 -4
- package/src/claude/inbound-model-options.ts +14 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +17 -1
- package/src/claude/intercept/local-ca.ts +7 -1
- package/src/claude/message-threads.ts +28 -0
- package/src/cli/account-api.ts +13 -0
- package/src/cli/account-auth.ts +49 -2
- package/src/cli/account-extended.ts +4 -2
- package/src/cli/account.ts +4 -2
- package/src/cli/capabilities.ts +17 -0
- package/src/cli/claude.ts +3 -1
- package/src/cli/dispatch.ts +62 -5
- package/src/cli/index.ts +84 -79
- package/src/cli/minimax.ts +4 -2
- package/src/cli/opencode.ts +6 -1
- package/src/cli/restart-handoff.ts +110 -0
- package/src/cli/status.ts +51 -0
- package/src/client/connect.ts +34 -15
- package/src/client/link-ingress.ts +102 -0
- package/src/client/link-join.ts +37 -16
- package/src/client/link-relay.ts +228 -45
- package/src/client/link-state.ts +54 -0
- package/src/client/link-status.ts +39 -0
- package/src/client/link-teardown.ts +2 -2
- package/src/client/link-tunnel.ts +615 -86
- package/src/client/machine-api.ts +10 -4
- package/src/client/machine-listener.ts +57 -10
- package/src/client/runtime.ts +210 -35
- package/src/clients/config-export/constants.ts +1 -12
- package/src/clients/config-export/contracts.ts +2 -0
- package/src/clients/config-export/model-metadata.ts +9 -3
- package/src/clients/config-export/omp.ts +1 -1
- package/src/clients/config-export/zcode-store.ts +2 -2
- package/src/clients/config-export.ts +36 -3
- package/src/codex/app-server-processes.ts +32 -0
- package/src/codex/app-server-restart-service.ts +20 -3
- package/src/codex/catalog/access-programs.ts +50 -0
- package/src/codex/catalog/build-entries.ts +13 -0
- package/src/codex/catalog/derive-entry.ts +1 -0
- package/src/codex/catalog/model-hints.ts +11 -0
- package/src/codex/catalog/parsing.ts +6 -2
- package/src/codex/catalog/provider-models.ts +81 -0
- package/src/codex/catalog/retained-sync.ts +3 -0
- package/src/codex/codex-write-lock.ts +2 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desired-state.ts +14 -4
- package/src/codex/home.ts +21 -3
- package/src/codex/inject/restore.ts +17 -0
- package/src/codex/inject/routing-classify.ts +3 -2
- package/src/codex/inject-coordination.ts +4 -1
- package/src/codex/inject.ts +8 -1
- package/src/codex/journal.ts +6 -2
- package/src/codex/management-convergence.ts +9 -0
- package/src/codex/model-entitlements.ts +28 -5
- package/src/codex/routing/idle-window.ts +58 -0
- package/src/codex/routing-drift.ts +134 -0
- package/src/codex/routing-healer.ts +419 -0
- package/src/codex/routing.ts +10 -0
- package/src/codex/runtime.ts +188 -57
- package/src/codex/sibling-handoff.ts +71 -0
- package/src/codex/sibling-start.ts +139 -0
- package/src/codex/sync.ts +5 -3
- package/src/combos/failover.ts +32 -5
- package/src/combos/request.ts +11 -3
- package/src/combos/reset-window.ts +10 -5
- package/src/combos/resolve.ts +36 -6
- package/src/config/diagnostics.ts +2 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/paths.ts +7 -1
- package/src/config/process-state.ts +10 -1
- package/src/config/schema/compaction-recovery.ts +15 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +9 -3
- package/src/generated/compatibility-version.json +360 -204
- package/src/images/loop.ts +47 -32
- package/src/integrations/catalog-refresh.ts +4 -0
- package/src/integrations/omp-yaml-source.ts +1 -0
- package/src/lib/claude-request-projection.ts +101 -0
- package/src/lib/local-account-switch-capability.ts +48 -0
- package/src/lib/local-upstream.ts +146 -0
- package/src/lib/package-tree-integrity.ts +25 -3
- package/src/lib/package-tree-retarget.ts +130 -0
- package/src/lib/process-control.ts +4 -1
- package/src/lib/system-restart-contract.ts +106 -0
- package/src/lib/upstream-retry.ts +57 -1
- package/src/link/ports.ts +9 -0
- package/src/link/ssh-argv.ts +16 -0
- package/src/link/ssh-runner.ts +70 -2
- package/src/link/tunnel-state.ts +58 -9
- package/src/oauth/account-quota-rank.ts +29 -18
- package/src/oauth/generic-account-failover.ts +179 -22
- package/src/oauth/index.ts +17 -4
- package/src/oauth/kiro-account-load.ts +107 -0
- package/src/oauth/kiro-device-login.ts +312 -0
- package/src/oauth/kiro-terminal-failover.ts +28 -0
- package/src/oauth/login-flow-state.ts +7 -1
- package/src/oauth/pool-settings-capability.ts +23 -14
- package/src/oauth/store.ts +56 -3
- package/src/oauth/types.ts +4 -0
- package/src/plugins/loader.ts +350 -0
- package/src/plugins/upstream-hooks.ts +119 -0
- package/src/protocols/encoders/adapter-events.ts +9 -2
- package/src/providers/account-quota-disk.ts +42 -4
- package/src/providers/codebuddy-models.ts +6 -2
- package/src/providers/command-code-efforts.ts +33 -10
- package/src/providers/kiro-account-state-disk.ts +44 -0
- package/src/providers/kiro-model-catalog.ts +162 -0
- package/src/providers/kiro-models.ts +5 -4
- package/src/providers/kiro-quota-metrics.ts +35 -0
- package/src/providers/kiro-usage.ts +118 -14
- package/src/providers/quota/account-cache.ts +43 -7
- package/src/providers/quota/antigravity.ts +8 -3
- package/src/providers/quota/kiro-account-probe.ts +12 -0
- package/src/providers/quota/vendor-probes-key.ts +31 -26
- package/src/providers/quota/vendor-probes-oauth.ts +16 -7
- package/src/providers/quota-types.ts +14 -0
- package/src/providers/quota.ts +21 -21
- package/src/providers/registry/entries-extended.ts +5 -3
- package/src/providers/request-pacing.ts +410 -12
- package/src/remote-control/workspace-codex-runtime.ts +4 -3
- package/src/responses/citation-markers.ts +132 -65
- package/src/responses/hosted-tool-policy.ts +14 -3
- package/src/responses/parser-content.ts +3 -2
- package/src/responses/parser.ts +4 -1
- package/src/responses/schema.ts +4 -0
- package/src/responses/visualization-directives.ts +182 -0
- package/src/server/chat-native-sse.ts +24 -12
- package/src/server/chat-native.ts +137 -68
- package/src/server/claude-messages.ts +147 -12
- package/src/server/index/link-listener.ts +7 -0
- package/src/server/index/optional-listeners.ts +4 -1
- package/src/server/index/package-tree-guard.ts +35 -16
- package/src/server/index/serve-options.ts +4 -2
- package/src/server/index/startup-warnings.ts +6 -0
- package/src/server/index.ts +7 -7
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/inference/context.ts +30 -2
- package/src/server/local-account-switch-auth.ts +79 -0
- package/src/server/management/agent-settings-routes.ts +11 -3
- package/src/server/management/config-routes.ts +28 -11
- package/src/server/management/context.ts +2 -0
- package/src/server/management/link-routes.ts +123 -39
- package/src/server/management/logs-usage-routes.ts +9 -42
- package/src/server/management/model-rows.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +67 -12
- package/src/server/management/route-registry.ts +5 -5
- package/src/server/management/sibling-guard.ts +60 -0
- package/src/server/management/storage-log-guard-routes.ts +72 -6
- package/src/server/management/system-restart.ts +92 -135
- package/src/server/management/system-routes.ts +1 -1
- package/src/server/management-api.ts +31 -3
- package/src/server/management-auth.ts +3 -0
- package/src/server/port-reclaim.ts +111 -1
- package/src/server/proxy-liveness.ts +16 -0
- package/src/server/relay.ts +16 -15
- package/src/server/request-log-filter.ts +28 -27
- package/src/server/request-log.ts +7 -3
- package/src/server/request-metrics.ts +28 -0
- package/src/server/responses/adapter-continuation.ts +151 -21
- package/src/server/responses/adapter-delivery.ts +21 -7
- package/src/server/responses/adapter-dispatch.ts +239 -51
- package/src/server/responses/codex-ws-pool.ts +5 -4
- package/src/server/responses/codex-ws-request.ts +4 -1
- package/src/server/responses/compact.ts +8 -4
- package/src/server/responses/compaction-recovery-policy.ts +116 -0
- package/src/server/responses/compaction-recovery.ts +330 -0
- package/src/server/responses/core-combo-failure.ts +1 -0
- package/src/server/responses/core-combo.ts +5 -1
- package/src/server/responses/core-lifetime.ts +22 -0
- package/src/server/responses/core-options.ts +16 -1
- package/src/server/responses/core.ts +14 -4
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +82 -31
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +12 -2
- package/src/server/responses/passthrough-execution.ts +2 -2
- package/src/server/responses/request-send-budget.ts +21 -5
- package/src/server/responses/request-transport.ts +116 -18
- package/src/server/responses/reset-replay.ts +32 -0
- package/src/server/responses/run-turn-execution.ts +136 -19
- package/src/server/responses/sidecar-execution.ts +41 -8
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses/ws-upstream.ts +44 -4
- package/src/server/restart-replacement.ts +382 -0
- package/src/server/startup-health-cache.ts +23 -4
- package/src/server/stop-teardown.ts +35 -1
- package/src/server/system-env.ts +3 -0
- package/src/service/windows-taskxml.ts +11 -7
- package/src/service/windows-wrapper-exit.ts +8 -0
- package/src/stall-timeout.ts +35 -10
- package/src/storage/policy-job.ts +4 -0
- package/src/storage/scanner.ts +177 -29
- package/src/storage/storage-mutation-coordinator.ts +17 -0
- package/src/tray/windows-tray.ps1 +50 -23
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +5 -1
- package/src/types/request.ts +13 -2
- package/src/update/index.ts +3 -1
- package/src/update/job.ts +6 -2
- package/src/update/mise-launcher-target.ts +100 -0
- package/src/update/worker-launch.ts +46 -0
- package/src/usage/log.ts +2 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-BJsT8Icc.css +0 -1
- package/gui/dist/assets/App-D3pNGiN4.js +0 -51
- package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
- package/gui/dist/assets/index-CouvvtMV.js +0 -86
- package/gui/dist/assets/tray-data-CYGdjCJ7.js +0 -1
|
@@ -407,17 +407,21 @@ interface CachedAccountModels {
|
|
|
407
407
|
readonly clientVersion: string;
|
|
408
408
|
readonly expiresAt: number;
|
|
409
409
|
readonly models: ReadonlySet<string>;
|
|
410
|
+
readonly accessProgramsByModel?: ReadonlyMap<string, CodexAvailableAccessPrograms>;
|
|
410
411
|
readonly confirmed: boolean;
|
|
411
412
|
readonly provenance?: CodexModelEntitlementProvenance;
|
|
412
413
|
}
|
|
413
414
|
|
|
414
415
|
export interface CodexModelEntitlementSnapshot {
|
|
415
416
|
readonly modelsByAccount: ReadonlyMap<string, ReadonlySet<string>>;
|
|
417
|
+
readonly accessProgramsByAccount?: ReadonlyMap<string, ReadonlyMap<string, CodexAvailableAccessPrograms>>;
|
|
416
418
|
readonly clientVersionByAccount: ReadonlyMap<string, string>;
|
|
417
419
|
readonly confirmedAccountIds: ReadonlySet<string>;
|
|
418
420
|
readonly credentialIdentities: ReadonlyMap<string, string>;
|
|
419
421
|
}
|
|
420
422
|
|
|
423
|
+
export type CodexAvailableAccessPrograms = Readonly<Record<string, readonly string[]>> | null;
|
|
424
|
+
|
|
421
425
|
export type CodexModelEntitlementState = "granted" | "denied" | "unknown";
|
|
422
426
|
|
|
423
427
|
export interface CodexModelEntitlementResolveOptions {
|
|
@@ -633,17 +637,29 @@ async function accountCredentialSnapshot(
|
|
|
633
637
|
}
|
|
634
638
|
}
|
|
635
639
|
|
|
636
|
-
|
|
640
|
+
/** Keep valid roster slugs while dropping malformed program metadata; preserve explicit null separately from absence. */
|
|
641
|
+
function parseAccountModels(text: string): { models: ReadonlySet<string>; accessProgramsByModel: ReadonlyMap<string, CodexAvailableAccessPrograms> } | null {
|
|
637
642
|
try {
|
|
638
643
|
const payload = JSON.parse(text) as { models?: unknown };
|
|
639
644
|
if (!Array.isArray(payload.models)) return null;
|
|
645
|
+
const accessProgramsByModel = new Map<string, CodexAvailableAccessPrograms>();
|
|
640
646
|
const models = payload.models.flatMap(entry => {
|
|
641
647
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
|
|
642
|
-
const row = entry as { slug?: unknown; supported_in_api?: unknown; visibility?: unknown };
|
|
648
|
+
const row = entry as { slug?: unknown; supported_in_api?: unknown; visibility?: unknown; available_access_programs?: unknown };
|
|
643
649
|
if (typeof row.slug !== "string" || row.supported_in_api !== true || row.visibility === "hide") return [];
|
|
650
|
+
const programs = row.available_access_programs;
|
|
651
|
+
if (programs === null) accessProgramsByModel.set(row.slug, null);
|
|
652
|
+
else if (programs && typeof programs === "object" && !Array.isArray(programs)) {
|
|
653
|
+
const record = programs as Record<string, unknown>;
|
|
654
|
+
if (Array.isArray(record.cyber) && record.cyber.every(item => typeof item === "string")) {
|
|
655
|
+
const validPrograms = Object.fromEntries(Object.entries(record).filter(([, value]) =>
|
|
656
|
+
Array.isArray(value) && value.every(item => typeof item === "string"))) as Record<string, string[]>;
|
|
657
|
+
accessProgramsByModel.set(row.slug, validPrograms);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
644
660
|
return [row.slug];
|
|
645
661
|
});
|
|
646
|
-
return new Set(models);
|
|
662
|
+
return { models: new Set(models), accessProgramsByModel };
|
|
647
663
|
} catch {
|
|
648
664
|
return null;
|
|
649
665
|
}
|
|
@@ -669,6 +685,7 @@ function isTimeoutError(error: unknown): boolean {
|
|
|
669
685
|
return error instanceof Error && error.name === "TimeoutError";
|
|
670
686
|
}
|
|
671
687
|
|
|
688
|
+
/** Fetch one bounded, version-scoped roster; failures and empty results remain unconfirmed for a short retry. */
|
|
672
689
|
async function fetchAccountModels(
|
|
673
690
|
credential: CodexModelEntitlementCredentialSnapshot,
|
|
674
691
|
fetcher: typeof fetch,
|
|
@@ -702,8 +719,8 @@ async function fetchAccountModels(
|
|
|
702
719
|
if (!body.displaySafe || body.truncated) {
|
|
703
720
|
return unconfirmedAccountModels(credential, clientVersion, now, { kind: "unparseable" });
|
|
704
721
|
}
|
|
705
|
-
const
|
|
706
|
-
if (
|
|
722
|
+
const parsed = parseAccountModels(body.text);
|
|
723
|
+
if (parsed === null) {
|
|
707
724
|
return unconfirmedAccountModels(credential, clientVersion, now, { kind: "unparseable" });
|
|
708
725
|
}
|
|
709
726
|
// A roster is a confirmation only when it lists something usable. `models` is a Set, and an
|
|
@@ -713,6 +730,7 @@ async function fetchAccountModels(
|
|
|
713
730
|
// account asked under too old a client version answers with no gated rows, and treating
|
|
714
731
|
// that as authoritative is exactly how 2.36.0 denied sol/terra/luna to accounts that own
|
|
715
732
|
// them (#3022). No usable rows means unconfirmed, on the 15s failure TTL, asked again.
|
|
733
|
+
const { models, accessProgramsByModel } = parsed;
|
|
716
734
|
const usable = models.size > 0;
|
|
717
735
|
if (!usable) {
|
|
718
736
|
return unconfirmedAccountModels(credential, clientVersion, now, { kind: "parsed-empty" });
|
|
@@ -735,6 +753,7 @@ async function fetchAccountModels(
|
|
|
735
753
|
? MODEL_ROSTER_TTL_MS
|
|
736
754
|
: MODEL_ROSTER_FAILURE_TTL_MS),
|
|
737
755
|
models,
|
|
756
|
+
accessProgramsByModel,
|
|
738
757
|
confirmed: true,
|
|
739
758
|
};
|
|
740
759
|
} catch (error) {
|
|
@@ -1173,6 +1192,10 @@ export async function resolveCodexModelEntitlements(
|
|
|
1173
1192
|
})));
|
|
1174
1193
|
return {
|
|
1175
1194
|
modelsByAccount: new Map(results.map(({ credential, result }) => [credential.accountId, result.models])),
|
|
1195
|
+
accessProgramsByAccount: new Map(results.flatMap(({ credential, result }) => (
|
|
1196
|
+
result.confirmed && result.accessProgramsByModel
|
|
1197
|
+
? [[credential.accountId, result.accessProgramsByModel] as const] : []
|
|
1198
|
+
))),
|
|
1176
1199
|
clientVersionByAccount: new Map(results.map(({ credential, result }) => (
|
|
1177
1200
|
[credential.accountId, result.clientVersion]
|
|
1178
1201
|
))),
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import type { CodexAccountUsabilityOptions } from "../account-usability";
|
|
3
|
+
import { pinnedCodexAccountId } from "../account-priority";
|
|
4
|
+
import { getAccountQuota, resetAtToMs } from "../quota";
|
|
5
|
+
import { manualPreferenceBlocks } from "./active-account";
|
|
6
|
+
import { codexPoolKeyForScope, isIndependentCodexQuotaScope, type CodexQuotaScope } from "./health-store";
|
|
7
|
+
import { getEligiblePoolAccounts, hasCodexQuotaHeadroom } from "./selection";
|
|
8
|
+
import { bindThreadAffinity } from "./thread-affinity";
|
|
9
|
+
|
|
10
|
+
const WINDOW_MS = 5 * 60 * 60_000;
|
|
11
|
+
const TOLERANCE_MS = 60_000;
|
|
12
|
+
const FRESHNESS_MS = 5 * 60_000;
|
|
13
|
+
// Reservation is synchronous, so concurrent resolves cannot start the same window twice.
|
|
14
|
+
// Keep the deadline fixed: idle observations may slide their reset forward on every poll.
|
|
15
|
+
const steeredUntil = new Map<string, number>();
|
|
16
|
+
|
|
17
|
+
export function clearIdleWindowSteering(): void {
|
|
18
|
+
steeredUntil.clear();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Only called for first placement, after conversation/family affinity has had precedence. */
|
|
22
|
+
export function pickIdleWindowAccount(
|
|
23
|
+
config: OcxConfig,
|
|
24
|
+
threadId: string | null,
|
|
25
|
+
now: number,
|
|
26
|
+
commit: boolean,
|
|
27
|
+
quotaScope?: CodexQuotaScope,
|
|
28
|
+
selectionOptions?: CodexAccountUsabilityOptions,
|
|
29
|
+
): string | null {
|
|
30
|
+
if (config.codexPool?.startIdleWindows !== true || isIndependentCodexQuotaScope(quotaScope)
|
|
31
|
+
|| pinnedCodexAccountId(config) !== undefined) return null;
|
|
32
|
+
const eligible = getEligiblePoolAccounts(config, undefined, now, quotaScope, selectionOptions, true);
|
|
33
|
+
for (const id of eligible) {
|
|
34
|
+
if (manualPreferenceBlocks(codexPoolKeyForScope(quotaScope), id)
|
|
35
|
+
|| selectionOptions?.deniedModelAccountIds?.has(id)
|
|
36
|
+
|| !hasCodexQuotaHeadroom(config, id, selectionOptions, now)) continue;
|
|
37
|
+
const quota = getAccountQuota(id);
|
|
38
|
+
const observed = quota?.shortObservedAt;
|
|
39
|
+
const reset = quota?.shortResetAt;
|
|
40
|
+
if (quota?.shortPercent !== 0 || quota.shortWindowSeconds !== WINDOW_MS / 1000
|
|
41
|
+
|| observed === undefined || !Number.isFinite(observed)
|
|
42
|
+
|| reset === undefined || !Number.isFinite(reset)
|
|
43
|
+
|| now < observed || now - observed > FRESHNESS_MS) continue;
|
|
44
|
+
const resetMs = resetAtToMs(reset);
|
|
45
|
+
const previous = steeredUntil.get(id);
|
|
46
|
+
if (Math.abs(resetMs - observed - WINDOW_MS) > TOLERANCE_MS || resetMs <= now
|
|
47
|
+
|| (previous !== undefined && (now <= previous || observed <= previous))) continue;
|
|
48
|
+
if (commit) {
|
|
49
|
+
for (const [accountId, until] of steeredUntil) {
|
|
50
|
+
if (until < now - FRESHNESS_MS) steeredUntil.delete(accountId);
|
|
51
|
+
}
|
|
52
|
+
steeredUntil.set(id, Math.max(resetMs, now + WINDOW_MS) + TOLERANCE_MS);
|
|
53
|
+
if (threadId) bindThreadAffinity(threadId, id, now, quotaScope);
|
|
54
|
+
}
|
|
55
|
+
return id;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does opencodex-owned Codex routing name a loopback port this process does not serve?
|
|
3
|
+
*
|
|
4
|
+
* `classifyCodexRouting` answers "opencodex-local" whatever the port, so a live proxy cannot tell
|
|
5
|
+
* from it that `config.toml` still points at a port a dead instance left behind. That is the state a
|
|
6
|
+
* killed second instance produced: the owner kept serving its port while every new Codex thread
|
|
7
|
+
* failed with "Connection refused" against the other one, and nothing looked again after startup.
|
|
8
|
+
*
|
|
9
|
+
* Pure: the caller passes the file bytes, the ports it serves, and (lazily) the journaled values.
|
|
10
|
+
* Only OWNED routing counts. Native config, a user's own root URL, a custom provider and an external
|
|
11
|
+
* `model_provider` are all `not-owned`, so nothing built on this can undo `ocx restore`, an eject, a
|
|
12
|
+
* Codex-OFF toggle or a stop teardown. Only loopback endpoints with an explicit port can be foreign:
|
|
13
|
+
* LAN and admission-token routing is never reported.
|
|
14
|
+
*/
|
|
15
|
+
import { externalCodexModelProvider } from "./inject/config-toml";
|
|
16
|
+
import { classifyRoutingEndpoint } from "./inject/routing-classify";
|
|
17
|
+
import {
|
|
18
|
+
OCX_SECTION_MARKER,
|
|
19
|
+
REALTIME_WS_BASE_URL_KEY,
|
|
20
|
+
isRootOpenaiBaseUrlLine,
|
|
21
|
+
isRootRealtimeWsBaseUrlLine,
|
|
22
|
+
providerTableString,
|
|
23
|
+
rootTomlString,
|
|
24
|
+
} from "./injected-marker";
|
|
25
|
+
|
|
26
|
+
export type CodexRoutingDriftKey =
|
|
27
|
+
| "openai_base_url"
|
|
28
|
+
| typeof REALTIME_WS_BASE_URL_KEY
|
|
29
|
+
| "model_providers.opencodex.base_url";
|
|
30
|
+
|
|
31
|
+
export interface CodexRoutingDriftTarget {
|
|
32
|
+
readonly key: CodexRoutingDriftKey;
|
|
33
|
+
readonly url: string;
|
|
34
|
+
/** Host as written, brackets removed (`127.0.0.1`, `localhost`, `::1`). */
|
|
35
|
+
readonly hostname: string;
|
|
36
|
+
readonly port: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type CodexRoutingDrift =
|
|
40
|
+
| { readonly kind: "none" }
|
|
41
|
+
| { readonly kind: "not-owned" }
|
|
42
|
+
| { readonly kind: "foreign"; readonly targets: readonly CodexRoutingDriftTarget[] };
|
|
43
|
+
|
|
44
|
+
/** The root URLs the last injection recorded writing (`journal.ts`), read without side effects. */
|
|
45
|
+
export interface JournaledCodexRouting {
|
|
46
|
+
readonly openaiBaseUrl: string | null;
|
|
47
|
+
readonly realtimeWsBaseUrl: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CodexRoutingDriftInput {
|
|
51
|
+
/** Ports this process serves: the bound port and the loopback listener port. */
|
|
52
|
+
readonly ownPorts: Iterable<number>;
|
|
53
|
+
/**
|
|
54
|
+
* Value evidence for a root line whose marker comment a Codex app rewrite dropped (#1798). A
|
|
55
|
+
* function is called at most once, and only when an unmarked root URL names a foreign port.
|
|
56
|
+
*/
|
|
57
|
+
readonly journaled?: JournaledCodexRouting | (() => JournaledCodexRouting);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The root line's value, and whether the ownership marker sits directly above it. */
|
|
61
|
+
function rootLine(content: string, key: string, isLine: (line: string) => boolean): { value: string | null; marked: boolean } {
|
|
62
|
+
const lines = content.split("\n");
|
|
63
|
+
const firstTable = lines.findIndex(line => /^\s*\[/.test(line));
|
|
64
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
65
|
+
for (let index = 0; index < rootEnd; index += 1) {
|
|
66
|
+
if (!isLine(lines[index]!)) continue;
|
|
67
|
+
return {
|
|
68
|
+
value: rootTomlString(lines[index]!, key),
|
|
69
|
+
marked: index > 0 && lines[index - 1]!.includes(OCX_SECTION_MARKER),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { value: null, marked: false };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A loopback endpoint with an explicit port, or null for anything the detector must ignore. */
|
|
76
|
+
function loopbackEndpoint(url: string): { hostname: string; port: number } | null {
|
|
77
|
+
if (classifyRoutingEndpoint(url) !== "local") return null;
|
|
78
|
+
try {
|
|
79
|
+
const parsed = new URL(url);
|
|
80
|
+
if (!parsed.port) return null;
|
|
81
|
+
const port = Number(parsed.port);
|
|
82
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
|
83
|
+
return { hostname: parsed.hostname.replace(/^\[|\]$/g, ""), port };
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function detectCodexRoutingDrift(content: string, input: CodexRoutingDriftInput): CodexRoutingDrift {
|
|
90
|
+
const own = new Set(input.ownPorts);
|
|
91
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
92
|
+
let journaled: JournaledCodexRouting | undefined;
|
|
93
|
+
const readJournaled = (): JournaledCodexRouting => {
|
|
94
|
+
if (journaled) return journaled;
|
|
95
|
+
const source = input.journaled;
|
|
96
|
+
journaled = typeof source === "function" ? source() : (source ?? { openaiBaseUrl: null, realtimeWsBaseUrl: null });
|
|
97
|
+
return journaled;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// `marked` is the cheap ownership proof; `owned` adds the journal's value evidence, which is read
|
|
101
|
+
// only for an unmarked line that names a foreign port.
|
|
102
|
+
const candidates: Array<{ key: CodexRoutingDriftKey; url: string; marked: boolean; owned: () => boolean }> = [];
|
|
103
|
+
const base = rootLine(normalized, "openai_base_url", isRootOpenaiBaseUrlLine);
|
|
104
|
+
if (base.value) {
|
|
105
|
+
const url = base.value;
|
|
106
|
+
candidates.push({ key: "openai_base_url", url, marked: base.marked, owned: () => base.marked || readJournaled().openaiBaseUrl === url });
|
|
107
|
+
}
|
|
108
|
+
const realtime = rootLine(normalized, REALTIME_WS_BASE_URL_KEY, isRootRealtimeWsBaseUrlLine);
|
|
109
|
+
if (realtime.value) {
|
|
110
|
+
const url = realtime.value;
|
|
111
|
+
candidates.push({ key: REALTIME_WS_BASE_URL_KEY, url, marked: realtime.marked, owned: () => realtime.marked || readJournaled().realtimeWsBaseUrl === url });
|
|
112
|
+
}
|
|
113
|
+
if (rootTomlString(normalized, "model_provider") === "opencodex") {
|
|
114
|
+
const url = providerTableString(normalized, "opencodex", "base_url");
|
|
115
|
+
if (url) candidates.push({ key: "model_providers.opencodex.base_url", url, marked: true, owned: () => true });
|
|
116
|
+
}
|
|
117
|
+
if (candidates.length === 0) return { kind: "not-owned" };
|
|
118
|
+
// An external provider owns routing even when an old opencodex line is still in the file.
|
|
119
|
+
if (externalCodexModelProvider(normalized) !== null) return { kind: "not-owned" };
|
|
120
|
+
|
|
121
|
+
const targets: CodexRoutingDriftTarget[] = [];
|
|
122
|
+
let ours = false;
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
const endpoint = loopbackEndpoint(candidate.url);
|
|
125
|
+
if (endpoint !== null && !own.has(endpoint.port)) {
|
|
126
|
+
if (candidate.owned()) targets.push({ key: candidate.key, url: candidate.url, ...endpoint });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// A URL naming a port we serve is never drift, whoever wrote it; neither is non-loopback routing.
|
|
130
|
+
ours ||= candidate.marked || endpoint !== null;
|
|
131
|
+
}
|
|
132
|
+
if (targets.length > 0) return { kind: "foreign", targets };
|
|
133
|
+
return ours ? { kind: "none" } : { kind: "not-owned" };
|
|
134
|
+
}
|