@christiandoxa/prodex 0.189.0 → 0.191.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/README.md CHANGED
@@ -41,6 +41,9 @@ If you only use one Codex account and do not need quota rotation, you probably d
41
41
 
42
42
  You need at least one logged-in Prodex profile.
43
43
 
44
+ <details>
45
+ <summary>Tool requirements</summary>
46
+
44
47
  | Tool | Used by |
45
48
  |---|---|
46
49
  | Codex CLI | `prodex`, `prodex run`, `prodex caveman`, `prodex super` |
@@ -48,6 +51,8 @@ You need at least one logged-in Prodex profile.
48
51
  | Claude-Mem | `mem` variants |
49
52
  | RTK | `rtk` variants and `prodex s` / `prodex super` |
50
53
 
54
+ </details>
55
+
51
56
  ## Supported providers
52
57
 
53
58
  Prodex supports two provider paths:
@@ -55,6 +60,9 @@ Prodex supports two provider paths:
55
60
  - **Profile-backed routing**: persisted profiles that Prodex can select, rotate, and inspect where provider APIs allow it.
56
61
  - **Runtime provider launch**: `prodex s gemini`, `prodex s deepseek`, or `prodex s --provider ...` starts Codex with a temporary provider bridge for that session.
57
62
 
63
+ <details>
64
+ <summary>Supported provider matrix</summary>
65
+
58
66
  | Provider | Launch to Codex | Auth path | Quota view | Notes |
59
67
  |---|---:|---|---:|---|
60
68
  | OpenAI / Codex | `prodex`, `prodex run`, `prodex s` | ChatGPT OAuth, device code, or OpenAI/API-compatible key via `prodex login` | Yes | Full quota preflight and profile auto-rotation. |
@@ -65,8 +73,13 @@ Prodex supports two provider paths:
65
73
  | Local OpenAI-compatible | `prodex super --url http://127.0.0.1:8131` | Local server auth/config | Health snapshot | `prodex quota --all --provider local --base-url ...` checks the local `/models` endpoint. |
66
74
  | Bedrock / custom Codex `model_provider` | `prodex run` / `prodex caveman` direct pass-through | Codex-owned config | Config snapshot | Prodex reports configured provider metadata; provider-side quota stays owned by Codex/upstream. |
67
75
 
76
+ </details>
77
+
68
78
  `prodex gateway` exposes the provider bridge as a standalone OpenAI-compatible service for non-Codex clients:
69
79
 
80
+ <details>
81
+ <summary>Gateway quickstart</summary>
82
+
70
83
  ```bash
71
84
  PRODEX_GATEWAY_TOKEN=change-me GEMINI_API_KEY=... prodex gateway --provider gemini
72
85
  auth_header="Authorization: Bearer $PRODEX_GATEWAY_TOKEN"
@@ -76,7 +89,16 @@ curl http://127.0.0.1:4000/v1/responses \
76
89
  -d '{"model":"prodex-fast","input":"hello"}'
77
90
  ```
78
91
 
79
- The gateway serves `/v1/responses`, `/v1/chat/completions`, `/v1/embeddings`, `/v1/images/*`, `/v1/audio/*`, `/v1/batches`, `/v1/rerank`, `/v1/a2a`, `/v1/messages`, and `/v1/models` where the selected upstream supports them. It adds `x-prodex-call-id` to responses, writes local request detail plus `gateway_spend` events to runtime logs, can export those events to JSONL or HTTP using generic, OTel, Datadog, or Langfuse-shaped payloads, supports policy-defined routing strategies (`fallback`, `round-robin`, `least-busy`, `lowest-cost`, `lowest-latency`, `rpm`, `tpm`, `first`) for model aliases/fallback chains, and can apply keyword/model, Presidio, and external webhook guardrails before calls and on outputs. Configure defaults under `[gateway]` in `policy.toml`.
92
+ </details>
93
+
94
+ <details>
95
+ <summary>Gateway capabilities</summary>
96
+
97
+ The gateway serves `/v1/responses`, `/v1/chat/completions`, `/v1/embeddings`, `/v1/images/*`, `/v1/audio/*`, `/v1/batches`, `/v1/rerank`, `/v1/a2a`, `/v1/messages`, and `/v1/models` where the selected upstream supports them. It adds `x-prodex-call-id` to responses, writes local request detail plus `gateway_spend` events for both `request` and `response` phases to runtime logs, can export those events to JSONL or HTTP using generic, OTel, Datadog, or Langfuse-shaped payloads, supports catalog-backed policy routing strategies (`fallback`, `round-robin`, `least-busy`, `lowest-cost`, `lowest-latency`, `rpm`, `tpm`, `first`) for model aliases/fallback chains, can enforce static virtual keys with persisted request/spend usage plus model/budget/RPM/TPM limits, supports file, SQLite, Postgres, or Redis-backed gateway admin/usage/ledger/SCIM state, and can apply keyword/model, local PII redaction, Presidio, and external webhook guardrails before calls and on outputs. Admin-token, trusted-proxy SSO, or OIDC/JWT bearer requests can list usage, create generated-token keys, rotate/disable/update/delete admin-managed keys, provision SSO users through SCIM-compatible `/v1/prodex/gateway/scim/v2/Users`, inspect usage at `/v1/prodex/gateway/keys` and `/v1/prodex/gateway/usage`, read recent billing ledger records with response-status/output-token reconciliation at `/v1/prodex/gateway/ledger`, read aggregated billing totals at `/v1/prodex/gateway/ledger/summary`, export billing CSV from `/v1/prodex/gateway/ledger.csv` and `/v1/prodex/gateway/ledger/summary.csv`, scrape Prometheus text metrics at `/v1/prodex/gateway/metrics`, inspect provider adapter contracts at `/v1/prodex/gateway/providers`, inspect active observability and guardrail configuration at `/v1/prodex/gateway/observability` and `/v1/prodex/gateway/guardrails`, fetch the machine-readable gateway contract at `/v1/prodex/gateway/openapi.json`, and open the built-in gateway admin dashboard at `/v1/prodex/gateway/admin`; policy/env-backed keys remain read-only, SCIM users can carry tenant/team/project/user/budget scopes for SSO/OIDC fallback, admin-managed key and SCIM user mutations are recorded in `prodex audit`, and additional admin-plane tokens can be `admin` or read-only `viewer` with optional virtual-key prefix plus tenant/team/project/user/budget scopes. Configure defaults under `[gateway]` in `policy.toml`; validate provider catalog edits with `npm run catalog:providers`.
98
+
99
+ JavaScript clients can use `@christiandoxa/prodex-gateway-sdk` for `/v1/responses` plus gateway key, usage, billing ledger, metrics, and OpenAPI admin calls.
100
+
101
+ </details>
80
102
 
81
103
  <details>
82
104
  <summary>Provider behavior details</summary>
@@ -105,6 +127,8 @@ Runtime proxy design contract:
105
127
  npm install -g @christiandoxa/prodex
106
128
  ```
107
129
 
130
+ The npm package uses its bundled `@openai/codex@latest` dependency by default. To deliberately use a separate Codex CLI from your machine, set `PRODEX_CODEX_BIN=/path/to/codex` or `PRODEX_CODEX_RESOLUTION=external`.
131
+
108
132
  </details>
109
133
 
110
134
  <details>
@@ -1056,6 +1080,7 @@ Contributor testing guidance lives in [docs/testing.md](./docs/testing.md), incl
1056
1080
  - [LOCAL.md](./LOCAL.md) — self-hosted local model setup and testing
1057
1081
  - [docs/state-model.md](./docs/state-model.md) — state ownership and persistence model
1058
1082
  - [docs/runtime-policy.md](./docs/runtime-policy.md) — runtime policy keys, environment overrides, and runtime log path resolution
1083
+ - [docs/deployment.md](./docs/deployment.md) — Docker Compose scaffold for the standalone gateway
1059
1084
  - [docs/testing.md](./docs/testing.md) — contributor testing guidance
1060
1085
 
1061
1086
  ## Support
@@ -47,41 +47,81 @@ const PLATFORM_TARGETS = {
47
47
  },
48
48
  };
49
49
 
50
- function resolveCodexBin() {
50
+ function currentPlatformTarget() {
51
+ return PLATFORM_TARGETS[process.platform]?.[process.arch] ?? null;
52
+ }
53
+
54
+ function resolveOpenAiCodexPackageRoot() {
51
55
  let packageJsonPath;
52
56
  try {
53
57
  packageJsonPath = requireFromHere.resolve("@openai/codex/package.json");
58
+ } catch {
59
+ return null;
60
+ }
61
+ return path.dirname(packageJsonPath);
62
+ }
63
+
64
+ function codexManagedEnv() {
65
+ const packageRoot = resolveOpenAiCodexPackageRoot();
66
+ if (!packageRoot) {
67
+ return process.env;
68
+ }
69
+ return {
70
+ ...process.env,
71
+ CODEX_MANAGED_BY_NPM: "1",
72
+ CODEX_MANAGED_PACKAGE_ROOT: fs.realpathSync(packageRoot),
73
+ };
74
+ }
75
+
76
+ function ensureNativeCodexExecutable(nativeBinaryPath) {
77
+ try {
78
+ fs.accessSync(nativeBinaryPath, fs.constants.X_OK);
79
+ return;
80
+ } catch {
81
+ repairNativeCodexExecutablePermissionBestEffort(nativeBinaryPath);
82
+ }
83
+
84
+ try {
85
+ fs.accessSync(nativeBinaryPath, fs.constants.X_OK);
86
+ return;
54
87
  } catch {
55
88
  process.stderr.write(
56
- "Unable to locate @openai/codex. Reinstall @christiandoxa/prodex so its runtime dependency is present.\n",
89
+ [
90
+ `Bundled Codex native binary is not executable: ${nativeBinaryPath}`,
91
+ "Reinstall @christiandoxa/prodex with optional dependencies enabled, or set PRODEX_CODEX_BIN to an existing Codex CLI.",
92
+ "",
93
+ ].join("\n"),
57
94
  );
58
- process.exit(1);
95
+ process.exit(126);
59
96
  }
97
+ }
60
98
 
61
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
62
- if (typeof packageJson.bin === "string") {
63
- return path.resolve(path.dirname(packageJsonPath), packageJson.bin);
99
+ function repairNativeCodexExecutablePermissionBestEffort(nativeBinaryPath) {
100
+ if (process.platform === "win32") {
101
+ return;
64
102
  }
65
- if (packageJson.bin && typeof packageJson.bin === "object") {
66
- const candidate = packageJson.bin.codex ?? Object.values(packageJson.bin)[0];
67
- if (typeof candidate === "string") {
68
- return path.resolve(path.dirname(packageJsonPath), candidate);
103
+ try {
104
+ const stats = fs.statSync(nativeBinaryPath);
105
+ if (!stats.isFile()) {
106
+ return;
69
107
  }
108
+ fs.chmodSync(nativeBinaryPath, (stats.mode & 0o777) | 0o755);
109
+ } catch {
110
+ // The explicit executable check below reports the actionable failure.
70
111
  }
71
- return path.resolve(path.dirname(packageJsonPath), "bin", "codex.js");
72
112
  }
73
113
 
74
- function explainBundledNativeCodexPermissionIssue() {
75
- const platformTarget = PLATFORM_TARGETS[process.platform]?.[process.arch];
76
- if (!platformTarget || process.platform === "win32") {
77
- return;
114
+ function resolveNativeCodexCommand() {
115
+ const platformTarget = currentPlatformTarget();
116
+ if (!platformTarget) {
117
+ return null;
78
118
  }
79
119
 
80
120
  let platformPackageJsonPath;
81
121
  try {
82
122
  platformPackageJsonPath = requireFromHere.resolve(`${platformTarget.packageName}/package.json`);
83
123
  } catch {
84
- return;
124
+ return null;
85
125
  }
86
126
 
87
127
  const nativeBinaryPath = path.join(
@@ -92,26 +132,66 @@ function explainBundledNativeCodexPermissionIssue() {
92
132
  platformTarget.binaryFileName,
93
133
  );
94
134
  if (!fs.existsSync(nativeBinaryPath)) {
95
- return;
96
- }
97
- try {
98
- fs.accessSync(nativeBinaryPath, fs.constants.X_OK);
99
- } catch {
100
135
  process.stderr.write(
101
136
  [
102
- `Bundled Codex native binary is not executable: ${nativeBinaryPath}`,
137
+ `Missing bundled Codex native binary at ${nativeBinaryPath}`,
103
138
  "Reinstall @christiandoxa/prodex with optional dependencies enabled, or set PRODEX_CODEX_BIN to an existing Codex CLI.",
104
139
  "",
105
140
  ].join("\n"),
106
141
  );
107
- process.exit(126);
142
+ process.exit(1);
143
+ }
144
+ ensureNativeCodexExecutable(nativeBinaryPath);
145
+
146
+ return {
147
+ command: nativeBinaryPath,
148
+ args: process.argv.slice(2),
149
+ env: codexManagedEnv(),
150
+ };
151
+ }
152
+
153
+ function resolveCodexJsCommand() {
154
+ const packageRoot = resolveOpenAiCodexPackageRoot();
155
+ if (!packageRoot) {
156
+ process.stderr.write(
157
+ "Unable to locate @openai/codex. Reinstall @christiandoxa/prodex so its runtime dependency is present.\n",
158
+ );
159
+ process.exit(1);
160
+ }
161
+ const packageJsonPath = path.join(packageRoot, "package.json");
162
+
163
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
164
+ if (typeof packageJson.bin === "string") {
165
+ return {
166
+ command: process.execPath,
167
+ args: [path.resolve(packageRoot, packageJson.bin), ...process.argv.slice(2)],
168
+ env: process.env,
169
+ };
170
+ }
171
+ if (packageJson.bin && typeof packageJson.bin === "object") {
172
+ const candidate = packageJson.bin.codex ?? Object.values(packageJson.bin)[0];
173
+ if (typeof candidate === "string") {
174
+ return {
175
+ command: process.execPath,
176
+ args: [path.resolve(packageRoot, candidate), ...process.argv.slice(2)],
177
+ env: process.env,
178
+ };
179
+ }
108
180
  }
181
+ return {
182
+ command: process.execPath,
183
+ args: [path.resolve(packageRoot, "bin", "codex.js"), ...process.argv.slice(2)],
184
+ env: process.env,
185
+ };
186
+ }
187
+
188
+ function resolveCodexCommand() {
189
+ return resolveNativeCodexCommand() ?? resolveCodexJsCommand();
109
190
  }
110
191
 
111
- const codexBin = resolveCodexBin();
112
- explainBundledNativeCodexPermissionIssue();
113
- const child = spawn(process.execPath, [codexBin, ...process.argv.slice(2)], {
114
- env: process.env,
192
+ const codexCommand = resolveCodexCommand();
193
+ const child = spawn(codexCommand.command, codexCommand.args, {
194
+ env: codexCommand.env,
115
195
  stdio: "inherit",
116
196
  });
117
197
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@christiandoxa/prodex",
3
- "version": "0.189.0",
3
+ "version": "0.191.0",
4
4
  "description": "Safe multi-account auto-rotate for Codex CLI with isolated CODEX_HOME profiles",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -16,12 +16,18 @@
16
16
  "@openai/codex": "latest"
17
17
  },
18
18
  "optionalDependencies": {
19
- "@christiandoxa/prodex-linux-x64": "0.189.0",
20
- "@christiandoxa/prodex-linux-arm64": "0.189.0",
21
- "@christiandoxa/prodex-darwin-x64": "0.189.0",
22
- "@christiandoxa/prodex-darwin-arm64": "0.189.0",
23
- "@christiandoxa/prodex-win32-x64": "0.189.0",
24
- "@christiandoxa/prodex-win32-arm64": "0.189.0"
19
+ "@christiandoxa/prodex-linux-x64": "0.191.0",
20
+ "@christiandoxa/prodex-linux-arm64": "0.191.0",
21
+ "@christiandoxa/prodex-darwin-x64": "0.191.0",
22
+ "@christiandoxa/prodex-darwin-arm64": "0.191.0",
23
+ "@christiandoxa/prodex-win32-x64": "0.191.0",
24
+ "@christiandoxa/prodex-win32-arm64": "0.191.0",
25
+ "@openai/codex-linux-x64": "npm:@openai/codex@linux-x64",
26
+ "@openai/codex-linux-arm64": "npm:@openai/codex@linux-arm64",
27
+ "@openai/codex-darwin-x64": "npm:@openai/codex@darwin-x64",
28
+ "@openai/codex-darwin-arm64": "npm:@openai/codex@darwin-arm64",
29
+ "@openai/codex-win32-x64": "npm:@openai/codex@win32-x64",
30
+ "@openai/codex-win32-arm64": "npm:@openai/codex@win32-arm64"
25
31
  },
26
32
  "engines": {
27
33
  "node": ">=18"
package/prodex CHANGED
@@ -97,7 +97,25 @@ function resolveExternalCodexBin() {
97
97
  if (process.env.PRODEX_CODEX_BIN) {
98
98
  return process.env.PRODEX_CODEX_BIN;
99
99
  }
100
- return resolveCommandFromPath("codex", process.env.PATH || "");
100
+ const resolution = (process.env.PRODEX_CODEX_RESOLUTION || "bundled").toLowerCase();
101
+ if (resolution === "bundled") {
102
+ return null;
103
+ }
104
+ if (resolution !== "external") {
105
+ process.stderr.write(
106
+ `Unsupported PRODEX_CODEX_RESOLUTION=${JSON.stringify(process.env.PRODEX_CODEX_RESOLUTION)}; expected "bundled" or "external".\n`,
107
+ );
108
+ process.exit(1);
109
+ }
110
+
111
+ const externalCodex = resolveCommandFromPath("codex", process.env.PATH || "");
112
+ if (!externalCodex) {
113
+ process.stderr.write(
114
+ "PRODEX_CODEX_RESOLUTION=external was set, but no executable codex was found on PATH.\n",
115
+ );
116
+ process.exit(1);
117
+ }
118
+ return externalCodex;
101
119
  }
102
120
 
103
121
  if (!platformPackage) {