@parall/codex-agent 1.26.3 → 1.27.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.
@@ -5,6 +5,7 @@ type CodexAppServerAdapterOptions = Pick<CodexAgentConfig, "approvalPolicy" | "c
5
5
  sessionManager: CodexSessionManager;
6
6
  log?: GatewayLogger;
7
7
  contextFilePath?: string;
8
+ useParallProvider?: boolean;
8
9
  };
9
10
  /**
10
11
  * Bridge driver backed by `codex app-server --listen stdio://`.
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,KAAK,4BAA4B,GAAG,IAAI,CACtC,gBAAgB,EACd,gBAAgB,GAChB,UAAU,GACV,WAAW,GACX,OAAO,GACP,iBAAiB,GACjB,SAAS,GACT,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,mBAAmB,CAAC;IACpC,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAOF;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAsB,YAAW,eAAe;IA6B/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IA5BjC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,IAAI,CAA+C;IAC3D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAS;IAEzB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;gBAYQ,IAAI,EAAE,4BAA4B;IAE/D,YAAY,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI;IAK/E,QAAQ,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAgLxG,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMjD,WAAW,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAuBhG,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,cAAc;IAKhB,IAAI;YAaI,aAAa;YAyBb,OAAO;IA8DrB,OAAO,CAAC,qBAAqB;YAiCf,UAAU;IAwCxB,OAAO,CAAC,iBAAiB;CA2B1B"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,KAAK,4BAA4B,GAAG,IAAI,CACtC,gBAAgB,EACd,gBAAgB,GAChB,UAAU,GACV,WAAW,GACX,OAAO,GACP,iBAAiB,GACjB,SAAS,GACT,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,mBAAmB,CAAC;IACpC,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAOF;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAsB,YAAW,eAAe;IA6B/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IA5BjC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,IAAI,CAA+C;IAC3D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAS;IAEzB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;gBAYQ,IAAI,EAAE,4BAA4B;IAE/D,YAAY,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI;IAK/E,QAAQ,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAwMxG,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMjD,WAAW,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAuBhG,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,cAAc;IAKhB,IAAI;YAaI,aAAa;YAyBb,OAAO;IA8DrB,OAAO,CAAC,qBAAqB;YAiCf,UAAU;IA2CxB,OAAO,CAAC,iBAAiB;CA2B1B"}
package/dist/dispatch.js CHANGED
@@ -163,6 +163,11 @@ export class CodexAppServerAdapter {
163
163
  // resume normally instead of permanently losing conversation context.
164
164
  // Fork sessions don't retry: agent-core spawns a fresh fork next trigger.
165
165
  if (!isMainSession) {
166
+ yield {
167
+ type: "runtime_session",
168
+ runtimeSessionId: threadId,
169
+ runtimeLaneKey: sessionKey,
170
+ };
166
171
  yield { type: "error", message: `Codex turn/start failed: ${message}` };
167
172
  return;
168
173
  }
@@ -173,6 +178,11 @@ export class CodexAppServerAdapter {
173
178
  freshThreadId = await this.openThread(client, { resumeId: undefined });
174
179
  }
175
180
  catch (createErr) {
181
+ yield {
182
+ type: "runtime_session",
183
+ runtimeSessionId: threadId,
184
+ runtimeLaneKey: sessionKey,
185
+ };
176
186
  yield { type: "error", message: `Codex turn/start failed; could not create replacement thread: ${errToString(createErr)}` };
177
187
  return;
178
188
  }
@@ -185,6 +195,11 @@ export class CodexAppServerAdapter {
185
195
  // issue. Don't clobber the persisted threadId; next dispatch will try
186
196
  // resume again.
187
197
  this.activeTurns.delete(freshThreadId);
198
+ yield {
199
+ type: "runtime_session",
200
+ runtimeSessionId: threadId,
201
+ runtimeLaneKey: sessionKey,
202
+ };
188
203
  yield { type: "error", message: `Codex turn/start failed after retry: ${errToString(retryErr)}` };
189
204
  return;
190
205
  }
@@ -196,6 +211,11 @@ export class CodexAppServerAdapter {
196
211
  threadId = freshThreadId;
197
212
  }
198
213
  void turnStartResult; // turnId comes back here but isn't needed — we key by threadId
214
+ yield {
215
+ type: "runtime_session",
216
+ runtimeSessionId: threadId,
217
+ runtimeLaneKey: sessionKey,
218
+ };
199
219
  while (true) {
200
220
  const envelope = await sink.next();
201
221
  if (envelope.kind === "turn_end") {
@@ -211,6 +231,10 @@ export class CodexAppServerAdapter {
211
231
  yield runtimeEvent;
212
232
  continue;
213
233
  }
234
+ if (runtimeEvent.type === "runtime_session") {
235
+ yield runtimeEvent;
236
+ continue;
237
+ }
214
238
  if (runtimeEvent.type === "text") {
215
239
  // Layer 0 symmetric output contract: Codex's plain text is never
216
240
  // projected as a chat message. Outbound messages must come from the
@@ -407,6 +431,9 @@ export class CodexAppServerAdapter {
407
431
  const commonParams = {
408
432
  approvalPolicy: normalizeApprovalPolicy(this.opts.approvalPolicy),
409
433
  };
434
+ if (this.opts.useParallProvider) {
435
+ commonParams.modelProvider = "parall";
436
+ }
410
437
  commonParams.sandbox = normalizeSandbox(this.opts.sandbox);
411
438
  if (this.opts.model)
412
439
  commonParams.model = this.opts.model;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { ApiError, ParallClient, ParallWs } from "@parall/sdk";
5
5
  import { buildCodexRuntimeKey, contextFilePathForSession, resolveCodexAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from "./config.js";
6
6
  import { CodexAppServerAdapter } from "./dispatch.js";
7
7
  import { CodexSessionManager } from "./session-manager.js";
8
- import { ensureCodexWorkspace, ensureWorkspaceTrusted } from "./workspace.js";
8
+ import { ensureCodexWorkspace, ensureParallProvider, ensureWorkspaceTrusted, isParallProxyMode } from "./workspace.js";
9
9
  function createLogger(prefix) {
10
10
  return {
11
11
  info: (msg) => console.log(`[${prefix}] ${msg}`),
@@ -36,6 +36,11 @@ async function main() {
36
36
  const me = await getAgentMeWithLegacyFallback(client, config.orgId);
37
37
  const agentUserId = me.id;
38
38
  ensureWorkspaceTrusted(config.codexHome, config.workspaceDir, log);
39
+ const useParallProvider = isParallProxyMode();
40
+ if (useParallProvider) {
41
+ ensureParallProvider(config.codexHome, config.apiUrl, log);
42
+ log.info("parall custom provider configured (Responses API HTTP/SSE mode)");
43
+ }
39
44
  ensureCodexWorkspace(config.workspaceDir, log, {
40
45
  userId: agentUserId,
41
46
  displayName: me.display_name,
@@ -70,6 +75,7 @@ async function main() {
70
75
  sessionManager,
71
76
  log,
72
77
  contextFilePath: mainContextFilePath,
78
+ useParallProvider,
73
79
  });
74
80
  const gateway = new ParallAgentGateway({
75
81
  accountId: agentUserId,
@@ -12,6 +12,29 @@ import type { AgentIdentity } from "@parall/agent-core";
12
12
  export declare function ensureWorkspaceTrusted(codexHome: string, workspaceDir: string, log?: {
13
13
  warn: (msg: string) => void;
14
14
  }): void;
15
+ /**
16
+ * Returns true when the agent is using the Parall LLM proxy (llm_source=parall)
17
+ * rather than a BYO custom provider or runtime_auth. Detected by checking
18
+ * whether OPENAI_BASE_URL points to the Parall API.
19
+ */
20
+ export declare function isParallProxyMode(env?: NodeJS.ProcessEnv): boolean;
21
+ /**
22
+ * Ensure a custom `parall` model provider is configured in the global Codex
23
+ * config. Codex CLI's built-in `openai` provider hardcodes
24
+ * `wss://api.openai.com` for the Responses API, ignoring OPENAI_BASE_URL.
25
+ * A custom provider with `supports_websockets = false` forces HTTP/SSE
26
+ * transport that correctly routes through `base_url`.
27
+ *
28
+ * Only written when the agent uses the Parall proxy (llm_source=parall).
29
+ * BYO custom providers (llm_source=custom) use the built-in openai
30
+ * provider with their own real API key, which works with the hardcoded
31
+ * api.openai.com endpoint.
32
+ *
33
+ * Provider-managed: overwritten on every boot (env vars are the SSOT).
34
+ */
35
+ export declare function ensureParallProvider(codexHome: string, apiUrl: string, log?: {
36
+ warn: (msg: string) => void;
37
+ }): void;
15
38
  export declare function ensureCodexWorkspace(workspaceDir: string, log?: {
16
39
  warn: (msg: string) => void;
17
40
  }, agentIdentity?: AgentIdentity): void;
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACpC,IAAI,CAyEN;AASD,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACrC,aAAa,CAAC,EAAE,aAAa,GAC5B,IAAI,CAqBN"}
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACpC,IAAI,CAyEN;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAK/E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACpC,IAAI,CA+CN;AASD,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACrC,aAAa,CAAC,EAAE,aAAa,GAC5B,IAAI,CAqBN"}
package/dist/workspace.js CHANGED
@@ -91,6 +91,80 @@ export function ensureWorkspaceTrusted(codexHome, workspaceDir, log) {
91
91
  log?.warn(`failed to write Codex project trust for ${normalizedPath}: ${String(err)}`);
92
92
  }
93
93
  }
94
+ /**
95
+ * Returns true when the agent is using the Parall LLM proxy (llm_source=parall)
96
+ * rather than a BYO custom provider or runtime_auth. Detected by checking
97
+ * whether OPENAI_BASE_URL points to the Parall API.
98
+ */
99
+ export function isParallProxyMode(env = process.env) {
100
+ const baseUrl = env.OPENAI_BASE_URL?.trim()?.replace(/\/+$/, "");
101
+ const apiUrl = env.PRLL_API_URL?.trim()?.replace(/\/+$/, "");
102
+ if (!baseUrl || !apiUrl)
103
+ return false;
104
+ return baseUrl === apiUrl || baseUrl.startsWith(`${apiUrl}/`);
105
+ }
106
+ /**
107
+ * Ensure a custom `parall` model provider is configured in the global Codex
108
+ * config. Codex CLI's built-in `openai` provider hardcodes
109
+ * `wss://api.openai.com` for the Responses API, ignoring OPENAI_BASE_URL.
110
+ * A custom provider with `supports_websockets = false` forces HTTP/SSE
111
+ * transport that correctly routes through `base_url`.
112
+ *
113
+ * Only written when the agent uses the Parall proxy (llm_source=parall).
114
+ * BYO custom providers (llm_source=custom) use the built-in openai
115
+ * provider with their own real API key, which works with the hardcoded
116
+ * api.openai.com endpoint.
117
+ *
118
+ * Provider-managed: overwritten on every boot (env vars are the SSOT).
119
+ */
120
+ export function ensureParallProvider(codexHome, apiUrl, log) {
121
+ const configPath = path.join(codexHome, "config.toml");
122
+ const baseUrl = apiUrl.replace(/\/$/, "") + "/api/llm/v1";
123
+ try {
124
+ let content = "";
125
+ try {
126
+ content = fs.readFileSync(configPath, "utf8");
127
+ }
128
+ catch (err) {
129
+ if (err.code !== "ENOENT")
130
+ throw err;
131
+ }
132
+ const sectionHeader = "[model_providers.parall]";
133
+ const authHeader = "[model_providers.parall.auth]";
134
+ const providerBlock = [
135
+ sectionHeader,
136
+ 'name = "Parall Proxy"',
137
+ `base_url = "${baseUrl}"`,
138
+ 'wire_api = "responses"',
139
+ "supports_websockets = false",
140
+ "requires_openai_auth = false",
141
+ "",
142
+ authHeader,
143
+ 'command = "printenv"',
144
+ 'args = ["OPENAI_API_KEY"]',
145
+ ].join("\n");
146
+ const headerIdx = content.indexOf(sectionHeader);
147
+ if (headerIdx !== -1) {
148
+ const sectionEnd = findSectionEnd(content, headerIdx + sectionHeader.length + 1);
149
+ // Only consume a contiguous [model_providers.parall.auth] subtable —
150
+ // one that sits within the provider's own section, not after unrelated
151
+ // tables that happen to appear between provider and auth.
152
+ const authIdx = content.indexOf(authHeader, headerIdx);
153
+ const blockEnd = authIdx !== -1 && authIdx < sectionEnd
154
+ ? findSectionEnd(content, authIdx + authHeader.length + 1)
155
+ : sectionEnd;
156
+ content = content.substring(0, headerIdx) + providerBlock + content.substring(blockEnd);
157
+ }
158
+ else {
159
+ content = content.trimEnd() + "\n\n" + providerBlock + "\n";
160
+ }
161
+ fs.mkdirSync(codexHome, { recursive: true });
162
+ fs.writeFileSync(configPath, content, "utf8");
163
+ }
164
+ catch (err) {
165
+ throw new Error(`failed to write Parall provider config: ${String(err)}`);
166
+ }
167
+ }
94
168
  function findSectionEnd(content, fromIndex) {
95
169
  // A TOML section ends at the next `[` that starts a new table header.
96
170
  // Match `[` at the beginning of a line (not inside a value).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/codex-agent",
3
- "version": "1.26.3",
3
+ "version": "1.27.0",
4
4
  "description": "Codex CLI bridge runtime for self-hosted Parall agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,9 +26,9 @@
26
26
  ],
27
27
  "dependencies": {
28
28
  "smol-toml": "^1.6.1",
29
- "@parall/agent-core": "1.26.3",
30
- "@parall/cli": "1.26.3",
31
- "@parall/sdk": "1.26.3"
29
+ "@parall/agent-core": "1.27.0",
30
+ "@parall/sdk": "1.27.0",
31
+ "@parall/cli": "1.27.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^22.0.0",
package/src/dispatch.ts CHANGED
@@ -36,6 +36,7 @@ type CodexAppServerAdapterOptions = Pick<
36
36
  sessionManager: CodexSessionManager;
37
37
  log?: GatewayLogger;
38
38
  contextFilePath?: string;
39
+ useParallProvider?: boolean;
39
40
  };
40
41
 
41
42
  type TurnEventEnvelope =
@@ -200,6 +201,11 @@ export class CodexAppServerAdapter implements DispatchAdapter {
200
201
  // resume normally instead of permanently losing conversation context.
201
202
  // Fork sessions don't retry: agent-core spawns a fresh fork next trigger.
202
203
  if (!isMainSession) {
204
+ yield {
205
+ type: "runtime_session",
206
+ runtimeSessionId: threadId,
207
+ runtimeLaneKey: sessionKey,
208
+ };
203
209
  yield { type: "error", message: `Codex turn/start failed: ${message}` };
204
210
  return;
205
211
  }
@@ -209,6 +215,11 @@ export class CodexAppServerAdapter implements DispatchAdapter {
209
215
  try {
210
216
  freshThreadId = await this.openThread(client, { resumeId: undefined });
211
217
  } catch (createErr) {
218
+ yield {
219
+ type: "runtime_session",
220
+ runtimeSessionId: threadId,
221
+ runtimeLaneKey: sessionKey,
222
+ };
212
223
  yield { type: "error", message: `Codex turn/start failed; could not create replacement thread: ${errToString(createErr)}` };
213
224
  return;
214
225
  }
@@ -220,6 +231,11 @@ export class CodexAppServerAdapter implements DispatchAdapter {
220
231
  // issue. Don't clobber the persisted threadId; next dispatch will try
221
232
  // resume again.
222
233
  this.activeTurns.delete(freshThreadId);
234
+ yield {
235
+ type: "runtime_session",
236
+ runtimeSessionId: threadId,
237
+ runtimeLaneKey: sessionKey,
238
+ };
223
239
  yield { type: "error", message: `Codex turn/start failed after retry: ${errToString(retryErr)}` };
224
240
  return;
225
241
  }
@@ -232,6 +248,11 @@ export class CodexAppServerAdapter implements DispatchAdapter {
232
248
  }
233
249
 
234
250
  void turnStartResult; // turnId comes back here but isn't needed — we key by threadId
251
+ yield {
252
+ type: "runtime_session",
253
+ runtimeSessionId: threadId,
254
+ runtimeLaneKey: sessionKey,
255
+ };
235
256
 
236
257
  while (true) {
237
258
  const envelope = await sink.next();
@@ -248,6 +269,10 @@ export class CodexAppServerAdapter implements DispatchAdapter {
248
269
  yield runtimeEvent;
249
270
  continue;
250
271
  }
272
+ if (runtimeEvent.type === "runtime_session") {
273
+ yield runtimeEvent;
274
+ continue;
275
+ }
251
276
  if (runtimeEvent.type === "text") {
252
277
  // Layer 0 symmetric output contract: Codex's plain text is never
253
278
  // projected as a chat message. Outbound messages must come from the
@@ -456,6 +481,9 @@ export class CodexAppServerAdapter implements DispatchAdapter {
456
481
  const commonParams: Record<string, unknown> = {
457
482
  approvalPolicy: normalizeApprovalPolicy(this.opts.approvalPolicy),
458
483
  };
484
+ if (this.opts.useParallProvider) {
485
+ commonParams.modelProvider = "parall";
486
+ }
459
487
  commonParams.sandbox = normalizeSandbox(this.opts.sandbox);
460
488
  if (this.opts.model) commonParams.model = this.opts.model;
461
489
  if (this.opts.reasoningEffort) {
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  } from "./config.js";
14
14
  import { CodexAppServerAdapter } from "./dispatch.js";
15
15
  import { CodexSessionManager } from "./session-manager.js";
16
- import { ensureCodexWorkspace, ensureWorkspaceTrusted } from "./workspace.js";
16
+ import { ensureCodexWorkspace, ensureParallProvider, ensureWorkspaceTrusted, isParallProxyMode } from "./workspace.js";
17
17
 
18
18
  function createLogger(prefix: string) {
19
19
  return {
@@ -48,6 +48,11 @@ async function main() {
48
48
  const me = await getAgentMeWithLegacyFallback(client, config.orgId);
49
49
  const agentUserId = me.id;
50
50
  ensureWorkspaceTrusted(config.codexHome, config.workspaceDir, log);
51
+ const useParallProvider = isParallProxyMode();
52
+ if (useParallProvider) {
53
+ ensureParallProvider(config.codexHome, config.apiUrl, log);
54
+ log.info("parall custom provider configured (Responses API HTTP/SSE mode)");
55
+ }
51
56
  ensureCodexWorkspace(config.workspaceDir, log, {
52
57
  userId: agentUserId,
53
58
  displayName: me.display_name,
@@ -87,6 +92,7 @@ async function main() {
87
92
  sessionManager,
88
93
  log,
89
94
  contextFilePath: mainContextFilePath,
95
+ useParallProvider,
90
96
  });
91
97
 
92
98
  const gateway = new ParallAgentGateway({
package/src/workspace.ts CHANGED
@@ -100,6 +100,85 @@ export function ensureWorkspaceTrusted(
100
100
  }
101
101
  }
102
102
 
103
+ /**
104
+ * Returns true when the agent is using the Parall LLM proxy (llm_source=parall)
105
+ * rather than a BYO custom provider or runtime_auth. Detected by checking
106
+ * whether OPENAI_BASE_URL points to the Parall API.
107
+ */
108
+ export function isParallProxyMode(env: NodeJS.ProcessEnv = process.env): boolean {
109
+ const baseUrl = env.OPENAI_BASE_URL?.trim()?.replace(/\/+$/, "");
110
+ const apiUrl = env.PRLL_API_URL?.trim()?.replace(/\/+$/, "");
111
+ if (!baseUrl || !apiUrl) return false;
112
+ return baseUrl === apiUrl || baseUrl.startsWith(`${apiUrl}/`);
113
+ }
114
+
115
+ /**
116
+ * Ensure a custom `parall` model provider is configured in the global Codex
117
+ * config. Codex CLI's built-in `openai` provider hardcodes
118
+ * `wss://api.openai.com` for the Responses API, ignoring OPENAI_BASE_URL.
119
+ * A custom provider with `supports_websockets = false` forces HTTP/SSE
120
+ * transport that correctly routes through `base_url`.
121
+ *
122
+ * Only written when the agent uses the Parall proxy (llm_source=parall).
123
+ * BYO custom providers (llm_source=custom) use the built-in openai
124
+ * provider with their own real API key, which works with the hardcoded
125
+ * api.openai.com endpoint.
126
+ *
127
+ * Provider-managed: overwritten on every boot (env vars are the SSOT).
128
+ */
129
+ export function ensureParallProvider(
130
+ codexHome: string,
131
+ apiUrl: string,
132
+ log?: { warn: (msg: string) => void },
133
+ ): void {
134
+ const configPath = path.join(codexHome, "config.toml");
135
+ const baseUrl = apiUrl.replace(/\/$/, "") + "/api/llm/v1";
136
+
137
+ try {
138
+ let content = "";
139
+ try {
140
+ content = fs.readFileSync(configPath, "utf8");
141
+ } catch (err) {
142
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
143
+ }
144
+
145
+ const sectionHeader = "[model_providers.parall]";
146
+ const authHeader = "[model_providers.parall.auth]";
147
+ const providerBlock = [
148
+ sectionHeader,
149
+ 'name = "Parall Proxy"',
150
+ `base_url = "${baseUrl}"`,
151
+ 'wire_api = "responses"',
152
+ "supports_websockets = false",
153
+ "requires_openai_auth = false",
154
+ "",
155
+ authHeader,
156
+ 'command = "printenv"',
157
+ 'args = ["OPENAI_API_KEY"]',
158
+ ].join("\n");
159
+
160
+ const headerIdx = content.indexOf(sectionHeader);
161
+ if (headerIdx !== -1) {
162
+ const sectionEnd = findSectionEnd(content, headerIdx + sectionHeader.length + 1);
163
+ // Only consume a contiguous [model_providers.parall.auth] subtable —
164
+ // one that sits within the provider's own section, not after unrelated
165
+ // tables that happen to appear between provider and auth.
166
+ const authIdx = content.indexOf(authHeader, headerIdx);
167
+ const blockEnd = authIdx !== -1 && authIdx < sectionEnd
168
+ ? findSectionEnd(content, authIdx + authHeader.length + 1)
169
+ : sectionEnd;
170
+ content = content.substring(0, headerIdx) + providerBlock + content.substring(blockEnd);
171
+ } else {
172
+ content = content.trimEnd() + "\n\n" + providerBlock + "\n";
173
+ }
174
+
175
+ fs.mkdirSync(codexHome, { recursive: true });
176
+ fs.writeFileSync(configPath, content, "utf8");
177
+ } catch (err) {
178
+ throw new Error(`failed to write Parall provider config: ${String(err)}`);
179
+ }
180
+ }
181
+
103
182
  function findSectionEnd(content: string, fromIndex: number): number {
104
183
  // A TOML section ends at the next `[` that starts a new table header.
105
184
  // Match `[` at the beginning of a line (not inside a value).