@bitkyc08/opencodex 2.55.0 → 2.57.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 +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -587,8 +587,14 @@ export async function inspectCodexCliInstall(
|
|
|
587
587
|
const platform = deps.platform ?? process.platform;
|
|
588
588
|
const candidate = observeCodexRuntimeCandidateReadOnly(deps);
|
|
589
589
|
if (!candidate) {
|
|
590
|
+
// This slice reads no candidate or configuration file on Windows, so an
|
|
591
|
+
// absent proof-captured environment candidate does not establish that no
|
|
592
|
+
// Codex CLI exists: a persisted selection is simply never consulted there.
|
|
593
|
+
// Report the deferral that actually happened instead of the stronger claim
|
|
594
|
+
// that the candidate is unavailable. POSIX retains its existing result
|
|
595
|
+
// when no candidate is observed.
|
|
590
596
|
return isWindowsPlatform(platform)
|
|
591
|
-
? unknownWindowsReport("
|
|
597
|
+
? unknownWindowsReport("windows_inspection_deferred")
|
|
592
598
|
: unknownReport("candidate_unavailable");
|
|
593
599
|
}
|
|
594
600
|
|
package/src/codex/convergence.ts
CHANGED
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
orderForSubagents,
|
|
50
50
|
} from "./catalog/sync";
|
|
51
51
|
import { multiAgentV2EnabledFromConfigText } from "./features";
|
|
52
|
-
import { exactComboCatalogSlugs } from "./catalog/aggregation";
|
|
52
|
+
import { enforceCatalogSlugUniqueness, exactComboCatalogSlugs } from "./catalog/aggregation";
|
|
53
53
|
import {
|
|
54
54
|
isNativeAliasCatalogEntry,
|
|
55
55
|
accountBoundNativeOpenAiSlugs,
|
|
@@ -386,7 +386,12 @@ function prepareCatalog(
|
|
|
386
386
|
: null,
|
|
387
387
|
);
|
|
388
388
|
finalizeAutoReviewModelOverride(mergedModels, catalogModels, config);
|
|
389
|
-
|
|
389
|
+
// The second writer of this file. A dashboard model toggle, a combo edit, or a Codex account
|
|
390
|
+
// login reaches `convergeCodexCatalog` and commits through `fixedCommit`, never through
|
|
391
|
+
// `writeRetainedCatalogSync`, so the #4730 uniqueness guard has to stand here too or the same
|
|
392
|
+
// `source-invalid` rejection returns by a different route. Silent because this merge runs under
|
|
393
|
+
// `warningPolicy: "suppress"`.
|
|
394
|
+
catalog.models = enforceCatalogSlugUniqueness(mergedModels, false);
|
|
390
395
|
return catalog;
|
|
391
396
|
}
|
|
392
397
|
|
|
@@ -116,11 +116,20 @@ export interface DesktopAppAdapter {
|
|
|
116
116
|
*
|
|
117
117
|
* `root` is expected to be `realpath`-resolved by discovery already.
|
|
118
118
|
*/
|
|
119
|
+
function isMembershipSeparator(character: string): boolean {
|
|
120
|
+
// `/` separates on every platform this runs on, and Windows accepts it wherever it
|
|
121
|
+
// accepts `\`. `\` is only a separator where the host says so: it is a legal
|
|
122
|
+
// FILENAME character on POSIX, so admitting it there would reopen the sibling hole
|
|
123
|
+
// this function exists to close.
|
|
124
|
+
return character === "/" || (sep === "\\" && character === "\\");
|
|
125
|
+
}
|
|
126
|
+
|
|
119
127
|
export function isUnderRoot(executable: string, root: string): boolean {
|
|
120
128
|
if (!executable || !root) return false;
|
|
121
129
|
if (executable === root) return true;
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
if (!executable.startsWith(root)) return false;
|
|
131
|
+
if (isMembershipSeparator(root[root.length - 1]!)) return true;
|
|
132
|
+
return isMembershipSeparator(executable[root.length] ?? "");
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
/**
|
|
@@ -29,16 +29,16 @@ const SHELL_BASENAME = "chatgpt.exe";
|
|
|
29
29
|
const POWERSHELL_PROBE_OPTIONS = { timeout: PROBE_TIMEOUT_MS, windowsHide: true } as const;
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* isUnderRoot
|
|
32
|
+
* isUnderRoot checks a lexical path boundary and is case-sensitive. Windows
|
|
33
33
|
* membership is case-insensitive, and this file is executed by Unix CI against
|
|
34
|
-
*
|
|
34
|
+
* mixed slash paths, so both slash forms are folded onto the host separator first.
|
|
35
35
|
* The boundary itself — sibling `OpenAI.Codex-evil` must not match root
|
|
36
36
|
* `OpenAI.Codex` — is still isUnderRoot's, which is why the PowerShell
|
|
37
37
|
* StartsWith is only a cheap pre-filter.
|
|
38
38
|
*/
|
|
39
39
|
function toHostMembershipPath(windowsPath: string): string {
|
|
40
40
|
const lowered = windowsPath.toLowerCase();
|
|
41
|
-
return
|
|
41
|
+
return lowered.replace(/[\\/]/g, sep);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function isMemberExecutable(executable: string, root: string): boolean {
|
|
@@ -91,10 +91,10 @@ function listPackageProcesses(exec: DesktopExec, install: DesktopAppInstall): De
|
|
|
91
91
|
const literal = install.root.replace(/'/g, "''");
|
|
92
92
|
const script = [
|
|
93
93
|
"$ErrorActionPreference='SilentlyContinue'",
|
|
94
|
-
`$root = '${literal}'`,
|
|
94
|
+
`$root = '${literal}'.Replace('/', '\\')`,
|
|
95
95
|
"$me = ([Security.Principal.WindowsIdentity]::GetCurrent()).Name",
|
|
96
96
|
"Get-CimInstance Win32_Process -Filter \"Name='ChatGPT.exe'\" |",
|
|
97
|
-
" Where-Object { $_.ExecutablePath -and $_.ExecutablePath.StartsWith($root, 'OrdinalIgnoreCase') } |",
|
|
97
|
+
" Where-Object { $_.ExecutablePath -and $_.ExecutablePath.Replace('/', '\\').StartsWith($root, 'OrdinalIgnoreCase') } |",
|
|
98
98
|
" ForEach-Object {",
|
|
99
99
|
" $o = Invoke-CimMethod -InputObject $_ -MethodName GetOwner",
|
|
100
100
|
" if ($o -and $o.ReturnValue -eq 0 -and $o.User) {",
|
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
// Holds INV-TOML-01 from structure/overview.md; keep the id here if this file is split or renamed.
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { contextCompatibleBaseLine } from "../context-compat";
|
|
4
|
+
import { resolveEffectiveProjectModelProvider } from "../project-config-warnings";
|
|
5
|
+
import {
|
|
6
|
+
OCX_SECTION_MARKER,
|
|
7
|
+
REALTIME_WS_BASE_URL_KEY,
|
|
8
|
+
isRootOpenaiBaseUrlLine,
|
|
9
|
+
isRootRealtimeWsBaseUrlLine,
|
|
10
|
+
tomlStringPattern,
|
|
11
|
+
} from "../injected-marker";
|
|
12
|
+
import {
|
|
13
|
+
CODEX_CONFIG_PATH,
|
|
14
|
+
DEFAULT_CATALOG_PATH,
|
|
15
|
+
parseTomlString,
|
|
16
|
+
resolveCodexConfigPath,
|
|
17
|
+
tomlString,
|
|
18
|
+
} from "../paths";
|
|
19
|
+
import {
|
|
20
|
+
type CodexRoutingTarget,
|
|
21
|
+
providerBaseHost,
|
|
22
|
+
routingTargetOrigin,
|
|
23
|
+
usesProviderTable,
|
|
24
|
+
validateCodexRoutingTarget,
|
|
25
|
+
} from "./routing-target";
|
|
26
|
+
|
|
27
|
+
export function externalCodexModelProvider(content: string): string | null {
|
|
28
|
+
const provider = resolveEffectiveProjectModelProvider(content).provider;
|
|
29
|
+
return provider && provider !== "openai" && provider !== "opencodex"
|
|
30
|
+
? provider
|
|
31
|
+
: null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function currentExternalCodexModelProvider(): string | null {
|
|
35
|
+
if (!existsSync(CODEX_CONFIG_PATH)) return null;
|
|
36
|
+
return externalCodexModelProvider(readFileSync(CODEX_CONFIG_PATH, "utf8"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detect the file's dominant line ending. Every transform in this module is LF-pure
|
|
41
|
+
* (split("\n") + hard "\n" joins), so CRLF configs (Windows-edited config.toml) are
|
|
42
|
+
* normalized to LF at the pipeline boundary and converted back on write — otherwise a
|
|
43
|
+
* single inject would leave a mixed-EOL file.
|
|
44
|
+
*/
|
|
45
|
+
export function dominantEol(content: string): "\r\n" | "\n" {
|
|
46
|
+
const crlf = (content.match(/\r\n/g) ?? []).length;
|
|
47
|
+
if (crlf === 0) return "\n";
|
|
48
|
+
const bareLf = (content.match(/\n/g) ?? []).length - crlf;
|
|
49
|
+
return crlf >= bareLf ? "\r\n" : "\n";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Normalize all line endings to `eol` (CRLF first collapsed to LF, then expanded). */
|
|
53
|
+
export function applyEol(content: string, eol: "\r\n" | "\n"): string {
|
|
54
|
+
const lf = content.replace(/\r\n/g, "\n");
|
|
55
|
+
return eol === "\n" ? lf : lf.replace(/\n/g, "\r\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildProviderTableBlock(
|
|
59
|
+
port: number,
|
|
60
|
+
supportsWebsockets?: boolean,
|
|
61
|
+
includeApiAuthHeader?: boolean,
|
|
62
|
+
hostname?: string,
|
|
63
|
+
): string;
|
|
64
|
+
export function buildProviderTableBlock(
|
|
65
|
+
target: CodexRoutingTarget,
|
|
66
|
+
supportsWebsockets?: boolean,
|
|
67
|
+
): string;
|
|
68
|
+
export function buildProviderTableBlock(
|
|
69
|
+
portOrTarget: number | CodexRoutingTarget,
|
|
70
|
+
supportsWebsockets = false,
|
|
71
|
+
includeApiAuthHeader = false,
|
|
72
|
+
hostname?: string,
|
|
73
|
+
): string {
|
|
74
|
+
const target = typeof portOrTarget === "number"
|
|
75
|
+
? validateCodexRoutingTarget({
|
|
76
|
+
baseUrl: `http://${providerBaseHost(hostname)}:${portOrTarget}/v1`,
|
|
77
|
+
requiresAdmissionToken: includeApiAuthHeader,
|
|
78
|
+
tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
|
|
79
|
+
})
|
|
80
|
+
: validateCodexRoutingTarget(portOrTarget);
|
|
81
|
+
return buildProviderTableBlockForTarget(target, supportsWebsockets);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildProviderTableBlockForTarget(
|
|
85
|
+
target: CodexRoutingTarget,
|
|
86
|
+
supportsWebsockets = false,
|
|
87
|
+
): string {
|
|
88
|
+
const lines = [
|
|
89
|
+
"",
|
|
90
|
+
OCX_SECTION_MARKER,
|
|
91
|
+
"[model_providers.opencodex]",
|
|
92
|
+
'name = "OpenCodex Proxy"',
|
|
93
|
+
`base_url = ${tomlString(target.baseUrl)}`,
|
|
94
|
+
'wire_api = "responses"',
|
|
95
|
+
// false only in the authless Desktop opt-in (#1107); true keeps the App/TUI account gate.
|
|
96
|
+
`requires_openai_auth = ${target.desktopAuthless === true ? "false" : "true"}`,
|
|
97
|
+
];
|
|
98
|
+
if (target.requiresAdmissionToken) {
|
|
99
|
+
// codex-cli 0.146+ contract (#2073): env_key sends Authorization: Bearer $VAR and
|
|
100
|
+
// hard-errors on a missing/empty variable instead of silently omitting auth. It
|
|
101
|
+
// coexists with requires_openai_auth (env_key wins wire auth; the flag keeps the
|
|
102
|
+
// login/account UX), and the server substitutes stored main auth for our admission
|
|
103
|
+
// bearer (#1686), so the modern form is strictly better than the legacy
|
|
104
|
+
// env_http_headers table this line used to emit.
|
|
105
|
+
lines.push(`env_key = ${tomlString(target.tokenEnv)}`);
|
|
106
|
+
}
|
|
107
|
+
if (supportsWebsockets) lines.push("supports_websockets = true");
|
|
108
|
+
return lines.join("\n") + "\n";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function buildOpenaiBaseUrlLine(
|
|
112
|
+
port: number,
|
|
113
|
+
hostname?: string,
|
|
114
|
+
): string;
|
|
115
|
+
export function buildOpenaiBaseUrlLine(target: CodexRoutingTarget): string;
|
|
116
|
+
export function buildOpenaiBaseUrlLine(
|
|
117
|
+
portOrTarget: number | CodexRoutingTarget,
|
|
118
|
+
hostname?: string,
|
|
119
|
+
): string {
|
|
120
|
+
return typeof portOrTarget === "number"
|
|
121
|
+
? `openai_base_url = "http://${providerBaseHost(hostname)}:${portOrTarget}/v1"`
|
|
122
|
+
: buildOpenaiBaseUrlLineForTarget(validateCodexRoutingTarget(portOrTarget));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildOpenaiBaseUrlLineForTarget(target: CodexRoutingTarget): string {
|
|
126
|
+
return `openai_base_url = ${tomlString(target.baseUrl)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Realtime sideband override (codex-rs `experimental_realtime_ws_base_url`), written with the
|
|
131
|
+
* SAME value as `openai_base_url`. Desktop voice creates its WebRTC call through the proxy
|
|
132
|
+
* (`POST /v1/live`, answered under the Pool account the proxy selects) but, since openai/codex
|
|
133
|
+
* 438c9e98d (#35830), joins the sideband at `wss://api.openai.com/v1/live/{callId}` with the
|
|
134
|
+
* app's own login unless this key redirects it. Two accounts, one call: the join 404s. Pointing
|
|
135
|
+
* the key at the proxy sends the join through `GET /v1/live/{callId}` (src/server/live.ts),
|
|
136
|
+
* where the same Pool account is reused. codex-rs turns `http` into `ws` and appends
|
|
137
|
+
* `/live/{callId}` itself; the value must stay the canonical `/v1` root.
|
|
138
|
+
*/
|
|
139
|
+
export function buildRealtimeWsBaseUrlLine(target: CodexRoutingTarget): string {
|
|
140
|
+
return `${REALTIME_WS_BASE_URL_KEY} = ${tomlString(target.baseUrl)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Design B root-key injection: place `OCX_SECTION_MARKER` + `openai_base_url` at the document
|
|
145
|
+
* ROOT (before the first table header). Idempotent: an existing marker-owned line is rewritten
|
|
146
|
+
* in place. A user's OWN root `openai_base_url` (no marker above it) is respected — we keep it
|
|
147
|
+
* and inject nothing, reporting `keptUserBaseUrl` so the caller can surface it.
|
|
148
|
+
*/
|
|
149
|
+
export function setRootOpenaiBaseUrl(
|
|
150
|
+
content: string,
|
|
151
|
+
port: number,
|
|
152
|
+
hostname?: string,
|
|
153
|
+
): { content: string; keptUserBaseUrl: boolean };
|
|
154
|
+
export function setRootOpenaiBaseUrl(
|
|
155
|
+
content: string,
|
|
156
|
+
target: CodexRoutingTarget,
|
|
157
|
+
): { content: string; keptUserBaseUrl: boolean };
|
|
158
|
+
export function setRootOpenaiBaseUrl(
|
|
159
|
+
content: string,
|
|
160
|
+
portOrTarget: number | CodexRoutingTarget,
|
|
161
|
+
hostname?: string,
|
|
162
|
+
): { content: string; keptUserBaseUrl: boolean } {
|
|
163
|
+
if (typeof portOrTarget !== "number") {
|
|
164
|
+
return setRootOpenaiBaseUrlForTarget(content, validateCodexRoutingTarget(portOrTarget));
|
|
165
|
+
}
|
|
166
|
+
const lines = content.split("\n");
|
|
167
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
168
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
169
|
+
const key = contextCompatibleBaseLine(content, buildOpenaiBaseUrlLine(portOrTarget, hostname));
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < rootEnd; i++) {
|
|
172
|
+
if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
|
|
173
|
+
const markerOwned = i > 0 && lines[i - 1].includes(OCX_SECTION_MARKER);
|
|
174
|
+
if (!markerOwned) return { content, keptUserBaseUrl: true };
|
|
175
|
+
lines[i] = key;
|
|
176
|
+
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (firstTable === -1) {
|
|
180
|
+
return {
|
|
181
|
+
content:
|
|
182
|
+
content.replace(/\n+$/, "") +
|
|
183
|
+
"\n" +
|
|
184
|
+
OCX_SECTION_MARKER +
|
|
185
|
+
"\n" +
|
|
186
|
+
key +
|
|
187
|
+
"\n",
|
|
188
|
+
keptUserBaseUrl: false,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
let insertAt = firstTable;
|
|
192
|
+
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
193
|
+
lines.splice(insertAt, 0, OCX_SECTION_MARKER, key);
|
|
194
|
+
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function setRootOpenaiBaseUrlForTarget(
|
|
198
|
+
content: string,
|
|
199
|
+
target: CodexRoutingTarget,
|
|
200
|
+
): { content: string; keptUserBaseUrl: boolean } {
|
|
201
|
+
const lines = content.split("\n");
|
|
202
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
203
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
204
|
+
const key = contextCompatibleBaseLine(content, buildOpenaiBaseUrlLineForTarget(target));
|
|
205
|
+
for (let index = 0; index < rootEnd; index += 1) {
|
|
206
|
+
if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
|
|
207
|
+
const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
|
|
208
|
+
if (!markerOwned) return { content, keptUserBaseUrl: true };
|
|
209
|
+
lines[index] = key;
|
|
210
|
+
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
211
|
+
}
|
|
212
|
+
if (firstTable === -1) {
|
|
213
|
+
return {
|
|
214
|
+
content: `${content.replace(/\n+$/, "")}\n${OCX_SECTION_MARKER}\n${key}\n`,
|
|
215
|
+
keptUserBaseUrl: false,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
let insertAt = firstTable;
|
|
219
|
+
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt -= 1;
|
|
220
|
+
lines.splice(insertAt, 0, OCX_SECTION_MARKER, key);
|
|
221
|
+
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Companion to `setRootOpenaiBaseUrlForTarget` for the realtime sideband override. Same
|
|
226
|
+
* ownership rule, applied per key: the line is ours only when the marker sits directly
|
|
227
|
+
* above it; a user's own line (no marker above it) is kept and nothing is injected. The
|
|
228
|
+
* key gets its OWN marker line rather than sharing the routing override's, so a user line
|
|
229
|
+
* that happens to sit right under our `openai_base_url` is never mistaken for ours.
|
|
230
|
+
* Placement: directly after the marker-owned `openai_base_url` pair. Only ever called on
|
|
231
|
+
* the Design B (loopback) path right after the routing override was written — the legacy
|
|
232
|
+
* provider-table form needs the admission-token header, which the sideband cannot carry.
|
|
233
|
+
*/
|
|
234
|
+
export function setRootRealtimeWsBaseUrl(
|
|
235
|
+
content: string,
|
|
236
|
+
target: CodexRoutingTarget,
|
|
237
|
+
): { content: string; keptUserRealtimeWsBaseUrl: boolean } {
|
|
238
|
+
const lines = content.split("\n");
|
|
239
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
240
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
241
|
+
const key = buildRealtimeWsBaseUrlLine(validateCodexRoutingTarget(target));
|
|
242
|
+
for (let index = 0; index < rootEnd; index += 1) {
|
|
243
|
+
if (!isRootRealtimeWsBaseUrlLine(lines[index])) continue;
|
|
244
|
+
const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
|
|
245
|
+
if (!markerOwned) return { content, keptUserRealtimeWsBaseUrl: true };
|
|
246
|
+
lines[index] = key;
|
|
247
|
+
return { content: lines.join("\n"), keptUserRealtimeWsBaseUrl: false };
|
|
248
|
+
}
|
|
249
|
+
for (let index = 0; index < rootEnd; index += 1) {
|
|
250
|
+
if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
|
|
251
|
+
if (!(index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER))) continue;
|
|
252
|
+
lines.splice(index + 1, 0, OCX_SECTION_MARKER, key);
|
|
253
|
+
return { content: lines.join("\n"), keptUserRealtimeWsBaseUrl: false };
|
|
254
|
+
}
|
|
255
|
+
// No marker-owned routing override to attach to: the override has no owner, so inject nothing.
|
|
256
|
+
return { content, keptUserRealtimeWsBaseUrl: false };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Remove the marker-owned root `openai_base_url` (marker line + the key line right after it).
|
|
261
|
+
* A user's own root override (no marker) survives; an orphaned marker with no key line after
|
|
262
|
+
* it is dropped too so repeated strip/inject cycles cannot accumulate marker comments.
|
|
263
|
+
* A marker-owned `experimental_realtime_ws_base_url` pair is removed by the same rule.
|
|
264
|
+
*/
|
|
265
|
+
export function stripInjectedOpenaiBaseUrl(content: string): string {
|
|
266
|
+
const lines = content.split("\n");
|
|
267
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
268
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
269
|
+
const drop = new Set<number>();
|
|
270
|
+
for (let i = 0; i < rootEnd; i++) {
|
|
271
|
+
if (!lines[i].includes(OCX_SECTION_MARKER)) continue;
|
|
272
|
+
if (i + 1 < rootEnd && (isRootOpenaiBaseUrlLine(lines[i + 1]) || isRootRealtimeWsBaseUrlLine(lines[i + 1]))) {
|
|
273
|
+
drop.add(i);
|
|
274
|
+
drop.add(i + 1);
|
|
275
|
+
} else if (i + 1 >= rootEnd || lines[i + 1].trim() === "") {
|
|
276
|
+
drop.add(i); // orphaned marker at root
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (drop.size === 0) return content;
|
|
280
|
+
return lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Strip every existing `model_provider` line that we must not duplicate: any line set to
|
|
285
|
+
* "opencodex" (wherever it sits — including a previously mis-nested one under a table), plus any
|
|
286
|
+
* ROOT-level model_provider (before the first table) of any value, since we override the global.
|
|
287
|
+
* A `model_provider` legitimately inside a user table/profile with a non-opencodex value is left
|
|
288
|
+
* untouched.
|
|
289
|
+
*/
|
|
290
|
+
export function stripExistingModelProvider(content: string): string {
|
|
291
|
+
const lines = content.split("\n");
|
|
292
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
293
|
+
const out: string[] = [];
|
|
294
|
+
lines.forEach((line, i) => {
|
|
295
|
+
if (/^\s*model_provider\s*=/.test(line)) {
|
|
296
|
+
const isOurs = /^\s*model_provider\s*=\s*"opencodex"\s*$/.test(line);
|
|
297
|
+
const isRoot = firstTable === -1 || i < firstTable;
|
|
298
|
+
if (isOurs || isRoot) return; // drop it
|
|
299
|
+
}
|
|
300
|
+
out.push(line);
|
|
301
|
+
});
|
|
302
|
+
return out.join("\n");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Drop ROOT-level `model_context_window` overrides (keys before the first table header). Codex
|
|
307
|
+
* treats this root key as a global override that wins over the per-model catalog values, so a stale
|
|
308
|
+
* `model_context_window = 1000000` makes every model (e.g. gpt-5.5) report a 1M window. User-owned
|
|
309
|
+
* compaction limits do not alter the advertised context window and must survive reinjection.
|
|
310
|
+
*/
|
|
311
|
+
export function stripRootContextWindowOverrides(content: string): string {
|
|
312
|
+
const lines = content.split("\n");
|
|
313
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
314
|
+
return lines
|
|
315
|
+
.filter((line, i) => {
|
|
316
|
+
const isRoot = firstTable === -1 || i < firstTable;
|
|
317
|
+
return !isRoot || !/^\s*model_context_window\s*=/.test(line);
|
|
318
|
+
})
|
|
319
|
+
.join("\n");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function stripRootRoutedModel(content: string): string {
|
|
323
|
+
const lines = content.split("\n");
|
|
324
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
325
|
+
return lines
|
|
326
|
+
.filter((line, i) => {
|
|
327
|
+
const isRoot = firstTable === -1 || i < firstTable;
|
|
328
|
+
if (!isRoot) return true;
|
|
329
|
+
const m = line.match(/^\s*model\s*=\s*("(?:\\.|[^"\\])*"|'[^']*')\s*$/);
|
|
330
|
+
if (!m) return true;
|
|
331
|
+
const model = parseTomlString(m[1]);
|
|
332
|
+
return !model?.includes("/");
|
|
333
|
+
})
|
|
334
|
+
.join("\n");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Insert `model_provider = "opencodex"` at the document ROOT — immediately before the first table
|
|
339
|
+
* header (TOML root keys must precede all tables). If there are no tables, append it to the root body.
|
|
340
|
+
*/
|
|
341
|
+
export function setRootModelProvider(content: string): string {
|
|
342
|
+
const lines = content.split("\n");
|
|
343
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
344
|
+
const key = 'model_provider = "opencodex"';
|
|
345
|
+
if (firstTable === -1) {
|
|
346
|
+
return content.replace(/\n+$/, "") + "\n" + key + "\n";
|
|
347
|
+
}
|
|
348
|
+
let insertAt = firstTable;
|
|
349
|
+
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
350
|
+
lines.splice(insertAt, 0, key);
|
|
351
|
+
return lines.join("\n");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function readRootModelCatalogPath(content: string): string | null {
|
|
355
|
+
const lines = content.split("\n");
|
|
356
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
357
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
358
|
+
const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
|
|
359
|
+
let ownedCatalogPath: string | null = null;
|
|
360
|
+
for (let index = 0; index < rootEnd; index += 1) {
|
|
361
|
+
const match = modelCatalogAssignment.exec(lines[index]);
|
|
362
|
+
if (!match) continue;
|
|
363
|
+
const catalogPath = parseTomlString(match[1]);
|
|
364
|
+
if (!isOpencodexCatalogPath(catalogPath)) return catalogPath;
|
|
365
|
+
ownedCatalogPath ??= catalogPath;
|
|
366
|
+
}
|
|
367
|
+
return ownedCatalogPath;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function setRootModelCatalogPath(content: string, catalogPath: string): string {
|
|
371
|
+
const lines = content.split("\n");
|
|
372
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
373
|
+
const key = `model_catalog_json = ${tomlString(catalogPath)}`;
|
|
374
|
+
const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
|
|
375
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
376
|
+
const ownedAssignments: number[] = [];
|
|
377
|
+
let hasUserAssignment = false;
|
|
378
|
+
for (let i = 0; i < rootEnd; i++) {
|
|
379
|
+
const m = modelCatalogAssignment.exec(lines[i]);
|
|
380
|
+
if (!m) continue;
|
|
381
|
+
const existing = parseTomlString(m[1]);
|
|
382
|
+
if (isOpencodexCatalogPath(existing)) {
|
|
383
|
+
ownedAssignments.push(i);
|
|
384
|
+
} else {
|
|
385
|
+
hasUserAssignment = true;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (hasUserAssignment) {
|
|
389
|
+
const owned = new Set(ownedAssignments);
|
|
390
|
+
return lines.filter((_, index) => !owned.has(index)).join("\n");
|
|
391
|
+
}
|
|
392
|
+
if (ownedAssignments.length > 0) {
|
|
393
|
+
lines[ownedAssignments[0]] = key;
|
|
394
|
+
const duplicates = new Set(ownedAssignments.slice(1));
|
|
395
|
+
return lines.filter((_, index) => !duplicates.has(index)).join("\n");
|
|
396
|
+
}
|
|
397
|
+
if (firstTable === -1) {
|
|
398
|
+
return content.replace(/\n+$/, "") + "\n" + key + "\n";
|
|
399
|
+
}
|
|
400
|
+
let insertAt = firstTable;
|
|
401
|
+
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
402
|
+
lines.splice(insertAt, 0, key);
|
|
403
|
+
return lines.join("\n");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function removeProfileSection(content: string): string {
|
|
407
|
+
const lines = content.split("\n");
|
|
408
|
+
const filtered: string[] = [];
|
|
409
|
+
let inProfile = false;
|
|
410
|
+
for (const line of lines) {
|
|
411
|
+
if (line.trim() === "[profiles.opencodex]") {
|
|
412
|
+
inProfile = true;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (inProfile) {
|
|
416
|
+
if (/^\s*\[/.test(line) && line.trim() !== "[profiles.opencodex]") {
|
|
417
|
+
inProfile = false;
|
|
418
|
+
filtered.push(line);
|
|
419
|
+
}
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
filtered.push(line);
|
|
423
|
+
}
|
|
424
|
+
return (
|
|
425
|
+
filtered
|
|
426
|
+
.join("\n")
|
|
427
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
428
|
+
.trimEnd() + "\n"
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function normalizeServiceTier(content: string): string {
|
|
433
|
+
return content.replace(
|
|
434
|
+
/^(\s*service_tier\s*=\s*)["']priority["']\s*$/gm,
|
|
435
|
+
'$1"fast"',
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function ensureFastModeFeature(content: string, fastMode?: boolean): string {
|
|
440
|
+
// Tri-state fast mode (see OcxConfig.fastMode): true forces `fast_mode = true`,
|
|
441
|
+
// false forces `fast_mode = false`, and undefined leaves the user's config
|
|
442
|
+
// untouched (no [features] table is added and an existing fast_mode line is
|
|
443
|
+
// preserved as-is). Table and key matching accept the valid TOML spellings
|
|
444
|
+
// `[features] # comment`, `["features"]` / `['features']`, and quoted keys.
|
|
445
|
+
const lines = content.split("\n");
|
|
446
|
+
const featuresHeader = /^\s*\[(["']?)\s*features\s*\1\]\s*(?:#.*)?$/;
|
|
447
|
+
const fastModeKey = /^\s*(?:"fast_mode"|'fast_mode'|fast_mode)\s*=/;
|
|
448
|
+
const featuresStart = lines.findIndex(line => featuresHeader.test(line));
|
|
449
|
+
if (featuresStart === -1) {
|
|
450
|
+
if (fastMode === undefined) return content;
|
|
451
|
+
return content.trimEnd() + "\n\n[features]\nfast_mode = " + (fastMode ? "true" : "false") + "\n";
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const nextTable = lines.findIndex(
|
|
455
|
+
(line, index) => index > featuresStart && /^\s*\[/.test(line),
|
|
456
|
+
);
|
|
457
|
+
const featuresEnd = nextTable === -1 ? lines.length : nextTable;
|
|
458
|
+
for (let i = featuresStart + 1; i < featuresEnd; i++) {
|
|
459
|
+
if (fastModeKey.test(lines[i])) {
|
|
460
|
+
if (fastMode === undefined) return lines.join("\n");
|
|
461
|
+
lines[i] = lines[i].replace(/^(\s*)(?:"fast_mode"|'fast_mode'|fast_mode)\s*=.*$/, `$1fast_mode = ${fastMode ? "true" : "false"}`);
|
|
462
|
+
return lines.join("\n");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (fastMode === undefined) return lines.join("\n");
|
|
467
|
+
let insertAt = featuresEnd;
|
|
468
|
+
while (insertAt > featuresStart + 1 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
469
|
+
lines.splice(insertAt, 0, `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
470
|
+
return lines.join("\n");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function isOpencodexCatalogPath(path: string): boolean {
|
|
474
|
+
return path.replace(/\\/g, "/").split("/").pop() === "opencodex-catalog.json";
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function stripOpencodexCatalogPath(content: string): string {
|
|
478
|
+
const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
|
|
479
|
+
const lines = content.split("\n");
|
|
480
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
481
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
482
|
+
return lines
|
|
483
|
+
.filter((line, index) => {
|
|
484
|
+
if (index >= rootEnd) return true;
|
|
485
|
+
const m = modelCatalogAssignment.exec(line);
|
|
486
|
+
return !m || !isOpencodexCatalogPath(parseTomlString(m[1]));
|
|
487
|
+
})
|
|
488
|
+
.join("\n");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets?: boolean, includeApiAuthHeader?: boolean, hostname?: string, fastMode?: boolean): string;
|
|
492
|
+
export function buildProfileFile(target: CodexRoutingTarget, catalogPath?: string | null, supportsWebsockets?: boolean, fastMode?: boolean): string;
|
|
493
|
+
export function buildProfileFile(
|
|
494
|
+
portOrTarget: number | CodexRoutingTarget,
|
|
495
|
+
catalogPath?: string | null,
|
|
496
|
+
supportsWebsockets = false,
|
|
497
|
+
includeApiAuthHeaderOrFastMode?: boolean,
|
|
498
|
+
hostname?: string,
|
|
499
|
+
fastMode?: boolean,
|
|
500
|
+
): string {
|
|
501
|
+
const target = typeof portOrTarget === "number"
|
|
502
|
+
? validateCodexRoutingTarget({
|
|
503
|
+
baseUrl: `http://${providerBaseHost(hostname)}:${portOrTarget}/v1`,
|
|
504
|
+
requiresAdmissionToken: includeApiAuthHeaderOrFastMode === true,
|
|
505
|
+
tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
|
|
506
|
+
})
|
|
507
|
+
: validateCodexRoutingTarget(portOrTarget);
|
|
508
|
+
return buildProfileFileForTarget(
|
|
509
|
+
target,
|
|
510
|
+
catalogPath,
|
|
511
|
+
supportsWebsockets,
|
|
512
|
+
typeof portOrTarget === "number" ? fastMode : includeApiAuthHeaderOrFastMode,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function buildProfileFileForTarget(
|
|
517
|
+
target: CodexRoutingTarget,
|
|
518
|
+
catalogPath?: string | null,
|
|
519
|
+
supportsWebsockets = false,
|
|
520
|
+
fastMode?: boolean,
|
|
521
|
+
): string {
|
|
522
|
+
const origin = routingTargetOrigin(target);
|
|
523
|
+
const host = new URL(origin).host;
|
|
524
|
+
// Design B (loopback): the reference/fallback file documents the root override form.
|
|
525
|
+
// Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
|
|
526
|
+
// the x-opencodex-api-key env header); explicit Desktop policies share that shape.
|
|
527
|
+
if (!usesProviderTable(target)) {
|
|
528
|
+
const lines = [
|
|
529
|
+
"# OpenCodex proxy fallback config (Design B)",
|
|
530
|
+
`# Root override that points Codex's built-in openai provider at the proxy on ${host}.`,
|
|
531
|
+
"# Merge these root keys into ~/.codex/config.toml manually if auto-injection was removed.",
|
|
532
|
+
buildOpenaiBaseUrlLineForTarget(target),
|
|
533
|
+
];
|
|
534
|
+
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
535
|
+
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`, "");
|
|
536
|
+
return lines.join("\n");
|
|
537
|
+
}
|
|
538
|
+
const lines = [
|
|
539
|
+
"# OpenCodex proxy profile — use with: codex --profile opencodex",
|
|
540
|
+
`# Routes all model requests through the opencodex proxy at ${host}`,
|
|
541
|
+
'model_provider = "opencodex"',
|
|
542
|
+
];
|
|
543
|
+
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
544
|
+
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
545
|
+
lines.push(buildProviderTableBlockForTarget(target, supportsWebsockets).trimEnd(), "");
|
|
546
|
+
return lines.join("\n");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function chooseCatalogPathForInjection(
|
|
550
|
+
content: string,
|
|
551
|
+
requested?: string | null,
|
|
552
|
+
): string | null {
|
|
553
|
+
if (requested !== undefined) return requested;
|
|
554
|
+
|
|
555
|
+
const existing = readRootModelCatalogPath(content);
|
|
556
|
+
if (existing) {
|
|
557
|
+
const resolved = resolveCodexConfigPath(existing);
|
|
558
|
+
if (!isOpencodexCatalogPath(resolved) || existsSync(resolved))
|
|
559
|
+
return existing;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return existsSync(DEFAULT_CATALOG_PATH) ? DEFAULT_CATALOG_PATH : null;
|
|
563
|
+
}
|