pi-openai-codex-compat 0.0.1-alpha.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/LICENSE +20 -0
  3. package/LICENSES/Apache-2.0.txt +201 -0
  4. package/LICENSES/pi-ai-MIT.txt +21 -0
  5. package/README.md +331 -0
  6. package/THIRD_PARTY_NOTICES.md +21 -0
  7. package/extensions/openai-codex-compat/apply-patch-diff-render.ts +436 -0
  8. package/extensions/openai-codex-compat/apply-patch-engine.ts +1004 -0
  9. package/extensions/openai-codex-compat/apply-patch-render.ts +133 -0
  10. package/extensions/openai-codex-compat/apply-patch.ts +142 -0
  11. package/extensions/openai-codex-compat/codex-protocol.ts +598 -0
  12. package/extensions/openai-codex-compat/codex-provider.ts +740 -0
  13. package/extensions/openai-codex-compat/codex-stream.ts +444 -0
  14. package/extensions/openai-codex-compat/codex-tool-surface.ts +186 -0
  15. package/extensions/openai-codex-compat/codex-transport.ts +855 -0
  16. package/extensions/openai-codex-compat/compaction-checkpoint.ts +304 -0
  17. package/extensions/openai-codex-compat/config.ts +268 -0
  18. package/extensions/openai-codex-compat/footer.ts +99 -0
  19. package/extensions/openai-codex-compat/image-generation-render.ts +166 -0
  20. package/extensions/openai-codex-compat/image-generation.ts +355 -0
  21. package/extensions/openai-codex-compat/index.ts +65 -0
  22. package/extensions/openai-codex-compat/model-policy.ts +67 -0
  23. package/extensions/openai-codex-compat/namespaced-tools.ts +43 -0
  24. package/extensions/openai-codex-compat/native-history.ts +78 -0
  25. package/extensions/openai-codex-compat/remote-compaction.ts +198 -0
  26. package/extensions/openai-codex-compat/request-options.ts +121 -0
  27. package/extensions/openai-codex-compat/responses-replay.ts +33 -0
  28. package/extensions/openai-codex-compat/settings-pane.ts +298 -0
  29. package/extensions/openai-codex-compat/tool-runtime.ts +32 -0
  30. package/extensions/openai-codex-compat/tools.ts +70 -0
  31. package/extensions/openai-codex-compat/vendor/pi-ai/README.md +15 -0
  32. package/extensions/openai-codex-compat/vendor/pi-ai/openai-responses-serialization.ts +660 -0
  33. package/extensions/openai-codex-compat/web-run-description.txt +105 -0
  34. package/extensions/openai-codex-compat/web-run-output.ts +172 -0
  35. package/extensions/openai-codex-compat/web-run-render.ts +681 -0
  36. package/extensions/openai-codex-compat/web-run-schema.ts +301 -0
  37. package/extensions/openai-codex-compat/web-run.ts +164 -0
  38. package/package.json +63 -0
@@ -0,0 +1,740 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type {
3
+ ExtensionAPI,
4
+ ExtensionContext,
5
+ SessionEntry,
6
+ ToolInfo,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ clampThinkingLevel,
10
+ createAssistantMessageEventStream,
11
+ type AssistantMessage,
12
+ type AssistantMessageEventStream,
13
+ type Context,
14
+ type Model,
15
+ type OpenAICodexResponsesOptions,
16
+ type Provider,
17
+ type SimpleStreamOptions,
18
+ type Tool,
19
+ type Usage,
20
+ } from "@earendil-works/pi-ai";
21
+ import {
22
+ checkpointData,
23
+ providerHistory,
24
+ searchCheckpoint,
25
+ type CheckpointData,
26
+ type GrammarToolInputProperties,
27
+ } from "./compaction-checkpoint.ts";
28
+ import {
29
+ collectRemoteCompaction,
30
+ isObject,
31
+ isResponsesItem,
32
+ remoteCompactionPayload,
33
+ withoutConversationInput,
34
+ type JsonRecord,
35
+ type ResponsesItem,
36
+ } from "./codex-protocol.ts";
37
+ import { processCodexStream } from "./codex-stream.ts";
38
+ import { CodexTransport } from "./codex-transport.ts";
39
+ import type { CodexCompatConfig, ImageDetail } from "./config.ts";
40
+ import { nativeResponseData, NATIVE_RESPONSE_ENTRY_TYPE } from "./native-history.ts";
41
+ import {
42
+ CODEX_NAMESPACED_TOOL_NAMES,
43
+ CODEX_TEXT_CONTENT_ITEM_TOOL_RESULT_NAMES,
44
+ } from "./namespaced-tools.ts";
45
+ import { normalizeReplayItem, stableResponsesJson } from "./responses-replay.ts";
46
+ import {
47
+ convertResponsesTools,
48
+ convertResponsesMessages,
49
+ createGrammarToolInputProperties,
50
+ type ResponsesItem as SerializedResponsesItem,
51
+ } from "./vendor/pi-ai/openai-responses-serialization.ts";
52
+
53
+ const CODEX_PROVIDER = "openai-codex";
54
+ const CODEX_API = "openai-codex-responses";
55
+ const CHECKPOINT_STATUS_ID = "openai-codex-compat-compaction";
56
+ const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
57
+
58
+ type ConfigResolver = (ctx: ExtensionContext) => CodexCompatConfig;
59
+
60
+ type MutableSessionManager = ExtensionContext["sessionManager"] & {
61
+ appendCompaction?<T>(
62
+ summary: string,
63
+ firstKeptEntryId: string,
64
+ tokensBefore: number,
65
+ details?: T,
66
+ fromHook?: boolean,
67
+ usage?: Usage,
68
+ ): string;
69
+ };
70
+
71
+ type RuntimeScope = {
72
+ sessionId: string;
73
+ manager: MutableSessionManager;
74
+ branch: SessionEntry[];
75
+ leafId: string | null;
76
+ contextTokens: number | null;
77
+ contextPercent: number | null;
78
+ config: CodexCompatConfig;
79
+ hasUI: boolean;
80
+ notify(message: string, level: "info" | "warning" | "error"): void;
81
+ setStatus(message: string | undefined): void;
82
+ };
83
+
84
+ type RequestTemplate = {
85
+ modelId: string;
86
+ payload: JsonRecord;
87
+ grammarToolInputProperties: GrammarToolInputProperties;
88
+ requestOptions: OpenAICodexResponsesOptions;
89
+ };
90
+
91
+ type CodexCompat = {
92
+ supportsToolSearch?: boolean;
93
+ supportsStrictMode?: boolean;
94
+ supportsOpenAIGrammarTools?: boolean;
95
+ };
96
+
97
+ function clampPromptCacheKey(key: string | undefined): string | undefined {
98
+ if (key === undefined) return undefined;
99
+ return Array.from(key).slice(0, 64).join("");
100
+ }
101
+
102
+ function markerSummary(): string {
103
+ return `OpenAI Codex remote compaction checkpoint (${randomUUID()}).`;
104
+ }
105
+
106
+ function splitDeferredTools(
107
+ context: Context,
108
+ enabled: boolean,
109
+ ): { immediate: Tool[]; deferred: Map<string, Tool> } {
110
+ const unique = new Map((context.tools ?? []).map((tool) => [tool.name, tool]));
111
+ if (!enabled) return { immediate: [...unique.values()], deferred: new Map() };
112
+
113
+ const deferredNames = new Set<string>();
114
+ const usedNames = new Set<string>();
115
+ for (const message of context.messages) {
116
+ if (message.role === "assistant") {
117
+ for (const block of message.content) {
118
+ if (block.type === "toolCall") usedNames.add(block.name);
119
+ }
120
+ } else if (message.role === "toolResult") {
121
+ for (const name of message.addedToolNames ?? []) {
122
+ if (!usedNames.has(name)) deferredNames.add(name);
123
+ }
124
+ }
125
+ }
126
+
127
+ const immediate: Tool[] = [];
128
+ const deferred = new Map<string, Tool>();
129
+ for (const [name, tool] of unique) {
130
+ if (deferredNames.has(name)) deferred.set(name, tool);
131
+ else immediate.push(tool);
132
+ }
133
+ return { immediate, deferred };
134
+ }
135
+
136
+ function transportOptions(
137
+ options: OpenAICodexResponsesOptions | undefined,
138
+ ): OpenAICodexResponsesOptions {
139
+ return options ?? {};
140
+ }
141
+
142
+ function nativeOverrideRequired(
143
+ rawItems: readonly ResponsesItem[],
144
+ canonicalItems: readonly SerializedResponsesItem[],
145
+ ): boolean {
146
+ if (rawItems.length !== canonicalItems.length) return true;
147
+ return rawItems.some(
148
+ (item, index) =>
149
+ stableResponsesJson(normalizeReplayItem(item)) !== stableResponsesJson(canonicalItems[index]),
150
+ );
151
+ }
152
+
153
+ function captureRawEvents(
154
+ events: AsyncIterable<JsonRecord>,
155
+ items: ResponsesItem[],
156
+ metadata: { serviceTier?: string },
157
+ ): AsyncIterable<JsonRecord> {
158
+ return {
159
+ async *[Symbol.asyncIterator]() {
160
+ for await (const event of events) {
161
+ if (event.type === "response.output_item.done" && isResponsesItem(event.item)) {
162
+ items.push(structuredClone(event.item));
163
+ }
164
+ if (
165
+ (event.type === "response.completed" || event.type === "response.incomplete") &&
166
+ isObject(event.response) &&
167
+ Array.isArray(event.response["output"])
168
+ ) {
169
+ const terminalItems = event.response["output"].filter(isResponsesItem);
170
+ if (terminalItems.length > 0) {
171
+ items.splice(0, items.length, ...terminalItems.map((item) => structuredClone(item)));
172
+ }
173
+ }
174
+ if (
175
+ (event.type === "response.completed" || event.type === "response.incomplete") &&
176
+ isObject(event.response) &&
177
+ typeof event.response.service_tier === "string"
178
+ ) {
179
+ metadata.serviceTier = event.response.service_tier;
180
+ }
181
+ yield event;
182
+ }
183
+ },
184
+ };
185
+ }
186
+
187
+ function updateInput(payload: JsonRecord, input: readonly ResponsesItem[]): JsonRecord {
188
+ const result: JsonRecord = {
189
+ ...payload,
190
+ input: input.map((item) => structuredClone(item)),
191
+ };
192
+ delete result.messages;
193
+ delete result.previous_response_id;
194
+ return result;
195
+ }
196
+
197
+ function successfulStopReason(
198
+ message: AssistantMessage,
199
+ ): message is AssistantMessage & { stopReason: "stop" | "length" | "toolUse" } {
200
+ return (
201
+ message.stopReason === "stop" ||
202
+ message.stopReason === "length" ||
203
+ message.stopReason === "toolUse"
204
+ );
205
+ }
206
+
207
+ function applyServiceTierPricing(
208
+ usage: Usage,
209
+ model: Model<any>,
210
+ requestedTier: unknown,
211
+ responseTier: string | undefined,
212
+ ): void {
213
+ const resolvedTier =
214
+ responseTier === "default" && (requestedTier === "priority" || requestedTier === "flex")
215
+ ? requestedTier
216
+ : (responseTier ?? requestedTier);
217
+ const multiplier =
218
+ resolvedTier === "flex"
219
+ ? 0.5
220
+ : resolvedTier === "priority"
221
+ ? model.id === "gpt-5.5"
222
+ ? 2.5
223
+ : 2
224
+ : 1;
225
+ if (multiplier === 1) return;
226
+ usage.cost.input *= multiplier;
227
+ usage.cost.output *= multiplier;
228
+ usage.cost.cacheRead *= multiplier;
229
+ usage.cost.cacheWrite *= multiplier;
230
+ usage.cost.total =
231
+ usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
232
+ }
233
+
234
+ function userEntryAfterLastSampled(branch: readonly SessionEntry[]): SessionEntry | undefined {
235
+ const lastSampledIndex = branch.findLastIndex(
236
+ (entry) =>
237
+ entry.type === "message" &&
238
+ (entry.message.role === "assistant" || entry.message.role === "toolResult"),
239
+ );
240
+ return branch
241
+ .slice(lastSampledIndex + 1)
242
+ .find((entry) => entry.type === "message" && entry.message.role === "user");
243
+ }
244
+
245
+ function splitUnsampledUserInput(options: {
246
+ branch: readonly SessionEntry[];
247
+ history: readonly ResponsesItem[];
248
+ model: Model<any>;
249
+ allTools: readonly ToolInfo[];
250
+ grammarToolInputProperties: GrammarToolInputProperties;
251
+ imageDetail: ImageDetail;
252
+ }):
253
+ | { kind: "none" | "found"; history: ResponsesItem[]; tail: ResponsesItem[] }
254
+ | { kind: "unsafe" } {
255
+ const firstUnsampled = userEntryAfterLastSampled(options.branch);
256
+ if (!firstUnsampled) {
257
+ return {
258
+ kind: "none",
259
+ history: options.history.map((item) => structuredClone(item)),
260
+ tail: [],
261
+ };
262
+ }
263
+
264
+ const unsampledIndex = options.branch.findIndex((entry) => entry.id === firstUnsampled.id);
265
+ const encoded = providerHistory({
266
+ branch: options.branch.slice(unsampledIndex),
267
+ wireModel: options.model,
268
+ allTools: options.allTools,
269
+ grammarToolInputProperties: options.grammarToolInputProperties,
270
+ imageDetail: options.imageDetail,
271
+ });
272
+ if (encoded.length === 0 || encoded.length > options.history.length) return { kind: "unsafe" };
273
+
274
+ const splitIndex = options.history.length - encoded.length;
275
+ if (JSON.stringify(options.history.slice(splitIndex)) !== JSON.stringify(encoded)) {
276
+ return { kind: "unsafe" };
277
+ }
278
+ return {
279
+ kind: "found",
280
+ history: options.history.slice(0, splitIndex).map((item) => structuredClone(item)),
281
+ tail: encoded.map((item) => structuredClone(item)),
282
+ };
283
+ }
284
+
285
+ export class CodexProviderRuntime {
286
+ readonly transport = new CodexTransport();
287
+ private readonly scopes = new Map<string, RuntimeScope>();
288
+ private readonly templates = new Map<string, RequestTemplate>();
289
+ private readonly requestTails = new Map<string, Promise<void>>();
290
+ private readonly pi: ExtensionAPI;
291
+ private readonly resolveConfig: ConfigResolver;
292
+
293
+ constructor(pi: ExtensionAPI, resolveConfig: ConfigResolver) {
294
+ this.pi = pi;
295
+ this.resolveConfig = resolveConfig;
296
+ }
297
+
298
+ captureScope(ctx: ExtensionContext): void {
299
+ const sessionId = ctx.sessionManager.getSessionId();
300
+ const usage = ctx.getContextUsage();
301
+ this.scopes.set(sessionId, {
302
+ sessionId,
303
+ manager: ctx.sessionManager as MutableSessionManager,
304
+ branch: ctx.sessionManager.getBranch() as SessionEntry[],
305
+ leafId: ctx.sessionManager.getLeafId(),
306
+ contextTokens: usage?.tokens ?? null,
307
+ contextPercent: usage?.percent ?? null,
308
+ config: this.resolveConfig(ctx),
309
+ hasUI: ctx.hasUI,
310
+ notify: (message, level) => ctx.ui.notify(message, level),
311
+ setStatus: (message) => ctx.ui.setStatus(CHECKPOINT_STATUS_ID, message),
312
+ });
313
+ }
314
+
315
+ clearSession(sessionId: string): void {
316
+ this.scopes.delete(sessionId);
317
+ this.templates.delete(sessionId);
318
+ this.requestTails.delete(sessionId);
319
+ this.transport.close(sessionId);
320
+ }
321
+
322
+ private async acquireRequest(sessionId: string | undefined): Promise<() => void> {
323
+ if (!sessionId) return () => {};
324
+ const previous = this.requestTails.get(sessionId) ?? Promise.resolve();
325
+ let releaseCurrent!: () => void;
326
+ const current = new Promise<void>((resolve) => {
327
+ releaseCurrent = resolve;
328
+ });
329
+ this.requestTails.set(sessionId, current);
330
+ await previous;
331
+ return () => {
332
+ releaseCurrent();
333
+ if (this.requestTails.get(sessionId) === current) this.requestTails.delete(sessionId);
334
+ };
335
+ }
336
+
337
+ createProvider(base: Provider): Provider {
338
+ return {
339
+ ...base,
340
+ stream: (model, context, options) =>
341
+ this.stream(model, context, options as OpenAICodexResponsesOptions | undefined),
342
+ streamSimple: (model, context, options) => this.streamSimple(model, context, options),
343
+ } as Provider;
344
+ }
345
+
346
+ private wireHistory(
347
+ model: Model<any>,
348
+ context: Context,
349
+ grammarToolInputProperties: GrammarToolInputProperties,
350
+ ): ResponsesItem[] {
351
+ const sessionId = (context as Context & { sessionId?: string }).sessionId;
352
+ const scope = sessionId ? this.scopes.get(sessionId) : undefined;
353
+ if (!scope) {
354
+ const compat = model.compat as CodexCompat | undefined;
355
+ const nativeItems = new Map<string, ResponsesItem[]>();
356
+ return convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, {
357
+ includeSystemPrompt: false,
358
+ grammarToolInputProperties,
359
+ deferredTools: splitDeferredTools(context, Boolean(compat?.supportsToolSearch)).deferred,
360
+ toolOptions: {
361
+ strict: null,
362
+ supportsStrictMode: compat?.supportsStrictMode ?? true,
363
+ supportsOpenAIGrammarTools: compat?.supportsOpenAIGrammarTools ?? false,
364
+ },
365
+ namespacedToolNames: CODEX_NAMESPACED_TOOL_NAMES,
366
+ textContentItemToolResultNames: CODEX_TEXT_CONTENT_ITEM_TOOL_RESULT_NAMES,
367
+ toolResultImageDetail: "auto",
368
+ nativeAssistantItems: nativeItems,
369
+ }) as ResponsesItem[];
370
+ }
371
+ return providerHistory({
372
+ branch: scope.manager.getBranch() as SessionEntry[],
373
+ wireModel: model,
374
+ allTools: this.pi.getAllTools(),
375
+ grammarToolInputProperties,
376
+ imageDetail: scope.config.imageDetail,
377
+ });
378
+ }
379
+
380
+ private buildRequestBody(
381
+ model: Model<any>,
382
+ context: Context,
383
+ options: OpenAICodexResponsesOptions,
384
+ sessionId: string | undefined,
385
+ grammarToolInputProperties: GrammarToolInputProperties,
386
+ ): JsonRecord {
387
+ const compat = model.compat as CodexCompat | undefined;
388
+ const toolPlacement = splitDeferredTools(context, Boolean(compat?.supportsToolSearch));
389
+ const body: JsonRecord = {
390
+ model: model.id,
391
+ store: false,
392
+ stream: true,
393
+ instructions: context.systemPrompt || "You are a helpful assistant.",
394
+ input: this.wireHistory(
395
+ model,
396
+ Object.assign({}, context, { sessionId }),
397
+ grammarToolInputProperties,
398
+ ),
399
+ text: { verbosity: options.textVerbosity ?? "low" },
400
+ include: ["reasoning.encrypted_content"],
401
+ prompt_cache_key: sessionId,
402
+ tool_choice: options.toolChoice ?? "auto",
403
+ parallel_tool_calls: true,
404
+ };
405
+ if (options.temperature !== undefined) body["temperature"] = options.temperature;
406
+ if (options.serviceTier !== undefined) body.service_tier = options.serviceTier;
407
+ if (toolPlacement.immediate.length > 0) {
408
+ body.tools = convertResponsesTools(toolPlacement.immediate, {
409
+ strict: null,
410
+ supportsStrictMode: compat?.supportsStrictMode ?? true,
411
+ supportsOpenAIGrammarTools: compat?.supportsOpenAIGrammarTools ?? false,
412
+ namespacedToolNames: CODEX_NAMESPACED_TOOL_NAMES,
413
+ });
414
+ }
415
+ if (options.reasoningEffort !== undefined) {
416
+ const mapped =
417
+ options.reasoningEffort === "none"
418
+ ? (model.thinkingLevelMap?.off ?? "none")
419
+ : (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort);
420
+ if (mapped !== null) {
421
+ body["reasoning"] = {
422
+ effort: mapped,
423
+ summary: options.reasoningSummary ?? "auto",
424
+ };
425
+ }
426
+ }
427
+ return body;
428
+ }
429
+
430
+ private async performCompaction(options: {
431
+ model: Model<any>;
432
+ requestOptions: OpenAICodexResponsesOptions;
433
+ history: ResponsesItem[];
434
+ postCompactionTail?: ResponsesItem[];
435
+ template: JsonRecord;
436
+ instructions: string;
437
+ grammarToolInputProperties: GrammarToolInputProperties;
438
+ priority: boolean;
439
+ }): Promise<{ checkpoint: CheckpointData; usage?: Usage }> {
440
+ const sessionId = options.requestOptions.sessionId;
441
+ if (!sessionId) throw new Error("Codex compaction requires a Pi session id.");
442
+ const payload = remoteCompactionPayload({
443
+ template: options.template,
444
+ modelId: options.model.id,
445
+ history: options.history,
446
+ instructions: options.instructions,
447
+ sessionId,
448
+ priority: options.priority,
449
+ });
450
+ const transformed = await options.requestOptions.onPayload?.(payload, options.model);
451
+ const request = isObject(transformed) ? transformed : payload;
452
+ const compacted = await collectRemoteCompaction(
453
+ this.transport.request(options.model, request, options.requestOptions),
454
+ options.model,
455
+ options.priority,
456
+ );
457
+ return {
458
+ checkpoint: checkpointData(
459
+ options.model.id,
460
+ options.history,
461
+ compacted.item,
462
+ options.postCompactionTail,
463
+ ),
464
+ ...(compacted.usage ? { usage: compacted.usage } : {}),
465
+ };
466
+ }
467
+
468
+ private async maybeCompactPercentage(
469
+ model: Model<any>,
470
+ context: Context,
471
+ options: OpenAICodexResponsesOptions,
472
+ body: JsonRecord,
473
+ grammarToolInputProperties: GrammarToolInputProperties,
474
+ ): Promise<JsonRecord> {
475
+ const sessionId = options.sessionId;
476
+ const scope = sessionId ? this.scopes.get(sessionId) : undefined;
477
+ const threshold = scope?.config.autoCompactAtPercent;
478
+ if (
479
+ !scope ||
480
+ threshold === undefined ||
481
+ scope.contextPercent === null ||
482
+ scope.contextPercent < threshold ||
483
+ !Array.isArray(body.input)
484
+ ) {
485
+ return body;
486
+ }
487
+
488
+ const branch = scope.manager.getBranch() as SessionEntry[];
489
+ const checkpoint = searchCheckpoint(branch);
490
+ const hasNewAssistant =
491
+ checkpoint.kind !== "found" ||
492
+ branch
493
+ .slice(checkpoint.entryIndex + 1)
494
+ .some((entry) => entry.type === "message" && entry.message.role === "assistant");
495
+ if (!hasNewAssistant) return body;
496
+
497
+ const history = body.input.map((item) => {
498
+ if (!isResponsesItem(item))
499
+ throw new Error("Codex request history contains an invalid item.");
500
+ return structuredClone(item);
501
+ });
502
+ const split = splitUnsampledUserInput({
503
+ branch,
504
+ history,
505
+ model,
506
+ allTools: this.pi.getAllTools(),
507
+ grammarToolInputProperties,
508
+ imageDetail: scope.config.imageDetail,
509
+ });
510
+ if (split.kind === "unsafe") {
511
+ scope.notify(
512
+ "OpenAI Codex percentage compaction was deferred because unsampled input could not be isolated safely.",
513
+ "warning",
514
+ );
515
+ return body;
516
+ }
517
+
518
+ scope.setStatus("Codex compacting…");
519
+ try {
520
+ const compacted = await this.performCompaction({
521
+ model,
522
+ requestOptions: options,
523
+ history: split.history,
524
+ postCompactionTail: split.tail,
525
+ template: withoutConversationInput(body),
526
+ instructions:
527
+ typeof body.instructions === "string"
528
+ ? body.instructions
529
+ : context.systemPrompt || "You are a helpful assistant.",
530
+ grammarToolInputProperties,
531
+ priority: scope.config.fastMode,
532
+ });
533
+ const firstKeptEntryId = userEntryAfterLastSampled(branch)?.id ?? scope.manager.getLeafId();
534
+ if (!firstKeptEntryId || typeof scope.manager.appendCompaction !== "function") {
535
+ throw new Error("Pi's mutable SessionManager is unavailable for percentage compaction.");
536
+ }
537
+ if (scope.manager.getLeafId() !== scope.leafId) {
538
+ throw new Error("Pi's active session branch changed while Codex was compacting.");
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);
555
+ }
556
+ }
557
+
558
+ stream(
559
+ model: Model<any>,
560
+ context: Context,
561
+ options?: OpenAICodexResponsesOptions,
562
+ ): AssistantMessageEventStream {
563
+ const stream = createAssistantMessageEventStream();
564
+ const requestOptions = transportOptions(options);
565
+ void (async () => {
566
+ const output: AssistantMessage = {
567
+ role: "assistant",
568
+ content: [],
569
+ api: CODEX_API,
570
+ provider: model.provider,
571
+ 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
+ },
580
+ stopReason: "pending",
581
+ timestamp: Date.now(),
582
+ };
583
+ const runtimeSessionId = requestOptions.sessionId;
584
+ const releaseRequest = await this.acquireRequest(runtimeSessionId);
585
+ try {
586
+ const cacheSessionId = clampPromptCacheKey(runtimeSessionId);
587
+ const grammarToolInputProperties = createGrammarToolInputProperties(
588
+ context.tools,
589
+ (model.compat as CodexCompat | undefined)?.supportsOpenAIGrammarTools ?? false,
590
+ );
591
+ let body = this.buildRequestBody(
592
+ model,
593
+ context,
594
+ requestOptions,
595
+ cacheSessionId,
596
+ grammarToolInputProperties,
597
+ );
598
+ const transformed = await requestOptions.onPayload?.(body, model);
599
+ if (isObject(transformed)) body = transformed;
600
+ if (runtimeSessionId) {
601
+ this.templates.set(runtimeSessionId, {
602
+ modelId: model.id,
603
+ payload: withoutConversationInput(body),
604
+ grammarToolInputProperties,
605
+ requestOptions: { ...requestOptions },
606
+ });
607
+ }
608
+ body = await this.maybeCompactPercentage(
609
+ model,
610
+ context,
611
+ requestOptions,
612
+ body,
613
+ grammarToolInputProperties,
614
+ );
615
+
616
+ const rawItems: ResponsesItem[] = [];
617
+ const responseMetadata: { serviceTier?: string } = {};
618
+ stream.push({ type: "start", partial: output });
619
+ await processCodexStream(
620
+ captureRawEvents(
621
+ this.transport.request(model, body, requestOptions),
622
+ rawItems,
623
+ responseMetadata,
624
+ ),
625
+ output,
626
+ stream,
627
+ model,
628
+ grammarToolInputProperties,
629
+ );
630
+ if (!successfulStopReason(output)) {
631
+ throw new Error(output.errorMessage || "Codex stream ended without a successful stop.");
632
+ }
633
+ applyServiceTierPricing(
634
+ output.usage,
635
+ model,
636
+ body.service_tier,
637
+ responseMetadata.serviceTier,
638
+ );
639
+
640
+ const compat = model.compat as CodexCompat | undefined;
641
+ const canonicalContext: Context = {
642
+ messages: [output],
643
+ ...(context.tools ? { tools: context.tools } : {}),
644
+ };
645
+ const canonicalItems = convertResponsesMessages(
646
+ model,
647
+ canonicalContext,
648
+ CODEX_TOOL_CALL_PROVIDERS,
649
+ {
650
+ includeSystemPrompt: false,
651
+ grammarToolInputProperties,
652
+ deferredTools: splitDeferredTools(context, Boolean(compat?.supportsToolSearch))
653
+ .deferred,
654
+ toolOptions: {
655
+ strict: null,
656
+ supportsStrictMode: compat?.supportsStrictMode ?? true,
657
+ supportsOpenAIGrammarTools: compat?.supportsOpenAIGrammarTools ?? false,
658
+ },
659
+ namespacedToolNames: CODEX_NAMESPACED_TOOL_NAMES,
660
+ textContentItemToolResultNames: CODEX_TEXT_CONTENT_ITEM_TOOL_RESULT_NAMES,
661
+ toolResultImageDetail:
662
+ (runtimeSessionId
663
+ ? this.scopes.get(runtimeSessionId)?.config.imageDetail
664
+ : undefined) ?? "auto",
665
+ },
666
+ ).filter(
667
+ (item) =>
668
+ item["type"] !== "function_call_output" && item["type"] !== "custom_tool_call_output",
669
+ );
670
+ if (rawItems.length > 0 && nativeOverrideRequired(rawItems, canonicalItems)) {
671
+ if (!output.responseId) throw new Error("Codex response is missing a response id.");
672
+ this.pi.appendEntry(
673
+ NATIVE_RESPONSE_ENTRY_TYPE,
674
+ nativeResponseData(model.id, output.responseId, rawItems),
675
+ );
676
+ }
677
+
678
+ stream.push({ type: "done", reason: output.stopReason, message: output });
679
+ stream.end();
680
+ } catch (error) {
681
+ output.stopReason = requestOptions.signal?.aborted ? "aborted" : "error";
682
+ output.errorMessage = error instanceof Error ? error.message : String(error);
683
+ stream.push({ type: "error", reason: output.stopReason, error: output });
684
+ stream.end();
685
+ } finally {
686
+ releaseRequest();
687
+ }
688
+ })();
689
+ return stream;
690
+ }
691
+
692
+ streamSimple(
693
+ model: Model<any>,
694
+ context: Context,
695
+ options?: SimpleStreamOptions,
696
+ ): AssistantMessageEventStream {
697
+ const effort = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
698
+ return this.stream(model, context, {
699
+ ...options,
700
+ ...(effort && effort !== "off" ? { reasoningEffort: effort } : {}),
701
+ });
702
+ }
703
+
704
+ latestTemplate(sessionId: string): RequestTemplate | undefined {
705
+ return this.templates.get(sessionId);
706
+ }
707
+
708
+ compact(options: {
709
+ model: Model<any>;
710
+ requestOptions: OpenAICodexResponsesOptions;
711
+ history: ResponsesItem[];
712
+ instructions: string;
713
+ grammarToolInputProperties: GrammarToolInputProperties;
714
+ template: JsonRecord;
715
+ priority: boolean;
716
+ }): Promise<{ checkpoint: CheckpointData; usage?: Usage }> {
717
+ return this.acquireRequest(options.requestOptions.sessionId).then(async (release) => {
718
+ try {
719
+ return await this.performCompaction(options);
720
+ } finally {
721
+ release();
722
+ }
723
+ });
724
+ }
725
+ }
726
+
727
+ export function registerCodexProvider(
728
+ pi: ExtensionAPI,
729
+ resolveConfig: ConfigResolver,
730
+ ): CodexProviderRuntime {
731
+ const runtime = new CodexProviderRuntime(pi, resolveConfig);
732
+ pi.on("session_start", (_event, ctx) => {
733
+ const base =
734
+ ctx.modelRegistry.getRegisteredNativeProvider(CODEX_PROVIDER) ??
735
+ ctx.modelRegistry.getProvider(CODEX_PROVIDER);
736
+ if (!base) throw new Error("Pi's built-in OpenAI Codex provider is unavailable.");
737
+ pi.registerProvider(runtime.createProvider(base));
738
+ });
739
+ return runtime;
740
+ }