pi-openai-codex-compat 0.0.2 → 0.0.4
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/CHANGELOG.md +86 -0
- package/README.md +86 -38
- package/extensions/openai-codex-compat/apply-patch-diff-render.ts +6 -2
- package/extensions/openai-codex-compat/apply-patch-engine.ts +89 -5
- package/extensions/openai-codex-compat/apply-patch.ts +4 -5
- package/extensions/openai-codex-compat/codex-cache-diagnostics.ts +97 -0
- package/extensions/openai-codex-compat/codex-cache-key.ts +9 -0
- package/extensions/openai-codex-compat/codex-installation.ts +51 -0
- package/extensions/openai-codex-compat/codex-metadata.ts +139 -0
- package/extensions/openai-codex-compat/codex-protocol.ts +4 -2
- package/extensions/openai-codex-compat/codex-provider.ts +708 -128
- package/extensions/openai-codex-compat/codex-stream.ts +137 -40
- package/extensions/openai-codex-compat/codex-thread-lineage.ts +156 -0
- package/extensions/openai-codex-compat/codex-transport.ts +1795 -199
- package/extensions/openai-codex-compat/compaction-checkpoint.ts +2 -2
- package/extensions/openai-codex-compat/config.ts +15 -2
- package/extensions/openai-codex-compat/image-generation-schema.ts +37 -0
- package/extensions/openai-codex-compat/image-generation.ts +25 -48
- package/extensions/openai-codex-compat/index.ts +13 -0
- package/extensions/openai-codex-compat/namespaced-tools.ts +2 -0
- package/extensions/openai-codex-compat/output-limit-continuation.ts +151 -0
- package/extensions/openai-codex-compat/provider-error.ts +79 -0
- package/extensions/openai-codex-compat/remote-compaction.ts +13 -0
- package/extensions/openai-codex-compat/request-options.ts +2 -2
- package/extensions/openai-codex-compat/responses-lite.ts +147 -0
- package/extensions/openai-codex-compat/responses-replay.ts +0 -7
- package/extensions/openai-codex-compat/settings-pane.ts +11 -0
- package/extensions/openai-codex-compat/web-run.ts +7 -0
- package/package.json +2 -1
|
@@ -17,7 +17,12 @@ import {
|
|
|
17
17
|
type SimpleStreamOptions,
|
|
18
18
|
type Tool,
|
|
19
19
|
type Usage,
|
|
20
|
+
uuidv7,
|
|
20
21
|
} from "@earendil-works/pi-ai";
|
|
22
|
+
import {
|
|
23
|
+
codexCacheDiagnosticContext,
|
|
24
|
+
type CodexCacheDiagnosticContext,
|
|
25
|
+
} from "./codex-cache-diagnostics.ts";
|
|
21
26
|
import {
|
|
22
27
|
checkpointData,
|
|
23
28
|
providerHistory,
|
|
@@ -34,9 +39,26 @@ import {
|
|
|
34
39
|
type JsonRecord,
|
|
35
40
|
type ResponsesItem,
|
|
36
41
|
} from "./codex-protocol.ts";
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import
|
|
42
|
+
import { codexCacheKey } from "./codex-cache-key.ts";
|
|
43
|
+
import { resolveCodexInstallationId } from "./codex-installation.ts";
|
|
44
|
+
import {
|
|
45
|
+
type CodexCompactionMetadata,
|
|
46
|
+
type CodexMetadataIdentity,
|
|
47
|
+
responsesCompactionV2Metadata,
|
|
48
|
+
withCodexRequestMetadata,
|
|
49
|
+
} from "./codex-metadata.ts";
|
|
50
|
+
import { resolveCodexThreadIdentity, type CodexThreadIdentity } from "./codex-thread-lineage.ts";
|
|
51
|
+
import { applyResponsesLite } from "./responses-lite.ts";
|
|
52
|
+
import { processCodexStream, type CodexStreamAttemptState } from "./codex-stream.ts";
|
|
53
|
+
import {
|
|
54
|
+
CodexTransport,
|
|
55
|
+
CodexTurnState,
|
|
56
|
+
validateCodexAuthentication,
|
|
57
|
+
type CodexContinuationHandle,
|
|
58
|
+
type CodexTransportDiagnostic,
|
|
59
|
+
type CodexWebSocketResponseHandle,
|
|
60
|
+
} from "./codex-transport.ts";
|
|
61
|
+
import { DEFAULT_CONFIG, type CodexCompatConfig, type ImageDetail } from "./config.ts";
|
|
40
62
|
import { nativeResponseData, NATIVE_RESPONSE_ENTRY_TYPE } from "./native-history.ts";
|
|
41
63
|
import {
|
|
42
64
|
CODEX_NAMESPACED_TOOL_NAMES,
|
|
@@ -49,10 +71,10 @@ import {
|
|
|
49
71
|
createGrammarToolInputProperties,
|
|
50
72
|
type ResponsesItem as SerializedResponsesItem,
|
|
51
73
|
} from "./vendor/pi-ai/openai-responses-serialization.ts";
|
|
74
|
+
import { formatProviderError } from "./provider-error.ts";
|
|
52
75
|
|
|
53
76
|
const CODEX_PROVIDER = "openai-codex";
|
|
54
77
|
const CODEX_API = "openai-codex-responses";
|
|
55
|
-
const CHECKPOINT_STATUS_ID = "openai-codex-compat-compaction";
|
|
56
78
|
const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
|
|
57
79
|
|
|
58
80
|
type ConfigResolver = (ctx: ExtensionContext) => CodexCompatConfig;
|
|
@@ -78,7 +100,6 @@ type RuntimeScope = {
|
|
|
78
100
|
config: CodexCompatConfig;
|
|
79
101
|
hasUI: boolean;
|
|
80
102
|
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
81
|
-
setStatus(message: string | undefined): void;
|
|
82
103
|
};
|
|
83
104
|
|
|
84
105
|
type RequestTemplate = {
|
|
@@ -88,16 +109,32 @@ type RequestTemplate = {
|
|
|
88
109
|
requestOptions: OpenAICodexResponsesOptions;
|
|
89
110
|
};
|
|
90
111
|
|
|
112
|
+
type ActiveAgentTurn = {
|
|
113
|
+
turnId: string;
|
|
114
|
+
startedAtUnixMs: number;
|
|
115
|
+
turnState: CodexTurnState;
|
|
116
|
+
};
|
|
117
|
+
|
|
91
118
|
type CodexCompat = {
|
|
92
119
|
supportsToolSearch?: boolean;
|
|
93
120
|
supportsStrictMode?: boolean;
|
|
94
121
|
supportsOpenAIGrammarTools?: boolean;
|
|
95
122
|
};
|
|
96
123
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
124
|
+
type CodexTerminalState = {
|
|
125
|
+
type?: "response.completed" | "response.incomplete" | "response.failed";
|
|
126
|
+
response?: JsonRecord;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type CodexResponseRetryPolicy = {
|
|
130
|
+
maxRetries: number;
|
|
131
|
+
baseDelayMs: number;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const DEFAULT_RESPONSE_RETRY_POLICY: CodexResponseRetryPolicy = {
|
|
135
|
+
maxRetries: 5,
|
|
136
|
+
baseDelayMs: 200,
|
|
137
|
+
};
|
|
101
138
|
|
|
102
139
|
function markerSummary(): string {
|
|
103
140
|
return `OpenAI Codex remote compaction checkpoint (${randomUUID()}).`;
|
|
@@ -153,7 +190,7 @@ function nativeOverrideRequired(
|
|
|
153
190
|
function captureRawEvents(
|
|
154
191
|
events: AsyncIterable<JsonRecord>,
|
|
155
192
|
items: ResponsesItem[],
|
|
156
|
-
|
|
193
|
+
terminalState?: CodexTerminalState,
|
|
157
194
|
): AsyncIterable<JsonRecord> {
|
|
158
195
|
return {
|
|
159
196
|
async *[Symbol.asyncIterator]() {
|
|
@@ -162,7 +199,9 @@ function captureRawEvents(
|
|
|
162
199
|
items.push(structuredClone(event.item));
|
|
163
200
|
}
|
|
164
201
|
if (
|
|
165
|
-
(event.type === "response.completed" ||
|
|
202
|
+
(event.type === "response.completed" ||
|
|
203
|
+
event.type === "response.incomplete" ||
|
|
204
|
+
event.type === "response.failed") &&
|
|
166
205
|
isObject(event.response) &&
|
|
167
206
|
Array.isArray(event.response["output"])
|
|
168
207
|
) {
|
|
@@ -172,11 +211,35 @@ function captureRawEvents(
|
|
|
172
211
|
}
|
|
173
212
|
}
|
|
174
213
|
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
214
|
+
terminalState &&
|
|
215
|
+
(event.type === "response.completed" ||
|
|
216
|
+
event.type === "response.incomplete" ||
|
|
217
|
+
event.type === "response.failed")
|
|
178
218
|
) {
|
|
179
|
-
|
|
219
|
+
terminalState.type = event.type;
|
|
220
|
+
if (isObject(event.response)) {
|
|
221
|
+
terminalState.response = structuredClone(event.response);
|
|
222
|
+
} else {
|
|
223
|
+
delete terminalState.response;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
yield event;
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function startOnFirstEvent(
|
|
233
|
+
events: AsyncIterable<JsonRecord>,
|
|
234
|
+
onStart: () => void,
|
|
235
|
+
): AsyncIterable<JsonRecord> {
|
|
236
|
+
return {
|
|
237
|
+
async *[Symbol.asyncIterator]() {
|
|
238
|
+
let started = false;
|
|
239
|
+
for await (const event of events) {
|
|
240
|
+
if (!started) {
|
|
241
|
+
started = true;
|
|
242
|
+
onStart();
|
|
180
243
|
}
|
|
181
244
|
yield event;
|
|
182
245
|
}
|
|
@@ -184,6 +247,99 @@ function captureRawEvents(
|
|
|
184
247
|
};
|
|
185
248
|
}
|
|
186
249
|
|
|
250
|
+
function clearStreamingScratchState(message: AssistantMessage): void {
|
|
251
|
+
for (const block of message.content) {
|
|
252
|
+
delete (block as { partialJson?: string }).partialJson;
|
|
253
|
+
delete (block as { customInput?: unknown }).customInput;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function emptyUsage(): Usage {
|
|
258
|
+
return {
|
|
259
|
+
input: 0,
|
|
260
|
+
output: 0,
|
|
261
|
+
cacheRead: 0,
|
|
262
|
+
cacheWrite: 0,
|
|
263
|
+
totalTokens: 0,
|
|
264
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function discardIncompleteAttemptContent(
|
|
269
|
+
message: AssistantMessage,
|
|
270
|
+
attempt: CodexStreamAttemptState,
|
|
271
|
+
): void {
|
|
272
|
+
const incomplete = [...attempt.startedContentIndexes]
|
|
273
|
+
.filter((index) => !attempt.completedContentIndexes.has(index))
|
|
274
|
+
.sort((left, right) => right - left);
|
|
275
|
+
for (const index of incomplete) message.content.splice(index, 1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function accumulateUsage(previous: Usage, current: Usage): Usage {
|
|
279
|
+
const previousReasoning = previous.reasoning;
|
|
280
|
+
const currentReasoning = current.reasoning;
|
|
281
|
+
return {
|
|
282
|
+
input: previous.input + current.input,
|
|
283
|
+
output: previous.output + current.output,
|
|
284
|
+
cacheRead: previous.cacheRead + current.cacheRead,
|
|
285
|
+
cacheWrite: previous.cacheWrite + current.cacheWrite,
|
|
286
|
+
...(previousReasoning === undefined && currentReasoning === undefined
|
|
287
|
+
? {}
|
|
288
|
+
: { reasoning: (previousReasoning ?? 0) + (currentReasoning ?? 0) }),
|
|
289
|
+
totalTokens: previous.totalTokens + current.totalTokens,
|
|
290
|
+
cost: {
|
|
291
|
+
input: previous.cost.input + current.cost.input,
|
|
292
|
+
output: previous.cost.output + current.cost.output,
|
|
293
|
+
cacheRead: previous.cost.cacheRead + current.cost.cacheRead,
|
|
294
|
+
cacheWrite: previous.cost.cacheWrite + current.cost.cacheWrite,
|
|
295
|
+
total: previous.cost.total + current.cost.total,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function retryableResponseFailure(response: JsonRecord | undefined): boolean {
|
|
301
|
+
const error = isObject(response?.["error"]) ? response["error"] : undefined;
|
|
302
|
+
const code = typeof error?.["code"] === "string" ? error["code"].toLowerCase() : "";
|
|
303
|
+
return !(
|
|
304
|
+
code === "context_length_exceeded" ||
|
|
305
|
+
code === "insufficient_quota" ||
|
|
306
|
+
code === "usage_not_included" ||
|
|
307
|
+
code === "cyber_policy" ||
|
|
308
|
+
code === "invalid_prompt" ||
|
|
309
|
+
code === "bio_policy"
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function responseRetryDelayMs(baseDelayMs: number, attempt: number): number {
|
|
314
|
+
if (baseDelayMs <= 0) return 0;
|
|
315
|
+
const exponential = baseDelayMs * 2 ** Math.max(0, attempt - 1);
|
|
316
|
+
return Math.floor(exponential * (0.9 + Math.random() * 0.2));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function waitForResponseRetry(milliseconds: number, signal?: AbortSignal): Promise<void> {
|
|
320
|
+
if (signal?.aborted) return Promise.reject(new Error("Request was aborted"));
|
|
321
|
+
if (milliseconds <= 0) return Promise.resolve();
|
|
322
|
+
return new Promise((resolve, reject) => {
|
|
323
|
+
const onAbort = () => {
|
|
324
|
+
clearTimeout(timer);
|
|
325
|
+
reject(new Error("Request was aborted"));
|
|
326
|
+
};
|
|
327
|
+
const timer = setTimeout(() => {
|
|
328
|
+
signal?.removeEventListener("abort", onAbort);
|
|
329
|
+
resolve();
|
|
330
|
+
}, milliseconds);
|
|
331
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function continueResponseBody(
|
|
336
|
+
body: JsonRecord,
|
|
337
|
+
responseItems: readonly ResponsesItem[],
|
|
338
|
+
): JsonRecord | undefined {
|
|
339
|
+
if (!Array.isArray(body.input) || !body.input.every(isResponsesItem)) return undefined;
|
|
340
|
+
return updateInput(body, [...body.input, ...responseItems]);
|
|
341
|
+
}
|
|
342
|
+
|
|
187
343
|
function updateInput(payload: JsonRecord, input: readonly ResponsesItem[]): JsonRecord {
|
|
188
344
|
const result: JsonRecord = {
|
|
189
345
|
...payload,
|
|
@@ -194,14 +350,15 @@ function updateInput(payload: JsonRecord, input: readonly ResponsesItem[]): Json
|
|
|
194
350
|
return result;
|
|
195
351
|
}
|
|
196
352
|
|
|
197
|
-
function
|
|
353
|
+
function assertSuccessfulOutput(
|
|
198
354
|
message: AssistantMessage,
|
|
199
|
-
): message is AssistantMessage & { stopReason: "stop" | "length" | "toolUse" } {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
355
|
+
): asserts message is AssistantMessage & { stopReason: "stop" | "length" | "toolUse" } {
|
|
356
|
+
if (message.stopReason === "pending") {
|
|
357
|
+
throw new Error("Codex stream ended without a stop reason");
|
|
358
|
+
}
|
|
359
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
360
|
+
throw new Error(message.errorMessage || "An unknown error occurred");
|
|
361
|
+
}
|
|
205
362
|
}
|
|
206
363
|
|
|
207
364
|
function applyServiceTierPricing(
|
|
@@ -286,13 +443,38 @@ export class CodexProviderRuntime {
|
|
|
286
443
|
readonly transport = new CodexTransport();
|
|
287
444
|
private readonly scopes = new Map<string, RuntimeScope>();
|
|
288
445
|
private readonly templates = new Map<string, RequestTemplate>();
|
|
446
|
+
private readonly prewarmedTemplates = new Set<string>();
|
|
289
447
|
private readonly requestTails = new Map<string, Promise<void>>();
|
|
448
|
+
private readonly activeAgentTurns = new Map<string, ActiveAgentTurn>();
|
|
449
|
+
private readonly windowNumbers = new Map<string, number>();
|
|
450
|
+
private readonly activeThreadIds = new Map<string, string>();
|
|
290
451
|
private readonly pi: ExtensionAPI;
|
|
291
452
|
private readonly resolveConfig: ConfigResolver;
|
|
453
|
+
private readonly installationId: string;
|
|
454
|
+
private readonly responseRetryPolicy: CodexResponseRetryPolicy;
|
|
292
455
|
|
|
293
|
-
constructor(
|
|
456
|
+
constructor(
|
|
457
|
+
pi: ExtensionAPI,
|
|
458
|
+
resolveConfig: ConfigResolver,
|
|
459
|
+
installationId: string = randomUUID(),
|
|
460
|
+
responseRetryPolicy: Partial<CodexResponseRetryPolicy> = {},
|
|
461
|
+
) {
|
|
294
462
|
this.pi = pi;
|
|
295
463
|
this.resolveConfig = resolveConfig;
|
|
464
|
+
this.installationId = installationId;
|
|
465
|
+
const maxRetries = responseRetryPolicy.maxRetries ?? DEFAULT_RESPONSE_RETRY_POLICY.maxRetries;
|
|
466
|
+
const baseDelayMs =
|
|
467
|
+
responseRetryPolicy.baseDelayMs ?? DEFAULT_RESPONSE_RETRY_POLICY.baseDelayMs;
|
|
468
|
+
if (!Number.isFinite(maxRetries) || maxRetries < 0) {
|
|
469
|
+
throw new Error(`Invalid Codex response maxRetries: ${String(maxRetries)}`);
|
|
470
|
+
}
|
|
471
|
+
if (!Number.isFinite(baseDelayMs) || baseDelayMs < 0) {
|
|
472
|
+
throw new Error(`Invalid Codex response baseDelayMs: ${String(baseDelayMs)}`);
|
|
473
|
+
}
|
|
474
|
+
this.responseRetryPolicy = {
|
|
475
|
+
maxRetries: Math.floor(maxRetries),
|
|
476
|
+
baseDelayMs: Math.floor(baseDelayMs),
|
|
477
|
+
};
|
|
296
478
|
}
|
|
297
479
|
|
|
298
480
|
captureScope(ctx: ExtensionContext): void {
|
|
@@ -308,18 +490,161 @@ export class CodexProviderRuntime {
|
|
|
308
490
|
config: this.resolveConfig(ctx),
|
|
309
491
|
hasUI: ctx.hasUI,
|
|
310
492
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
311
|
-
setStatus: (message) => ctx.ui.setStatus(CHECKPOINT_STATUS_ID, message),
|
|
312
493
|
});
|
|
313
494
|
}
|
|
314
495
|
|
|
496
|
+
beginAgentTurn(ctx: ExtensionContext): void {
|
|
497
|
+
this.activeAgentTurns.set(ctx.sessionManager.getSessionId(), {
|
|
498
|
+
turnId: uuidv7(),
|
|
499
|
+
startedAtUnixMs: Date.now(),
|
|
500
|
+
turnState: new CodexTurnState(),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
endAgentTurn(ctx: ExtensionContext): void {
|
|
505
|
+
this.activeAgentTurns.delete(ctx.sessionManager.getSessionId());
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
updateSessionConfig(sessionId: string, config: CodexCompatConfig): void {
|
|
509
|
+
const scope = this.scopes.get(sessionId);
|
|
510
|
+
if (scope) scope.config = config;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private responsesLiteEnabled(sessionId: string | undefined): boolean {
|
|
514
|
+
return (
|
|
515
|
+
(sessionId ? this.scopes.get(sessionId)?.config.responsesLite : undefined) ??
|
|
516
|
+
DEFAULT_CONFIG.responsesLite
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private agentTurn(sessionId: string | undefined): ActiveAgentTurn {
|
|
521
|
+
return (
|
|
522
|
+
(sessionId ? this.activeAgentTurns.get(sessionId) : undefined) ?? {
|
|
523
|
+
turnId: uuidv7(),
|
|
524
|
+
startedAtUnixMs: Date.now(),
|
|
525
|
+
turnState: new CodexTurnState(),
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private metadataIdentity(
|
|
531
|
+
metadataSessionId: string | undefined,
|
|
532
|
+
turn?: ActiveAgentTurn,
|
|
533
|
+
runtimeSessionId = metadataSessionId,
|
|
534
|
+
): CodexMetadataIdentity {
|
|
535
|
+
const thread: Partial<CodexThreadIdentity> = runtimeSessionId
|
|
536
|
+
? this.threadIdentity(runtimeSessionId)
|
|
537
|
+
: metadataSessionId
|
|
538
|
+
? { threadId: metadataSessionId }
|
|
539
|
+
: {};
|
|
540
|
+
const windowKey =
|
|
541
|
+
runtimeSessionId && thread.threadId ? `${runtimeSessionId}\0${thread.threadId}` : undefined;
|
|
542
|
+
return {
|
|
543
|
+
installationId: this.installationId,
|
|
544
|
+
...(thread.threadId ? { threadId: thread.threadId } : {}),
|
|
545
|
+
...(thread.forkedFromThreadId ? { forkedFromThreadId: thread.forkedFromThreadId } : {}),
|
|
546
|
+
windowNumber: windowKey ? (this.windowNumbers.get(windowKey) ?? 0) : 0,
|
|
547
|
+
...(turn ? { turnStartedAtUnixMs: turn.startedAtUnixMs } : {}),
|
|
548
|
+
threadSource: "user",
|
|
549
|
+
// Pi extensions execute without Codex's platform sandbox.
|
|
550
|
+
sandbox: "none",
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private threadIdentity(sessionId: string): CodexThreadIdentity {
|
|
555
|
+
const branch = this.scopes.get(sessionId)?.manager.getBranch() as SessionEntry[] | undefined;
|
|
556
|
+
return resolveCodexThreadIdentity(sessionId, branch ?? []);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private clearPrewarmState(sessionId: string): void {
|
|
560
|
+
for (const key of this.prewarmedTemplates) {
|
|
561
|
+
if (key.startsWith(`${sessionId}\0`)) this.prewarmedTemplates.delete(key);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private activateThread(sessionId: string | undefined): void {
|
|
566
|
+
if (!sessionId) return;
|
|
567
|
+
const threadId = this.threadIdentity(sessionId).threadId;
|
|
568
|
+
const previous = this.activeThreadIds.get(sessionId);
|
|
569
|
+
if (previous && previous !== threadId) {
|
|
570
|
+
this.transport.close(sessionId);
|
|
571
|
+
this.clearPrewarmState(sessionId);
|
|
572
|
+
}
|
|
573
|
+
this.activeThreadIds.set(sessionId, threadId);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private advanceWindow(sessionId: string): void {
|
|
577
|
+
const threadId = this.threadIdentity(sessionId).threadId;
|
|
578
|
+
const key = `${sessionId}\0${threadId}`;
|
|
579
|
+
this.windowNumbers.set(key, (this.windowNumbers.get(key) ?? 0) + 1);
|
|
580
|
+
}
|
|
581
|
+
|
|
315
582
|
clearSession(sessionId: string): void {
|
|
316
583
|
this.scopes.delete(sessionId);
|
|
317
584
|
this.templates.delete(sessionId);
|
|
585
|
+
this.clearPrewarmState(sessionId);
|
|
318
586
|
this.requestTails.delete(sessionId);
|
|
587
|
+
this.activeAgentTurns.delete(sessionId);
|
|
588
|
+
for (const key of this.windowNumbers.keys()) {
|
|
589
|
+
if (key.startsWith(`${sessionId}\0`)) this.windowNumbers.delete(key);
|
|
590
|
+
}
|
|
591
|
+
this.activeThreadIds.delete(sessionId);
|
|
319
592
|
this.transport.close(sessionId);
|
|
320
593
|
}
|
|
321
594
|
|
|
322
|
-
private async
|
|
595
|
+
private async maybePrewarm(options: {
|
|
596
|
+
model: Model<any>;
|
|
597
|
+
body: JsonRecord;
|
|
598
|
+
fullBody: JsonRecord;
|
|
599
|
+
requestOptions: OpenAICodexResponsesOptions;
|
|
600
|
+
accountId: string;
|
|
601
|
+
diagnostics: CodexTransportDiagnostic[];
|
|
602
|
+
turnState: CodexTurnState;
|
|
603
|
+
cacheDiagnostics: CodexCacheDiagnosticContext;
|
|
604
|
+
}): Promise<void> {
|
|
605
|
+
const sessionId = options.requestOptions.sessionId;
|
|
606
|
+
if (
|
|
607
|
+
!sessionId ||
|
|
608
|
+
options.requestOptions.cacheRetention === "none" ||
|
|
609
|
+
options.requestOptions.transport === "sse" ||
|
|
610
|
+
!Array.isArray(options.body.input) ||
|
|
611
|
+
!Array.isArray(options.fullBody.input)
|
|
612
|
+
) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const key = `${sessionId}\0${options.model.id}\0${options.cacheDiagnostics.envelope}`;
|
|
616
|
+
if (this.prewarmedTemplates.has(key)) return;
|
|
617
|
+
this.prewarmedTemplates.add(key);
|
|
618
|
+
|
|
619
|
+
const cacheSessionId = codexCacheKey(sessionId);
|
|
620
|
+
const prewarmBody = withCodexRequestMetadata(
|
|
621
|
+
options.body,
|
|
622
|
+
cacheSessionId,
|
|
623
|
+
{ kind: "prewarm" },
|
|
624
|
+
"",
|
|
625
|
+
this.metadataIdentity(cacheSessionId, undefined, sessionId),
|
|
626
|
+
);
|
|
627
|
+
try {
|
|
628
|
+
await this.transport.prewarm(options.model, prewarmBody, {
|
|
629
|
+
...options.requestOptions,
|
|
630
|
+
accountId: options.accountId,
|
|
631
|
+
turnState: options.turnState,
|
|
632
|
+
cacheDiagnostics: options.cacheDiagnostics,
|
|
633
|
+
requestKind: "prewarm",
|
|
634
|
+
onTransportDiagnostic(diagnostic) {
|
|
635
|
+
options.diagnostics.push(diagnostic);
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
} catch {
|
|
639
|
+
// Warmup is best-effort. The transport has already activated sticky SSE
|
|
640
|
+
// after exhausting its WebSocket retry budget.
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private async acquireRequest(
|
|
645
|
+
sessionId: string | undefined,
|
|
646
|
+
signal?: AbortSignal,
|
|
647
|
+
): Promise<() => void> {
|
|
323
648
|
if (!sessionId) return () => {};
|
|
324
649
|
const previous = this.requestTails.get(sessionId) ?? Promise.resolve();
|
|
325
650
|
let releaseCurrent!: () => void;
|
|
@@ -327,11 +652,44 @@ export class CodexProviderRuntime {
|
|
|
327
652
|
releaseCurrent = resolve;
|
|
328
653
|
});
|
|
329
654
|
this.requestTails.set(sessionId, current);
|
|
330
|
-
|
|
331
|
-
|
|
655
|
+
let released = false;
|
|
656
|
+
const release = () => {
|
|
657
|
+
if (released) return;
|
|
658
|
+
released = true;
|
|
332
659
|
releaseCurrent();
|
|
333
660
|
if (this.requestTails.get(sessionId) === current) this.requestTails.delete(sessionId);
|
|
334
661
|
};
|
|
662
|
+
|
|
663
|
+
if (signal?.aborted) {
|
|
664
|
+
void previous.then(release);
|
|
665
|
+
throw new Error("Request was aborted");
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
let onAbort: (() => void) | undefined;
|
|
669
|
+
try {
|
|
670
|
+
await Promise.race([
|
|
671
|
+
previous,
|
|
672
|
+
...(signal
|
|
673
|
+
? [
|
|
674
|
+
new Promise<never>((_resolve, reject) => {
|
|
675
|
+
onAbort = () => reject(new Error("Request was aborted"));
|
|
676
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
677
|
+
}),
|
|
678
|
+
]
|
|
679
|
+
: []),
|
|
680
|
+
]);
|
|
681
|
+
} catch (error) {
|
|
682
|
+
void previous.then(release);
|
|
683
|
+
throw error;
|
|
684
|
+
} finally {
|
|
685
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (signal?.aborted) {
|
|
689
|
+
release();
|
|
690
|
+
throw new Error("Request was aborted");
|
|
691
|
+
}
|
|
692
|
+
return release;
|
|
335
693
|
}
|
|
336
694
|
|
|
337
695
|
createProvider(base: Provider): Provider {
|
|
@@ -358,7 +716,7 @@ export class CodexProviderRuntime {
|
|
|
358
716
|
grammarToolInputProperties,
|
|
359
717
|
deferredTools: splitDeferredTools(context, Boolean(compat?.supportsToolSearch)).deferred,
|
|
360
718
|
toolOptions: {
|
|
361
|
-
strict:
|
|
719
|
+
strict: false,
|
|
362
720
|
supportsStrictMode: compat?.supportsStrictMode ?? true,
|
|
363
721
|
supportsOpenAIGrammarTools: compat?.supportsOpenAIGrammarTools ?? false,
|
|
364
722
|
},
|
|
@@ -381,32 +739,45 @@ export class CodexProviderRuntime {
|
|
|
381
739
|
model: Model<any>,
|
|
382
740
|
context: Context,
|
|
383
741
|
options: OpenAICodexResponsesOptions,
|
|
384
|
-
|
|
742
|
+
runtimeSessionId: string | undefined,
|
|
743
|
+
cacheSessionId: string | undefined,
|
|
385
744
|
grammarToolInputProperties: GrammarToolInputProperties,
|
|
745
|
+
turnId: string,
|
|
386
746
|
): JsonRecord {
|
|
387
747
|
const compat = model.compat as CodexCompat | undefined;
|
|
388
748
|
const toolPlacement = splitDeferredTools(context, Boolean(compat?.supportsToolSearch));
|
|
389
|
-
|
|
749
|
+
let body: JsonRecord = {
|
|
390
750
|
model: model.id,
|
|
391
751
|
store: false,
|
|
392
752
|
stream: true,
|
|
393
753
|
instructions: context.systemPrompt || "You are a helpful assistant.",
|
|
394
754
|
input: this.wireHistory(
|
|
395
755
|
model,
|
|
396
|
-
Object.assign({}, context, { sessionId }),
|
|
756
|
+
Object.assign({}, context, { sessionId: runtimeSessionId }),
|
|
397
757
|
grammarToolInputProperties,
|
|
398
758
|
),
|
|
399
759
|
text: { verbosity: options.textVerbosity ?? "low" },
|
|
400
760
|
include: ["reasoning.encrypted_content"],
|
|
401
|
-
prompt_cache_key:
|
|
761
|
+
prompt_cache_key: cacheSessionId,
|
|
402
762
|
tool_choice: options.toolChoice ?? "auto",
|
|
403
763
|
parallel_tool_calls: true,
|
|
764
|
+
tools: [],
|
|
404
765
|
};
|
|
405
|
-
|
|
766
|
+
body = withCodexRequestMetadata(
|
|
767
|
+
body,
|
|
768
|
+
cacheSessionId,
|
|
769
|
+
{ kind: "turn" },
|
|
770
|
+
turnId,
|
|
771
|
+
this.metadataIdentity(
|
|
772
|
+
cacheSessionId,
|
|
773
|
+
this.activeAgentTurns.get(runtimeSessionId ?? ""),
|
|
774
|
+
runtimeSessionId,
|
|
775
|
+
),
|
|
776
|
+
);
|
|
406
777
|
if (options.serviceTier !== undefined) body.service_tier = options.serviceTier;
|
|
407
778
|
if (toolPlacement.immediate.length > 0) {
|
|
408
779
|
body.tools = convertResponsesTools(toolPlacement.immediate, {
|
|
409
|
-
strict:
|
|
780
|
+
strict: false,
|
|
410
781
|
supportsStrictMode: compat?.supportsStrictMode ?? true,
|
|
411
782
|
supportsOpenAIGrammarTools: compat?.supportsOpenAIGrammarTools ?? false,
|
|
412
783
|
namespacedToolNames: CODEX_NAMESPACED_TOOL_NAMES,
|
|
@@ -436,24 +807,74 @@ export class CodexProviderRuntime {
|
|
|
436
807
|
instructions: string;
|
|
437
808
|
grammarToolInputProperties: GrammarToolInputProperties;
|
|
438
809
|
priority: boolean;
|
|
810
|
+
compactionMetadata: CodexCompactionMetadata;
|
|
811
|
+
agentTurn?: ActiveAgentTurn;
|
|
812
|
+
responsesLiteEnabled?: boolean;
|
|
439
813
|
}): Promise<{ checkpoint: CheckpointData; usage?: Usage }> {
|
|
440
814
|
const sessionId = options.requestOptions.sessionId;
|
|
441
815
|
if (!sessionId) throw new Error("Codex compaction requires a Pi session id.");
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
816
|
+
this.activateThread(sessionId);
|
|
817
|
+
const agentTurn = options.agentTurn ?? this.agentTurn(sessionId);
|
|
818
|
+
const responsesLiteEnabled =
|
|
819
|
+
options.responsesLiteEnabled ?? this.responsesLiteEnabled(sessionId);
|
|
820
|
+
const accountId = validateCodexAuthentication(options.model, options.requestOptions.apiKey);
|
|
821
|
+
const payload = withCodexRequestMetadata(
|
|
822
|
+
remoteCompactionPayload({
|
|
823
|
+
template: options.template,
|
|
824
|
+
modelId: options.model.id,
|
|
825
|
+
history: options.history,
|
|
826
|
+
instructions: options.instructions,
|
|
827
|
+
sessionId:
|
|
828
|
+
options.requestOptions.cacheRetention === "none" ? undefined : codexCacheKey(sessionId),
|
|
829
|
+
priority: options.priority,
|
|
830
|
+
}),
|
|
831
|
+
options.requestOptions.cacheRetention === "none" ? undefined : codexCacheKey(sessionId),
|
|
832
|
+
{ kind: "compaction", compaction: options.compactionMetadata },
|
|
833
|
+
agentTurn.turnId,
|
|
834
|
+
this.metadataIdentity(
|
|
835
|
+
options.requestOptions.cacheRetention === "none" ? undefined : codexCacheKey(sessionId),
|
|
836
|
+
agentTurn,
|
|
837
|
+
sessionId,
|
|
838
|
+
),
|
|
839
|
+
);
|
|
450
840
|
const transformed = await options.requestOptions.onPayload?.(payload, options.model);
|
|
451
|
-
const
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
options.
|
|
841
|
+
const ordinaryRequest = transformed === undefined ? payload : (transformed as JsonRecord);
|
|
842
|
+
const request = applyResponsesLite(ordinaryRequest, options.model.id, responsesLiteEnabled);
|
|
843
|
+
const staticRequest = applyResponsesLite(
|
|
844
|
+
updateInput(ordinaryRequest, []),
|
|
845
|
+
options.model.id,
|
|
846
|
+
responsesLiteEnabled,
|
|
847
|
+
);
|
|
848
|
+
const cacheDiagnostics = codexCacheDiagnosticContext(
|
|
849
|
+
ordinaryRequest,
|
|
850
|
+
request,
|
|
851
|
+
staticRequest,
|
|
852
|
+
options.model.id,
|
|
853
|
+
responsesLiteEnabled,
|
|
456
854
|
);
|
|
855
|
+
let webSocketResponseHandle: CodexWebSocketResponseHandle | undefined;
|
|
856
|
+
let compacted: Awaited<ReturnType<typeof collectRemoteCompaction>>;
|
|
857
|
+
try {
|
|
858
|
+
compacted = await collectRemoteCompaction(
|
|
859
|
+
this.transport.request(options.model, request, {
|
|
860
|
+
...options.requestOptions,
|
|
861
|
+
accountId,
|
|
862
|
+
requestKind: "compaction",
|
|
863
|
+
turnState: agentTurn.turnState,
|
|
864
|
+
cacheDiagnostics,
|
|
865
|
+
onWebSocketResponseHandle(handle) {
|
|
866
|
+
webSocketResponseHandle = handle;
|
|
867
|
+
},
|
|
868
|
+
}),
|
|
869
|
+
options.model,
|
|
870
|
+
options.priority,
|
|
871
|
+
);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
if (!options.requestOptions.signal?.aborted) {
|
|
874
|
+
webSocketResponseHandle?.failParsing(error);
|
|
875
|
+
}
|
|
876
|
+
throw error;
|
|
877
|
+
}
|
|
457
878
|
return {
|
|
458
879
|
checkpoint: checkpointData(
|
|
459
880
|
options.model.id,
|
|
@@ -471,12 +892,16 @@ export class CodexProviderRuntime {
|
|
|
471
892
|
options: OpenAICodexResponsesOptions,
|
|
472
893
|
body: JsonRecord,
|
|
473
894
|
grammarToolInputProperties: GrammarToolInputProperties,
|
|
895
|
+
agentTurn: ActiveAgentTurn,
|
|
896
|
+
responsesLiteEnabled: boolean,
|
|
474
897
|
): Promise<JsonRecord> {
|
|
475
898
|
const sessionId = options.sessionId;
|
|
476
899
|
const scope = sessionId ? this.scopes.get(sessionId) : undefined;
|
|
477
900
|
const threshold = scope?.config.autoCompactAtPercent;
|
|
901
|
+
if (!sessionId || !scope) {
|
|
902
|
+
return body;
|
|
903
|
+
}
|
|
478
904
|
if (
|
|
479
|
-
!scope ||
|
|
480
905
|
threshold === undefined ||
|
|
481
906
|
scope.contextPercent === null ||
|
|
482
907
|
scope.contextPercent < threshold ||
|
|
@@ -515,44 +940,50 @@ export class CodexProviderRuntime {
|
|
|
515
940
|
return body;
|
|
516
941
|
}
|
|
517
942
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
scope.manager.appendCompaction(
|
|
541
|
-
markerSummary(),
|
|
542
|
-
firstKeptEntryId,
|
|
543
|
-
scope.contextTokens ?? 0,
|
|
544
|
-
compacted.checkpoint,
|
|
545
|
-
true,
|
|
546
|
-
compacted.usage,
|
|
547
|
-
);
|
|
548
|
-
scope.notify(
|
|
549
|
-
`OpenAI Codex context compacted at ${scope.contextPercent.toFixed(1)}% and will continue.`,
|
|
550
|
-
"info",
|
|
551
|
-
);
|
|
552
|
-
return updateInput(body, compacted.checkpoint.history);
|
|
553
|
-
} finally {
|
|
554
|
-
scope.setStatus(undefined);
|
|
943
|
+
const compacted = await this.performCompaction({
|
|
944
|
+
model,
|
|
945
|
+
requestOptions: options,
|
|
946
|
+
history: split.history,
|
|
947
|
+
postCompactionTail: split.tail,
|
|
948
|
+
template: withoutConversationInput(body),
|
|
949
|
+
instructions:
|
|
950
|
+
typeof body.instructions === "string"
|
|
951
|
+
? body.instructions
|
|
952
|
+
: context.systemPrompt || "You are a helpful assistant.",
|
|
953
|
+
grammarToolInputProperties,
|
|
954
|
+
priority: scope.config.fastMode,
|
|
955
|
+
compactionMetadata: responsesCompactionV2Metadata("auto", "context_limit", "pre_turn"),
|
|
956
|
+
agentTurn,
|
|
957
|
+
responsesLiteEnabled,
|
|
958
|
+
});
|
|
959
|
+
const firstKeptEntryId = userEntryAfterLastSampled(branch)?.id ?? scope.manager.getLeafId();
|
|
960
|
+
if (!firstKeptEntryId || typeof scope.manager.appendCompaction !== "function") {
|
|
961
|
+
throw new Error("Pi's mutable SessionManager is unavailable for percentage compaction.");
|
|
962
|
+
}
|
|
963
|
+
if (scope.manager.getLeafId() !== scope.leafId) {
|
|
964
|
+
throw new Error("Pi's active session branch changed while Codex was compacting.");
|
|
555
965
|
}
|
|
966
|
+
scope.manager.appendCompaction(
|
|
967
|
+
markerSummary(),
|
|
968
|
+
firstKeptEntryId,
|
|
969
|
+
scope.contextTokens ?? 0,
|
|
970
|
+
compacted.checkpoint,
|
|
971
|
+
true,
|
|
972
|
+
compacted.usage,
|
|
973
|
+
);
|
|
974
|
+
scope.notify(
|
|
975
|
+
`OpenAI Codex context compacted at ${scope.contextPercent.toFixed(1)}% and will continue.`,
|
|
976
|
+
"info",
|
|
977
|
+
);
|
|
978
|
+
this.advanceWindow(sessionId);
|
|
979
|
+
const cacheSessionId = options.cacheRetention === "none" ? undefined : codexCacheKey(sessionId);
|
|
980
|
+
return withCodexRequestMetadata(
|
|
981
|
+
updateInput(body, compacted.checkpoint.history),
|
|
982
|
+
cacheSessionId,
|
|
983
|
+
{ kind: "turn" },
|
|
984
|
+
agentTurn.turnId,
|
|
985
|
+
this.metadataIdentity(cacheSessionId, agentTurn, sessionId),
|
|
986
|
+
);
|
|
556
987
|
}
|
|
557
988
|
|
|
558
989
|
stream(
|
|
@@ -569,21 +1000,20 @@ export class CodexProviderRuntime {
|
|
|
569
1000
|
api: CODEX_API,
|
|
570
1001
|
provider: model.provider,
|
|
571
1002
|
model: model.id,
|
|
572
|
-
usage:
|
|
573
|
-
input: 0,
|
|
574
|
-
output: 0,
|
|
575
|
-
cacheRead: 0,
|
|
576
|
-
cacheWrite: 0,
|
|
577
|
-
totalTokens: 0,
|
|
578
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
579
|
-
},
|
|
1003
|
+
usage: emptyUsage(),
|
|
580
1004
|
stopReason: "pending",
|
|
581
1005
|
timestamp: Date.now(),
|
|
582
1006
|
};
|
|
583
1007
|
const runtimeSessionId = requestOptions.sessionId;
|
|
584
|
-
|
|
1008
|
+
let releaseRequest = () => {};
|
|
585
1009
|
try {
|
|
586
|
-
const
|
|
1010
|
+
const accountId = validateCodexAuthentication(model, requestOptions.apiKey);
|
|
1011
|
+
releaseRequest = await this.acquireRequest(runtimeSessionId, requestOptions.signal);
|
|
1012
|
+
this.activateThread(runtimeSessionId);
|
|
1013
|
+
const cacheSessionId =
|
|
1014
|
+
requestOptions.cacheRetention === "none" ? undefined : codexCacheKey(runtimeSessionId);
|
|
1015
|
+
const agentTurn = this.agentTurn(runtimeSessionId);
|
|
1016
|
+
const responsesLiteEnabled = this.responsesLiteEnabled(runtimeSessionId);
|
|
587
1017
|
const grammarToolInputProperties = createGrammarToolInputProperties(
|
|
588
1018
|
context.tools,
|
|
589
1019
|
(model.compat as CodexCompat | undefined)?.supportsOpenAIGrammarTools ?? false,
|
|
@@ -592,11 +1022,13 @@ export class CodexProviderRuntime {
|
|
|
592
1022
|
model,
|
|
593
1023
|
context,
|
|
594
1024
|
requestOptions,
|
|
1025
|
+
runtimeSessionId,
|
|
595
1026
|
cacheSessionId,
|
|
596
1027
|
grammarToolInputProperties,
|
|
1028
|
+
agentTurn.turnId,
|
|
597
1029
|
);
|
|
598
1030
|
const transformed = await requestOptions.onPayload?.(body, model);
|
|
599
|
-
if (
|
|
1031
|
+
if (transformed !== undefined) body = transformed as JsonRecord;
|
|
600
1032
|
if (runtimeSessionId) {
|
|
601
1033
|
this.templates.set(runtimeSessionId, {
|
|
602
1034
|
modelId: model.id,
|
|
@@ -611,31 +1043,151 @@ export class CodexProviderRuntime {
|
|
|
611
1043
|
requestOptions,
|
|
612
1044
|
body,
|
|
613
1045
|
grammarToolInputProperties,
|
|
1046
|
+
agentTurn,
|
|
1047
|
+
responsesLiteEnabled,
|
|
1048
|
+
);
|
|
1049
|
+
const ordinaryBody = body;
|
|
1050
|
+
const staticBody = applyResponsesLite(
|
|
1051
|
+
updateInput(ordinaryBody, []),
|
|
1052
|
+
model.id,
|
|
1053
|
+
responsesLiteEnabled,
|
|
1054
|
+
);
|
|
1055
|
+
body = applyResponsesLite(ordinaryBody, model.id, responsesLiteEnabled);
|
|
1056
|
+
const cacheDiagnostics = codexCacheDiagnosticContext(
|
|
1057
|
+
ordinaryBody,
|
|
1058
|
+
body,
|
|
1059
|
+
staticBody,
|
|
1060
|
+
model.id,
|
|
1061
|
+
responsesLiteEnabled,
|
|
614
1062
|
);
|
|
615
1063
|
|
|
616
1064
|
const rawItems: ResponsesItem[] = [];
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
await processCodexStream(
|
|
620
|
-
captureRawEvents(
|
|
621
|
-
this.transport.request(model, body, requestOptions),
|
|
622
|
-
rawItems,
|
|
623
|
-
responseMetadata,
|
|
624
|
-
),
|
|
625
|
-
output,
|
|
626
|
-
stream,
|
|
1065
|
+
const prewarmDiagnostics: CodexTransportDiagnostic[] = [];
|
|
1066
|
+
await this.maybePrewarm({
|
|
627
1067
|
model,
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
1068
|
+
body: staticBody,
|
|
1069
|
+
fullBody: body,
|
|
1070
|
+
requestOptions,
|
|
1071
|
+
accountId,
|
|
1072
|
+
diagnostics: prewarmDiagnostics,
|
|
1073
|
+
turnState: agentTurn.turnState,
|
|
1074
|
+
cacheDiagnostics,
|
|
1075
|
+
});
|
|
1076
|
+
if (prewarmDiagnostics.length > 0) output.diagnostics = prewarmDiagnostics;
|
|
1077
|
+
let continuationHandle: CodexContinuationHandle | undefined;
|
|
1078
|
+
let webSocketResponseHandle: CodexWebSocketResponseHandle | undefined;
|
|
1079
|
+
let startEmitted = false;
|
|
1080
|
+
const emitStart = () => {
|
|
1081
|
+
if (startEmitted) return;
|
|
1082
|
+
startEmitted = true;
|
|
1083
|
+
stream.push({ type: "start", partial: output });
|
|
1084
|
+
};
|
|
1085
|
+
const transportRequestOptions = {
|
|
1086
|
+
...requestOptions,
|
|
1087
|
+
accountId,
|
|
1088
|
+
requestKind: "turn" as const,
|
|
1089
|
+
turnState: agentTurn.turnState,
|
|
1090
|
+
cacheDiagnostics,
|
|
1091
|
+
onContinuationReady(handle: CodexContinuationHandle) {
|
|
1092
|
+
continuationHandle = handle;
|
|
1093
|
+
},
|
|
1094
|
+
onWebSocketResponseHandle(handle: CodexWebSocketResponseHandle) {
|
|
1095
|
+
webSocketResponseHandle = handle;
|
|
1096
|
+
},
|
|
1097
|
+
onTransportStart: emitStart,
|
|
1098
|
+
onTransportDiagnostic(diagnostic: CodexTransportDiagnostic) {
|
|
1099
|
+
output.diagnostics = [...(output.diagnostics ?? []), diagnostic];
|
|
1100
|
+
},
|
|
1101
|
+
};
|
|
1102
|
+
let responseRequests = 0;
|
|
1103
|
+
let responseRetries = 0;
|
|
1104
|
+
while (true) {
|
|
1105
|
+
responseRequests += 1;
|
|
1106
|
+
const attemptItems: ResponsesItem[] = [];
|
|
1107
|
+
const terminalState: CodexTerminalState = {};
|
|
1108
|
+
const attemptState: CodexStreamAttemptState = {
|
|
1109
|
+
startedContentIndexes: new Set(),
|
|
1110
|
+
completedContentIndexes: new Set(),
|
|
1111
|
+
};
|
|
1112
|
+
const usageBeforeAttempt = structuredClone(output.usage);
|
|
1113
|
+
output.usage = emptyUsage();
|
|
1114
|
+
try {
|
|
1115
|
+
await processCodexStream(
|
|
1116
|
+
startOnFirstEvent(
|
|
1117
|
+
captureRawEvents(
|
|
1118
|
+
this.transport.request(model, body, transportRequestOptions),
|
|
1119
|
+
attemptItems,
|
|
1120
|
+
terminalState,
|
|
1121
|
+
),
|
|
1122
|
+
emitStart,
|
|
1123
|
+
),
|
|
1124
|
+
output,
|
|
1125
|
+
stream,
|
|
1126
|
+
model,
|
|
1127
|
+
grammarToolInputProperties,
|
|
1128
|
+
{
|
|
1129
|
+
attemptState,
|
|
1130
|
+
applyServiceTierPricing(usage, responseServiceTier) {
|
|
1131
|
+
applyServiceTierPricing(usage, model, body.service_tier, responseServiceTier);
|
|
1132
|
+
},
|
|
1133
|
+
},
|
|
1134
|
+
);
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
output.usage = usageBeforeAttempt;
|
|
1137
|
+
if (!requestOptions.signal?.aborted) {
|
|
1138
|
+
webSocketResponseHandle?.failParsing(error);
|
|
1139
|
+
}
|
|
1140
|
+
throw error;
|
|
1141
|
+
}
|
|
1142
|
+
output.usage = accumulateUsage(usageBeforeAttempt, output.usage);
|
|
1143
|
+
rawItems.push(...attemptItems.map((item) => structuredClone(item)));
|
|
1144
|
+
|
|
1145
|
+
const nextBody = continueResponseBody(body, attemptItems);
|
|
1146
|
+
const attemptHasToolCall = [...attemptState.completedContentIndexes].some(
|
|
1147
|
+
(index) => output.content[index]?.type === "toolCall",
|
|
1148
|
+
);
|
|
1149
|
+
discardIncompleteAttemptContent(output, attemptState);
|
|
1150
|
+
const retryableTerminal =
|
|
1151
|
+
terminalState.type === "response.incomplete" ||
|
|
1152
|
+
(terminalState.type === "response.failed" &&
|
|
1153
|
+
retryableResponseFailure(terminalState.response));
|
|
1154
|
+
if (
|
|
1155
|
+
retryableTerminal &&
|
|
1156
|
+
nextBody &&
|
|
1157
|
+
responseRetries < this.responseRetryPolicy.maxRetries
|
|
1158
|
+
) {
|
|
1159
|
+
responseRetries += 1;
|
|
1160
|
+
body = nextBody;
|
|
1161
|
+
output.stopReason = "pending";
|
|
1162
|
+
delete output.errorMessage;
|
|
1163
|
+
delete output.rawStopReason;
|
|
1164
|
+
await waitForResponseRetry(
|
|
1165
|
+
responseRetryDelayMs(this.responseRetryPolicy.baseDelayMs, responseRetries),
|
|
1166
|
+
requestOptions.signal,
|
|
1167
|
+
);
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
if (
|
|
1172
|
+
terminalState.type === "response.completed" &&
|
|
1173
|
+
terminalState.response?.["end_turn"] === false &&
|
|
1174
|
+
!attemptHasToolCall
|
|
1175
|
+
) {
|
|
1176
|
+
if (!nextBody) {
|
|
1177
|
+
throw new Error(
|
|
1178
|
+
"Codex requested a follow-up response, but its completed output could not be appended to request history.",
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
responseRetries = 0;
|
|
1182
|
+
body = nextBody;
|
|
1183
|
+
output.stopReason = "pending";
|
|
1184
|
+
delete output.errorMessage;
|
|
1185
|
+
delete output.rawStopReason;
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
break;
|
|
632
1189
|
}
|
|
633
|
-
|
|
634
|
-
output.usage,
|
|
635
|
-
model,
|
|
636
|
-
body.service_tier,
|
|
637
|
-
responseMetadata.serviceTier,
|
|
638
|
-
);
|
|
1190
|
+
if (requestOptions.signal?.aborted) throw new Error("Request was aborted");
|
|
639
1191
|
|
|
640
1192
|
const compat = model.compat as CodexCompat | undefined;
|
|
641
1193
|
const canonicalContext: Context = {
|
|
@@ -652,7 +1204,7 @@ export class CodexProviderRuntime {
|
|
|
652
1204
|
deferredTools: splitDeferredTools(context, Boolean(compat?.supportsToolSearch))
|
|
653
1205
|
.deferred,
|
|
654
1206
|
toolOptions: {
|
|
655
|
-
strict:
|
|
1207
|
+
strict: false,
|
|
656
1208
|
supportsStrictMode: compat?.supportsStrictMode ?? true,
|
|
657
1209
|
supportsOpenAIGrammarTools: compat?.supportsOpenAIGrammarTools ?? false,
|
|
658
1210
|
},
|
|
@@ -667,19 +1219,37 @@ export class CodexProviderRuntime {
|
|
|
667
1219
|
(item) =>
|
|
668
1220
|
item["type"] !== "function_call_output" && item["type"] !== "custom_tool_call_output",
|
|
669
1221
|
);
|
|
670
|
-
|
|
1222
|
+
const persistNativeItems =
|
|
1223
|
+
rawItems.length > 0 &&
|
|
1224
|
+
(responseRequests > 1 || nativeOverrideRequired(rawItems, canonicalItems));
|
|
1225
|
+
if (persistNativeItems) {
|
|
671
1226
|
if (!output.responseId) throw new Error("Codex response is missing a response id.");
|
|
672
1227
|
this.pi.appendEntry(
|
|
673
1228
|
NATIVE_RESPONSE_ENTRY_TYPE,
|
|
674
1229
|
nativeResponseData(model.id, output.responseId, rawItems),
|
|
675
1230
|
);
|
|
676
1231
|
}
|
|
1232
|
+
try {
|
|
1233
|
+
assertSuccessfulOutput(output);
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
webSocketResponseHandle?.discard();
|
|
1236
|
+
throw error;
|
|
1237
|
+
}
|
|
1238
|
+
const readyContinuation = continuationHandle;
|
|
1239
|
+
if (
|
|
1240
|
+
responseRequests === 1 &&
|
|
1241
|
+
readyContinuation &&
|
|
1242
|
+
readyContinuation.responseId === output.responseId
|
|
1243
|
+
) {
|
|
1244
|
+
readyContinuation.replaceResponseItems(persistNativeItems ? rawItems : canonicalItems);
|
|
1245
|
+
}
|
|
677
1246
|
|
|
678
1247
|
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
679
1248
|
stream.end();
|
|
680
1249
|
} catch (error) {
|
|
1250
|
+
clearStreamingScratchState(output);
|
|
681
1251
|
output.stopReason = requestOptions.signal?.aborted ? "aborted" : "error";
|
|
682
|
-
output.errorMessage =
|
|
1252
|
+
output.errorMessage = formatProviderError(error);
|
|
683
1253
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
684
1254
|
stream.end();
|
|
685
1255
|
} finally {
|
|
@@ -694,6 +1264,7 @@ export class CodexProviderRuntime {
|
|
|
694
1264
|
context: Context,
|
|
695
1265
|
options?: SimpleStreamOptions,
|
|
696
1266
|
): AssistantMessageEventStream {
|
|
1267
|
+
if (!options?.apiKey) throw new Error(`No API key for provider: ${model.provider}`);
|
|
697
1268
|
const effort = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
|
|
698
1269
|
return this.stream(model, context, {
|
|
699
1270
|
...options,
|
|
@@ -705,7 +1276,7 @@ export class CodexProviderRuntime {
|
|
|
705
1276
|
return this.templates.get(sessionId);
|
|
706
1277
|
}
|
|
707
1278
|
|
|
708
|
-
compact(options: {
|
|
1279
|
+
async compact(options: {
|
|
709
1280
|
model: Model<any>;
|
|
710
1281
|
requestOptions: OpenAICodexResponsesOptions;
|
|
711
1282
|
history: ResponsesItem[];
|
|
@@ -713,14 +1284,21 @@ export class CodexProviderRuntime {
|
|
|
713
1284
|
grammarToolInputProperties: GrammarToolInputProperties;
|
|
714
1285
|
template: JsonRecord;
|
|
715
1286
|
priority: boolean;
|
|
1287
|
+
compactionMetadata: CodexCompactionMetadata;
|
|
716
1288
|
}): Promise<{ checkpoint: CheckpointData; usage?: Usage }> {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1289
|
+
validateCodexAuthentication(options.model, options.requestOptions.apiKey);
|
|
1290
|
+
const release = await this.acquireRequest(
|
|
1291
|
+
options.requestOptions.sessionId,
|
|
1292
|
+
options.requestOptions.signal,
|
|
1293
|
+
);
|
|
1294
|
+
try {
|
|
1295
|
+
const compacted = await this.performCompaction(options);
|
|
1296
|
+
const sessionId = options.requestOptions.sessionId;
|
|
1297
|
+
if (sessionId) this.advanceWindow(sessionId);
|
|
1298
|
+
return compacted;
|
|
1299
|
+
} finally {
|
|
1300
|
+
release();
|
|
1301
|
+
}
|
|
724
1302
|
}
|
|
725
1303
|
}
|
|
726
1304
|
|
|
@@ -728,7 +1306,7 @@ export function registerCodexProvider(
|
|
|
728
1306
|
pi: ExtensionAPI,
|
|
729
1307
|
resolveConfig: ConfigResolver,
|
|
730
1308
|
): CodexProviderRuntime {
|
|
731
|
-
const runtime = new CodexProviderRuntime(pi, resolveConfig);
|
|
1309
|
+
const runtime = new CodexProviderRuntime(pi, resolveConfig, resolveCodexInstallationId());
|
|
732
1310
|
pi.on("session_start", (_event, ctx) => {
|
|
733
1311
|
const base =
|
|
734
1312
|
ctx.modelRegistry.getRegisteredNativeProvider(CODEX_PROVIDER) ??
|
|
@@ -736,5 +1314,7 @@ export function registerCodexProvider(
|
|
|
736
1314
|
if (!base) throw new Error("Pi's built-in OpenAI Codex provider is unavailable.");
|
|
737
1315
|
pi.registerProvider(runtime.createProvider(base));
|
|
738
1316
|
});
|
|
1317
|
+
pi.on("agent_start", (_event, ctx) => runtime.beginAgentTurn(ctx));
|
|
1318
|
+
pi.on("agent_end", (_event, ctx) => runtime.endAgentTurn(ctx));
|
|
739
1319
|
return runtime;
|
|
740
1320
|
}
|