pi-openai-codex-compat 0.0.3 → 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.
@@ -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,
@@ -35,15 +40,25 @@ import {
35
40
  type ResponsesItem,
36
41
  } from "./codex-protocol.ts";
37
42
  import { codexCacheKey } from "./codex-cache-key.ts";
38
- import { processCodexStream } from "./codex-stream.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";
39
53
  import {
40
54
  CodexTransport,
55
+ CodexTurnState,
41
56
  validateCodexAuthentication,
42
57
  type CodexContinuationHandle,
43
58
  type CodexTransportDiagnostic,
44
59
  type CodexWebSocketResponseHandle,
45
60
  } from "./codex-transport.ts";
46
- import type { CodexCompatConfig, ImageDetail } from "./config.ts";
61
+ import { DEFAULT_CONFIG, type CodexCompatConfig, type ImageDetail } from "./config.ts";
47
62
  import { nativeResponseData, NATIVE_RESPONSE_ENTRY_TYPE } from "./native-history.ts";
48
63
  import {
49
64
  CODEX_NAMESPACED_TOOL_NAMES,
@@ -94,12 +109,33 @@ type RequestTemplate = {
94
109
  requestOptions: OpenAICodexResponsesOptions;
95
110
  };
96
111
 
112
+ type ActiveAgentTurn = {
113
+ turnId: string;
114
+ startedAtUnixMs: number;
115
+ turnState: CodexTurnState;
116
+ };
117
+
97
118
  type CodexCompat = {
98
119
  supportsToolSearch?: boolean;
99
120
  supportsStrictMode?: boolean;
100
121
  supportsOpenAIGrammarTools?: boolean;
101
122
  };
102
123
 
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
+ };
138
+
103
139
  function markerSummary(): string {
104
140
  return `OpenAI Codex remote compaction checkpoint (${randomUUID()}).`;
105
141
  }
@@ -154,6 +190,7 @@ function nativeOverrideRequired(
154
190
  function captureRawEvents(
155
191
  events: AsyncIterable<JsonRecord>,
156
192
  items: ResponsesItem[],
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" || event.type === "response.incomplete") &&
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
  ) {
@@ -171,6 +210,19 @@ function captureRawEvents(
171
210
  items.splice(0, items.length, ...terminalItems.map((item) => structuredClone(item)));
172
211
  }
173
212
  }
213
+ if (
214
+ terminalState &&
215
+ (event.type === "response.completed" ||
216
+ event.type === "response.incomplete" ||
217
+ event.type === "response.failed")
218
+ ) {
219
+ terminalState.type = event.type;
220
+ if (isObject(event.response)) {
221
+ terminalState.response = structuredClone(event.response);
222
+ } else {
223
+ delete terminalState.response;
224
+ }
225
+ }
174
226
  yield event;
175
227
  }
176
228
  },
@@ -202,6 +254,92 @@ function clearStreamingScratchState(message: AssistantMessage): void {
202
254
  }
203
255
  }
204
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
+
205
343
  function updateInput(payload: JsonRecord, input: readonly ResponsesItem[]): JsonRecord {
206
344
  const result: JsonRecord = {
207
345
  ...payload,
@@ -305,13 +443,38 @@ export class CodexProviderRuntime {
305
443
  readonly transport = new CodexTransport();
306
444
  private readonly scopes = new Map<string, RuntimeScope>();
307
445
  private readonly templates = new Map<string, RequestTemplate>();
446
+ private readonly prewarmedTemplates = new Set<string>();
308
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>();
309
451
  private readonly pi: ExtensionAPI;
310
452
  private readonly resolveConfig: ConfigResolver;
453
+ private readonly installationId: string;
454
+ private readonly responseRetryPolicy: CodexResponseRetryPolicy;
311
455
 
312
- constructor(pi: ExtensionAPI, resolveConfig: ConfigResolver) {
456
+ constructor(
457
+ pi: ExtensionAPI,
458
+ resolveConfig: ConfigResolver,
459
+ installationId: string = randomUUID(),
460
+ responseRetryPolicy: Partial<CodexResponseRetryPolicy> = {},
461
+ ) {
313
462
  this.pi = pi;
314
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
+ };
315
478
  }
316
479
 
317
480
  captureScope(ctx: ExtensionContext): void {
@@ -330,13 +493,154 @@ export class CodexProviderRuntime {
330
493
  });
331
494
  }
332
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
+
333
582
  clearSession(sessionId: string): void {
334
583
  this.scopes.delete(sessionId);
335
584
  this.templates.delete(sessionId);
585
+ this.clearPrewarmState(sessionId);
336
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);
337
592
  this.transport.close(sessionId);
338
593
  }
339
594
 
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
+
340
644
  private async acquireRequest(
341
645
  sessionId: string | undefined,
342
646
  signal?: AbortSignal,
@@ -412,7 +716,7 @@ export class CodexProviderRuntime {
412
716
  grammarToolInputProperties,
413
717
  deferredTools: splitDeferredTools(context, Boolean(compat?.supportsToolSearch)).deferred,
414
718
  toolOptions: {
415
- strict: null,
719
+ strict: false,
416
720
  supportsStrictMode: compat?.supportsStrictMode ?? true,
417
721
  supportsOpenAIGrammarTools: compat?.supportsOpenAIGrammarTools ?? false,
418
722
  },
@@ -438,10 +742,11 @@ export class CodexProviderRuntime {
438
742
  runtimeSessionId: string | undefined,
439
743
  cacheSessionId: string | undefined,
440
744
  grammarToolInputProperties: GrammarToolInputProperties,
745
+ turnId: string,
441
746
  ): JsonRecord {
442
747
  const compat = model.compat as CodexCompat | undefined;
443
748
  const toolPlacement = splitDeferredTools(context, Boolean(compat?.supportsToolSearch));
444
- const body: JsonRecord = {
749
+ let body: JsonRecord = {
445
750
  model: model.id,
446
751
  store: false,
447
752
  stream: true,
@@ -456,12 +761,23 @@ export class CodexProviderRuntime {
456
761
  prompt_cache_key: cacheSessionId,
457
762
  tool_choice: options.toolChoice ?? "auto",
458
763
  parallel_tool_calls: true,
764
+ tools: [],
459
765
  };
460
- if (options.temperature !== undefined) body["temperature"] = options.temperature;
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
+ );
461
777
  if (options.serviceTier !== undefined) body.service_tier = options.serviceTier;
462
778
  if (toolPlacement.immediate.length > 0) {
463
779
  body.tools = convertResponsesTools(toolPlacement.immediate, {
464
- strict: null,
780
+ strict: false,
465
781
  supportsStrictMode: compat?.supportsStrictMode ?? true,
466
782
  supportsOpenAIGrammarTools: compat?.supportsOpenAIGrammarTools ?? false,
467
783
  namespacedToolNames: CODEX_NAMESPACED_TOOL_NAMES,
@@ -491,21 +807,51 @@ export class CodexProviderRuntime {
491
807
  instructions: string;
492
808
  grammarToolInputProperties: GrammarToolInputProperties;
493
809
  priority: boolean;
810
+ compactionMetadata: CodexCompactionMetadata;
811
+ agentTurn?: ActiveAgentTurn;
812
+ responsesLiteEnabled?: boolean;
494
813
  }): Promise<{ checkpoint: CheckpointData; usage?: Usage }> {
495
814
  const sessionId = options.requestOptions.sessionId;
496
815
  if (!sessionId) throw new Error("Codex compaction requires a Pi session id.");
816
+ this.activateThread(sessionId);
817
+ const agentTurn = options.agentTurn ?? this.agentTurn(sessionId);
818
+ const responsesLiteEnabled =
819
+ options.responsesLiteEnabled ?? this.responsesLiteEnabled(sessionId);
497
820
  const accountId = validateCodexAuthentication(options.model, options.requestOptions.apiKey);
498
- const payload = remoteCompactionPayload({
499
- template: options.template,
500
- modelId: options.model.id,
501
- history: options.history,
502
- instructions: options.instructions,
503
- sessionId:
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(
504
835
  options.requestOptions.cacheRetention === "none" ? undefined : codexCacheKey(sessionId),
505
- priority: options.priority,
506
- });
836
+ agentTurn,
837
+ sessionId,
838
+ ),
839
+ );
507
840
  const transformed = await options.requestOptions.onPayload?.(payload, options.model);
508
- const request = transformed === undefined ? payload : (transformed as JsonRecord);
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,
854
+ );
509
855
  let webSocketResponseHandle: CodexWebSocketResponseHandle | undefined;
510
856
  let compacted: Awaited<ReturnType<typeof collectRemoteCompaction>>;
511
857
  try {
@@ -513,6 +859,9 @@ export class CodexProviderRuntime {
513
859
  this.transport.request(options.model, request, {
514
860
  ...options.requestOptions,
515
861
  accountId,
862
+ requestKind: "compaction",
863
+ turnState: agentTurn.turnState,
864
+ cacheDiagnostics,
516
865
  onWebSocketResponseHandle(handle) {
517
866
  webSocketResponseHandle = handle;
518
867
  },
@@ -543,12 +892,16 @@ export class CodexProviderRuntime {
543
892
  options: OpenAICodexResponsesOptions,
544
893
  body: JsonRecord,
545
894
  grammarToolInputProperties: GrammarToolInputProperties,
895
+ agentTurn: ActiveAgentTurn,
896
+ responsesLiteEnabled: boolean,
546
897
  ): Promise<JsonRecord> {
547
898
  const sessionId = options.sessionId;
548
899
  const scope = sessionId ? this.scopes.get(sessionId) : undefined;
549
900
  const threshold = scope?.config.autoCompactAtPercent;
901
+ if (!sessionId || !scope) {
902
+ return body;
903
+ }
550
904
  if (
551
- !scope ||
552
905
  threshold === undefined ||
553
906
  scope.contextPercent === null ||
554
907
  scope.contextPercent < threshold ||
@@ -599,6 +952,9 @@ export class CodexProviderRuntime {
599
952
  : context.systemPrompt || "You are a helpful assistant.",
600
953
  grammarToolInputProperties,
601
954
  priority: scope.config.fastMode,
955
+ compactionMetadata: responsesCompactionV2Metadata("auto", "context_limit", "pre_turn"),
956
+ agentTurn,
957
+ responsesLiteEnabled,
602
958
  });
603
959
  const firstKeptEntryId = userEntryAfterLastSampled(branch)?.id ?? scope.manager.getLeafId();
604
960
  if (!firstKeptEntryId || typeof scope.manager.appendCompaction !== "function") {
@@ -619,7 +975,15 @@ export class CodexProviderRuntime {
619
975
  `OpenAI Codex context compacted at ${scope.contextPercent.toFixed(1)}% and will continue.`,
620
976
  "info",
621
977
  );
622
- return updateInput(body, compacted.checkpoint.history);
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
+ );
623
987
  }
624
988
 
625
989
  stream(
@@ -636,14 +1000,7 @@ export class CodexProviderRuntime {
636
1000
  api: CODEX_API,
637
1001
  provider: model.provider,
638
1002
  model: model.id,
639
- usage: {
640
- input: 0,
641
- output: 0,
642
- cacheRead: 0,
643
- cacheWrite: 0,
644
- totalTokens: 0,
645
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
646
- },
1003
+ usage: emptyUsage(),
647
1004
  stopReason: "pending",
648
1005
  timestamp: Date.now(),
649
1006
  };
@@ -652,8 +1009,11 @@ export class CodexProviderRuntime {
652
1009
  try {
653
1010
  const accountId = validateCodexAuthentication(model, requestOptions.apiKey);
654
1011
  releaseRequest = await this.acquireRequest(runtimeSessionId, requestOptions.signal);
1012
+ this.activateThread(runtimeSessionId);
655
1013
  const cacheSessionId =
656
1014
  requestOptions.cacheRetention === "none" ? undefined : codexCacheKey(runtimeSessionId);
1015
+ const agentTurn = this.agentTurn(runtimeSessionId);
1016
+ const responsesLiteEnabled = this.responsesLiteEnabled(runtimeSessionId);
657
1017
  const grammarToolInputProperties = createGrammarToolInputProperties(
658
1018
  context.tools,
659
1019
  (model.compat as CodexCompat | undefined)?.supportsOpenAIGrammarTools ?? false,
@@ -665,6 +1025,7 @@ export class CodexProviderRuntime {
665
1025
  runtimeSessionId,
666
1026
  cacheSessionId,
667
1027
  grammarToolInputProperties,
1028
+ agentTurn.turnId,
668
1029
  );
669
1030
  const transformed = await requestOptions.onPayload?.(body, model);
670
1031
  if (transformed !== undefined) body = transformed as JsonRecord;
@@ -682,9 +1043,37 @@ export class CodexProviderRuntime {
682
1043
  requestOptions,
683
1044
  body,
684
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,
685
1062
  );
686
1063
 
687
1064
  const rawItems: ResponsesItem[] = [];
1065
+ const prewarmDiagnostics: CodexTransportDiagnostic[] = [];
1066
+ await this.maybePrewarm({
1067
+ model,
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;
688
1077
  let continuationHandle: CodexContinuationHandle | undefined;
689
1078
  let webSocketResponseHandle: CodexWebSocketResponseHandle | undefined;
690
1079
  let startEmitted = false;
@@ -696,6 +1085,9 @@ export class CodexProviderRuntime {
696
1085
  const transportRequestOptions = {
697
1086
  ...requestOptions,
698
1087
  accountId,
1088
+ requestKind: "turn" as const,
1089
+ turnState: agentTurn.turnState,
1090
+ cacheDiagnostics,
699
1091
  onContinuationReady(handle: CodexContinuationHandle) {
700
1092
  continuationHandle = handle;
701
1093
  },
@@ -707,38 +1099,95 @@ export class CodexProviderRuntime {
707
1099
  output.diagnostics = [...(output.diagnostics ?? []), diagnostic];
708
1100
  },
709
1101
  };
710
- try {
711
- await processCodexStream(
712
- startOnFirstEvent(
713
- captureRawEvents(
714
- this.transport.request(model, body, transportRequestOptions),
715
- rawItems,
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,
716
1123
  ),
717
- emitStart,
718
- ),
719
- output,
720
- stream,
721
- model,
722
- grammarToolInputProperties,
723
- {
724
- applyServiceTierPricing(usage, responseServiceTier) {
725
- applyServiceTierPricing(usage, model, body.service_tier, responseServiceTier);
1124
+ output,
1125
+ stream,
1126
+ model,
1127
+ grammarToolInputProperties,
1128
+ {
1129
+ attemptState,
1130
+ applyServiceTierPricing(usage, responseServiceTier) {
1131
+ applyServiceTierPricing(usage, model, body.service_tier, responseServiceTier);
1132
+ },
726
1133
  },
727
- },
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",
728
1148
  );
729
- } catch (error) {
730
- if (!requestOptions.signal?.aborted) {
731
- webSocketResponseHandle?.failParsing(error);
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;
732
1169
  }
733
- throw error;
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;
734
1189
  }
735
1190
  if (requestOptions.signal?.aborted) throw new Error("Request was aborted");
736
- try {
737
- assertSuccessfulOutput(output);
738
- } catch (error) {
739
- webSocketResponseHandle?.discard();
740
- throw error;
741
- }
742
1191
 
743
1192
  const compat = model.compat as CodexCompat | undefined;
744
1193
  const canonicalContext: Context = {
@@ -755,7 +1204,7 @@ export class CodexProviderRuntime {
755
1204
  deferredTools: splitDeferredTools(context, Boolean(compat?.supportsToolSearch))
756
1205
  .deferred,
757
1206
  toolOptions: {
758
- strict: null,
1207
+ strict: false,
759
1208
  supportsStrictMode: compat?.supportsStrictMode ?? true,
760
1209
  supportsOpenAIGrammarTools: compat?.supportsOpenAIGrammarTools ?? false,
761
1210
  },
@@ -771,7 +1220,8 @@ export class CodexProviderRuntime {
771
1220
  item["type"] !== "function_call_output" && item["type"] !== "custom_tool_call_output",
772
1221
  );
773
1222
  const persistNativeItems =
774
- rawItems.length > 0 && nativeOverrideRequired(rawItems, canonicalItems);
1223
+ rawItems.length > 0 &&
1224
+ (responseRequests > 1 || nativeOverrideRequired(rawItems, canonicalItems));
775
1225
  if (persistNativeItems) {
776
1226
  if (!output.responseId) throw new Error("Codex response is missing a response id.");
777
1227
  this.pi.appendEntry(
@@ -779,8 +1229,18 @@ export class CodexProviderRuntime {
779
1229
  nativeResponseData(model.id, output.responseId, rawItems),
780
1230
  );
781
1231
  }
1232
+ try {
1233
+ assertSuccessfulOutput(output);
1234
+ } catch (error) {
1235
+ webSocketResponseHandle?.discard();
1236
+ throw error;
1237
+ }
782
1238
  const readyContinuation = continuationHandle;
783
- if (readyContinuation && readyContinuation.responseId === output.responseId) {
1239
+ if (
1240
+ responseRequests === 1 &&
1241
+ readyContinuation &&
1242
+ readyContinuation.responseId === output.responseId
1243
+ ) {
784
1244
  readyContinuation.replaceResponseItems(persistNativeItems ? rawItems : canonicalItems);
785
1245
  }
786
1246
 
@@ -824,6 +1284,7 @@ export class CodexProviderRuntime {
824
1284
  grammarToolInputProperties: GrammarToolInputProperties;
825
1285
  template: JsonRecord;
826
1286
  priority: boolean;
1287
+ compactionMetadata: CodexCompactionMetadata;
827
1288
  }): Promise<{ checkpoint: CheckpointData; usage?: Usage }> {
828
1289
  validateCodexAuthentication(options.model, options.requestOptions.apiKey);
829
1290
  const release = await this.acquireRequest(
@@ -831,7 +1292,10 @@ export class CodexProviderRuntime {
831
1292
  options.requestOptions.signal,
832
1293
  );
833
1294
  try {
834
- return await this.performCompaction(options);
1295
+ const compacted = await this.performCompaction(options);
1296
+ const sessionId = options.requestOptions.sessionId;
1297
+ if (sessionId) this.advanceWindow(sessionId);
1298
+ return compacted;
835
1299
  } finally {
836
1300
  release();
837
1301
  }
@@ -842,7 +1306,7 @@ export function registerCodexProvider(
842
1306
  pi: ExtensionAPI,
843
1307
  resolveConfig: ConfigResolver,
844
1308
  ): CodexProviderRuntime {
845
- const runtime = new CodexProviderRuntime(pi, resolveConfig);
1309
+ const runtime = new CodexProviderRuntime(pi, resolveConfig, resolveCodexInstallationId());
846
1310
  pi.on("session_start", (_event, ctx) => {
847
1311
  const base =
848
1312
  ctx.modelRegistry.getRegisteredNativeProvider(CODEX_PROVIDER) ??
@@ -850,5 +1314,7 @@ export function registerCodexProvider(
850
1314
  if (!base) throw new Error("Pi's built-in OpenAI Codex provider is unavailable.");
851
1315
  pi.registerProvider(runtime.createProvider(base));
852
1316
  });
1317
+ pi.on("agent_start", (_event, ctx) => runtime.beginAgentTurn(ctx));
1318
+ pi.on("agent_end", (_event, ctx) => runtime.endAgentTurn(ctx));
853
1319
  return runtime;
854
1320
  }