@bitkyc08/opencodex 2.55.0 → 2.56.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/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- 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-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- 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 +611 -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 +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- 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 +554 -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 +422 -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 +95 -28
- 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 +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -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 +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- 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/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/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 +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- 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 +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -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/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 +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -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 +335 -0
- package/src/server/index.ts +40 -2547
- 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/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -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 +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -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
|
+
}
|