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.
- package/CHANGELOG.md +36 -0
- package/README.md +71 -37
- 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/codex-cache-diagnostics.ts +97 -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 +1 -0
- package/extensions/openai-codex-compat/codex-provider.ts +524 -58
- package/extensions/openai-codex-compat/codex-stream.ts +52 -26
- package/extensions/openai-codex-compat/codex-thread-lineage.ts +156 -0
- package/extensions/openai-codex-compat/codex-transport.ts +1129 -86
- package/extensions/openai-codex-compat/compaction-checkpoint.ts +2 -2
- package/extensions/openai-codex-compat/config.ts +13 -0
- package/extensions/openai-codex-compat/index.ts +6 -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/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/settings-pane.ts +11 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.4 - 2026-08-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Persist and send the official Codex installation/window request identity used for backend affinity, advancing the window after successful compaction.
|
|
10
|
+
- Persist structured Codex transport-recovery diagnostics on assistant messages for rejected or locally bypassed WebSocket continuations and WebSocket-to-SSE recovery, including request modes, cache-affinity preservation, and exact response ids.
|
|
11
|
+
- Prewarm only the static instruction/tool prefix with Responses v2 `generate: false`, generate the dynamic conversation from its continuation, and persist whether prewarm established continuation state.
|
|
12
|
+
- Send Pi-derived Codex session, thread, turn, and request-kind metadata through `client_metadata` and compatible request headers, with one turn id shared across each Pi agent run.
|
|
13
|
+
- Capture server-issued Codex turn state once per agent run, replay it across WebSocket retries, SSE requests, and WebSocket-to-SSE fallback, and include the exact value in transport diagnostics.
|
|
14
|
+
- Persist cache diagnostics with exact session, account, cache, turn, response, and routing-state identifiers alongside transport selection, full/delta request sizes, static-prefix fingerprints, and reported cache read/write usage.
|
|
15
|
+
- Record the exact baseline and current history items when local WebSocket continuation validation rejects a changed prefix.
|
|
16
|
+
- Persist hidden thread markers for finalized `/tree` branches, send branch-specific Codex thread/fork/window lineage while retaining the root prompt-cache identity, and reset WebSocket continuation state when switching threads.
|
|
17
|
+
- Add a default-off `responsesLite` setting, environment override, and `/codex-settings` control for switching supported GPT-5.6 models between ordinary Responses and Responses Lite.
|
|
18
|
+
- Use Codex's Responses Lite input envelope, metadata, request header, tool grouping, and all-turn reasoning context for GPT-5.6 Sol, Terra, and Luna.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Align Responses Lite with current Codex default-tool namespaces, HTTP/WebSocket metadata placement, prewarm identity, and per-request WebSocket timing metadata.
|
|
23
|
+
- Align ordinary function declarations with Codex's explicit `strict: false` and ignore Pi's generic temperature option because Codex Responses has no temperature request field.
|
|
24
|
+
- Send the official thread-scoped `x-client-request-id` on Responses HTTP/SSE requests and require all prompt-cache affinity identifiers to match before diagnostics report alignment.
|
|
25
|
+
- Continue provider-owned sampling when `response.completed.end_turn` is false, and resample retryable `response.failed` and `response.incomplete` events from the completed output history already received.
|
|
26
|
+
- Send official nested Responses Compaction v2 metadata, classifying manual, threshold, provider-boundary, and overflow compaction by trigger, reason, phase, implementation, and strategy.
|
|
27
|
+
- Align first-party Codex Responses, WebSocket, and compaction requests with the official model-and-tier routing hint.
|
|
28
|
+
- Match official Codex request continuity more closely by always serializing the ordinary `tools` array, omitting the legacy SSE beta header, ignoring response-only stream options and internal item metadata during WebSocket delta comparison, retaining healthy session sockets, and applying jittered retry backoff.
|
|
29
|
+
- Resample retryable SSE HTTP failures and dropped streams up to five times before model-visible output while preserving cache and account identity; fail closed after visible output.
|
|
30
|
+
- Retry retryable WebSocket failures on up to five fresh connections before selecting sticky SSE when no model-visible output has been emitted.
|
|
31
|
+
- Treat WebSocket response metadata as non-visible during retry decisions and send the Responses Lite HTTP marker only on SSE while retaining its WebSocket body metadata.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Preserve completed native output items, discard only unfinished attempt content, and accumulate usage across provider-owned response resampling.
|
|
36
|
+
- Normalize missing, empty, and `functions` namespaces for Responses Lite function and custom calls, preserve the server's flat default calls in continuation history, and reject invalid default-namespace members before sending.
|
|
37
|
+
- Omit `reasoning.mode` for the default GPT-5.6 standard mode and send the field only when pro mode is selected.
|
|
38
|
+
- Continue interrupted Codex tasks automatically after an exact output-token-limit response, including after successful Pi threshold compaction, without treating incomplete WebSocket responses as completed continuation state.
|
|
39
|
+
- Render delete-and-recreate replacements and repeated in-place `apply_patch` operations as one logical file with a recomputed initial-to-final diff instead of counting the path multiple times.
|
|
40
|
+
|
|
5
41
|
## 0.0.3 - 2026-08-04
|
|
6
42
|
|
|
7
43
|
### Added
|
package/README.md
CHANGED
|
@@ -31,46 +31,61 @@ Authenticate through Pi if needed:
|
|
|
31
31
|
|
|
32
32
|
## Compatibility baseline and differences
|
|
33
33
|
|
|
34
|
-
The compatibility baseline is official Codex CLI `0.146.0`, released July 29, 2026. Upstream `main` at commit `
|
|
34
|
+
The compatibility baseline is official Codex CLI `0.146.0`, released July 29, 2026. Upstream `main` at commit `0bdce9f424eb9b39d7b3a8811742d10b6fbf8d54` was also inspected on August 7, 2026, including post-release routing and default-tool namespace changes. Current upstream, rather than the older installed CLI, is authoritative where they differ: commit `f21dc46388` replaced direct Lite function/custom declarations with one canonical `functions` namespace. This section is the package's explicit compatibility contract: it distinguishes close protocol adaptations from deliberate Pi behavior, configurable defaults, known gaps, and unsupported Codex runtimes. See the [Responses Lite compatibility report](RESPONSES_LITE_COMPATIBILITY.md) and [Codex caching and transport comparison](CODEX_CACHE_RESEARCH.md) for source revisions, request-path findings, and live cache trajectories.
|
|
35
35
|
|
|
36
36
|
### Configurable defaults that differ from Codex
|
|
37
37
|
|
|
38
|
-
| Area | This package by default
|
|
39
|
-
| --------------------------------------------- |
|
|
40
|
-
| Generated-image detail sent back to the model | Sends image tool-result content with `input_image.detail: "auto"`. On GPT-5.6, `auto` uses original-size image accounting.
|
|
41
|
-
| Image-generation tool | Enabled whenever an `openai-codex` model is selected. Backend capability and account failures surface when the tool executes.
|
|
42
|
-
| Standalone `web.run` | Disabled by default; when enabled, preferred over hosted `web_search` and sent with the complete reserved schema and description.
|
|
43
|
-
| Hosted web search | Disabled by default; when enabled, injected only while `web.run` is inactive.
|
|
44
|
-
| Coding mutation tools | Enables `apply_patch` and suppresses Pi's active `edit` and `write` tools.
|
|
45
|
-
| Codex tool background | Uses a subtle theme-derived surface for extension-owned Codex tools.
|
|
46
|
-
| Auto-compaction trigger | Relies on Pi's reserve-token threshold unless a percentage is configured.
|
|
47
|
-
| Fast mode | Uses the normal tier.
|
|
48
|
-
|
|
|
38
|
+
| Area | This package by default | Official Codex | Configuration |
|
|
39
|
+
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
40
|
+
| Generated-image detail sent back to the model | Sends image tool-result content with `input_image.detail: "auto"`. On GPT-5.6, `auto` uses original-size image accounting. | Uses `high`. | `imageDetail`: `auto`, `low`, `high`, or `original`. |
|
|
41
|
+
| Image-generation tool | Enabled whenever an `openai-codex` model is selected. Backend capability and account failures surface when the tool executes. | Stable and enabled by default, but additionally gated by plan, model, provider, authentication, image-generation, and namespace capabilities. | `imageGeneration`: boolean. |
|
|
42
|
+
| Standalone `web.run` | Disabled by default; when enabled, preferred over hosted `web_search` and sent with the complete reserved schema and description. | Enabled by default for `gpt-5.6-sol` through Responses Lite; otherwise subject to standalone-search feature and runtime gates. | `webRun`: boolean. |
|
|
43
|
+
| Hosted web search | Disabled by default; when enabled, injected only for ordinary Responses while `web.run` is inactive. Responses Lite omits hosted tools. | Omitted for `gpt-5.6-sol` while standalone `web.run` is available; otherwise defaults to cached mode when hosted search is supported. | `webRun` and `webSearch`: `disabled`, `cached`, `indexed`, or `live`. |
|
|
44
|
+
| Coding mutation tools | Enables `apply_patch` and suppresses Pi's active `edit` and `write` tools. | Chooses its tool surface from model metadata and runtime capabilities; there are no Pi `edit` or `write` tools to suppress. | `applyPatch`: boolean. |
|
|
45
|
+
| Codex tool background | Uses a subtle theme-derived surface for extension-owned Codex tools. | Uses Codex's own TUI activity cells rather than Pi tool rows. | `toolBackground`: `subtle`, `status`, or `none`. |
|
|
46
|
+
| Auto-compaction trigger | Relies on Pi's reserve-token threshold unless a percentage is configured. | Tracks Codex's model/token-budget state before and between sampling steps. | `autoCompactAtPercent`: percentage or unset. Pi's own compaction settings remain separate. |
|
|
47
|
+
| Fast mode | Uses the normal tier. | Uses the configured Codex service tier. | `fastMode`: boolean; `true` requests the priority tier. |
|
|
48
|
+
| Responses Lite | Disabled; supported GPT-5.6 models use ordinary Responses. | Enabled according to Codex model metadata. | `responsesLite`: boolean; `true` enables Responses Lite. |
|
|
49
|
+
| Text and reasoning request controls | Sends low text verbosity and automatic reasoning summaries; omits the default GPT-5.6 standard mode and sends `reasoning.mode` only for pro mode. | Resolves these controls through Codex configuration, model metadata, and turn state. | `textVerbosity`, `reasoningSummary`, and `reasoningMode`. |
|
|
49
50
|
|
|
50
51
|
`web.run` is a reserved GPT-5.6 tool name. Its declaration therefore reproduces the complete current Codex post-normalization `SearchCommands` schema and official tool description instead of using Pi's normal compact tool schema. This intentionally omits generated annotations such as `format` and `minimum` that Codex removes before sending the declaration to Responses.
|
|
51
52
|
|
|
52
53
|
### Non-configurable implementation differences
|
|
53
54
|
|
|
54
|
-
| Area | Difference
|
|
55
|
-
| --------------------------- |
|
|
56
|
-
| Session storage | Pi remains the canonical session owner. Opaque Codex compaction checkpoints are stored in Pi `compaction` entries, and otherwise lossy Responses output is stored in sparse custom native-response entries. Official Codex owns a rollout/thread store directly.
|
|
57
|
-
| Branching | Checkpoints and native response overrides follow Pi's active session branch. Official Codex uses its own thread, turn, rollback, fork, and context-window lineage.
|
|
58
|
-
| Model switching | This package rejects model switches while the active Pi branch contains a native Codex checkpoint because the checkpoint is model-specific.
|
|
59
|
-
| System instructions | Pi rebuilds the current system prompt
|
|
60
|
-
| Turn metadata | Requests
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
| Image
|
|
71
|
-
| Image
|
|
72
|
-
|
|
|
73
|
-
|
|
|
55
|
+
| Area | Difference |
|
|
56
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
57
|
+
| Session storage | Pi remains the canonical session owner. Opaque Codex compaction checkpoints are stored in Pi `compaction` entries, and otherwise lossy Responses output is stored in sparse custom native-response entries. Official Codex owns a rollout/thread store directly. |
|
|
58
|
+
| Branching | Checkpoints and native response overrides follow Pi's active session branch. Official Codex uses its own thread, turn, rollback, fork, and context-window lineage. |
|
|
59
|
+
| Model switching | This package rejects model switches while the active Pi branch contains a native Codex checkpoint because the checkpoint is model-specific. |
|
|
60
|
+
| System instructions | Pi rebuilds the current system prompt. Responses Lite models prepend it as developer input after `additional_tools`; other models send it through Responses `instructions`. Normal Pi history does not store it as replayed system/developer input. `/reload` updates the next request without rewriting old checkpoints. |
|
|
61
|
+
| Turn metadata | Requests send a persisted installation id plus Pi-derived session, thread, context-window, turn, source, sandbox, request-kind, and nested compaction-operation metadata in `client_metadata` and compatible headers. The in-memory context-window number advances after successful compaction. One turn id is reused throughout a Pi agent run, while prewarm has its own id. First-party requests also carry Codex's model-and-tier routing hint. The provider captures the server-issued `x-codex-turn-state` once per agent run, replays it on WebSocket retries, SSE requests, and WebSocket-to-SSE fallback, and records all identity values in transport diagnostics. Pi does not reconstruct prior window number after extension reload/session resume or reproduce workspace Git/parent/subagent/Code Mode metadata. Each marked Pi tree branch receives its own persisted thread UUID. |
|
|
62
|
+
| Cache preparation | Before the first cache-enabled WebSocket turn, the package prewarms only the stable instruction/tool prefix: ordinary Responses uses empty `input`, while Responses Lite uses `additional_tools` plus the developer instructions. The first generated request then contributes only dynamic conversation input to the continuation. No explicit prompt-cache breakpoints are added. |
|
|
63
|
+
| Mid-turn compaction | Provider-boundary percentage compaction installs a checkpoint and continues the intercepted request. Pi threshold compaction normally runs after the agent response; after Codex output-token truncation, the extension queues a hidden continuation so threshold compaction completes before sampling resumes. Official Codex owns this sampling and compaction loop directly. |
|
|
64
|
+
| Provider-owned follow-up | Completed responses with `end_turn: false` continue immediately from completed native output without synthetic user input. Retryable `response.failed` and all `response.incomplete` events are resampled with the official five-retry stream budget, preserving completed output and cumulative usage while excluding unfinished attempt content. A `max_output_tokens` response that exhausts this budget still becomes Pi `stopReason: "length"` and uses the extension's unbounded host-level continuation recovery. |
|
|
65
|
+
| Compaction lifecycle events | Percentage compaction writes through Pi's mutable session manager but cannot emit Pi's internal `session_compact` event through the public extension API. Manual, threshold, and overflow compactions initiated by Pi do emit the normal lifecycle. |
|
|
66
|
+
| Header hooks | An internal percentage-compaction request reuses the already transformed provider headers. It cannot independently rerun Pi's `before_provider_headers` hook. |
|
|
67
|
+
| Native retained context | Deliberately differs from current Codex. The package retains recent user/developer/system messages under the 64k budget before the opaque compaction item. Current Codex applies a second installed-history filter that drops developer/system wrappers and non-real-user messages, can retain eligible structured agent commentary, and trims oversized function outputs before compaction. Pi keeps its existing checkpoint shape by design. |
|
|
68
|
+
| Tool namespaces | Responses Lite groups Pi's ordinary function/custom declarations into upstream's canonical `functions` namespace and maps that default namespace back to bare Pi names. Pi registers dotted names such as `web.run` as exact flat identifiers, so the provider converts only the fixed extension-owned allowlist into non-default Responses namespace/member identities and rejects unknown or ambiguously flat namespaced calls. |
|
|
69
|
+
| Capability gating | Tool activation is based on the selected `openai-codex` provider plus package settings. It does not reproduce every official model-metadata, plan, feature-stage, executor, mode, or account gate. |
|
|
70
|
+
| Sandbox and approvals | Pi extensions run with full process permissions. `apply_patch`, local image reads, generated-image writes, and sibling Codex endpoints do not use Codex's sandbox, permission-profile, or approval lifecycle. |
|
|
71
|
+
| Image tool instructions | The package retains the server-reserved image-generation schema while replacing Rust-specific path annotations and Codex Code Mode instructions with model-facing descriptions, a prompt snippet, and system-prompt guidelines. Image-count bounds and selector exclusivity are enforced before execution. |
|
|
72
|
+
| Image artifact hint | When image saving succeeds, this package always returns the path hint, says “the generated image,” and has no 1,024-byte cutoff. Official Codex says “a generated image” and omits the hint when it exceeds 1,024 UTF-8 bytes. |
|
|
73
|
+
| Image artifacts | Generated files use Pi's agent directory and the Pi session/tool-call IDs. Official Codex uses its own artifact/output-directory lifecycle. |
|
|
74
|
+
| Web references | `web.run` structured results are retained branch-locally in Pi tool-result details rather than Codex extension events, and hosted native items are preserved for provider replay. Reference IDs are resolved remotely by `alpha/search`, as in Codex. Hosted citation annotations remain a separate unimplemented path. |
|
|
75
|
+
| UI | Pi renders its own conversation, footer, settings pane, branches, and compaction lifecycle. Extension-owned Codex tools have dedicated Pi renderers, but do not reproduce Codex app-server `WebSearchItem` or image-generation lifecycle notifications. |
|
|
76
|
+
|
|
77
|
+
Nested compaction metadata uses the official Responses Compaction v2 implementation and memento
|
|
78
|
+
strategy. Manual compaction is user-requested and standalone; threshold and provider-boundary
|
|
79
|
+
compaction are automatic context-limit operations in the pre-turn phase; overflow recovery is the
|
|
80
|
+
corresponding mid-turn operation.
|
|
81
|
+
|
|
82
|
+
When a finalized user prompt creates a new `/tree` branch, the extension inserts a hidden,
|
|
83
|
+
context-free custom marker as that prompt's parent. Navigation alone writes nothing. The marker is
|
|
84
|
+
hidden by Pi's default, no-tools, and user-only tree filters and appears only in the all-entries
|
|
85
|
+
filter. Root `session_id` and `prompt_cache_key` remain stable, while the branch gets a UUID
|
|
86
|
+
`thread_id`, `forked_from_thread_id`, and thread-scoped window number. Switching threads closes the
|
|
87
|
+
old WebSocket and discards its incompatible `previous_response_id` baseline; the new full-history
|
|
88
|
+
request remains eligible to reuse the common backend-cached prefix under the unchanged cache key.
|
|
74
89
|
|
|
75
90
|
### Tool and runtime coverage
|
|
76
91
|
|
|
@@ -138,6 +153,8 @@ Create a global configuration file at:
|
|
|
138
153
|
~/.pi/agent/openai-codex-compat.json
|
|
139
154
|
```
|
|
140
155
|
|
|
156
|
+
The extension also creates `openai-codex-compat-installation-id` in the active Pi agent directory. It contains the stable UUID used for official Codex installation metadata and is reused across sessions.
|
|
157
|
+
|
|
141
158
|
A trusted project can override it at:
|
|
142
159
|
|
|
143
160
|
```text
|
|
@@ -153,6 +170,7 @@ Example:
|
|
|
153
170
|
```json
|
|
154
171
|
{
|
|
155
172
|
"fastMode": true,
|
|
173
|
+
"responsesLite": true,
|
|
156
174
|
"toolBackground": "subtle",
|
|
157
175
|
"applyPatch": true,
|
|
158
176
|
"imageGeneration": true,
|
|
@@ -171,6 +189,7 @@ Defaults:
|
|
|
171
189
|
| Setting | Values | Default | Behavior |
|
|
172
190
|
| ---------------------- | ---------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
173
191
|
| `fastMode` | boolean | `false` | Adds `service_tier: "priority"` to requests while retaining the current `openai-codex` provider and model. |
|
|
192
|
+
| `responsesLite` | boolean | `false` | Uses Codex's Responses Lite input envelope on supported GPT-5.6 models when enabled. By default, those models use ordinary Responses instructions and tools. |
|
|
174
193
|
| `toolBackground` | `subtle`, `status`, `none` | `subtle` | Controls the shared self-rendered background for `apply_patch`, `image_gen.imagegen`, and `web.run`. `status` uses Pi's pending/success/error backgrounds; `none` keeps the custom layout transparent. |
|
|
175
194
|
| `applyPatch` | boolean | `true` | On selected `openai-codex` models, uses the extension's `apply_patch` tool instead of Pi's active `edit` and `write` tools. Other providers always use their normal Pi tool set. |
|
|
176
195
|
| `imageGeneration` | boolean | `true` | Enables the extension-owned `image_gen.imagegen` tool on selected `openai-codex` models. |
|
|
@@ -180,7 +199,7 @@ Defaults:
|
|
|
180
199
|
| `webSearch` | `disabled`, `cached`, `indexed`, `live` | `disabled` | Controls hosted search and standalone-search external access. `disabled` removes hosted search but leaves an independently enabled `web.run` in cached-only mode; `indexed` prefers indexed content; `live` permits live external access. |
|
|
181
200
|
| `textVerbosity` | `low`, `medium`, `high` | `low` | Sets Responses API `text.verbosity`. |
|
|
182
201
|
| `reasoningSummary` | `auto`, `concise`, `detailed`, `off` | `auto` | Sets `reasoning.summary` when reasoning is enabled; `off` omits the summary parameter. |
|
|
183
|
-
| `reasoningMode` | `standard`, `pro` | `standard` |
|
|
202
|
+
| `reasoningMode` | `standard`, `pro` | `standard` | Controls GPT-5.6 execution mode independently of Pi's reasoning-effort control. The default omits `reasoning.mode`; `pro` sends `reasoning.mode: "pro"`. |
|
|
184
203
|
|
|
185
204
|
Invalid JSON setting values are ignored and invalid JSON does not prevent Pi from starting. The settings pane never writes on ordinary changes, refuses to overwrite invalid JSON when `Enter` or `Ctrl+S` attempts to save, and retains unknown keys when saving. Project configuration is read only when the project is trusted.
|
|
186
205
|
|
|
@@ -189,6 +208,7 @@ Every setting can also be overridden for one Pi process with an environment vari
|
|
|
189
208
|
| Setting | Environment variable |
|
|
190
209
|
| ---------------------- | ------------------------------------------------ |
|
|
191
210
|
| `fastMode` | `PI_OPENAI_CODEX_COMPAT_FAST_MODE` |
|
|
211
|
+
| `responsesLite` | `PI_OPENAI_CODEX_COMPAT_RESPONSES_LITE` |
|
|
192
212
|
| `toolBackground` | `PI_OPENAI_CODEX_COMPAT_TOOL_BACKGROUND` |
|
|
193
213
|
| `applyPatch` | `PI_OPENAI_CODEX_COMPAT_APPLY_PATCH` |
|
|
194
214
|
| `imageGeneration` | `PI_OPENAI_CODEX_COMPAT_IMAGE_GENERATION` |
|
|
@@ -208,6 +228,7 @@ For example:
|
|
|
208
228
|
|
|
209
229
|
```bash
|
|
210
230
|
PI_OPENAI_CODEX_COMPAT_WEB_RUN=off \
|
|
231
|
+
PI_OPENAI_CODEX_COMPAT_RESPONSES_LITE=off \
|
|
211
232
|
PI_OPENAI_CODEX_COMPAT_IMAGE_DETAIL=high \
|
|
212
233
|
PI_OPENAI_CODEX_COMPAT_AUTO_COMPACT_AT_PERCENT=90 \
|
|
213
234
|
pi
|
|
@@ -222,7 +243,19 @@ The extension handles native compaction for `openai-codex`. It follows the Codex
|
|
|
222
243
|
3. Retain approximately 64,000 tokens of recent user, developer, and system context.
|
|
223
244
|
4. Persist the opaque checkpoint in the Pi session and replay it on later requests.
|
|
224
245
|
|
|
225
|
-
Ordinary responses and compaction use the same extension-managed SSE/WebSocket transport.
|
|
246
|
+
Ordinary responses and compaction use the same extension-managed SSE/WebSocket transport. Before the first WebSocket turn for a session and model, the provider performs a best-effort v2 `generate: false` prewarm of the static instruction/tool prefix, then generates the dynamic conversation input from its continuation. With `responsesLite: false`, supported GPT-5.6 models use the ordinary Responses envelope and receive their own ordinary-prefix prewarm.
|
|
247
|
+
|
|
248
|
+
Healthy session WebSockets remain available until the server closes them or Pi tears down the session. Retryable WebSocket failures before model-visible output receive up to five fresh-connection retries before the session switches to sticky SSE. Retryable SSE HTTP failures and dropped streams receive up to five same-request resampling attempts before model-visible output. Both transports use Codex-style exponential backoff with ±10% jitter and preserve the same prompt-cache, session, account, installation, and window identities. Server metadata is not considered model-visible output, so a routing-state-only response can still be retried safely. Transport failures after model-visible output fail closed rather than risk duplicate text or tool calls. Explicit retryable `response.failed`/`response.incomplete` protocol terminals instead return to the provider-owned sampling loop, which preserves completed output items as the next request's history.
|
|
249
|
+
|
|
250
|
+
The provider stores a native response override only when Pi's canonical assistant representation cannot round-trip the provider output exactly; normal text, reasoning, and tool responses therefore do not duplicate session data. Native overrides are associated with canonical assistants by response id and replayed only when they are present on the active Pi branch.
|
|
251
|
+
|
|
252
|
+
Transparent prewarm, requests, continuation, and transport recovery are recorded in the resulting assistant message's `diagnostics` array in Pi's session JSONL:
|
|
253
|
+
|
|
254
|
+
- `codex_transport_prewarm` records whether static prewarm completed, established continuation state, and received turn state.
|
|
255
|
+
- `codex_transport_request` records the selected transport, full/delta input counts and byte sizes, exact session/account/cache/turn/response/routing-state identifiers, static-prefix and request-template fingerprints, instruction/tool fingerprints, cache affinity, and reported cache read/write token usage.
|
|
256
|
+
- `codex_transport_recovery` identifies fresh-WebSocket and SSE retries, rejected or locally bypassed continuations, and WebSocket-to-SSE recovery, including the exact triggering error, attempted request modes, and whether cache and account affinity were preserved.
|
|
257
|
+
|
|
258
|
+
Diagnostics intentionally retain exact request, cache-affinity, response, account, and server routing identifiers so a local Pi session file contains enough information to trace retries and cache behavior directly. Prompt and tool contents are still represented by byte counts and SHA-256 fingerprints rather than duplicated into every diagnostic.
|
|
226
259
|
|
|
227
260
|
Any model switch is rejected while the active branch contains a native Codex checkpoint because checkpoints are model-specific. Navigate to a branch before the checkpoint or start a new session before switching. Toggling fast mode does not change the model id or invalidate the checkpoint.
|
|
228
261
|
|
|
@@ -348,9 +381,10 @@ npm run check
|
|
|
348
381
|
npm test
|
|
349
382
|
```
|
|
350
383
|
|
|
351
|
-
Run the credentialed Codex
|
|
352
|
-
|
|
353
|
-
each
|
|
384
|
+
Run the credentialed Pi/Codex integration tests separately. They load the real
|
|
385
|
+
extension into headless Pi sessions, use the real WebSocket service, and ask
|
|
386
|
+
the model to report all prior history markers after each text and tool
|
|
387
|
+
continuation:
|
|
354
388
|
|
|
355
389
|
```bash
|
|
356
390
|
mise run test:live:codex
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
visibleWidth,
|
|
8
8
|
wrapTextWithAnsi,
|
|
9
9
|
} from "@earendil-works/pi-tui";
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
type AppliedPatchChange,
|
|
12
|
+
type ApplyPatchDetails,
|
|
13
|
+
coalesceAppliedPatchChangesForRendering,
|
|
14
|
+
} from "./apply-patch-engine.ts";
|
|
11
15
|
import { usesLightToolPalette } from "./codex-tool-surface.ts";
|
|
12
16
|
|
|
13
17
|
type DiffLineKind = "add" | "delete" | "context";
|
|
@@ -154,7 +158,7 @@ export function isApplyPatchDetails(value: unknown): value is ApplyPatchDetails
|
|
|
154
158
|
|
|
155
159
|
function sortedChanges(details: ApplyPatchDetails, cwd: string): AppliedPatchChange[] {
|
|
156
160
|
if (!isApplyPatchDetails(details)) return [];
|
|
157
|
-
return details.changes.toSorted((left, right) =>
|
|
161
|
+
return coalesceAppliedPatchChangesForRendering(details.changes, cwd).toSorted((left, right) =>
|
|
158
162
|
comparePaths(resolve(cwd, left.path), resolve(cwd, right.path)),
|
|
159
163
|
);
|
|
160
164
|
}
|
|
@@ -674,6 +674,90 @@ function diffDetails(
|
|
|
674
674
|
return { displayDiff, additions, deletions };
|
|
675
675
|
}
|
|
676
676
|
|
|
677
|
+
function initialContent(change: AppliedPatchChange): string | undefined {
|
|
678
|
+
if (change.kind === "add") return change.overwrittenContent;
|
|
679
|
+
if (change.kind === "delete") return change.content;
|
|
680
|
+
return change.oldContent;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function finalContent(change: AppliedPatchChange): string | undefined {
|
|
684
|
+
if (change.kind === "delete") return undefined;
|
|
685
|
+
if (change.kind === "add") return change.content;
|
|
686
|
+
return change.newContent;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export function coalesceAppliedPatchChangesForRendering(
|
|
690
|
+
changes: readonly AppliedPatchChange[],
|
|
691
|
+
cwd: string,
|
|
692
|
+
): AppliedPatchChange[] {
|
|
693
|
+
const groups = new Map<string, { firstIndex: number; changes: AppliedPatchChange[] }>();
|
|
694
|
+
const rendered: Array<{ index: number; change: AppliedPatchChange }> = [];
|
|
695
|
+
|
|
696
|
+
for (const [index, change] of changes.entries()) {
|
|
697
|
+
if (change.kind === "update" && change.moveTo) {
|
|
698
|
+
// Moves span source and destination identities, so retain their existing operation-level row.
|
|
699
|
+
rendered.push({ index, change });
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
const key = resolvePatchPath(cwd, change.path);
|
|
703
|
+
const group = groups.get(key);
|
|
704
|
+
if (group) {
|
|
705
|
+
group.changes.push(change);
|
|
706
|
+
} else {
|
|
707
|
+
groups.set(key, { firstIndex: index, changes: [change] });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
for (const group of groups.values()) {
|
|
712
|
+
const first = group.changes[0]!;
|
|
713
|
+
const last = group.changes.at(-1)!;
|
|
714
|
+
if (group.changes.length === 1) {
|
|
715
|
+
rendered.push({ index: group.firstIndex, change: first });
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const oldContent = initialContent(first);
|
|
720
|
+
const newContent = finalContent(last);
|
|
721
|
+
if (oldContent === undefined) {
|
|
722
|
+
if (newContent === undefined) continue;
|
|
723
|
+
rendered.push({
|
|
724
|
+
index: group.firstIndex,
|
|
725
|
+
change: {
|
|
726
|
+
kind: "add",
|
|
727
|
+
path: first.path,
|
|
728
|
+
content: newContent,
|
|
729
|
+
...diffDetails("", newContent),
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (newContent === undefined) {
|
|
735
|
+
rendered.push({
|
|
736
|
+
index: group.firstIndex,
|
|
737
|
+
change: {
|
|
738
|
+
kind: "delete",
|
|
739
|
+
path: first.path,
|
|
740
|
+
content: oldContent,
|
|
741
|
+
...diffDetails(oldContent, ""),
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
rendered.push({
|
|
747
|
+
index: group.firstIndex,
|
|
748
|
+
change: {
|
|
749
|
+
kind: "update",
|
|
750
|
+
path: first.path,
|
|
751
|
+
oldContent,
|
|
752
|
+
newContent,
|
|
753
|
+
...diffDetails(oldContent, newContent),
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return rendered.toSorted((left, right) => left.index - right.index).map(({ change }) => change);
|
|
759
|
+
}
|
|
760
|
+
|
|
677
761
|
function emptyDetails(): ApplyPatchDetails {
|
|
678
762
|
return {
|
|
679
763
|
status: "completed",
|
|
@@ -883,10 +967,10 @@ export async function previewPatch(cwd: string, patch: string): Promise<ApplyPat
|
|
|
883
967
|
const operations = resolveOperations(cwd, parsed.operations);
|
|
884
968
|
await verifyOperations(operations);
|
|
885
969
|
const details = emptyDetails();
|
|
886
|
-
const changes =
|
|
970
|
+
const changes: AppliedPatchChange[] = [];
|
|
887
971
|
for (const operation of operations) {
|
|
888
972
|
if (operation.kind === "add") {
|
|
889
|
-
changes.
|
|
973
|
+
changes.push({
|
|
890
974
|
kind: "add",
|
|
891
975
|
path: operation.path,
|
|
892
976
|
content: operation.content,
|
|
@@ -897,7 +981,7 @@ export async function previewPatch(cwd: string, patch: string): Promise<ApplyPat
|
|
|
897
981
|
operation.absolutePath,
|
|
898
982
|
`Failed to read ${operation.absolutePath}`,
|
|
899
983
|
);
|
|
900
|
-
changes.
|
|
984
|
+
changes.push({
|
|
901
985
|
kind: "delete",
|
|
902
986
|
path: operation.path,
|
|
903
987
|
content,
|
|
@@ -909,7 +993,7 @@ export async function previewPatch(cwd: string, patch: string): Promise<ApplyPat
|
|
|
909
993
|
`Failed to read file to update ${operation.absolutePath}`,
|
|
910
994
|
);
|
|
911
995
|
const newContent = deriveNewContent(oldContent, operation.chunks, operation.absolutePath);
|
|
912
|
-
changes.
|
|
996
|
+
changes.push({
|
|
913
997
|
kind: "update",
|
|
914
998
|
path: operation.path,
|
|
915
999
|
...(operation.moveTo ? { moveTo: operation.moveTo } : {}),
|
|
@@ -919,7 +1003,7 @@ export async function previewPatch(cwd: string, patch: string): Promise<ApplyPat
|
|
|
919
1003
|
});
|
|
920
1004
|
}
|
|
921
1005
|
}
|
|
922
|
-
details.changes =
|
|
1006
|
+
details.changes = coalesceAppliedPatchChangesForRendering(changes, cwd);
|
|
923
1007
|
for (const change of details.changes) {
|
|
924
1008
|
if (change.kind === "add") details.added.push(change.path);
|
|
925
1009
|
else if (change.kind === "delete") details.deleted.push(change.path);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { type JsonRecord } from "./codex-protocol.ts";
|
|
3
|
+
import { usesResponsesLite } from "./responses-lite.ts";
|
|
4
|
+
|
|
5
|
+
type Fingerprint = {
|
|
6
|
+
bytes: number;
|
|
7
|
+
sha256: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type CodexCacheDiagnosticContext = {
|
|
11
|
+
envelope: "responses" | "responses_lite";
|
|
12
|
+
prewarmMode: "static";
|
|
13
|
+
fullInputItems: number;
|
|
14
|
+
staticInputItems: number;
|
|
15
|
+
staticPrefixBytes: number;
|
|
16
|
+
staticPrefixSha256: string;
|
|
17
|
+
staticRequestBytes: number;
|
|
18
|
+
staticRequestSha256: string;
|
|
19
|
+
instructionsBytes?: number;
|
|
20
|
+
instructionsSha256?: string;
|
|
21
|
+
toolsBytes?: number;
|
|
22
|
+
toolsSha256?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function jsonFingerprint(value: unknown): Fingerprint {
|
|
26
|
+
const json = JSON.stringify(value);
|
|
27
|
+
return {
|
|
28
|
+
bytes: Buffer.byteLength(json, "utf8"),
|
|
29
|
+
sha256: createHash("sha256").update(json, "utf8").digest("hex"),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function textFingerprint(value: string): Fingerprint {
|
|
34
|
+
return {
|
|
35
|
+
bytes: Buffer.byteLength(value, "utf8"),
|
|
36
|
+
sha256: createHash("sha256").update(value, "utf8").digest("hex"),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function inputLength(payload: JsonRecord): number {
|
|
41
|
+
return Array.isArray(payload.input) ? payload.input.length : 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function staticRequest(payload: JsonRecord): JsonRecord {
|
|
45
|
+
const result = structuredClone(payload);
|
|
46
|
+
delete result.client_metadata;
|
|
47
|
+
delete result.prompt_cache_key;
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function codexCacheDiagnosticContext(
|
|
52
|
+
ordinaryBody: JsonRecord,
|
|
53
|
+
fullWireBody: JsonRecord,
|
|
54
|
+
staticWireBody: JsonRecord,
|
|
55
|
+
modelId: string,
|
|
56
|
+
responsesLiteEnabled = true,
|
|
57
|
+
): CodexCacheDiagnosticContext {
|
|
58
|
+
const lite = usesResponsesLite(modelId, responsesLiteEnabled);
|
|
59
|
+
const staticPrefix = lite
|
|
60
|
+
? (staticWireBody.input ?? [])
|
|
61
|
+
: {
|
|
62
|
+
...("instructions" in ordinaryBody ? { instructions: ordinaryBody.instructions } : {}),
|
|
63
|
+
...("tools" in ordinaryBody ? { tools: ordinaryBody.tools } : {}),
|
|
64
|
+
};
|
|
65
|
+
const prefixFingerprint = jsonFingerprint(staticPrefix);
|
|
66
|
+
const requestFingerprint = jsonFingerprint(staticRequest(staticWireBody));
|
|
67
|
+
const instructionFingerprint =
|
|
68
|
+
typeof ordinaryBody.instructions === "string"
|
|
69
|
+
? textFingerprint(ordinaryBody.instructions)
|
|
70
|
+
: undefined;
|
|
71
|
+
const toolsFingerprint = Array.isArray(ordinaryBody.tools)
|
|
72
|
+
? jsonFingerprint(ordinaryBody.tools)
|
|
73
|
+
: undefined;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
envelope: lite ? "responses_lite" : "responses",
|
|
77
|
+
prewarmMode: "static",
|
|
78
|
+
fullInputItems: inputLength(fullWireBody),
|
|
79
|
+
staticInputItems: inputLength(staticWireBody),
|
|
80
|
+
staticPrefixBytes: prefixFingerprint.bytes,
|
|
81
|
+
staticPrefixSha256: prefixFingerprint.sha256,
|
|
82
|
+
staticRequestBytes: requestFingerprint.bytes,
|
|
83
|
+
staticRequestSha256: requestFingerprint.sha256,
|
|
84
|
+
...(instructionFingerprint
|
|
85
|
+
? {
|
|
86
|
+
instructionsBytes: instructionFingerprint.bytes,
|
|
87
|
+
instructionsSha256: instructionFingerprint.sha256,
|
|
88
|
+
}
|
|
89
|
+
: {}),
|
|
90
|
+
...(toolsFingerprint
|
|
91
|
+
? {
|
|
92
|
+
toolsBytes: toolsFingerprint.bytes,
|
|
93
|
+
toolsSha256: toolsFingerprint.sha256,
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export const CODEX_INSTALLATION_ID_FILE = "openai-codex-compat-installation-id";
|
|
7
|
+
|
|
8
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
9
|
+
|
|
10
|
+
function readInstallationId(path: string): string {
|
|
11
|
+
const value = readFileSync(path, "utf8").trim();
|
|
12
|
+
if (!UUID_PATTERN.test(value)) {
|
|
13
|
+
throw new Error(`Invalid Codex installation id in ${path}`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveCodexInstallationId(agentDir = getAgentDir()): string {
|
|
19
|
+
const path = join(agentDir, CODEX_INSTALLATION_ID_FILE);
|
|
20
|
+
try {
|
|
21
|
+
return readInstallationId(path);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (
|
|
24
|
+
!(error instanceof Error) ||
|
|
25
|
+
!("code" in error) ||
|
|
26
|
+
(error as NodeJS.ErrnoException).code !== "ENOENT"
|
|
27
|
+
) {
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
mkdirSync(agentDir, { recursive: true });
|
|
33
|
+
const installationId = randomUUID();
|
|
34
|
+
try {
|
|
35
|
+
writeFileSync(path, `${installationId}\n`, {
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
flag: "wx",
|
|
38
|
+
mode: 0o644,
|
|
39
|
+
});
|
|
40
|
+
return installationId;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (
|
|
43
|
+
error instanceof Error &&
|
|
44
|
+
"code" in error &&
|
|
45
|
+
(error as NodeJS.ErrnoException).code === "EEXIST"
|
|
46
|
+
) {
|
|
47
|
+
return readInstallationId(path);
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|