@parall/codex-agent 1.26.3 → 1.28.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/dist/dispatch.d.ts +1 -0
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +27 -0
- package/dist/index.js +7 -1
- package/dist/workspace.d.ts +23 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +74 -0
- package/package.json +4 -4
- package/src/dispatch.ts +28 -0
- package/src/index.ts +7 -1
- package/src/workspace.ts +79 -0
package/dist/dispatch.d.ts
CHANGED
|
@@ -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://`.
|
package/dist/dispatch.d.ts.map
CHANGED
|
@@ -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;
|
|
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,
|
package/dist/workspace.d.ts
CHANGED
|
@@ -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;
|
package/dist/workspace.d.ts.map
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.28.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.
|
|
30
|
-
"@parall/cli": "1.
|
|
31
|
-
"@parall/sdk": "1.
|
|
29
|
+
"@parall/agent-core": "1.28.0",
|
|
30
|
+
"@parall/cli": "1.28.0",
|
|
31
|
+
"@parall/sdk": "1.28.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).
|