pi-openai-codex-compat 0.0.12 → 0.0.14
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 +56 -0
- package/README.md +23 -20
- package/extensions/openai-codex-compat/codex-protocol.ts +20 -10
- package/extensions/openai-codex-compat/codex-provider/codex-provider-history.ts +1 -4
- package/extensions/openai-codex-compat/codex-provider/codex-provider-runtime.ts +25 -44
- package/extensions/openai-codex-compat/compaction-checkpoint.ts +136 -51
- package/extensions/openai-codex-compat/image-generation.ts +1 -1
- package/extensions/openai-codex-compat/index.ts +2 -1
- package/extensions/openai-codex-compat/pi-runtime.ts +33 -7
- package/extensions/openai-codex-compat/remote-compaction.ts +30 -10
- package/extensions/openai-codex-compat/tool-runtime.ts +1 -4
- package/extensions/openai-codex-compat/vendor/pi-ai/README.md +9 -8
- package/extensions/openai-codex-compat/vendor/pi-ai/openai-responses-serialization.ts +28 -19
- package/extensions/openai-codex-compat/web-run.ts +1 -1
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.14 - 2026-09-22
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Require Pi `>=0.87.0 <0.88.0` and use Pi 0.87.0 as the development and
|
|
10
|
+
packaged-CLI integration baseline. The load-time host check now also
|
|
11
|
+
requires Pi 0.87's session projection API.
|
|
12
|
+
- Honour Pi 0.87 `context_edit` entries when building request history from
|
|
13
|
+
the branch, so omitted or replaced messages match what Pi's own adapters
|
|
14
|
+
send.
|
|
15
|
+
- Preserve mid-conversation prompt updates when filtering compaction summaries.
|
|
16
|
+
- Skip percentage compaction when nothing has been sampled yet. Pi 0.87
|
|
17
|
+
estimates the prompt and tool declarations before the first response, so a
|
|
18
|
+
low threshold could trigger an empty compaction request.
|
|
19
|
+
- Send Pi's leading system message as the prompt and later system messages as
|
|
20
|
+
inline developer items, exactly as Pi AI's Codex adapter does. Previously
|
|
21
|
+
every system message was replayed into one `instructions` string, so any
|
|
22
|
+
prompt-section change rewrote the stable prefix. Models without
|
|
23
|
+
mid-conversation system-message support still receive one collapsed prompt.
|
|
24
|
+
- Keep every developer and system message in the native compaction checkpoint,
|
|
25
|
+
in place and outside the 64k retained-context budget, so prompt updates
|
|
26
|
+
survive compaction. Only user messages are charged to the budget.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Keep the conversation in overflow-recovery compaction on Pi 0.87. Pi now
|
|
31
|
+
records the failed attempt's omission as a session entry whose parent is
|
|
32
|
+
that attempt; removing the attempt from the branch copy broke the entry
|
|
33
|
+
chain and the compaction request contained only the recovered prefix.
|
|
34
|
+
- Hold a system message that lands between a tool call and its results until
|
|
35
|
+
after those results in replayed history, matching Pi AI.
|
|
36
|
+
- Send the branch's leading system message as compaction `instructions`
|
|
37
|
+
instead of Pi's fully replayed prompt, so Pi-driven compaction shares the
|
|
38
|
+
turn requests' prompt prefix.
|
|
39
|
+
- Reuse the last turn's exact tool declarations for `/compact` and threshold
|
|
40
|
+
compaction while they still describe the active tools. Rebuilding them from
|
|
41
|
+
the tool registry lost Pi's strict schemas for built-in tools, so every
|
|
42
|
+
Pi-driven compaction request missed the turn requests' prompt-cache prefix.
|
|
43
|
+
|
|
44
|
+
## 0.0.13 - 2026-09-21
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- Declare the complete current tool set in the top-level `tools` field on
|
|
49
|
+
every request and never inside `input`, as the official Codex client does.
|
|
50
|
+
Tools added mid-session were replayed as inline `additional_tools` items;
|
|
51
|
+
the Codex backend then often treated other tools as unavailable, and after a
|
|
52
|
+
percentage-triggered compaction the inline declarations were gone entirely,
|
|
53
|
+
so the model reported that it had no callable tools. A mid-session tool
|
|
54
|
+
change now costs one prompt-cache miss instead.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- Use Pi 0.86.1 as the development and packaged-CLI integration baseline. The
|
|
59
|
+
supported range stays `>=0.86.0 <0.87.0`.
|
|
60
|
+
|
|
5
61
|
## 0.0.12 - 2026-09-20
|
|
6
62
|
|
|
7
63
|
### Fixed
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Pi provides the Codex OAuth flow and model catalog. At session start, this packa
|
|
|
22
22
|
## Requirements
|
|
23
23
|
|
|
24
24
|
- Node.js 22.19 or newer
|
|
25
|
-
- Pi `>=0.
|
|
25
|
+
- Pi `>=0.87.0 <0.88.0`
|
|
26
26
|
- An OpenAI Codex login in Pi
|
|
27
27
|
|
|
28
28
|
Restart Pi after upgrading its runtime. `/reload` reloads extensions but cannot
|
|
@@ -67,8 +67,8 @@ The compatibility baseline is official Codex CLI `0.149.1`, released August 24,
|
|
|
67
67
|
| 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. |
|
|
68
68
|
| 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. |
|
|
69
69
|
| Model switching | This package rejects model switches while the active Pi branch contains a native Codex checkpoint because the checkpoint is model-specific. |
|
|
70
|
-
| System instructions |
|
|
71
|
-
| Dynamic tool declarations |
|
|
70
|
+
| System instructions | Send the leading Pi system message as the prompt, as Pi AI's Codex adapter does: Responses Lite models prepend it as developer input after `additional_tools`; other models send it through Responses `instructions`. Later Pi system messages (`/reload`, section changes) travel inline as developer items on models that accept mid-conversation system messages and collapse into the leading prompt otherwise. A forced prompt replaces the leading message. Compaction snapshots and resumed sessions lead with Pi's replayed prompt without rewriting old checkpoints. |
|
|
71
|
+
| Dynamic tool declarations | Send the complete current tool set, replayed from Pi's `toolsAdded` and `toolsRemoved` system messages, in the top-level `tools` field on every request, as the official Codex client does. Tool declarations never travel inside `input`: inline `additional_tools` items and synthetic tool-search pairs made the Codex backend unreliable about which tools exist. A mid-session tool change costs one prompt-cache miss, and native checkpoints need no declaration rebase. |
|
|
72
72
|
| 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. |
|
|
73
73
|
| 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. |
|
|
74
74
|
| Mid-turn compaction | Provider-boundary percentage compaction preserves a successful `end_turn:false` prefix as its own Pi assistant message, installs a checkpoint, and continues without synthetic model input. 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. |
|
|
@@ -76,7 +76,7 @@ The compatibility baseline is official Codex CLI `0.149.1`, released August 24,
|
|
|
76
76
|
| Transport recovery | WebSocket failures before model-visible output receive five fresh-connection retries before sticky SSE fallback; SSE transport failures receive five retries. Official Codex `0.149.1` separately retries sampling connection-establishment failures indefinitely with delays capped at 60 seconds. The package remains bounded because Fetch does not expose Reqwest's narrower connection-error category, and a phase-only approximation could indefinitely repeat requests that reached the server. |
|
|
77
77
|
| Compaction lifecycle events | Pre-turn percentage compaction writes through Pi's mutable session manager and cannot emit Pi's internal `session_compact` event through the public extension API. Mid-response percentage boundaries and manual, threshold, or overflow compactions initiated by Pi emit the normal lifecycle. |
|
|
78
78
|
| Header hooks | An internal percentage-compaction request reuses the already transformed provider headers. It cannot independently rerun Pi's `before_provider_headers` hook. |
|
|
79
|
-
| Native retained context | Deliberately differs from current Codex. The package retains recent user
|
|
79
|
+
| Native retained context | Deliberately differs from current Codex. The package retains recent user messages under a text-only 64k budget before the opaque compaction item, and keeps every developer and system message in place outside that budget so prompt updates are never compacted away. 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. Codex `0.149.1` also adds a default-disabled mode that charges retained images to the budget and keeps each image with adjacent harness labels as an atomic boundary unit. Pi keeps its existing checkpoint shape by design. |
|
|
80
80
|
| 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. |
|
|
81
81
|
| 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. |
|
|
82
82
|
| Sandbox and approvals | Pi extensions run with full process permissions. Command tools, `apply_patch`, local image reads, generated-image writes, and sibling Codex endpoints do not use Codex's sandbox, permission-profile, environment, or approval lifecycle. |
|
|
@@ -115,19 +115,19 @@ The package implements the Codex-specific pieces that fit a provider compatibili
|
|
|
115
115
|
|
|
116
116
|
The following official Codex facilities are not exact equivalents in this package:
|
|
117
117
|
|
|
118
|
-
| Official Codex facility | Pi/package behavior
|
|
119
|
-
| ----------------------------------------------------------------------- |
|
|
120
|
-
| `view_image` | Pi `read` already accepts images; no canonical `view_image` alias is registered.
|
|
121
|
-
| `update_plan`, `request_user_input`, permissions, and environment tools | Not implemented by this package.
|
|
122
|
-
| `send_user_message_async` | Not implemented; Pi owns visible assistant output and user-message history, while this package does not own Codex's asynchronous message-injection lifecycle.
|
|
123
|
-
| Context-window and clock tools | Not implemented. Compaction remains host/provider managed rather than model managed.
|
|
124
|
-
| MCP resources and dynamic MCP tools | Pi does not provide this package with Codex's MCP runtime.
|
|
125
|
-
| Plugin/connector installation | Not implemented; package installation remains an explicit Pi/user operation.
|
|
126
|
-
| Searchable `tool_search` catalog |
|
|
127
|
-
| Multi-agent V1/V2 coordination | Out of scope; no Codex agent tree, mailbox, task-path, or fork-depth runtime is implemented.
|
|
128
|
-
| JavaScript Code Mode and yielded cells | Out of scope; no V8 isolate, nested tool namespace, cell storage, or `wait` lifecycle is implemented.
|
|
129
|
-
| Goals, memories, and remote skill-resource tools | Not implemented; Pi's sessions, files, and native skills remain separate systems.
|
|
130
|
-
| Remote/deferred execution environments | Not implemented.
|
|
118
|
+
| Official Codex facility | Pi/package behavior |
|
|
119
|
+
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
120
|
+
| `view_image` | Pi `read` already accepts images; no canonical `view_image` alias is registered. |
|
|
121
|
+
| `update_plan`, `request_user_input`, permissions, and environment tools | Not implemented by this package. |
|
|
122
|
+
| `send_user_message_async` | Not implemented; Pi owns visible assistant output and user-message history, while this package does not own Codex's asynchronous message-injection lifecycle. |
|
|
123
|
+
| Context-window and clock tools | Not implemented. Compaction remains host/provider managed rather than model managed. |
|
|
124
|
+
| MCP resources and dynamic MCP tools | Pi does not provide this package with Codex's MCP runtime. |
|
|
125
|
+
| Plugin/connector installation | Not implemented; package installation remains an explicit Pi/user operation. |
|
|
126
|
+
| Searchable `tool_search` catalog | This package declares every tool at the top level and does not implement Codex's searchable deferred-tool catalog and ranking runtime. |
|
|
127
|
+
| Multi-agent V1/V2 coordination | Out of scope; no Codex agent tree, mailbox, task-path, or fork-depth runtime is implemented. |
|
|
128
|
+
| JavaScript Code Mode and yielded cells | Out of scope; no V8 isolate, nested tool namespace, cell storage, or `wait` lifecycle is implemented. |
|
|
129
|
+
| Goals, memories, and remote skill-resource tools | Not implemented; Pi's sessions, files, and native skills remain separate systems. |
|
|
130
|
+
| Remote/deferred execution environments | Not implemented. |
|
|
131
131
|
|
|
132
132
|
See [`OFFICIAL_CODEX_CLI_TOOL_CATALOG.md`](OFFICIAL_CODEX_CLI_TOOL_CATALOG.md) for the complete researched Codex tool inventory, and [`CUSTOM_CODEX_PROVIDER_WEB_REFERENCES.md`](CUSTOM_CODEX_PROVIDER_WEB_REFERENCES.md) for the unimplemented citation/reference design.
|
|
133
133
|
|
|
@@ -327,7 +327,8 @@ The extension handles native compaction for `openai-codex`. It follows the Codex
|
|
|
327
327
|
|
|
328
328
|
1. Send normal Responses history followed by `{ "type": "compaction_trigger" }`.
|
|
329
329
|
2. Validate the returned opaque `compaction` item.
|
|
330
|
-
3. Retain approximately 64,000 tokens of recent user, developer
|
|
330
|
+
3. Retain approximately 64,000 tokens of recent user messages, plus every developer and
|
|
331
|
+
system message outside that budget.
|
|
331
332
|
4. Persist the opaque checkpoint in the Pi session and replay it on later requests.
|
|
332
333
|
|
|
333
334
|
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 models use the ordinary Responses envelope and receive their own ordinary-prefix prewarm.
|
|
@@ -587,7 +588,7 @@ mise run test:live:codex
|
|
|
587
588
|
The task obtains the local Codex bearer token and runs the tests with
|
|
588
589
|
`gpt-5.6-luna` at medium reasoning effort.
|
|
589
590
|
It also packs the extension and loads that archive through the shipped Pi
|
|
590
|
-
0.
|
|
591
|
+
0.87.0 CLI. Ordinary Responses and Responses Lite tests exercise tool calls,
|
|
591
592
|
prompt reload, native compaction, and persisted-session resume against Codex.
|
|
592
593
|
Test credentials stay in memory and child-process environments. Test sessions
|
|
593
594
|
and configuration are isolated from the user's agent directory.
|
|
@@ -606,7 +607,9 @@ search, and compaction continuation.
|
|
|
606
607
|
2. The command builds the exact package locally and runs live CLI tests against that archive. It also runs the existing live SDK tests against checkout source. Both suites must pass before it records the archive's SHA-256 in an SSH-signed release commit, proves a clean rebuild is reproducible, and creates a lightweight tag. Missing credentials or failing live tests stop the release.
|
|
607
608
|
3. Inspect the result, then push atomically with `git push --atomic origin main vX.Y.Z`.
|
|
608
609
|
4. A read-only GitHub Actions job validates and packs the package. After approval in the tag-restricted `npm-publish` environment, a separate GitHub-owned job verifies the signature and signed digest before attesting and staging that exact archive through npm trusted publishing.
|
|
609
|
-
5.
|
|
610
|
+
5. A final job creates the immutable GitHub release for the tag from the same verified archive, its
|
|
611
|
+
checksum, and the version's `CHANGELOG.md` section (`Unreleased` for prereleases).
|
|
612
|
+
6. Approve the staged package on npmjs.com, or with `npm stage approve <stage-id>`.
|
|
610
613
|
|
|
611
614
|
Stable releases use `latest`; prereleases derive their npm dist-tag from the first prerelease identifier.
|
|
612
615
|
|
|
@@ -276,17 +276,23 @@ function shortenMessage(
|
|
|
276
276
|
: undefined;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
function
|
|
280
|
-
return (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
function messageRole(item: ResponsesInputItem): string | undefined {
|
|
280
|
+
return Value.Check(RESPONSES_MESSAGE_ITEM_SCHEMA, item) ? item.role : undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Developer and system messages carry prompt state, not conversation history. */
|
|
284
|
+
function isInstructionMessage(item: ResponsesInputItem): boolean {
|
|
285
|
+
const role = messageRole(item);
|
|
286
|
+
return role === "developer" || role === "system";
|
|
284
287
|
}
|
|
285
288
|
|
|
286
289
|
/**
|
|
287
|
-
* Reproduce Codex remote-compaction-v2 history installation
|
|
288
|
-
* user
|
|
289
|
-
* chronological order. The oldest selected message may be
|
|
290
|
+
* Reproduce Codex remote-compaction-v2 history installation for user messages:
|
|
291
|
+
* select recent user messages from newest to oldest under the budget, then
|
|
292
|
+
* restore chronological order. The oldest selected user message may be
|
|
293
|
+
* truncated. Developer and system messages are prompt updates rather than
|
|
294
|
+
* conversation history: every one of them is retained in place, outside the
|
|
295
|
+
* budget, and never truncated.
|
|
290
296
|
*/
|
|
291
297
|
export function selectRetainedContext(
|
|
292
298
|
history: readonly ResponsesInputItem[],
|
|
@@ -295,10 +301,14 @@ export function selectRetainedContext(
|
|
|
295
301
|
let remaining = budget;
|
|
296
302
|
const newestFirst: ResponsesInputItem[] = [];
|
|
297
303
|
|
|
298
|
-
for (let index = history.length - 1; index >= 0
|
|
304
|
+
for (let index = history.length - 1; index >= 0; index--) {
|
|
299
305
|
const item = history[index];
|
|
300
306
|
if (item === undefined) continue;
|
|
301
|
-
if (
|
|
307
|
+
if (isInstructionMessage(item)) {
|
|
308
|
+
newestFirst.push(structuredClone(item));
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (remaining <= 0 || messageRole(item) !== "user") continue;
|
|
302
312
|
|
|
303
313
|
const tokens = Math.max(1, messageTextTokens(item));
|
|
304
314
|
if (tokens <= remaining) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SessionEntry
|
|
1
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
3
|
import { providerHistory, type GrammarToolInputProperties } from "../compaction-checkpoint.ts";
|
|
4
4
|
import type { ImageDetail } from "../config.ts";
|
|
@@ -37,7 +37,6 @@ export function splitUnsampledUserInput(options: {
|
|
|
37
37
|
branch: readonly SessionEntry[];
|
|
38
38
|
history: readonly ResponsesInputItem[];
|
|
39
39
|
model: Model<Api>;
|
|
40
|
-
allTools: readonly ToolInfo[];
|
|
41
40
|
grammarToolInputProperties: GrammarToolInputProperties;
|
|
42
41
|
imageDetail: ImageDetail;
|
|
43
42
|
}):
|
|
@@ -56,10 +55,8 @@ export function splitUnsampledUserInput(options: {
|
|
|
56
55
|
const encoded = providerHistory({
|
|
57
56
|
branch: options.branch.slice(unsampledIndex),
|
|
58
57
|
wireModel: options.model,
|
|
59
|
-
allTools: options.allTools,
|
|
60
58
|
grammarToolInputProperties: options.grammarToolInputProperties,
|
|
61
59
|
imageDetail: options.imageDetail,
|
|
62
|
-
anchorsToolAdditions: false,
|
|
63
60
|
});
|
|
64
61
|
if (encoded.length === 0 || encoded.length > options.history.length) return { kind: "unsafe" };
|
|
65
62
|
|
|
@@ -5,10 +5,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
5
5
|
import {
|
|
6
6
|
clampThinkingLevel,
|
|
7
7
|
createAssistantMessageEventStream,
|
|
8
|
-
|
|
8
|
+
getCurrentTools,
|
|
9
9
|
getDeclaredTools,
|
|
10
|
-
normalizeContext,
|
|
11
|
-
resolveTranscriptTools,
|
|
12
10
|
type Api,
|
|
13
11
|
type AssistantMessage,
|
|
14
12
|
type AssistantMessageEventStream,
|
|
@@ -30,6 +28,8 @@ import {
|
|
|
30
28
|
checkpointData,
|
|
31
29
|
providerHistory,
|
|
32
30
|
remoteCompactionMarkerSummary,
|
|
31
|
+
requestInstructions,
|
|
32
|
+
resolveRequestTranscript,
|
|
33
33
|
responsesCompatibility,
|
|
34
34
|
searchCheckpoint,
|
|
35
35
|
type CheckpointData,
|
|
@@ -495,7 +495,6 @@ export class CodexProviderRuntime {
|
|
|
495
495
|
context: TranscriptContext,
|
|
496
496
|
grammarToolInputProperties: GrammarToolInputProperties,
|
|
497
497
|
sessionId: string | undefined,
|
|
498
|
-
anchorsToolAdditions: boolean,
|
|
499
498
|
): ResponsesInputItem[] {
|
|
500
499
|
const scope = sessionId ? this.scopes.get(sessionId) : undefined;
|
|
501
500
|
const compat = responsesCompatibility(model.compat);
|
|
@@ -503,10 +502,9 @@ export class CodexProviderRuntime {
|
|
|
503
502
|
const nativeItems = new Map<string, ResponsesOutputItem[]>();
|
|
504
503
|
const serializationOptions: NonNullable<Parameters<typeof convertResponsesMessages>[3]> = {
|
|
505
504
|
includeSystemPrompt: false,
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
supportsToolSearch: anchorsToolAdditions && (compat.supportsToolSearch ?? false),
|
|
505
|
+
supportsMidConvoSystemMessages: compat.supportsMidConvoSystemMessages ?? false,
|
|
506
|
+
supportsAdditionalTools: false,
|
|
507
|
+
supportsToolSearch: false,
|
|
510
508
|
grammarToolInputProperties,
|
|
511
509
|
toolOptions: {
|
|
512
510
|
strict: false,
|
|
@@ -525,10 +523,8 @@ export class CodexProviderRuntime {
|
|
|
525
523
|
return providerHistory({
|
|
526
524
|
branch: scope.manager.getBranch(),
|
|
527
525
|
wireModel: model,
|
|
528
|
-
allTools: this.pi.getAllTools(),
|
|
529
526
|
grammarToolInputProperties,
|
|
530
527
|
imageDetail: scope.config.imageDetail,
|
|
531
|
-
anchorsToolAdditions,
|
|
532
528
|
});
|
|
533
529
|
}
|
|
534
530
|
|
|
@@ -543,39 +539,21 @@ export class CodexProviderRuntime {
|
|
|
543
539
|
turnId,
|
|
544
540
|
} = options;
|
|
545
541
|
const compat = responsesCompatibility(model.compat);
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const toolPlacement = resolveTranscriptTools(
|
|
556
|
-
context.messages,
|
|
557
|
-
!compactionPending &&
|
|
558
|
-
((compat.supportsAdditionalTools ?? false) || (compat.supportsToolSearch ?? false)),
|
|
559
|
-
);
|
|
560
|
-
// A forced prompt can project all tools into a single leading system message.
|
|
561
|
-
// Do not replay additions from the stored branch after that projection.
|
|
562
|
-
const anchorsToolAdditions =
|
|
563
|
-
toolPlacement.anchorsAdditions &&
|
|
564
|
-
context.messages
|
|
565
|
-
.slice(1)
|
|
566
|
-
.some((message) => message.role === "system" && (message.toolsAdded?.length ?? 0) > 0);
|
|
542
|
+
// Every request declares the complete current tool set at the top level,
|
|
543
|
+
// as the official Codex client does. Replaying additions inside the input
|
|
544
|
+
// made the Codex backend unreliable about which tools exist, and a native
|
|
545
|
+
// checkpoint drops such history anyway. A mid-session tool change costs one
|
|
546
|
+
// cache miss.
|
|
547
|
+
const requestTools = getCurrentTools(context.messages);
|
|
548
|
+
// The leading system message is the prompt; later system messages travel
|
|
549
|
+
// inline in `input` (or collapsed into the leading message by
|
|
550
|
+
// resolveRequestTranscript), as Pi AI's Codex adapter does.
|
|
567
551
|
let body: JsonRecord = {
|
|
568
552
|
model: model.id,
|
|
569
553
|
store: false,
|
|
570
554
|
stream: true,
|
|
571
|
-
instructions:
|
|
572
|
-
input: this.wireHistory(
|
|
573
|
-
model,
|
|
574
|
-
context,
|
|
575
|
-
grammarToolInputProperties,
|
|
576
|
-
runtimeSessionId,
|
|
577
|
-
anchorsToolAdditions,
|
|
578
|
-
),
|
|
555
|
+
instructions: requestInstructions(context) || "You are a helpful assistant.",
|
|
556
|
+
input: this.wireHistory(model, context, grammarToolInputProperties, runtimeSessionId),
|
|
579
557
|
text: { verbosity: requestOptions.textVerbosity ?? "low" },
|
|
580
558
|
include: ["reasoning.encrypted_content"],
|
|
581
559
|
tool_choice: requestOptions.toolChoice ?? "auto",
|
|
@@ -594,8 +572,8 @@ export class CodexProviderRuntime {
|
|
|
594
572
|
),
|
|
595
573
|
});
|
|
596
574
|
if (requestOptions.serviceTier !== undefined) body.service_tier = requestOptions.serviceTier;
|
|
597
|
-
if (
|
|
598
|
-
body.tools = convertResponsesTools(
|
|
575
|
+
if (requestTools.length > 0) {
|
|
576
|
+
body.tools = convertResponsesTools(requestTools, {
|
|
599
577
|
strict: false,
|
|
600
578
|
supportsStrictMode: compat.supportsStrictMode ?? true,
|
|
601
579
|
supportsOpenAIGrammarTools: compat.supportsOpenAIGrammarTools ?? false,
|
|
@@ -758,7 +736,6 @@ export class CodexProviderRuntime {
|
|
|
758
736
|
branch,
|
|
759
737
|
history,
|
|
760
738
|
model,
|
|
761
|
-
allTools: this.pi.getAllTools(),
|
|
762
739
|
grammarToolInputProperties,
|
|
763
740
|
imageDetail: scope.config.imageDetail,
|
|
764
741
|
});
|
|
@@ -769,6 +746,10 @@ export class CodexProviderRuntime {
|
|
|
769
746
|
);
|
|
770
747
|
return body;
|
|
771
748
|
}
|
|
749
|
+
// Pi 0.87 estimates the prompt and tool declarations before any response
|
|
750
|
+
// usage exists, so a low threshold can be reached before anything was
|
|
751
|
+
// sampled. There is nothing to compact until the first response.
|
|
752
|
+
if (split.history.length === 0) return body;
|
|
772
753
|
|
|
773
754
|
const compacted = await this.performCompaction({
|
|
774
755
|
model,
|
|
@@ -778,7 +759,7 @@ export class CodexProviderRuntime {
|
|
|
778
759
|
template: withoutConversationInput(body),
|
|
779
760
|
instructions: isString(body.instructions)
|
|
780
761
|
? body.instructions
|
|
781
|
-
:
|
|
762
|
+
: requestInstructions(context) || "You are a helpful assistant.",
|
|
782
763
|
grammarToolInputProperties,
|
|
783
764
|
priority: scope.config.fastMode,
|
|
784
765
|
compactionMetadata: responsesCompactionV2Metadata("auto", "context_limit", "pre_turn"),
|
|
@@ -820,7 +801,7 @@ export class CodexProviderRuntime {
|
|
|
820
801
|
input: Context,
|
|
821
802
|
options?: OpenAICodexResponsesOptions,
|
|
822
803
|
): AssistantMessageEventStream {
|
|
823
|
-
const context =
|
|
804
|
+
const context = resolveRequestTranscript(model, input);
|
|
824
805
|
const stream = createAssistantMessageEventStream();
|
|
825
806
|
const requestOptions = transportOptions(options);
|
|
826
807
|
const output: AssistantMessage = {
|
|
@@ -2,12 +2,24 @@ import { isBoolean, isString } from "./value-contracts.ts";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
buildSessionContext,
|
|
5
|
+
buildSessionProjection,
|
|
5
6
|
convertToLlm,
|
|
6
7
|
sessionEntryToContextMessages,
|
|
7
8
|
type SessionEntry,
|
|
8
9
|
type ToolInfo,
|
|
9
10
|
} from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import
|
|
11
|
+
import {
|
|
12
|
+
getInitialSystemMessage,
|
|
13
|
+
getSystemMessageText,
|
|
14
|
+
normalizeContext,
|
|
15
|
+
resolveTranscript,
|
|
16
|
+
type Api,
|
|
17
|
+
type Context,
|
|
18
|
+
type Message,
|
|
19
|
+
type Model,
|
|
20
|
+
type SystemMessage,
|
|
21
|
+
type TranscriptContext,
|
|
22
|
+
} from "@earendil-works/pi-ai";
|
|
11
23
|
import { Value } from "typebox/value";
|
|
12
24
|
import { APPLY_PATCH_LARK_GRAMMAR, APPLY_PATCH_TOOL_NAME } from "./apply-patch.ts";
|
|
13
25
|
import { CODEX_TOOL_CALL_PROVIDERS } from "./codex-identifiers.ts";
|
|
@@ -34,6 +46,8 @@ import {
|
|
|
34
46
|
import type { ResponsesToolDefinition } from "./responses-tool-schema.ts";
|
|
35
47
|
import { convertResponsesMessages } from "./vendor/pi-ai/openai-responses-serialization.ts";
|
|
36
48
|
|
|
49
|
+
type AgentMessages = ReturnType<typeof sessionEntryToContextMessages>;
|
|
50
|
+
|
|
37
51
|
export const CHECKPOINT_ENTRY_TYPE = "openai-codex-compat-remote-compaction";
|
|
38
52
|
export const CHECKPOINT_FORMAT_VERSION = 1;
|
|
39
53
|
|
|
@@ -58,17 +72,16 @@ export type CheckpointSearch =
|
|
|
58
72
|
export type GrammarToolInputProperties = ReadonlyMap<string, string>;
|
|
59
73
|
|
|
60
74
|
export interface ResponsesCompatibility {
|
|
61
|
-
|
|
75
|
+
supportsMidConvoSystemMessages?: boolean;
|
|
62
76
|
supportsOpenAIGrammarTools?: boolean;
|
|
63
77
|
supportsStrictMode?: boolean;
|
|
64
|
-
supportsToolSearch?: boolean;
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
export function responsesCompatibility(value: unknown): ResponsesCompatibility {
|
|
68
81
|
const compatibility: ResponsesCompatibility = {};
|
|
69
82
|
if (!isObject(value)) return compatibility;
|
|
70
|
-
if (isBoolean(value["
|
|
71
|
-
compatibility.
|
|
83
|
+
if (isBoolean(value["supportsMidConvoSystemMessages"])) {
|
|
84
|
+
compatibility.supportsMidConvoSystemMessages = value["supportsMidConvoSystemMessages"];
|
|
72
85
|
}
|
|
73
86
|
if (isBoolean(value["supportsOpenAIGrammarTools"])) {
|
|
74
87
|
compatibility.supportsOpenAIGrammarTools = value["supportsOpenAIGrammarTools"];
|
|
@@ -76,12 +89,37 @@ export function responsesCompatibility(value: unknown): ResponsesCompatibility {
|
|
|
76
89
|
if (isBoolean(value["supportsStrictMode"])) {
|
|
77
90
|
compatibility.supportsStrictMode = value["supportsStrictMode"];
|
|
78
91
|
}
|
|
79
|
-
if (isBoolean(value["supportsToolSearch"])) {
|
|
80
|
-
compatibility.supportsToolSearch = value["supportsToolSearch"];
|
|
81
|
-
}
|
|
82
92
|
return compatibility;
|
|
83
93
|
}
|
|
84
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Resolve Pi's transcript the way Pi AI's Codex adapter does before building a
|
|
97
|
+
* request: models that accept mid-conversation system messages keep later
|
|
98
|
+
* system messages in place, and every other model collapses them into the
|
|
99
|
+
* leading system message.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveRequestTranscript(model: Model<Api>, context: Context): TranscriptContext {
|
|
102
|
+
return resolveTranscript(
|
|
103
|
+
normalizeContext(context),
|
|
104
|
+
responsesCompatibility(model.compat).supportsMidConvoSystemMessages ?? false,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The leading system message of a resolved transcript, rendered as Responses `instructions`. */
|
|
109
|
+
export function requestInstructions(context: TranscriptContext): string {
|
|
110
|
+
const initial = getInitialSystemMessage(context.messages);
|
|
111
|
+
return initial ? getSystemMessageText(initial) : "";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** `instructions` for a request built from a session branch rather than Pi's live transcript. */
|
|
115
|
+
export function branchInstructions(model: Model<Api>, branch: readonly SessionEntry[]): string {
|
|
116
|
+
return requestInstructions(
|
|
117
|
+
resolveRequestTranscript(model, {
|
|
118
|
+
messages: convertToLlm(buildSessionContext([...branch]).messages),
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
85
123
|
function responsesToolParameters(tool: ToolInfo): JsonRecord {
|
|
86
124
|
if (!isObject(tool.parameters)) {
|
|
87
125
|
throw new Error(`Tool ${tool.name} must have JSON object parameters.`);
|
|
@@ -143,6 +181,43 @@ export function activeResponsesTools(
|
|
|
143
181
|
: undefined;
|
|
144
182
|
}
|
|
145
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Whether a serialized Responses `tools` list declares exactly the active tools,
|
|
186
|
+
* by name and description. Namespace members count as `namespace.member`.
|
|
187
|
+
*
|
|
188
|
+
* A cached turn template carries the exact declarations the turn requests sent,
|
|
189
|
+
* including strict schema conversion that `ToolInfo` cannot reproduce. Reusing
|
|
190
|
+
* it keeps the compaction request on the turn requests' prompt-cache prefix.
|
|
191
|
+
*/
|
|
192
|
+
export function declaresActiveTools(
|
|
193
|
+
serialized: readonly JsonRecord[],
|
|
194
|
+
allTools: readonly ToolInfo[],
|
|
195
|
+
activeNames: readonly string[],
|
|
196
|
+
): boolean {
|
|
197
|
+
const declared = new Map<string, string | undefined>();
|
|
198
|
+
for (const item of serialized) {
|
|
199
|
+
if (item.type === "namespace" && isString(item.name) && Array.isArray(item.tools)) {
|
|
200
|
+
for (const member of item.tools) {
|
|
201
|
+
if (!isObject(member) || !isString(member.name)) return false;
|
|
202
|
+
declared.set(
|
|
203
|
+
`${item.name}.${member.name}`,
|
|
204
|
+
isString(member["description"]) ? member["description"] : undefined,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
} else if ((item.type === "function" || item.type === "custom") && isString(item.name)) {
|
|
208
|
+
declared.set(item.name, isString(item["description"]) ? item["description"] : undefined);
|
|
209
|
+
} else {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const enabled = new Set(activeNames);
|
|
214
|
+
const active = allTools.filter((tool) => enabled.has(tool.name));
|
|
215
|
+
return (
|
|
216
|
+
declared.size === active.length &&
|
|
217
|
+
active.every((tool) => declared.has(tool.name) && declared.get(tool.name) === tool.description)
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
146
221
|
export function remoteCompactionMarkerSummary(): string {
|
|
147
222
|
return `OpenAI Codex remote compaction checkpoint (${randomUUID()}).`;
|
|
148
223
|
}
|
|
@@ -150,37 +225,44 @@ export function remoteCompactionMarkerSummary(): string {
|
|
|
150
225
|
type EncodeMessagesOptions = {
|
|
151
226
|
model: Model<Api>;
|
|
152
227
|
messages: Message[];
|
|
153
|
-
allTools: readonly ToolInfo[];
|
|
154
228
|
grammarToolInputProperties: GrammarToolInputProperties;
|
|
155
229
|
imageDetail: ImageDetail;
|
|
156
|
-
anchorsToolAdditions?: boolean;
|
|
157
230
|
nativeAssistantItems?: ReadonlyMap<string, readonly ResponsesOutputItem[]>;
|
|
158
231
|
};
|
|
159
232
|
|
|
160
233
|
export type EncodeSessionEntriesOptions = {
|
|
161
234
|
model: Model<Api>;
|
|
162
235
|
entries: readonly SessionEntry[];
|
|
163
|
-
allTools: readonly ToolInfo[];
|
|
164
236
|
grammarToolInputProperties?: GrammarToolInputProperties;
|
|
165
237
|
imageDetail?: ImageDetail;
|
|
166
238
|
nativeAssistantItems?: ReadonlyMap<string, readonly ResponsesOutputItem[]>;
|
|
167
239
|
initialSystemMessage?: SystemMessage;
|
|
168
|
-
|
|
240
|
+
/** Model-visible messages per entry. Defaults to the entry's raw messages. */
|
|
241
|
+
entryMessages?: (entry: SessionEntry) => AgentMessages;
|
|
169
242
|
};
|
|
170
243
|
|
|
171
|
-
/**
|
|
244
|
+
/**
|
|
245
|
+
* Encode Pi's canonical messages using Pi AI's OpenAI Responses serializer.
|
|
246
|
+
*
|
|
247
|
+
* The leading system message travels as `instructions`, so it is excluded here.
|
|
248
|
+
* Later system messages become inline developer items on models that accept
|
|
249
|
+
* mid-conversation system messages and collapse into the leading message
|
|
250
|
+
* otherwise, exactly as Pi AI's Codex adapter does.
|
|
251
|
+
*
|
|
252
|
+
* Tool declarations never enter the input: every request carries the complete
|
|
253
|
+
* current tool set at the top level, as the official Codex client does. Inline
|
|
254
|
+
* `additional_tools` items and synthetic tool-search pairs made the Codex
|
|
255
|
+
* backend unreliable about which tools exist, so neither is emitted.
|
|
256
|
+
*/
|
|
172
257
|
function encodeMessages(options: EncodeMessagesOptions): ResponsesInputItem[] {
|
|
173
258
|
const { model, messages, grammarToolInputProperties, imageDetail, nativeAssistantItems } =
|
|
174
259
|
options;
|
|
175
260
|
const compat = responsesCompatibility(model.compat);
|
|
176
261
|
const serializationOptions: NonNullable<Parameters<typeof convertResponsesMessages>[3]> = {
|
|
177
262
|
includeSystemPrompt: false,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
options.anchorsToolAdditions !== false && (compat.supportsAdditionalTools ?? false),
|
|
182
|
-
supportsToolSearch:
|
|
183
|
-
options.anchorsToolAdditions !== false && (compat.supportsToolSearch ?? false),
|
|
263
|
+
supportsMidConvoSystemMessages: compat.supportsMidConvoSystemMessages ?? false,
|
|
264
|
+
supportsAdditionalTools: false,
|
|
265
|
+
supportsToolSearch: false,
|
|
184
266
|
grammarToolInputProperties,
|
|
185
267
|
toolOptions: {
|
|
186
268
|
strict: false,
|
|
@@ -203,27 +285,24 @@ export function encodeSessionEntries(options: EncodeSessionEntriesOptions): Resp
|
|
|
203
285
|
const {
|
|
204
286
|
model,
|
|
205
287
|
entries,
|
|
206
|
-
allTools,
|
|
207
288
|
grammarToolInputProperties = new Map(),
|
|
208
289
|
imageDetail = "auto",
|
|
209
290
|
nativeAssistantItems,
|
|
291
|
+
entryMessages = sessionEntryToContextMessages,
|
|
210
292
|
} = options;
|
|
211
|
-
const messages = entries.flatMap((entry) =>
|
|
293
|
+
const messages = entries.flatMap((entry) => entryMessages(entry));
|
|
212
294
|
const encodeOptions: EncodeMessagesOptions = {
|
|
213
295
|
model,
|
|
214
296
|
// A partial tail can start with a system update. Seed its prior state so
|
|
215
|
-
// the serializer
|
|
297
|
+
// the serializer renders that update inline instead of dropping it as the
|
|
298
|
+
// leading system message that `instructions` already carries.
|
|
216
299
|
messages: [
|
|
217
300
|
options.initialSystemMessage ?? { role: "system", content: "", timestamp: 0 },
|
|
218
301
|
...convertToLlm(messages),
|
|
219
302
|
],
|
|
220
|
-
allTools,
|
|
221
303
|
grammarToolInputProperties,
|
|
222
304
|
imageDetail,
|
|
223
305
|
};
|
|
224
|
-
if (options.anchorsToolAdditions !== undefined) {
|
|
225
|
-
encodeOptions.anchorsToolAdditions = options.anchorsToolAdditions;
|
|
226
|
-
}
|
|
227
306
|
if (nativeAssistantItems) encodeOptions.nativeAssistantItems = nativeAssistantItems;
|
|
228
307
|
return encodeMessages(encodeOptions);
|
|
229
308
|
}
|
|
@@ -339,40 +418,52 @@ export function checkpointData(
|
|
|
339
418
|
/**
|
|
340
419
|
* Materialize provider history for the active branch. Checkpoint history
|
|
341
420
|
* replaces everything before its entry; later branch entries form the tail.
|
|
421
|
+
*
|
|
422
|
+
* Ordinary requests read the session projection, so `context_edit` omissions
|
|
423
|
+
* and replacements apply exactly as they do for Pi's own adapters. Recovery
|
|
424
|
+
* compaction reads the raw entries instead: Pi 0.87 omits the failed or
|
|
425
|
+
* truncated attempt through a `context_edit` child of that entry before it
|
|
426
|
+
* runs `session_before_compact`. The serializer already skips error and
|
|
427
|
+
* aborted assistants, and a truncated boundary is committed progress that the
|
|
428
|
+
* checkpoint must keep. Entries are never removed from the branch copy because
|
|
429
|
+
* the omission entry's parent chain would break and the history would vanish.
|
|
342
430
|
*/
|
|
343
431
|
export function providerHistory(options: {
|
|
344
432
|
branch: readonly SessionEntry[];
|
|
345
433
|
wireModel: Model<Api>;
|
|
346
|
-
allTools: readonly ToolInfo[];
|
|
347
434
|
grammarToolInputProperties?: GrammarToolInputProperties;
|
|
348
435
|
imageDetail?: ImageDetail;
|
|
349
436
|
recoverLatestOverflowPrefix?: boolean;
|
|
350
|
-
anchorsToolAdditions?: boolean;
|
|
351
437
|
}): ResponsesInputItem[] {
|
|
352
438
|
const branch = [...options.branch];
|
|
353
439
|
let recoveredPrefix: ResponsesInputItem[] = [];
|
|
354
440
|
if (options.recoverLatestOverflowPrefix) {
|
|
355
|
-
const
|
|
356
|
-
(
|
|
441
|
+
const entry = branch.findLast(
|
|
442
|
+
(candidate) => candidate.type === "message" && candidate.message.role === "assistant",
|
|
357
443
|
);
|
|
358
|
-
const entry = index >= 0 ? branch[index] : undefined;
|
|
359
444
|
if (
|
|
360
445
|
entry?.type === "message" &&
|
|
361
446
|
entry.message.role === "assistant" &&
|
|
362
|
-
|
|
447
|
+
entry.message.stopReason === "error" &&
|
|
448
|
+
entry.message.responseId
|
|
363
449
|
) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
) ?? [];
|
|
371
|
-
}
|
|
372
|
-
branch.splice(index, 1);
|
|
450
|
+
recoveredPrefix =
|
|
451
|
+
nativeCommittedPrefixBeforeOverflow(
|
|
452
|
+
branch,
|
|
453
|
+
options.wireModel.id,
|
|
454
|
+
entry.message.responseId,
|
|
455
|
+
) ?? [];
|
|
373
456
|
}
|
|
374
457
|
}
|
|
375
458
|
|
|
459
|
+
const projection = buildSessionProjection(branch);
|
|
460
|
+
const projected = new Map<string, AgentMessages>();
|
|
461
|
+
for (const entry of projection.entries) projected.set(entry.sourceEntry.id, entry.messages);
|
|
462
|
+
const entryMessages = (entry: SessionEntry): AgentMessages =>
|
|
463
|
+
options.recoverLatestOverflowPrefix
|
|
464
|
+
? sessionEntryToContextMessages(entry)
|
|
465
|
+
: (projected.get(entry.id) ?? []);
|
|
466
|
+
|
|
376
467
|
const checkpoint = searchCheckpoint(branch);
|
|
377
468
|
const nativeAssistantItems = nativeResponseOverrides(branch, options.wireModel.id);
|
|
378
469
|
if (checkpoint.kind === "corrupt") {
|
|
@@ -392,31 +483,25 @@ export function providerHistory(options: {
|
|
|
392
483
|
...encodeSessionEntries({
|
|
393
484
|
model: options.wireModel,
|
|
394
485
|
entries: branch.slice(checkpoint.entryIndex + 1),
|
|
395
|
-
allTools: options.allTools,
|
|
396
486
|
grammarToolInputProperties: options.grammarToolInputProperties ?? new Map(),
|
|
397
487
|
imageDetail: options.imageDetail ?? "auto",
|
|
398
488
|
nativeAssistantItems,
|
|
489
|
+
entryMessages,
|
|
399
490
|
...(initialSystemMessage ? { initialSystemMessage } : {}),
|
|
400
|
-
...(options.anchorsToolAdditions === undefined
|
|
401
|
-
? {}
|
|
402
|
-
: { anchorsToolAdditions: options.anchorsToolAdditions }),
|
|
403
491
|
}),
|
|
404
492
|
...recoveredPrefix,
|
|
405
493
|
];
|
|
406
494
|
}
|
|
407
495
|
|
|
408
|
-
const context = buildSessionContext(branch);
|
|
409
496
|
return [
|
|
410
497
|
...encodeMessages({
|
|
411
498
|
model: options.wireModel,
|
|
412
|
-
messages: convertToLlm(
|
|
413
|
-
|
|
499
|
+
messages: convertToLlm(
|
|
500
|
+
projection.entries.flatMap((entry) => entryMessages(entry.sourceEntry)),
|
|
501
|
+
),
|
|
414
502
|
grammarToolInputProperties: options.grammarToolInputProperties ?? new Map(),
|
|
415
503
|
imageDetail: options.imageDetail ?? "auto",
|
|
416
504
|
nativeAssistantItems,
|
|
417
|
-
...(options.anchorsToolAdditions === undefined
|
|
418
|
-
? {}
|
|
419
|
-
: { anchorsToolAdditions: options.anchorsToolAdditions }),
|
|
420
505
|
}),
|
|
421
506
|
...recoveredPrefix,
|
|
422
507
|
];
|
|
@@ -306,7 +306,7 @@ export default function registerImageGeneration(
|
|
|
306
306
|
},
|
|
307
307
|
});
|
|
308
308
|
const config = resolveConfig(ctx);
|
|
309
|
-
const history = codexToolHistory(
|
|
309
|
+
const history = codexToolHistory(ctx, model, config.imageDetail);
|
|
310
310
|
const request = await imageRequest(params, history);
|
|
311
311
|
const authentication = await codexToolAuthentication(ctx, model);
|
|
312
312
|
const callId = toolCallId.split("|")[0] || toolCallId;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import * as piAi from "@earendil-works/pi-ai";
|
|
3
|
+
import * as piCodingAgent from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { requirePiTranscriptRuntime } from "./pi-runtime.ts";
|
|
4
5
|
import { DEFAULT_CONFIG, loadConfig, type CodexCompatConfig } from "./config.ts";
|
|
5
6
|
import type { ConfigContext } from "./config-context.ts";
|
|
@@ -23,7 +24,7 @@ export {
|
|
|
23
24
|
} from "./codex-transport.ts";
|
|
24
25
|
|
|
25
26
|
export default function registerOpenAICodexCompat(pi: ExtensionAPI): void {
|
|
26
|
-
requirePiTranscriptRuntime(piAi);
|
|
27
|
+
requirePiTranscriptRuntime(piAi, piCodingAgent);
|
|
27
28
|
let activeConfig: CodexCompatConfig | undefined;
|
|
28
29
|
const resolveConfig = (ctx: ConfigContext): CodexCompatConfig => {
|
|
29
30
|
activeConfig ??= loadConfig(ctx.cwd, ctx.isProjectTrusted());
|
|
@@ -1,19 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Every Pi 0.86 transcript API this extension imports as a value from
|
|
3
|
+
* `@earendil-works/pi-ai`. Pi's extension loader turns a missing named import
|
|
4
|
+
* into `undefined` rather than a load error, so an older host fails at first use
|
|
5
|
+
* unless the surface is checked up front. Keep this list equal to the value
|
|
6
|
+
* imports in `codex-provider/codex-provider-runtime.ts`, `compaction-checkpoint.ts`,
|
|
7
|
+
* and `vendor/pi-ai/openai-responses-serialization.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export const TRANSCRIPT_APIS = [
|
|
10
|
+
"getCurrentTools",
|
|
4
11
|
"getDeclaredTools",
|
|
5
12
|
"getInitialSystemMessage",
|
|
13
|
+
"getSystemMessageText",
|
|
14
|
+
"normalizeContext",
|
|
15
|
+
"renderSystemMessageUpdate",
|
|
16
|
+
"resolveTranscript",
|
|
6
17
|
"resolveTranscriptTools",
|
|
7
18
|
] as const;
|
|
8
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Every Pi 0.87 session API this extension imports as a value from
|
|
22
|
+
* `@earendil-works/pi-coding-agent`. Keep this list equal to the value imports
|
|
23
|
+
* in `compaction-checkpoint.ts`.
|
|
24
|
+
*/
|
|
25
|
+
export const SESSION_APIS = ["buildSessionProjection"] as const;
|
|
26
|
+
|
|
27
|
+
export const REQUIRED_PI = "Pi 0.87.x";
|
|
28
|
+
|
|
9
29
|
/** Check the loaded host, not package metadata that PI_PACKAGE_DIR can override. */
|
|
10
|
-
export function requirePiTranscriptRuntime(
|
|
11
|
-
|
|
30
|
+
export function requirePiTranscriptRuntime(
|
|
31
|
+
transcriptApi: Record<string, unknown>,
|
|
32
|
+
sessionApi: Record<string, unknown>,
|
|
33
|
+
): void {
|
|
34
|
+
const missing = [
|
|
35
|
+
...TRANSCRIPT_APIS.filter((name) => typeof transcriptApi[name] !== "function"),
|
|
36
|
+
...SESSION_APIS.filter((name) => typeof sessionApi[name] !== "function"),
|
|
37
|
+
];
|
|
12
38
|
if (missing.length > 0) {
|
|
13
39
|
throw new Error(
|
|
14
|
-
|
|
40
|
+
`pi-openai-codex-compat requires a running ${REQUIRED_PI} runtime. ` +
|
|
15
41
|
`Missing host APIs: ${missing.join(", ")}. ` +
|
|
16
|
-
|
|
42
|
+
`Exit Pi and start ${REQUIRED_PI} after updating. /reload cannot upgrade the running ` +
|
|
17
43
|
"runtime, and PI_PACKAGE_DIR can make an older executable report a newer version.",
|
|
18
44
|
);
|
|
19
45
|
}
|
|
@@ -2,7 +2,7 @@ import { isString } from "./value-contracts.ts";
|
|
|
2
2
|
import type {
|
|
3
3
|
BeforeProviderHeadersEvent,
|
|
4
4
|
CompactionResult,
|
|
5
|
-
|
|
5
|
+
ContextWithSystemEvent,
|
|
6
6
|
ExtensionAPI,
|
|
7
7
|
ExtensionContext,
|
|
8
8
|
SessionBeforeCompactEvent,
|
|
@@ -18,6 +18,8 @@ import { addRemoteCompactionFeature, isObject, type JsonRecord } from "./codex-p
|
|
|
18
18
|
import { CODEX_API, CODEX_PROVIDER } from "./codex-identifiers.ts";
|
|
19
19
|
import {
|
|
20
20
|
activeResponsesTools,
|
|
21
|
+
branchInstructions,
|
|
22
|
+
declaresActiveTools,
|
|
21
23
|
providerHistory,
|
|
22
24
|
remoteCompactionMarkerSummary,
|
|
23
25
|
responsesCompatibility,
|
|
@@ -50,9 +52,9 @@ export type RemoteCompactionLifecycleHandler = (
|
|
|
50
52
|
) => void;
|
|
51
53
|
|
|
52
54
|
export type RemoteCompactionContextHandler = (
|
|
53
|
-
event: Pick<
|
|
55
|
+
event: Pick<ContextWithSystemEvent, "messages">,
|
|
54
56
|
ctx: RemoteCompactionContext,
|
|
55
|
-
) => { messages:
|
|
57
|
+
) => { messages: ContextWithSystemEvent["messages"] } | undefined;
|
|
56
58
|
|
|
57
59
|
export type RemoteCompactionHeadersHandler = (
|
|
58
60
|
event: Pick<BeforeProviderHeadersEvent, "headers">,
|
|
@@ -114,7 +116,12 @@ export function remoteCompactionApi(pi: ExtensionAPI): RemoteCompactionApi {
|
|
|
114
116
|
getAllTools: () => pi.getAllTools(),
|
|
115
117
|
onBeforeProviderHeaders: (handler) =>
|
|
116
118
|
pi.on("before_provider_headers", (event, ctx) => handler(event, context(ctx))),
|
|
117
|
-
|
|
119
|
+
// The full-transcript event returns messages verbatim. A changed `context`
|
|
120
|
+
// result would fold every later system message into the leading one, so
|
|
121
|
+
// `instructions` would no longer be the branch's leading prompt and the
|
|
122
|
+
// inline developer items in `input` would repeat the folded updates.
|
|
123
|
+
onContext: (handler) =>
|
|
124
|
+
pi.on("context_with_system", (event, ctx) => handler(event, context(ctx))),
|
|
118
125
|
onSessionBeforeCompact: (handler) =>
|
|
119
126
|
pi.on("session_before_compact", (event, ctx) => handler(event, context(ctx))),
|
|
120
127
|
onSessionShutdown: (handler) =>
|
|
@@ -245,17 +252,23 @@ export default function registerRemoteCompaction(
|
|
|
245
252
|
const history = providerHistory({
|
|
246
253
|
branch: event.branchEntries,
|
|
247
254
|
wireModel: ctx.model,
|
|
248
|
-
allTools,
|
|
249
255
|
grammarToolInputProperties,
|
|
250
256
|
imageDetail: config.imageDetail,
|
|
251
257
|
recoverLatestOverflowPrefix: event.reason === "overflow" && event.willRetry,
|
|
252
|
-
anchorsToolAdditions: false,
|
|
253
258
|
});
|
|
254
|
-
//
|
|
255
|
-
//
|
|
259
|
+
// Reuse the last turn's exact declarations when they still describe the
|
|
260
|
+
// active tools, so the compaction request shares the turn requests'
|
|
261
|
+
// prompt-cache prefix. Rebuild from the registry only when the active
|
|
262
|
+
// set changed since that turn or no turn template exists.
|
|
256
263
|
const template: JsonRecord = { ...matching?.payload };
|
|
264
|
+
const activeNames = pi.getActiveTools();
|
|
265
|
+
const cachedTools = Array.isArray(matching?.payload.tools)
|
|
266
|
+
? matching.payload.tools.filter(isObject)
|
|
267
|
+
: undefined;
|
|
257
268
|
template.tools =
|
|
258
|
-
|
|
269
|
+
cachedTools && declaresActiveTools(cachedTools, allTools, activeNames)
|
|
270
|
+
? cachedTools
|
|
271
|
+
: (activeResponsesTools(allTools, activeNames, grammarToolInputProperties) ?? []);
|
|
259
272
|
const requestOptions: OpenAICodexResponsesOptions = {
|
|
260
273
|
...matching?.requestOptions,
|
|
261
274
|
apiKey: authentication.apiKey,
|
|
@@ -267,7 +280,14 @@ export default function registerRemoteCompaction(
|
|
|
267
280
|
model: ctx.model,
|
|
268
281
|
requestOptions,
|
|
269
282
|
history,
|
|
270
|
-
|
|
283
|
+
// Turn requests send the branch's leading system message as `instructions`
|
|
284
|
+
// and carry later system messages inline in the history. A branch without
|
|
285
|
+
// a leading system message (a pre-0.86 session) falls back to Pi's
|
|
286
|
+
// current prompt so the compaction still sees instructions.
|
|
287
|
+
instructions: instructionsForCompaction(
|
|
288
|
+
branchInstructions(ctx.model, event.branchEntries) || ctx.getSystemPrompt(),
|
|
289
|
+
event.customInstructions,
|
|
290
|
+
),
|
|
271
291
|
grammarToolInputProperties:
|
|
272
292
|
matching?.grammarToolInputProperties ??
|
|
273
293
|
requestGrammarToolInputProperties(template, allTools),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
3
3
|
import { providerHistory } from "./compaction-checkpoint.ts";
|
|
4
4
|
import type { ImageDetail } from "./config.ts";
|
|
@@ -12,7 +12,6 @@ export interface CodexToolAuthentication {
|
|
|
12
12
|
export type ToolAuthenticationContext = {
|
|
13
13
|
modelRegistry: Pick<ExtensionContext["modelRegistry"], "getApiKeyAndHeaders">;
|
|
14
14
|
};
|
|
15
|
-
export type ToolHistoryApi = Pick<ExtensionAPI, "getAllTools">;
|
|
16
15
|
export type ToolHistoryContext = {
|
|
17
16
|
sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch">;
|
|
18
17
|
};
|
|
@@ -32,7 +31,6 @@ export async function codexToolAuthentication(
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
export function codexToolHistory(
|
|
35
|
-
pi: ToolHistoryApi,
|
|
36
34
|
ctx: ToolHistoryContext,
|
|
37
35
|
model: Model<Api>,
|
|
38
36
|
imageDetail: ImageDetail = "auto",
|
|
@@ -40,7 +38,6 @@ export function codexToolHistory(
|
|
|
40
38
|
return providerHistory({
|
|
41
39
|
branch: ctx.sessionManager.getBranch(),
|
|
42
40
|
wireModel: model,
|
|
43
|
-
allTools: pi.getAllTools(),
|
|
44
41
|
imageDetail,
|
|
45
42
|
});
|
|
46
43
|
}
|
|
@@ -8,14 +8,15 @@ Pi AI's transcript helpers replay system sections and tool declarations.
|
|
|
8
8
|
`supportsAdditionalTools` and `supportsToolSearch` select the dynamic tool
|
|
9
9
|
history representation for additive `toolsAdded` system messages:
|
|
10
10
|
message-anchored `additional_tools` or a `tool_search_call`/`tool_search_output`
|
|
11
|
-
pair.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
pair. The copy keeps both options so the wire-equivalence tests hold, but Compat
|
|
12
|
+
always passes `false`: it declares every tool at the top level.
|
|
13
|
+
|
|
14
|
+
System messages follow Pi AI exactly. Compat passes `includeSystemPrompt: false`
|
|
15
|
+
because the leading system message travels as `instructions`, and forwards the
|
|
16
|
+
model's `supportsMidConvoSystemMessages` flag so later system messages render
|
|
17
|
+
inline through `renderSystemMessageUpdate` or collapse into the leading message.
|
|
18
|
+
A system message that lands between a tool call and its results is held until
|
|
19
|
+
after those results, as upstream `transformMessages` does.
|
|
19
20
|
|
|
20
21
|
The optional `namespacedToolNames`, `textContentItemToolResultNames`, and `toolResultImageDetail` paths are extension-owned additions. Without those options, serialization must continue to match Pi AI. `namespacedToolNames` groups only the fixed Codex allowlist into native Responses namespaces and replays namespace/member call identities. `textContentItemToolResultNames` preserves Codex tools such as `web.run` whose successful text output is transported as an `input_text` content-item array instead of Pi's usual plain string. `toolResultImageDetail` overrides the otherwise canonical `auto` detail used for image tool-result content.
|
|
21
22
|
|
|
@@ -53,8 +53,6 @@ type ToolResultOutput =
|
|
|
53
53
|
|
|
54
54
|
type ConvertResponsesMessagesOptions = {
|
|
55
55
|
includeSystemPrompt?: boolean;
|
|
56
|
-
/** Compat sends the complete current prompt outside replay history. */
|
|
57
|
-
includeSystemUpdates?: boolean;
|
|
58
56
|
grammarToolInputProperties?: ReadonlyMap<string, string>;
|
|
59
57
|
supportsMidConvoSystemMessages?: boolean;
|
|
60
58
|
supportsAdditionalTools?: boolean;
|
|
@@ -551,25 +549,33 @@ function transformMessages(
|
|
|
551
549
|
const result: Message[] = [];
|
|
552
550
|
let pendingToolCalls: ToolCall[] = [];
|
|
553
551
|
let existingToolResultIds = new Set<string>();
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
552
|
+
// System messages are transparent to tool-call accounting: one that lands between a
|
|
553
|
+
// tool call and its results is held back and emitted after the results (synthetic
|
|
554
|
+
// ones included), so it never causes a duplicate result for a call answered later.
|
|
555
|
+
const heldSystemMessages: Message[] = [];
|
|
556
|
+
const closePendingToolCalls = () => {
|
|
557
|
+
if (pendingToolCalls.length > 0) {
|
|
558
|
+
for (const toolCall of pendingToolCalls) {
|
|
559
|
+
if (existingToolResultIds.has(toolCall.id)) continue;
|
|
560
|
+
result.push({
|
|
561
|
+
role: "toolResult",
|
|
562
|
+
toolCallId: toolCall.id,
|
|
563
|
+
toolName: toolCall.name,
|
|
564
|
+
content: [{ type: "text", text: "No result provided" }],
|
|
565
|
+
isError: true,
|
|
566
|
+
timestamp: Date.now(),
|
|
567
|
+
} satisfies ToolResultMessage);
|
|
568
|
+
}
|
|
569
|
+
pendingToolCalls = [];
|
|
570
|
+
existingToolResultIds = new Set();
|
|
565
571
|
}
|
|
566
|
-
|
|
567
|
-
|
|
572
|
+
result.push(...heldSystemMessages);
|
|
573
|
+
heldSystemMessages.length = 0;
|
|
568
574
|
};
|
|
569
575
|
|
|
570
576
|
for (const message of transformed) {
|
|
571
577
|
if (message.role === "assistant") {
|
|
572
|
-
|
|
578
|
+
closePendingToolCalls();
|
|
573
579
|
const assistantMessage = message;
|
|
574
580
|
if (assistantMessage.stopReason === "error" || assistantMessage.stopReason === "aborted") {
|
|
575
581
|
continue;
|
|
@@ -585,14 +591,17 @@ function transformMessages(
|
|
|
585
591
|
} else if (message.role === "toolResult") {
|
|
586
592
|
existingToolResultIds.add(message.toolCallId);
|
|
587
593
|
result.push(message);
|
|
594
|
+
} else if (message.role === "system") {
|
|
595
|
+
if (pendingToolCalls.length > 0) heldSystemMessages.push(message);
|
|
596
|
+
else result.push(message);
|
|
588
597
|
} else if (message.role === "user") {
|
|
589
|
-
|
|
598
|
+
closePendingToolCalls();
|
|
590
599
|
result.push(message);
|
|
591
600
|
} else {
|
|
592
601
|
result.push(message);
|
|
593
602
|
}
|
|
594
603
|
}
|
|
595
|
-
|
|
604
|
+
closePendingToolCalls();
|
|
596
605
|
return result;
|
|
597
606
|
}
|
|
598
607
|
|
|
@@ -737,7 +746,7 @@ export function convertResponsesMessages(
|
|
|
737
746
|
tools: convertResponsesTools(addedTools, { ...toolOptions, deferLoading: true }),
|
|
738
747
|
});
|
|
739
748
|
}
|
|
740
|
-
if (isLeadingSystemMessage
|
|
749
|
+
if (!isLeadingSystemMessage || includeSystemPrompt) {
|
|
741
750
|
const text = isLeadingSystemMessage
|
|
742
751
|
? getSystemMessageText(message)
|
|
743
752
|
: renderSystemMessageUpdate(message);
|
|
@@ -176,7 +176,7 @@ export default function registerWebRun(
|
|
|
176
176
|
}
|
|
177
177
|
const authentication = await codexToolAuthentication(ctx, model);
|
|
178
178
|
const config = resolveConfig(ctx);
|
|
179
|
-
const history = codexToolHistory(
|
|
179
|
+
const history = codexToolHistory(ctx, model, config.imageDetail);
|
|
180
180
|
const input = recentSearchInput(history);
|
|
181
181
|
const body: JsonRecord = {
|
|
182
182
|
id: ctx.sessionManager.getSessionId(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openai-codex-compat",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "OpenAI Codex compatibility for Pi with native compaction, fast mode, and Codex-optimized capabilities",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@2h2d/oxlint-config": "0.1.1",
|
|
53
53
|
"@2h2d/ts-config": "0.0.1",
|
|
54
|
-
"@earendil-works/pi-ai": "0.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
56
|
-
"@earendil-works/pi-tui": "0.
|
|
54
|
+
"@earendil-works/pi-ai": "0.87.0",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "0.87.0",
|
|
56
|
+
"@earendil-works/pi-tui": "0.87.0",
|
|
57
57
|
"@types/node": "22.20.1",
|
|
58
58
|
"oxfmt": "0.64.0",
|
|
59
59
|
"oxlint": "1.79.0",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"typescript": "7.0.2"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@earendil-works/pi-ai": ">=0.
|
|
66
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
67
|
-
"@earendil-works/pi-tui": ">=0.
|
|
65
|
+
"@earendil-works/pi-ai": ">=0.87.0 <0.88.0",
|
|
66
|
+
"@earendil-works/pi-coding-agent": ">=0.87.0 <0.88.0",
|
|
67
|
+
"@earendil-works/pi-tui": ">=0.87.0 <0.88.0",
|
|
68
68
|
"typebox": "*"
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|